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