Skip to main content

prikk_store/
worktree_patch.rs

1//! Worktree-to-patch authoring.
2//!
3//! Turns worktree changes into a node-addressed, role-bound Ed25519 AUTHOR-signed Patch envelope
4//! against a replay-derived baseline and appends it to the active WAL. The change-detection,
5//! operation-mapping, minting, mode-normalization, and canonical-ordering logic lives in
6//! [`node_authoring`]; signing goes through the injected [`crate::AuthorSigner`] boundary (no
7//! placeholder signer). Existing paths resolve to their persisted `node_id`; fresh nodes are minted
8//! in canonical create order; text edits go through the shared `text_span` module. Rename inference
9//! and symlink authoring remain out of scope.
10
11use prikk_error::{PrikkError, Result};
12use prikk_object::ObjectId;
13
14use crate::author_signing::AuthorSigner;
15use crate::layout::RepositoryLayout;
16use crate::node_id_gen::NodeIdGenerator;
17
18mod node_authoring;
19
20/// Result of authoring and appending a node-addressed patch from worktree changes.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct WorktreePatchCommitReport {
23    /// Baseline ref used to classify changes.
24    pub ref_name: String,
25    /// Patch object ID appended to the active WAL.
26    pub patch_id: ObjectId,
27    /// WAL sequence assigned to the patch envelope.
28    pub wal_sequence: u64,
29    /// Number of patch operations emitted.
30    pub operation_count: usize,
31    /// Number of Blob object references written or reused for operation payloads.
32    pub referenced_blob_count: usize,
33    /// Number of `EditText` operations emitted (text-file modifications).
34    pub text_edit_count: usize,
35    /// Operation summaries in emitted order.
36    pub changes: Vec<WorktreePatchOperationSummary>,
37}
38
39/// Summary of one generated patch operation.
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct WorktreePatchOperationSummary {
42    /// Repository-relative path.
43    pub path: String,
44    /// Generated operation kind.
45    pub operation: WorktreePatchOperationKind,
46}
47
48/// Generated operation kind for CLI/reporting.
49#[derive(Debug, Clone, Copy, PartialEq, Eq)]
50pub enum WorktreePatchOperationKind {
51    /// A new file will be represented as `CreateFile`.
52    CreateFile,
53    /// A missing tracked file will be represented as `DeleteFile`.
54    DeleteFile,
55    /// A modified tracked file will be represented as `ReplaceBinary`.
56    ReplaceBinary,
57    /// A modified UTF-8 text file is represented as an arbitrary-span `EditText`.
58    EditText,
59    /// A regular file whose normalized mode changed is represented as `ChangePerm`.
60    ChangePerm,
61}
62
63impl WorktreePatchOperationKind {
64    /// Stable CLI label.
65    #[must_use]
66    pub const fn as_str(self) -> &'static str {
67        match self {
68            Self::CreateFile => "create-file",
69            Self::DeleteFile => "delete-file",
70            Self::ReplaceBinary => "replace-binary",
71            Self::EditText => "edit-text",
72            Self::ChangePerm => "change-perm",
73        }
74    }
75}
76
77/// DC-57 default hard block on active (queued, unsealed) patches — NFR-PERF-02's default 1000,
78/// overridable per invocation via `PRIKK_ACTIVE_PATCH_LIMIT` at the CLI boundary, never persisted.
79pub const DEFAULT_ACTIVE_PATCH_LIMIT: usize = 1000;
80
81/// DC-57 (NFR-PERF-02): true when the active WAL already holds `active_patch_limit` or more queued
82/// patches, so appending one more must be refused. "Active patches" has exactly one definition — the
83/// active WAL's record count — and this is the one comparison every authoring path
84/// (`node_authoring.rs::author_inner`, `active.rs::ActiveSession::append_patch`) calls, rather than
85/// each reimplementing it. `>=`, not `>`: once the queue already holds the limit, no more may join it.
86#[must_use]
87pub(crate) const fn active_patch_limit_exceeded(
88    current_count: usize,
89    active_patch_limit: usize,
90) -> bool {
91    current_count >= active_patch_limit
92}
93
94#[cfg(test)]
95mod threshold_tests {
96    use super::active_patch_limit_exceeded;
97
98    /// DC-57 criterion 5: the literal boundary values named in the RFC, tested directly against the
99    /// one shared comparison — this is the pure-arithmetic half of the boundary proof; the
100    /// integration half (proving it is actually wired into `author_inner` before any write, with a
101    /// small scaled limit) lives in `worktree_patch/tests.rs`.
102    #[test]
103    fn boundary_values_match_the_rfc() {
104        assert!(!active_patch_limit_exceeded(799, 800));
105        assert!(active_patch_limit_exceeded(800, 800));
106        assert!(!active_patch_limit_exceeded(999, 1000));
107        assert!(active_patch_limit_exceeded(1000, 1000));
108        assert!(active_patch_limit_exceeded(1001, 1000));
109    }
110}
111
112/// Options for authoring a node-addressed patch from worktree changes.
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub struct WorktreePatchCommitOptions {
115    /// Retained for API compatibility. Existing-node `NodeKind` is now authoritative for the
116    /// modified-file mapping (text files author `EditText`, binary files author `ReplaceBinary`),
117    /// so this flag no longer drives kind selection and is a no-op.
118    pub prefer_text_edits: bool,
119    /// DC-57 (NFR-PERF-02): the active-patch count — the active WAL's record count, the one
120    /// definition every authoring path uses — must be strictly less than this before `author_inner`
121    /// does anything else. Defaults to [`DEFAULT_ACTIVE_PATCH_LIMIT`]; the CLI overrides it from
122    /// `PRIKK_ACTIVE_PATCH_LIMIT`, failing closed on a malformed value rather than silently keeping
123    /// the default.
124    pub active_patch_limit: usize,
125}
126
127impl WorktreePatchCommitOptions {
128    /// Default options (kind-driven mapping; `prefer_text_edits` is a no-op, see the field).
129    #[must_use]
130    pub const fn file_level() -> Self {
131        Self {
132            prefer_text_edits: false,
133            active_patch_limit: DEFAULT_ACTIVE_PATCH_LIMIT,
134        }
135    }
136
137    /// Options with `prefer_text_edits` set; retained for API compatibility (no-op, see the field).
138    #[must_use]
139    pub const fn prefer_text_edits() -> Self {
140        Self {
141            prefer_text_edits: true,
142            active_patch_limit: DEFAULT_ACTIVE_PATCH_LIMIT,
143        }
144    }
145
146    /// Override the active-patch hard-block limit (DC-57). The CLI calls this with a value parsed
147    /// from `PRIKK_ACTIVE_PATCH_LIMIT`; everything else keeps the default.
148    #[must_use]
149    pub const fn with_active_patch_limit(mut self, limit: usize) -> Self {
150        self.active_patch_limit = limit;
151        self
152    }
153}
154
155impl Default for WorktreePatchCommitOptions {
156    fn default() -> Self {
157        Self::file_level()
158    }
159}
160
161/// Author a node-addressed patch from worktree changes against the replay-derived baseline, sign it
162/// with a real role-bound Ed25519 AUTHOR signature from the injected `signer`, and append it to the
163/// active WAL (DC-09 Phase 4.4a, R1). Existing paths resolve to their persisted `node_id`; fresh
164/// nodes are minted through the production [`NodeIdGenerator`]; text edits go through the shared
165/// `text_span` module. There is no placeholder signing path.
166pub fn commit_worktree_changes_signed(
167    layout: &RepositoryLayout,
168    ref_name: &str,
169    message: &str,
170    options: WorktreePatchCommitOptions,
171    signer: &impl AuthorSigner,
172) -> Result<WorktreePatchCommitReport> {
173    layout.require_current_format()?;
174    let mut generator = NodeIdGenerator::production();
175    node_authoring::author_worktree_patch(
176        layout,
177        ref_name,
178        message,
179        options,
180        &mut generator,
181        signer,
182    )
183}
184
185/// Test-only entry that injects a deterministic node-id generator and an explicit signer so authoring
186/// is reproducible.
187#[cfg(test)]
188pub(crate) fn commit_worktree_changes_with_generator<S, A>(
189    layout: &RepositoryLayout,
190    ref_name: &str,
191    message: &str,
192    options: WorktreePatchCommitOptions,
193    generator: &mut NodeIdGenerator<S>,
194    signer: &A,
195) -> Result<WorktreePatchCommitReport>
196where
197    S: crate::node_id_gen::NodeIdEntropySource,
198    A: AuthorSigner,
199{
200    node_authoring::author_worktree_patch(layout, ref_name, message, options, generator, signer)
201}
202
203/// Assign a contiguous `op_seq` (1-based) for the operation at `index`. Used by node-addressed
204/// worktree authoring.
205pub(crate) fn next_op_seq(index: usize) -> Result<u32> {
206    let next = index
207        .checked_add(1)
208        .ok_or_else(|| PrikkError::CanonicalEncoding("operation count overflow".to_string()))?;
209    u32::try_from(next)
210        .map_err(|_| PrikkError::CanonicalEncoding("operation count exceeds u32".to_string()))
211}
212
213#[cfg(test)]
214mod tests;