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//! git-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 Git 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 label (tag) attached to a revision via Lore metadata.
92///
93/// Lore has no first-class "tag" object — labels are stored as metadata
94/// under the reserved key `nap.labels`.  See the [`LabelConvention`]
95/// documentation for how tags round-trip through the metadata system.
96#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
97pub struct Label {
98    /// The revision this label points to.
99    pub revision_signature: String,
100    /// Label/tag names applied to this revision.
101    pub names: Vec<String>,
102}
103
104/// A directory- or file-level access-control entry.
105///
106/// Lore's stock server has no native path ACL — this is enforced at the
107/// application layer by [`PermissionGate`](crate::permission_gate::PermissionGate).
108#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
109pub struct Permission {
110    /// Directory or file path prefix this rule applies to.
111    pub path_prefix: String,
112    /// User or role identifier.
113    pub principal: String,
114    /// Granted access level.
115    pub access: AccessLevel,
116}
117
118/// Access level for a permission entry.
119#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
120pub enum AccessLevel {
121    /// Read-only access.
122    Read,
123    /// Read + write access.
124    Write,
125    /// No access (explicit deny).
126    None,
127}
128
129/// A contextual document tracked in the Lore VCS with associated metadata
130/// and dependency edges for AI context-graph assembly.
131#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
132pub struct ContextDocument {
133    /// Path within the repository (e.g. `/context/task-123.md`).
134    pub path: String,
135    /// Arbitrary key-value metadata stored via `lore file metadata set`.
136    pub metadata: std::collections::HashMap<String, String>,
137    /// Other files this document depends on (the AI relevance graph).
138    pub depends_on: Vec<String>,
139}
140
141/// Metadata about a single VCS commit, returned by `log()`.
142/// Kept for backward compatibility with existing callers.
143#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
144pub struct CommitInfo {
145    /// The commit hash/identifier (Lore revision signature).
146    pub id: String,
147    /// Parent commit hash (None for root).
148    pub parent: Option<String>,
149    /// Commit author.
150    pub author: String,
151    /// Commit message.
152    pub message: String,
153    /// Commit timestamp (RFC 3339).
154    pub timestamp: String,
155}
156
157// ---------------------------------------------------------------------------
158// VcsBackend trait — low-level VCS abstraction
159// ---------------------------------------------------------------------------
160
161/// Low-level abstraction over a version control system.
162///
163/// [`LoreBackend`](crate::vcs_lore::LoreBackend)
164///
165/// Most consumer code should use [`RepoService`] instead — it adds
166/// permissions, context-document management, and a
167/// workspace-lifecycle API on top of this trait.
168pub trait VcsBackend: Send + Sync {
169    /// Initialize a new repository at the given path.
170    fn init(&self, path: &Path) -> Result<(), NapError>;
171
172    /// Stage all files and create a commit.
173    fn commit(&self, path: &Path, message: &str, author: &str) -> Result<String, NapError>;
174
175    /// Read a file's content at a specific ref (branch, tag, or commit hash).
176    /// If `reference` is None, reads from the current working tree.
177    fn read_file_at_ref(
178        &self,
179        repo_path: &Path,
180        file_path: &str,
181        reference: Option<&str>,
182    ) -> Result<String, NapError>;
183
184    /// Get the commit log for the repository, optionally filtered to a specific file.
185    fn log(
186        &self,
187        path: &Path,
188        file: Option<&str>,
189        limit: usize,
190    ) -> Result<Vec<CommitInfo>, NapError>;
191
192    /// Create a new branch.
193    fn create_branch(&self, path: &Path, name: &str) -> Result<(), NapError>;
194
195    /// Switch to a branch.
196    fn switch_branch(&self, path: &Path, name: &str) -> Result<(), NapError>;
197
198    /// Create a tag at the current HEAD.
199    fn create_tag(&self, path: &Path, name: &str) -> Result<(), NapError>;
200
201    /// Get the current branch name.
202    fn current_branch(&self, path: &Path) -> Result<String, NapError>;
203
204    /// Get the HEAD commit hash.
205    fn head_hash(&self, path: &Path) -> Result<String, NapError>;
206
207    /// Revert a commit by creating a new commit that undoes it.
208    fn revert(&self, _path: &Path, _commit_hash: &str) -> Result<String, NapError> {
209        Err(NapError::VcsError(
210            "revert not supported by this VCS backend".to_string(),
211        ))
212    }
213
214    /// List all branches.
215    fn list_branches(&self, path: &Path) -> Result<Vec<String>, NapError>;
216
217    /// List all tags.
218    fn list_tags(&self, path: &Path) -> Result<Vec<String>, NapError>;
219
220    /// Resolve the most recent commit hash on a given branch.
221    ///
222    /// The default implementation returns an error — backends that support
223    /// branch-based resolution must override this.
224    fn resolve_branch_head(&self, path: &Path, branch: &str) -> Result<String, NapError> {
225        let _ = (path, branch);
226        Err(NapError::VcsError(
227            "resolve_branch_head not supported by this VCS backend".to_string(),
228        ))
229    }
230
231    // ── Remote operations ────────────────────────────────────────────
232
233    /// Add a remote.
234    fn add_remote(&self, path: &Path, name: &str, url: &str) -> Result<(), NapError>;
235
236    /// Remove a remote.
237    fn remove_remote(&self, path: &Path, name: &str) -> Result<(), NapError>;
238
239    /// List remotes as `(name, url)` pairs.
240    fn list_remotes(&self, path: &Path) -> Result<Vec<(String, String)>, NapError>;
241
242    /// Push the current branch to its upstream / a named remote.
243    fn push(&self, path: &Path, remote: Option<&str>, branch: Option<&str>)
244    -> Result<(), NapError>;
245
246    /// Pull the current branch from its upstream / a named remote.
247    fn pull(&self, path: &Path, remote: Option<&str>, branch: Option<&str>)
248    -> Result<(), NapError>;
249
250    /// Get the remote URL base for constructing repository URLs.
251    ///
252    /// The default implementation returns an error — backends that support
253    /// remote URL construction must override this.
254    fn remote_url_base(&self) -> Result<String, NapError> {
255        Err(NapError::VcsError(
256            "remote_url_base not supported by this VCS backend".to_string(),
257        ))
258    }
259}
260
261// ---------------------------------------------------------------------------
262// CommitInfo convenience — used by the resolver and history views
263// ---------------------------------------------------------------------------
264
265impl CommitInfo {
266    /// Build a `CommitInfo` from a lore revision's structured output.
267    /// The `timestamp` field is best-effort; Lore may not provide it in all
268    /// output modes.
269    pub fn from_lore_revision(
270        signature: &str,
271        parent: Option<&str>,
272        author: &str,
273        message: &str,
274        timestamp: &str,
275    ) -> Self {
276        Self {
277            id: signature.to_string(),
278            parent: parent.map(|p| p.to_string()),
279            author: author.to_string(),
280            message: message.to_string(),
281            timestamp: if timestamp.is_empty() {
282                chrono::Utc::now().to_rfc3339()
283            } else {
284                timestamp.to_string()
285            },
286        }
287    }
288}