Skip to main content

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/// Stable identity and configured remote for a version-control repository.
146#[derive(Debug, Clone, PartialEq, Eq)]
147pub struct VcsRepositoryDescriptor {
148    pub id: String,
149    pub remote_url: String,
150}
151
152/// Immutable content address for one file at one revision.
153#[derive(Debug, Clone, PartialEq, Eq)]
154pub struct VcsContentAddress {
155    pub hash: String,
156    pub context: String,
157}
158
159impl VcsContentAddress {
160    pub fn as_lore_address(&self) -> String {
161        format!("{}-{}", self.hash, self.context)
162    }
163}
164
165// ---------------------------------------------------------------------------
166// VcsBackend trait — low-level VCS abstraction
167// ---------------------------------------------------------------------------
168
169/// Low-level abstraction over a version control system.
170///
171/// [`LoreBackend`](crate::vcs_lore::LoreBackend)
172///
173/// Most consumer code should use [`RepoService`] instead — it adds
174/// permissions, context-document management, and a
175/// workspace-lifecycle API on top of this trait.
176pub trait VcsBackend: Send + Sync {
177    /// Initialize a new repository at the given path.
178    fn init(&self, path: &Path) -> Result<(), NapError>;
179
180    /// Stage all files and create a commit.
181    fn commit(&self, path: &Path, message: &str, author: &str) -> Result<String, NapError>;
182
183    /// Read a file's content at a specific ref (branch, tag, or commit hash).
184    /// If `reference` is None, reads from the current working tree.
185    fn read_file_at_ref(
186        &self,
187        repo_path: &Path,
188        file_path: &str,
189        reference: Option<&str>,
190    ) -> Result<String, NapError>;
191
192    /// Read arbitrary file bytes at a specific ref.
193    ///
194    /// Backends should override this when their storage is binary-safe. The
195    /// default preserves compatibility for text-only backends.
196    fn read_file_bytes_at_ref(
197        &self,
198        repo_path: &Path,
199        file_path: &str,
200        reference: Option<&str>,
201    ) -> Result<Vec<u8>, NapError> {
202        self.read_file_at_ref(repo_path, file_path, reference)
203            .map(String::into_bytes)
204    }
205
206    /// Get the commit log for the repository, optionally filtered to a specific file.
207    fn log(
208        &self,
209        path: &Path,
210        file: Option<&str>,
211        limit: usize,
212    ) -> Result<Vec<CommitInfo>, NapError>;
213
214    /// Create a new branch.
215    fn create_branch(&self, path: &Path, name: &str) -> Result<(), NapError>;
216
217    /// Switch to a branch.
218    fn switch_branch(&self, path: &Path, name: &str) -> Result<(), NapError>;
219
220    /// Get the current branch name.
221    fn current_branch(&self, path: &Path) -> Result<String, NapError>;
222
223    /// Get the HEAD commit hash.
224    fn head_hash(&self, path: &Path) -> Result<String, NapError>;
225
226    /// Revert a commit by creating a new commit that undoes it.
227    fn revert(&self, _path: &Path, _commit_hash: &str) -> Result<String, NapError> {
228        Err(NapError::VcsError(
229            "revert not supported by this VCS backend".to_string(),
230        ))
231    }
232
233    /// List all branches.
234    fn list_branches(&self, path: &Path) -> Result<Vec<String>, NapError>;
235
236    /// Resolve the most recent commit hash on a given branch.
237    ///
238    /// The default implementation returns an error — backends that support
239    /// branch-based resolution must override this.
240    fn resolve_branch_head(&self, path: &Path, branch: &str) -> Result<String, NapError> {
241        let _ = (path, branch);
242        Err(NapError::VcsError(
243            "resolve_branch_head not supported by this VCS backend".to_string(),
244        ))
245    }
246
247    /// Return the repository ID separately from any file-address context.
248    fn repository_descriptor(&self, _path: &Path) -> Result<VcsRepositoryDescriptor, NapError> {
249        Err(NapError::VcsError(
250            "repository_descriptor not supported by this VCS backend".to_string(),
251        ))
252    }
253
254    /// Reuse the active Lore login's unexpired repository token for an authorized HTTP recipient.
255    /// Implementations must not log tokens or return an unscoped authentication token.
256    fn http_bearer_token(
257        &self,
258        _repo_path: &Path,
259        _repository_id: &str,
260        _http_origin: &str,
261    ) -> Result<Option<String>, NapError> {
262        Ok(None)
263    }
264
265    /// Return the immutable content address of a file at a pinned revision.
266    fn file_content_address_at_ref(
267        &self,
268        _repo_path: &Path,
269        _file_path: &str,
270        _reference: &str,
271    ) -> Result<VcsContentAddress, NapError> {
272        Err(NapError::VcsError(
273            "file_content_address_at_ref not supported by this VCS backend".to_string(),
274        ))
275    }
276
277    // ── Remote operations ────────────────────────────────────────────
278
279    /// Add a remote.
280    fn add_remote(&self, path: &Path, name: &str, url: &str) -> Result<(), NapError>;
281
282    /// Remove a remote.
283    fn remove_remote(&self, path: &Path, name: &str) -> Result<(), NapError>;
284
285    /// List remotes as `(name, url)` pairs.
286    fn list_remotes(&self, path: &Path) -> Result<Vec<(String, String)>, NapError>;
287
288    /// Push the current branch to its upstream / a named remote.
289    fn push(&self, path: &Path, remote: Option<&str>, branch: Option<&str>)
290    -> Result<(), NapError>;
291
292    /// Pull the current branch from its upstream / a named remote.
293    fn pull(&self, path: &Path, remote: Option<&str>, branch: Option<&str>)
294    -> Result<(), NapError>;
295
296    /// Get the remote URL base for constructing repository URLs.
297    ///
298    /// The default implementation returns an error — backends that support
299    /// remote URL construction must override this.
300    fn remote_url_base(&self) -> Result<String, NapError> {
301        Err(NapError::VcsError(
302            "remote_url_base not supported by this VCS backend".to_string(),
303        ))
304    }
305
306    /// Read file metadata attached to a path at a specific revision.
307    ///
308    /// Lore file metadata is addressed by working-tree path plus revision,
309    /// not by the raw content hash of the file bytes.
310    fn file_metadata_at_ref(
311        &self,
312        _repo_path: &Path,
313        _file_path: &str,
314        _reference: &str,
315    ) -> Result<Option<BTreeMap<String, String>>, NapError> {
316        Ok(None)
317    }
318
319    /// Read a readable immutable provenance artifact by Lore address/hash.
320    ///
321    /// Production Lore verifies immutable fragment addressing internally. NAP
322    /// uses this only for known readable provenance artifacts, never arbitrary
323    /// binary assets.
324    fn read_provenance_blob(&self, _repo_path: &Path, _address: &str) -> Result<String, NapError> {
325        Err(NapError::VcsError(
326            "read_provenance_blob not supported by this VCS backend".to_string(),
327        ))
328    }
329}
330
331// ---------------------------------------------------------------------------
332// CommitInfo convenience — used by the resolver and history views
333// ---------------------------------------------------------------------------
334
335impl CommitInfo {
336    /// Build a `CommitInfo` from a lore revision's structured output.
337    /// The `timestamp` field is best-effort; Lore may not provide it in all
338    /// output modes.
339    pub fn from_lore_revision(
340        signature: &str,
341        parent: Option<&str>,
342        author: &str,
343        message: &str,
344        timestamp: &str,
345    ) -> Self {
346        Self {
347            id: signature.to_string(),
348            parent: parent.map(|p| p.to_string()),
349            author: author.to_string(),
350            message: message.to_string(),
351            timestamp: if timestamp.is_empty() {
352                chrono::Utc::now().to_rfc3339()
353            } else {
354                timestamp.to_string()
355            },
356        }
357    }
358}