Skip to main content

nap_core/
vcs.rs

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