nap_core/vcs.rs
1//! VCS backend abstraction and Lore VCS type system.
2//!
3//! **Lore** — a centralized VCS with
4//! global revision numbers, file-level metadata, dependency graphs, and
5//! style branching.
6//!
7//! The [`VcsBackend`] trait is the low-level seam between NAP and any VCS.
8//! [`LoreBackend`](crate::vcs_lore::LoreBackend) is the only production
9//! implementation. Higher-level workflows (context docs, permissions) live in [`RepoService`].
10//!
11//! ## Architecture
12//!
13//! ```text
14//! Consumer code → RepoService (stable boundary)
15//! │
16//! ▼
17//! VcsBackend trait
18//! │
19//! LoreBackend (adapter)
20//! │
21//! LoreProcessRunner (CLI executor)
22//! │
23//! loreserver (authoritative store)
24//! ```
25
26use std::collections::BTreeMap;
27use std::path::Path;
28
29use crate::error::NapError;
30
31// ---------------------------------------------------------------------------
32// Core VCS types (Lore-native, also serve as the RepoService vocabulary)
33// ---------------------------------------------------------------------------
34
35/// A Lore repository identity — analogous to a remote, but with a
36/// workspace-scoped multi-tenant owner.
37#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
38pub struct Repository {
39 /// Stable internal identifier, set via `lore repository create --id`.
40 pub id: String,
41 /// Workspace that owns this repository (multi-tenancy boundary).
42 pub workspace_id: String,
43 /// Lore `lore://` remote URL on the loreserver.
44 pub remote_url: String,
45}
46
47/// A local working copy of a Lore repository.
48#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
49pub struct Workspace {
50 /// The repository this workspace belongs to.
51 pub repository_id: String,
52 /// Local filesystem path to the working tree.
53 pub path: String,
54 /// Current branch.
55 pub branch: String,
56 /// Whether this workspace is durable, ephemeral, or virtual.
57 pub mode: WorkspaceMode,
58}
59
60/// How a workspace tracks state locally.
61#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
62pub enum WorkspaceMode {
63 /// Full local working tree with tracking (default for interactive use).
64 Durable,
65 /// Memory-only tracking — no local repo state left behind (for agents).
66 Ephemeral,
67 /// Split-write filesystem — like ephemeral but with a writable overlay.
68 Virtual,
69}
70
71/// A single revision (commit) in the Lore VCS.
72///
73/// Lore revisions have both a content-hash **signature** (BLAKE3 SHA)
74/// and a monotonically incrementing global **number** (like an SVN revision
75/// or Perforce changelist). NAP exposes both.
76#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
77pub struct Revision {
78 /// Lore revision hash signature (content-addressed).
79 pub signature: String,
80 /// Lore global revision number (monotonic, cross-branch).
81 pub number: u64,
82 /// Branch this revision was committed on.
83 pub branch: String,
84 /// Commit message.
85 pub message: String,
86 /// Author identity string.
87 pub author: String,
88 /// Parent revision signature, if any.
89 pub parent_signature: Option<String>,
90}
91
92/// A directory- or file-level access-control entry.
93///
94/// Lore's stock server has no native path ACL — this is enforced at the
95/// application layer by [`PermissionGate`](crate::permission_gate::PermissionGate).
96#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
97pub struct Permission {
98 /// Directory or file path prefix this rule applies to.
99 pub path_prefix: String,
100 /// User or role identifier.
101 pub principal: String,
102 /// Granted access level.
103 pub access: AccessLevel,
104}
105
106/// Access level for a permission entry.
107#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
108pub enum AccessLevel {
109 /// Read-only access.
110 Read,
111 /// Read + write access.
112 Write,
113 /// No access (explicit deny).
114 None,
115}
116
117/// A contextual document tracked in the Lore VCS with associated metadata
118/// and dependency edges for AI context-graph assembly.
119#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
120pub struct ContextDocument {
121 /// Path within the repository (e.g. `/context/task-123.md`).
122 pub path: String,
123 /// Arbitrary key-value metadata stored via `lore file metadata set`.
124 pub metadata: std::collections::HashMap<String, String>,
125 /// Other files this document depends on (the AI relevance graph).
126 pub depends_on: Vec<String>,
127}
128
129/// Metadata about a single VCS commit, returned by `log()`.
130/// Kept for backward compatibility with existing callers.
131#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
132pub struct CommitInfo {
133 /// The commit hash/identifier (Lore revision signature).
134 pub id: String,
135 /// Parent commit hash (None for root).
136 pub parent: Option<String>,
137 /// Commit author.
138 pub author: String,
139 /// Commit message.
140 pub message: String,
141 /// Commit timestamp (RFC 3339).
142 pub timestamp: String,
143}
144
145// ---------------------------------------------------------------------------
146// VcsBackend trait — low-level VCS abstraction
147// ---------------------------------------------------------------------------
148
149/// Low-level abstraction over a version control system.
150///
151/// [`LoreBackend`](crate::vcs_lore::LoreBackend)
152///
153/// Most consumer code should use [`RepoService`] instead — it adds
154/// permissions, context-document management, and a
155/// workspace-lifecycle API on top of this trait.
156pub trait VcsBackend: Send + Sync {
157 /// Initialize a new repository at the given path.
158 fn init(&self, path: &Path) -> Result<(), NapError>;
159
160 /// Stage all files and create a commit.
161 fn commit(&self, path: &Path, message: &str, author: &str) -> Result<String, NapError>;
162
163 /// Read a file's content at a specific ref (branch, tag, or commit hash).
164 /// If `reference` is None, reads from the current working tree.
165 fn read_file_at_ref(
166 &self,
167 repo_path: &Path,
168 file_path: &str,
169 reference: Option<&str>,
170 ) -> Result<String, NapError>;
171
172 /// Get the commit log for the repository, optionally filtered to a specific file.
173 fn log(
174 &self,
175 path: &Path,
176 file: Option<&str>,
177 limit: usize,
178 ) -> Result<Vec<CommitInfo>, NapError>;
179
180 /// Create a new branch.
181 fn create_branch(&self, path: &Path, name: &str) -> Result<(), NapError>;
182
183 /// Switch to a branch.
184 fn switch_branch(&self, path: &Path, name: &str) -> Result<(), NapError>;
185
186 /// Get the current branch name.
187 fn current_branch(&self, path: &Path) -> Result<String, NapError>;
188
189 /// Get the HEAD commit hash.
190 fn head_hash(&self, path: &Path) -> Result<String, NapError>;
191
192 /// Revert a commit by creating a new commit that undoes it.
193 fn revert(&self, _path: &Path, _commit_hash: &str) -> Result<String, NapError> {
194 Err(NapError::VcsError(
195 "revert not supported by this VCS backend".to_string(),
196 ))
197 }
198
199 /// List all branches.
200 fn list_branches(&self, path: &Path) -> Result<Vec<String>, NapError>;
201
202 /// Resolve the most recent commit hash on a given branch.
203 ///
204 /// The default implementation returns an error — backends that support
205 /// branch-based resolution must override this.
206 fn resolve_branch_head(&self, path: &Path, branch: &str) -> Result<String, NapError> {
207 let _ = (path, branch);
208 Err(NapError::VcsError(
209 "resolve_branch_head not supported by this VCS backend".to_string(),
210 ))
211 }
212
213 // ── Remote operations ────────────────────────────────────────────
214
215 /// Add a remote.
216 fn add_remote(&self, path: &Path, name: &str, url: &str) -> Result<(), NapError>;
217
218 /// Remove a remote.
219 fn remove_remote(&self, path: &Path, name: &str) -> Result<(), NapError>;
220
221 /// List remotes as `(name, url)` pairs.
222 fn list_remotes(&self, path: &Path) -> Result<Vec<(String, String)>, NapError>;
223
224 /// Push the current branch to its upstream / a named remote.
225 fn push(&self, path: &Path, remote: Option<&str>, branch: Option<&str>)
226 -> Result<(), NapError>;
227
228 /// Pull the current branch from its upstream / a named remote.
229 fn pull(&self, path: &Path, remote: Option<&str>, branch: Option<&str>)
230 -> Result<(), NapError>;
231
232 /// Get the remote URL base for constructing repository URLs.
233 ///
234 /// The default implementation returns an error — backends that support
235 /// remote URL construction must override this.
236 fn remote_url_base(&self) -> Result<String, NapError> {
237 Err(NapError::VcsError(
238 "remote_url_base not supported by this VCS backend".to_string(),
239 ))
240 }
241
242 /// Read file metadata attached to a path at a specific revision.
243 ///
244 /// Lore file metadata is addressed by working-tree path plus revision,
245 /// not by the raw content hash of the file bytes.
246 fn file_metadata_at_ref(
247 &self,
248 _repo_path: &Path,
249 _file_path: &str,
250 _reference: &str,
251 ) -> Result<Option<BTreeMap<String, String>>, NapError> {
252 Ok(None)
253 }
254
255 /// Read a readable immutable provenance artifact by Lore address/hash.
256 ///
257 /// Production Lore verifies immutable fragment addressing internally. NAP
258 /// uses this only for known readable provenance artifacts, never arbitrary
259 /// binary assets.
260 fn read_provenance_blob(&self, _repo_path: &Path, _address: &str) -> Result<String, NapError> {
261 Err(NapError::VcsError(
262 "read_provenance_blob not supported by this VCS backend".to_string(),
263 ))
264 }
265}
266
267// ---------------------------------------------------------------------------
268// CommitInfo convenience — used by the resolver and history views
269// ---------------------------------------------------------------------------
270
271impl CommitInfo {
272 /// Build a `CommitInfo` from a lore revision's structured output.
273 /// The `timestamp` field is best-effort; Lore may not provide it in all
274 /// output modes.
275 pub fn from_lore_revision(
276 signature: &str,
277 parent: Option<&str>,
278 author: &str,
279 message: &str,
280 timestamp: &str,
281 ) -> Self {
282 Self {
283 id: signature.to_string(),
284 parent: parent.map(|p| p.to_string()),
285 author: author.to_string(),
286 message: message.to_string(),
287 timestamp: if timestamp.is_empty() {
288 chrono::Utc::now().to_rfc3339()
289 } else {
290 timestamp.to_string()
291 },
292 }
293 }
294}