Prikk Documentation
Prikk is a design-first experimental VCS. This documentation is intentionally short in the early implementation phase and will grow as FDD-approved implementation areas land.
About the name. Prikk is the Norwegian word for dot. It was chosen because it opens with the p of patches, and because a patch history is exactly that: dots — patches as nodes — connected into a DAG.
For the current architecture and trust boundaries, start with:
Security and Signing Setup
This guide describes the current operator setup for Prikk signing and repository-local maintainer trust. For the full security model, see the trust and threat model. For verification diagnostics after setup, see integrity and recovery diagnostics. For the physical trust-store paths, see repository layout and authority.
Core Caveats
- Prikk is early implementation software and is not a production Git replacement.
- Current key input is environment-variable based and intentionally minimal.
- Seeds are secret key material. Prikk does not store, encrypt, rotate, revoke, expire, back up, or generate private keys.
- Prikk currently has no key-generation command and no command that derives a public key from a seed.
- Operators must obtain matched Ed25519 seed and public-key material with external tooling.
- Maintainer trust is repository-local, held as a set of adopted MAINTAINER keys with
required = 1(any one adopted key’s signature suffices), and enforces trust-on-first-use per key id. - AUTHOR signatures are real Ed25519 signatures, but Prikk does not currently enforce a repository-wide AUTHOR trust policy.
- MAINTAINER key revocation exists (
prikk trust maintainer remove); there is no key rotation, hardware signing, remote trust, sync trust, hosted identity, multi-maintainer threshold policy, or stable migration policy yet.
Current Signing Roles
Prikk currently uses role-bound Ed25519 signatures.
AUTHOR signing is used for Patch envelopes produced by commit and rollback-draft authoring paths. The AUTHOR signature identifies the key used by the authoring path, but it is not checked against a repository-wide AUTHOR trust store.
MAINTAINER signing is used for publication objects. Seal signs Block, RefState, and RefUpdate envelopes with the configured MAINTAINER signer and verifies that signer against the repository-local maintainer trust policy before publishing.
The signature preimage binds the signature algorithm, object type, object id, signer role, and key id.
Current Key Inputs
The CLI reads AUTHOR key material from:
PRIKK_AUTHOR_KEY_IDPRIKK_AUTHOR_SEED
The CLI reads MAINTAINER key material from:
PRIKK_MAINTAINER_KEY_IDPRIKK_MAINTAINER_SEED
Each seed value is a caller-supplied 32-byte Ed25519 secret seed encoded as 64 hex characters. Missing variables, empty key ids, wrong-length seed hex, and non-hex seed bytes fail closed before signing.
Prikk does not currently derive the MAINTAINER public key from PRIKK_MAINTAINER_SEED. The operator
must provide the matching public key separately when configuring repository-local maintainer trust.
Maintainer Trust Store Setup
The current commands for repository-local MAINTAINER trust are:
prikk trust maintainer add --key-id ID --public-key HEX
prikk trust maintainer remove --key-id ID
ID must match the MAINTAINER key id used by PRIKK_MAINTAINER_KEY_ID. HEX must be the lowercase
64-hex-character Ed25519 public key that matches PRIKK_MAINTAINER_SEED.
add writes the trusted public key and adds it to the repository’s adopted-key set, with required = 1
continuing to mean any one adopted key’s signature suffices. Adopting a key id already in the set with
the same public key succeeds idempotently; adopting it again with a different public key is refused.
This refusal is Prikk’s trust-on-first-use enforcement: the first public key seen for a key id is the
one trusted for that id, permanently, even after removal — remove takes a key id out of the adopted
set, but re-adding the same id later with a different public key is still refused. There is still no
remote trust distribution.
Minimal Local Workflow
Use placeholders for seed and key values in documentation, scripts, and notes. Before running the
workflow below, populate the local shell variables AUTHOR_SECRET_SEED_64_HEX,
MAINTAINER_SECRET_SEED_64_HEX, and MAINTAINER_PUBLIC_KEY_64_HEX with key material generated and
handled outside Prikk.
prikk init ./sample-repo
export PRIKK_AUTHOR_KEY_ID="author-key-id"
export PRIKK_AUTHOR_SEED="$AUTHOR_SECRET_SEED_64_HEX"
export PRIKK_MAINTAINER_KEY_ID="maintainer-key-id"
export PRIKK_MAINTAINER_SEED="$MAINTAINER_SECRET_SEED_64_HEX"
(cd ./sample-repo && prikk trust maintainer add \
--key-id "$PRIKK_MAINTAINER_KEY_ID" \
--public-key "$MAINTAINER_PUBLIC_KEY_64_HEX")
echo "hello prikk" > ./sample-repo/readme.txt
(cd ./sample-repo && prikk commit -m "genesis")
(cd ./sample-repo && prikk seal --allow-no-audit)
(cd ./sample-repo && prikk verify)
The MAINTAINER seed and public key above must be matched private/public halves of one Ed25519 keypair. If they do not match, seal fails because the configured signer is not trusted by the repository-local policy.
Seed Handling Warnings
Any seed or key values published in Prikk’s README, quick start, docs, tests, review packages, or issue comments are public examples. They are compromised by publication and must never be used for real signing.
Do not commit real seeds, paste them into issues, store them in shell history, print them in CI logs, or put them in release artifacts. Prikk does not currently provide a secret-storage boundary; the operator owns secret generation, storage, backup, rotation, and destruction outside Prikk.
Failure and Diagnostic Hints
Missing PRIKK_AUTHOR_KEY_ID or PRIKK_AUTHOR_SEED prevents commands that need AUTHOR signing from
creating signed Patch envelopes.
Missing PRIKK_MAINTAINER_KEY_ID or PRIKK_MAINTAINER_SEED prevents seal from creating signed
publication objects.
Malformed seed hex is rejected before signing. Empty key ids and unsafe key ids are rejected by shared signature validation.
An untrusted MAINTAINER signer prevents seal from publishing. A repository with publication objects
that do not verify against the local trust policy reports publication-trust issues through verify and
doctor.
The current CLI wording is human diagnostic output, not a stable machine-readable key-management contract.
Deferred Work
Still deferred: key-generation commands, public-key derivation commands, local secret storage, keychain integration, passphrase handling, key rotation, key expiration, compromise recovery, hardware signing, multi-maintainer thresholds, repository-wide AUTHOR trust policy (including AUTHOR-identity revocation — only MAINTAINER key revocation is supported), remote trust, hosted identity, JSON key-management output, stable trust-policy migration, stable repository-format migration, and production readiness.
Claim-to-Source Anchors
| Claim | Source anchors |
|---|---|
| AUTHOR and MAINTAINER production signing use real Ed25519 signatures. | author_signing.rs, maintainer_signing.rs, DC-10, DC-11 |
| Signature preimages bind algorithm, object type, object id, signer role, and key id. | signature.rs, author_signing.rs, maintainer_signing.rs |
| The CLI reads AUTHOR and MAINTAINER key material from environment variables and expects 64-hex secret seeds. | main.rs, author_signing.rs, maintainer_signing.rs |
Prikk currently exposes trust maintainer add but no key-generation or public-key-derivation command. | main.rs, help.rs, DC-30 |
The maintainer trust store is repository-local and fixed to one MAINTAINER key with required = 1. | trust.rs, DC-11, trust and threat model |
| Seal verifies the configured MAINTAINER signer against local trust before publication. | seal.rs, trust.rs |
| Verify checks publication trust for Block, RefState, and RefUpdate objects against local MAINTAINER trust. | verify.rs, trust.rs, integrity and recovery diagnostics |
| Current AUTHOR signatures are not checked against a repository-wide AUTHOR trust policy. | verify.rs, rollback_verify.rs, trust and threat model |
Provenance
This guide implements DC-30. It is documentation-only and does not change signing, trust, CLI, object schema, repository format, verification, seal, or repository behavior.
History Inspection
PR-014 added a small read-only history view for early sealed repositories. PR-030 extends that view with rollback block classification.
prikk log [path] [--limit N] [--ref REF]
The command follows the current RefState chain from newest to oldest and validates that each entry targets a persisted Block object.
For each entry, the CLI reports:
- the target Block ID;
- RefState ID and update sequence;
- Block kind;
- parent and Patch counts;
- rollback block classification;
- rollback-marked Patch count;
- required attestation count;
- previous RefState ID.
A Block is classified as a rollback Block when it references at least one Patch envelope carrying the current development rollback marker and that Patch payload decodes under the supported replay subset.
History inspection does not yet perform full block-DAG traversal, path-aware history queries, rollback authorization checks, or patch algebra.
Worktree Status
PR-018 adds read-only worktree status against snapshot-backed baselines.
prikk worktree-status [path] [--ref REF]
The command compares the current worktree with the snapshot manifest referenced by the selected ref.
It reports missing, modified, untracked, and unsupported paths.
It does not write the worktree. Patch generation is handled separately by prikk commit --from-worktree.
The scanner is intentionally conservative:
.prikk/metadata is ignored;- existing path-safety validation is reused;
- non-ASCII paths remain unsupported until Unicode NFC normalization is implemented;
- no writes are performed.
For the exact repository path validator rules, see path and worktree safety.
Checkout Planning
PR-017 includes a read-only checkout plan:
prikk checkout --plan-only [path] [--ref heads/main]
The command validates the current RefState target and reports whether checkout would need snapshot materialization or patch application.
For snapshot-backed blocks, first validate the snapshot manifest:
prikk checkout --snapshot-plan [path] [--ref heads/main]
Then explicitly materialize validated snapshot files:
prikk checkout --snapshot-materialize [path] [--ref heads/main]
Snapshot materialization writes only validated regular files. Supported patch replay and
materialization are available through --patch-plan, --patch-materialize, and
--patch-materialize-delete, but full patch algebra remains deferred. For the shared path and
worktree safety boundary, see the path and worktree safety reference.
Snapshot Checkout Planning
PR-017 keeps a read-only snapshot checkout planning path.
The command:
prikk checkout --snapshot-plan [path] [--ref REF]
validates the current ref, target block, snapshot Blob object, and snapshot manifest paths. It does not write the worktree.
The current path-safety scaffold is deliberately conservative. It rejects:
- absolute paths
.and..components- empty components
- backslashes and colon characters
- control characters
- Windows reserved names such as
CON,NUL,COM1, andLPT1 - non-ASCII paths until Unicode NFC normalization is implemented
- duplicate paths and case-insensitive collisions
Use prikk checkout --snapshot-materialize to write validated snapshot files. Supported patch replay
is available separately, while full patch algebra remains deferred. For exact validator rules, see
path and worktree safety.
Snapshot Materialization
PR-017 adds an explicit, opt-in snapshot materialization path:
prikk checkout --snapshot-materialize [path] [--ref REF]
The command writes files only from a validated snapshot manifest. It does not apply patch algebra, does not remove extra files, and refuses to overwrite existing files with different content. It also refuses symlinked parent directories and symlink targets so snapshot checkout cannot be used to write outside the repository worktree.
The path validator remains conservative: non-ASCII paths are deferred until Unicode NFC
normalization is implemented, and paths targeting .prikk/ are rejected. For the exact validator and
write-safety caveats, see path and worktree safety.
Worktree Patch Authoring
prikk commit authors the current worktree into a node-addressed patch and appends it to the active
WAL. The patch carries a real role-bound Ed25519 AUTHOR signature.
# Key material is supplied via the environment (a minimal key-input mechanism, not a trust store):
export PRIKK_AUTHOR_KEY_ID="dev-author"
export PRIKK_AUTHOR_SEED="<64 hex chars>"
prikk commit -m "record changes"
# --text-edits is accepted for compatibility; text nodes author EditText either way:
prikk commit --text-edits -m "record text changes"
# Explicit unborn local branch genesis:
prikk commit --ref heads/topic -m "start topic"
--from-worktree is still accepted for backward compatibility but is now the only behavior, so it can
be omitted.
Baseline
Authoring compares the worktree against a baseline node lifecycle state:
- Published ref: the baseline is reconstructed from authoritative node-addressed replay of the
heads/main(or--ref) lineage — never from a snapshot manifest. - Genesis (fresh local branch): when a valid
heads/*target ref has never been published, the first commit authors against an empty baseline, so every worktree file becomes aCreateFile. The followingseal --ref heads/<branch>publishes the first block as a Root block.
commit --ref heads/topic on an unborn ref creates an independent Root history from the current
worktree. It does not copy/fork heads/main, switch the checkout branch, or create a merge base.
The active WAL is single-commit for this stage. A second commit before seal fails closed, and the active
WAL records the target ref so seal --ref heads/main cannot publish a patch authored for
heads/topic.
Operation mapping
Existing-node kind is authoritative:
- new file →
CreateFile(fresh CSPRNG-mintednode_id, normalized mode) - removed tracked file →
DeleteNode - modified text file → deterministic arbitrary-span
EditText - modified binary file →
ReplaceBinary - permission-only change →
ChangePerm
Path handling is strict: non-UTF-8 worktree paths fail closed, and traversal/reserved-name/collision rules apply as elsewhere. For the exact repository path and worktree scanner boundary, see path and worktree safety.
Out of scope (this stage)
- symlink authoring (fails closed on all symlinks)
- text↔binary kind transitions (fail closed)
- rename detection (a move is a
DeleteNode+CreateFile) - branch switching or branch copy/fork from an existing tip
- multi-commit queued active sessions or per-ref active WALs
- multi-operation text diff minimization, commutation, conflict witnesses
Signature scope: worktree commits are role-bound Ed25519 AUTHOR-signed. This does not imply
trust-store enforcement, key management, MAINTAINER/publication signing, rollback authorization, or
publication-grade repository trust.
Content-Anchored Text Edits
DC-12 supports deterministic arbitrary-span EditText generation and replay for existing text-file
nodes. A modified text file is represented as one enclosing span selected by byte LCP/LCS and widened
to UTF-8 character boundaries. The record remains the FDD-03 node-addressed, span-anchored EditText
shape; no offsets or new identity fields are added.
Author text edits from worktree changes with:
prikk commit --from-worktree --text-edits -m "record text changes"
--text-edits is retained for compatibility. Existing-node kind is authoritative, so text-file
modifications author EditText and binary-file modifications author ReplaceBinary.
Generation and replay remain conservative:
- Only modified tracked files are candidates.
- Both baseline and current bytes must be valid UTF-8.
- Text edits use a single deterministic enclosing span; multi-operation diff minimization is deferred.
- Byte-level differences that split a multibyte character are widened to the enclosing UTF-8 character.
- Binary or invalid UTF-8 modifications fail closed for text nodes; they do not become
ReplaceBinary. - Replay localizes by
old_span_text, left/right anchor hashes, andspan_id, then splices exactly.
This deliberately avoids byte offsets and line offsets. Presentation offsets may be derived later, but they are not part of patch identity or replay preconditions.
Current validation rules:
old_span_hashis exactly 32 bytes.old_span_hashmust equaltext_span_hash(old_span_text).old_span_textandreplacement_textmust be well-formed UTF-8.- The target
node_idmust name a liveTextFileduring replay.
Deferred work:
- multi-operation text diff minimization
- direct inverse and rollback extension for arbitrary spans
- commutation and conflict witnesses
Supported Patch Replay Planning
Read-only patch replay handles the current conservative operation subset, including DC-12 arbitrary-span text edits.
The command is:
prikk checkout --patch-plan [path] [--ref REF]
It walks the single-parent block chain from oldest to newest, loads any snapshot Blob attached to a block, and applies supported Patch operations in block patch order.
Supported operations:
CreateFileDeleteFileEditTextfor deterministic content-anchored arbitrary spansReplaceBinary(DC-73)ChangePerm(DC-73)
Unsupported operations still fail the plan clearly:
RenamePath— not a node-model gap:commitnever authors it, renames become delete+createCreateSymlink— not a node-model gap: symlink authoring is refused outright- merge/conflict algebra
This command does not write the worktree. It only proves that the current sealed history can be replayed into an in-memory snapshot manifest using the operation subset implemented so far.
Supported Patch Materialization
PR-021 added an explicit materialization command for the supported patch replay result:
prikk checkout --patch-materialize [path] [--ref REF]
This command reuses the read-only replay support from checkout --patch-plan and writes the
resulting file manifest into the worktree through a conservative, mode-aware materializer. It is a
separate code path from snapshot checkout’s materializer (DC-73): the replayed manifest carries mode
bits derived from CreateFile/ChangePerm history, which the snapshot-blob wire format does not
encode, so the two are deliberately not unified.
PR-022 adds deletion-aware materialization as a separate opt-in command:
prikk checkout --patch-materialize-delete [path] [--ref REF]
Supported operation subset:
CreateFileDeleteFile- deterministic arbitrary-span
EditText ReplaceBinary(DC-73)ChangePerm(DC-73) — the mode bit is written, not only the content
Safety boundaries:
- Existing files with identical bytes and mode are left unchanged.
- Existing files with different bytes are refused.
- Existing files with identical bytes but a different mode have their mode corrected in place.
--patch-materializenever deletes files.--patch-materialize-deletedeletes only files explicitly removed by replayedDeleteFileoperations.- Deletion is refused unless the current worktree bytes still match the old Blob precondition.
- Extra untracked files are never deleted.
- Symlinked parents, symlink targets, non-file targets, and
.prikk/metadata paths remain refused. - Renames, symlinks, merge conflicts, and full patch algebra remain later increments — not a node-model
gap for either:
commitnever authorsRenamePath(renames become delete+create) orCreateSymlink(refused outright), so there is nothing in ordinary history for materialization to act on (DC-73).
This command is useful for exercising the current Prikk object/WAL/ref/block pipeline end-to-end, but it is not yet a complete checkout implementation. For the shared write-safety boundary and its race caveats, see path and worktree safety.
Supported Patch Deletions
PR-022 adds an explicit deletion plan for the supported patch replay result:
prikk checkout --patch-delete-plan [path] [--ref REF]
It also adds an opt-in materialization mode that removes eligible files:
prikk checkout --patch-materialize-delete [path] [--ref REF]
Deletion is intentionally narrow. Prikk deletes only files that the replayed patch chain removed
with a DeleteFile operation. Before removing a worktree file, Prikk checks that the current file
bytes still match the operation’s old Blob precondition.
Safety boundaries:
- Arbitrary untracked files are never deleted.
- Modified deleted files are refused.
- Symlink targets and non-file targets are refused.
- General checkout pruning remains deferred.
- Text edits, renames, chmod, symlinks, merge conflicts, inverse logic, and full patch algebra remain later increments.
For exact deletion and materialization safety boundaries, see path and worktree safety.
Supported Patch Inverse Planning
PR-026 adds read-only inverse planning for the supported patch-operation subset.
prikk inverse-plan [path] [--ref REF]
The command walks the same single-parent sealed block chain used by supported patch replay. While validating and replaying the chain, Prikk derives an unsigned inverse Patch payload in reverse application order.
Supported inverse shapes in PR-026:
CreateFile→ inverseDeleteFileDeleteFile→ inverseCreateFile
Safety boundaries:
- The command is read-only.
- The inverse Patch is not written to the object store.
- The reported inverse Patch ID is only an unsigned deterministic planning hint.
EditTextdirect inverse for arbitrary spans remains deferred until the required round-trip vectors land.- Rollback refs, authorization policy, conflict witnesses, commutation, and confluence remain later increments.
Rollback Preview
PR-027 adds a non-mutating rollback preview for the supported patch-operation subset.
The command is:
prikk rollback-preview [path] [--ref REF]
The preview performs two read-only validations:
- derive the unsigned inverse Patch payload for the supported operation subset;
- replay the current target state from the supported single-parent block chain.
It then compares the current replayed state with the latest snapshot baseline in that replay window. The result is a file-level preview of what rollback would need to create, delete, or replace.
No repository state is changed. The command does not write objects, append WAL records, publish refs, or modify the worktree.
Supported operation subset:
CreateFileDeleteFile- deterministic arbitrary-span
EditText ReplaceBinary(DC-73)ChangePerm(DC-73)
Deferred:
- mutating rollback commands
- rollback ref publication policy
- authorization and audit policy for rollback
- commutation, confluence, and conflict witnesses
- plugin execution and remote sync
Rollback Draft
prikk rollback-draft appends an explicit rollback-draft Patch for the supported patch-operation subset.
The command is:
prikk rollback-draft --append-inverse [path] [--ref REF] -m "rollback message"
The command performs the same supported inverse validation used by inverse-plan and
rollback-preview, marks the inverse Patch payload as PatchPurpose::RollbackDraft, signs it with a
real role-bound Ed25519 AUTHOR signature, then appends the Patch envelope to the active WAL. Key material
uses the same environment variables as prikk commit: PRIKK_AUTHOR_KEY_ID and PRIKK_AUTHOR_SEED.
Safety rules:
--append-inverseis required.-m <message>is required and must not be empty.- the target ref must be published and must resolve to a supported single-parent block chain.
- the supported replay/inverse subset must validate successfully.
- the active WAL must be empty.
- the active WAL must not contain a trailing partial record.
What it mutates:
- appends one AUTHOR-signed rollback-draft Patch envelope to
.prikk/active/default/queue.wal.
What it does not mutate:
- it does not write object files directly.
- it does not publish refs.
- it does not modify the worktree.
- it does not authorize rollback by policy.
PR-029 adds a pre-seal verification command:
prikk rollback-draft-verify [path] [--ref REF]
After reviewing and verifying the draft, the existing local seal scaffold can publish it:
prikk seal --allow-no-audit
The seal path is unchanged, but prikk log and prikk verify classify the sealed Block as a rollback
Block when it contains Patch objects with PatchPurpose::RollbackDraft.
Supported inverse operation subset:
CreateFile->DeleteFileDeleteFile->CreateFile- deterministic arbitrary-span
EditText->EditText ReplaceBinary->ReplaceBinary(old/new blob swapped; DC-73)ChangePerm->ChangePerm(old/new mode swapped; DC-73)
Deferred:
RenamePathandCreateSymlinkinverse — not a node-model gap, an authoring one:commitnever producesRenamePath(renames become delete+create) and symlink authoring is refused outright, so there is nothing in ordinary history for either inverse to act on (DC-73)- rollback-specific ref publication policy
- authorization and audit policy for rollback
- rollback branch/reflog semantics
- worktree rollback materialization policy
- commutation, confluence, and conflict witnesses
- plugin execution and remote sync
Rollback Draft Verification
prikk rollback-draft-verify is a non-mutating verification step for rollback drafts that are waiting in
the active WAL.
prikk rollback-draft-verify [path] [--ref REF]
The command verifies that:
- the active WAL has no trailing partial record;
- the active WAL contains exactly one record;
- the record is a Patch envelope;
- the Patch payload carries
PatchPurpose::RollbackDraft; - the Patch carries an AUTHOR signature and reports its real key id;
- the Patch payload decodes under the currently supported replay subset;
- the Patch payload exactly matches the inverse Patch currently derived from the selected ref.
This makes the rollback draft path easier to audit before seal --allow-no-audit publishes the active WAL
into a block.
Repository verification integration
prikk verify also counts active WAL records classified as rollback drafts. For those records, verification decodes the Patch payload purpose and the supported replay subset. This check is intentionally weaker than rollback-draft-verify because repository-level verification has no selected ref target.
prikk verify also counts sealed rollback Blocks and sealed rollback Patch references after a rollback draft has been sealed through the existing seal path.
The broader repository verification and doctor diagnostic boundary is described in the
integrity and recovery diagnostics reference.
Current limits
Rollback draft verification still does not implement:
- rollback-specific ref publication;
- rollback authorization policy;
- audit-plugin approval;
- rollback worktree mutation;
- arbitrary-span text rollback;
- commutation, confluence, or conflict witnesses.
Sealed Rollback History
Prikk provides read-only classification for rollback Patch objects after they are sealed into a normal Block by the existing seal path.
Rollback identity lives in the Patch payload as PatchPurpose::RollbackDraft; it is not encoded in an
AUTHOR key id. When a sealed Block references such a Patch, Prikk verifies that the Patch payload decodes
under the supported replay subset and then classifies the Block as a rollback Block for history and
verification output.
Pre-DC-10 rollback drafts that used the old development key-id marker are pre-stability artifacts. Current classification does not recognize that marker in production logic.
CLI
prikk log [path] [--ref REF]
prikk verify [path]
prikk log reports, per history entry:
rollback-block: true|false
rollback-patches: N
prikk verify reports repository-wide counts:
checked rollback blocks: N
checked sealed rollback patches: N
checked rollback draft WAL records: N
The sealed counts cover persisted Blocks and Patch objects. The active draft count covers the active WAL before seal.
Scope
PR-030 is intentionally classification-only. It does not introduce rollback-specific refs, rollback authorization, rollback worktree writes, or rollback-specific seal semantics.
Deferred work remains:
- rollback-specific ref policy
- rollback authorization and audit policy
- worktree rollback writes
- arbitrary-span text rollback
- commutation / confluence / conflict witnesses
- audit plugins and sync
Merge Evidence
DC-22 (0.15.0) adds prikk merge-evidence, the first public read-only UX over the DC-21
merge/conflict evidence vocabulary. DC-23 (0.16.0) stabilizes its text output.
For the current concepts behind operation ordering, commutation, confluence, evidence outcomes, reason codes, and proof phases, see Patch Algebra and Merge Evidence.
For the planning classification layer over the same explicit-input evidence, see Merge Plan. Neither command executes a merge — for that, see Merge (DC-74), which reuses this same evidence to decide whether to seal.
prikk merge-evidence \
--baseline-block BLOCK \
(--left-block BLOCK | --left-ref REF) \
(--right-block BLOCK | --right-ref REF) \
[path]
The command derives the single-parent candidate sequences from an explicit sealed baseline to two explicit left/right targets, runs the read-only merge/conflict evidence analysis, and prints the resulting report.
Selector rules:
--baseline-blockis required and names the sealed baseline block.- Each side must choose exactly one selector:
--left-blockor--left-ref, and--right-blockor--right-ref. - A
--left-ref/--right-refvalue may name a received ref (remotes/<name>, DC-85), previewing evidence against imported history exactly as it would for a local branch. This is read-only, so a preview can name aremotes/ref on either side even thoughprikk mergeitself never accepts one as--into— the preview does not claim the plan it shows is executable as-is. - The optional positional argument is the repository root, as with other commands. It is not a path filter.
The command is read-only. It does not infer merge bases, execute merges, publish merge commits, write refs or WAL records, materialize worktree conflicts, or persist proof/witness objects.
Output
Output is text-only and intended for human diagnostics. It is not a durable machine-readable schema. DC-23 makes the shape easier to scan:
merge evidence
baseline block: <block-id>
left selector: ref heads/topic-a
left target block: <block-id>
left operations: 3
right selector: block <block-id>
right target block: <block-id>
right operations: 2
outcome: Conflict
reason: pair_conflict
items: 1 displayed of 1
cross:
left[0] op_seq=1 ChangePerm src/lib.rs
right[0] op_seq=1 ChangePerm src/lib.rs
outcome: Conflict
reason: pair_conflict
phase: classification
note: read-only evidence; no merge commit, ref update, WAL write, or worktree change was performed
Reading the output:
- both sides show the submitted selector text and the resolved target block identity;
- left and right operation counts are shown separately;
- the full-report
outcomeandreasonare shown before item details, and are computed over the full candidate sequences; items: N displayed of Nreports the displayed and total item counts (equal in this release; DC-23 adds no display filtering);- cross-side items render as a
cross:block with separateleft[...]andright[...]operation lines, rather than an ambiguous one-line form; - report-level items render as
report:without a fake operation label; - DC-21 outcome and reason-code names are preserved exactly.
Privacy: the output never includes raw text spans, replacement text, blob bytes, absolute host paths,
.prikk private paths, signer secrets, key material, or arbitrary object debug dumps. Displayed paths
are repository-relative.
Exit Status
| Condition | Exit |
|---|---|
| Valid request and a DC-21 evidence report was produced, for any outcome | 0 |
| Invalid CLI arguments, missing selectors, or ambiguous selectors | 1 |
| Selector, ancestry, object, or ref failure prevents building the report | 1 |
| Unexpected internal error | 1 |
Process success is independent of the evidence outcome: a produced report exits 0 even when the
outcome is Conflict.
Deferred
prikk merge (DC-74) executes confluent merges — see the merge guide; this command
stays read-only regardless. Still deferred:
- conflict resolution;
- automatic merge-base discovery;
- branch merge semantics beyond a two-sided confluent merge;
- display-path filtering and scoped/path-limited merge analysis;
- persisted proof/witness/merge-evidence objects;
- JSON or other machine-readable output;
- public
prikk-replayAPI stabilization.
Merge Plan
DC-25 (0.17.0) adds prikk merge-plan, a read-only planning classification over the
existing merge evidence report. It answers what Prikk can say about the selected
explicit inputs today; it does not execute or prepare a merge commit.
For the current concepts behind evidence outcomes, reason codes, proof phases, and the
ConfluentSubset mapping, see
Patch Algebra and Merge Evidence.
prikk merge-plan \
--baseline-block BLOCK \
(--left-block BLOCK | --left-ref REF) \
(--right-block BLOCK | --right-ref REF) \
[path]
Selector rules:
--baseline-blockis required and names the sealed baseline block.- Each side must choose exactly one selector:
--left-blockor--left-ref, and--right-blockor--right-ref. - A ref selector resolves through the current local branch target block, or, for a
remotes/<name>value (DC-85), through the received ref it names — read-only, so either side may preview a received ref even thoughprikk mergeitself never accepts one as--into. - The optional positional argument is the repository root. It is not a path filter.
The command is read-only. It does not infer merge bases, execute merges, publish merge commits, write objects, refs or WAL records, materialize worktree conflicts, or persist proof/witness/plan objects.
Output
Output is text-only and intended for human planning diagnostics. It is not a durable machine-readable schema.
merge plan
baseline block: <block-id>
left selector: ref heads/topic-a
left target block: <block-id>
left operations: 3
right selector: block <block-id>
right target block: <block-id>
right operations: 2
status: BlockedConflict
evidence outcome: Conflict
reason: pair_conflict
action: inspect evidence; conflict resolution is not implemented
items: 1 displayed of 1
cross:
left[0] op_seq=1 ChangePerm src/lib.rs
right[0] op_seq=1 ChangePerm src/lib.rs
outcome: Conflict
reason: pair_conflict
phase: classification
note: read-only plan; no merge commit, ref update, WAL write, object write, or worktree change was performed
Plan status maps the underlying evidence outcome to a non-executable classification:
| Evidence outcome | Plan status |
|---|---|
Confluent | ConfluentSubset |
Conflict | BlockedConflict |
OrderedDependency | BlockedOrderedDependency |
Unsupported | BlockedUnsupported |
Deferred | BlockedDeferred |
NotConfluent | BlockedNotConfluent |
EvidenceFailure | BlockedEvidenceFailure |
InvalidCandidate | BlockedInvalidCandidate |
ConfluentSubset means the selected candidates are proven confluent only for the currently supported
operation subset. It is not a whole-merge guarantee and does not mean Prikk can create a merge commit.
Exit Status
| Condition | Exit |
|---|---|
| Valid request and a merge plan was produced, for any plan status | 0 |
| Invalid CLI arguments, missing selectors, or ambiguous selectors | 1 |
| Selector, ancestry, object, or ref failure prevents identifying the requested inputs | 1 |
| Unexpected internal error | 1 |
Process success is independent of plan status: a produced BlockedConflict plan exits 0.
Deferred
- Conflict resolution —
prikk merge(DC-74) executes confluent merges, but detection only, same as this command; - automatic merge-base discovery;
- multi-parent blocks and the structural merge record they would provide (DC-75, proposed);
- active-WAL merge drafts and worktree conflict materialization;
- display-path filtering and scoped/path-limited merge analysis;
- persisted proof/witness/merge-plan objects;
- JSON or other machine-readable output;
- public
prikk-replayAPI stabilization.
Merge
DC-74 adds prikk merge, the first command that executes a merge rather than only reporting on one.
It builds on the same read-only evidence merge evidence and
merge plan already report — see those pages, and
Patch Algebra and Merge Evidence, for the underlying confluence
concepts.
prikk merge --allow-no-audit \
--baseline-block BLOCK \
--into REF \
--from REF \
[path]
--baseline-blockis required and names the sealed baseline block confluence is proven against.--intois the ref the merge advances. It must currently be published and must be the branch the caller has maintainer signing authority to seal.--fromis the ref merged in. Its patches since the baseline are what get adopted. It may be a local branch, or a received ref (remotes/<name>, DC-85) imported byprikk bundle import— see Merging from a received ref below.--allow-no-auditis required, matchingseal’s own flag: this command signs and publishes new sealed history, and audit plugins are not implemented.- The optional positional argument is the repository root.
What a merge does — and does not do
A merge authors nothing. --from’s patches since the baseline are adopted verbatim: the
exact same canonical bytes, the exact same ObjectId, the exact same author signature as when they
were originally sealed. prikk merge never decodes, re-derives, or re-signs a patch. Only the new
Block, RefState, and RefUpdate are signed — with the maintainer key, exactly as an ordinary
seal signs them.
The two sides must be proven confluent from the given baseline — the same evidence
merge-evidence/merge-plan already compute, reused rather than duplicated. Any outcome other than
Confluent (Conflict, Deferred, NotConfluent, Unsupported, OrderedDependency,
EvidenceFailure, InvalidCandidate) refuses the merge. Refusal writes nothing: no object, WAL
record, or ref update of any kind is created until confluence is confirmed, so a refused merge leaves
--into exactly where it was.
What gets recorded
Merge blocks are BlockKind::Merge, naming both parents (DC-75). parent_block_ids holds
--into’s prior tip and --from’s adopted tip, sorted per the format’s uniqueness invariant. A
separate mainline_parent_id field names which one is --into’s side, since sorted order carries no
positional meaning. State derivation and replay follow the mainline parent only — the same shape as
an ordinary single-parent block — while the secondary parent’s own chain is verified independently by
the ordinary full-object-store scan every other block already gets.
The baseline is recorded, and independently re-derived. merge_baseline_block_id states what
--baseline-block was at seal time — a claim, not a trust boundary: ordinary verify computes the
true merge base itself (a full-parent reachability walk) and reports disagreement if the recorded
baseline is not it. Authorship is unaffected (the adopted patches still carry their original author’s
signature).
This discharges the release condition DC-74 attached to this command: sealed history now structurally records a merge, re-checkable by a later verifier from sealed history alone.
Merging from a received ref
--from accepts remotes/<name> — a ref imported by prikk bundle import --input FILE (produced on
the other side by prikk bundle export --ref REF --output FILE) — exactly as it accepts a local
branch. --into never does: it must always be a genuine local branch, since publishing a ref only ever
writes the local ref store.
Adopting content from a received ref requires the maintainer key that sealed it to already be
trusted here. Received content arrives via import_bundle with no trust check at all — deliberate,
per DC-78 Stage 3: importing is not trusting. A local-to-local merge needs no equivalent check, because
every block reachable from a local ref was itself created through this repository’s own seal/merge
path, each already gated by trust at creation. A received ref’s blocks were never gated on the way in,
so prikk merge checks them itself, before --into advances: every block it would adopt must carry a
signature from a currently-adopted maintainer key, or the merge is refused with no trusted MAINTAINER signature and writes nothing.
If you meet that refusal, do not treat it as an error to clear. Running prikk trust maintainer add
for whatever key the bundle happened to carry is exactly the decision this check exists to make you
take deliberately, not by reflex. Trusting a maintainer key means trusting every block that key has
ever sealed or ever will — confirm you mean to extend that trust to this specific origin before adding
it, the same judgment call trust maintainer add already asks of a purely local setup.
Conflicts
Detection only. A resolution is itself a signed patch — a trust question prikk merge does not
decide. patch_algebra’s conservative subset (DC-16, DC-18) governs what can be proven confluent at
all; if it is too narrow to merge something that should be mergeable, that is its own finding, not
something this command works around.
Compatibility
verify, doctor, rollback-preview, and DC-64’s incremental lifecycle cache all continue to work
against a repository containing a merged block — tested, not argued
(crates/prikk-cli/tests/dc74_merge_execution.rs).
Deferred
- Automatic merge-base discovery —
--baseline-blockstays explicit. - Conflict arbitration / resolution.
- Widening
patch_algebra’s conservative subset. - Merging more than two sides in one command.
- Populating
PatchPayload.parent_patch_ids— no construction site sets it; the patch DAG it implies is a different structure than the block-parentage DC-75 records, answering a different question.
Sync
RFC 116 adds prikk sync: negotiation-as-artifacts between two repositories, with no network code
in prikk itself. RFC 117 stage 3 extends the same artifact to carry tags, adopted separately from
sealing. See Security and Signing Setup for the maintainer key both sides need
before sealing or adopting anything sync brings in.
prikk sync summary --output <file>
prikk sync compare --summary <file>
prikk sync have <ref> --output <file>
prikk sync build <ref> --have <file> --output <file>
prikk sync accept <file> [--claims-out <file>]
prikk sync pending
prikk sync seal <ref> --claim <id>
prikk sync seal <ref> --claims <file>
prikk sync tags
prikk sync adopt-tag <name>
The loop, as a person actually runs it
Two repositories, A (has content B wants) and B (wants it), moving files by whatever means the two operators already have — email, a shared drive, a USB stick. Every step below names the file the previous step produced.
A: prikk sync summary --output summary.bin
B: prikk sync compare --summary summary.bin
B: prikk sync have <ref> --output have.bin
A: prikk sync build <ref> --have have.bin --output artifact.bin
B: prikk sync accept artifact.bin --claims-out claims.txt
B: prikk sync pending # optional, observational
B: prikk sync seal <ref> --claims claims.txt
B: prikk sync tags # optional, observational
B: prikk sync adopt-tag <name> # per tag, if any arrived
summary publishes every heads/* ref A holds, each with its own patch-set digest and count —
a few hundred bytes regardless of history size. compare reads a summary against B’s own refs
and reports each as in-sync, differs, remote-only (B lacks it), or local-only (A lacks it).
have is B’s own reachable patch-id list for one ref B wants — the input build needs to
compute exactly what B is missing. build is A’s side of that computation: it writes an
artifact carrying the delta, one recognition claim per block the delta touches, and every local tag
whose target lies within the ref’s ancestry. accept verifies and stores everything the
artifact carries — patches, blobs, claims, tags — and reports what it found; it does not seal or
adopt anything by itself. pending lists patches accept has stored that are not yet reachable
from any of B’s own blocks. seal takes the claim ids accept wrote out and turns the accepted
patches into B’s own sealed blocks, under B’s own maintainer key. tags lists every tag B has
received but not adopted, with its current signature outcome and whether B’s own history can resolve
it yet. adopt-tag creates B’s own local tag for one received tag, once B holds the same patch
set locally.
What it does — and does not do
prikk does not move the bytes, and does not encrypt them. A file sync build writes contains
repository content in the clear. prikk guarantees integrity and authenticity — every object is
content-addressed and every claim, tag, and sealed publication is signed — never secrecy. The channel
that moves the file is the operator’s choice and the operator’s responsibility. What travels is
narrower than “everything,” though: the artifact carries only the objects the delta and the ancestry
walk actually name, plus public key material for the patches’ own authors — never a secret key or
seed, which live only in each operator’s own environment variables and never appear in any artifact.
prikk stays off the network by design, not by omission. Every check in the accept path already treats the artifact as untrusted input from an unknown origin; adding a transport would add attack surface without adding verification strength, so RFC 116 ruled negotiation-as-artifacts first and network code out of scope for now.
The receiver seals and adopts under its own key, always. Accepting an artifact never adopts a
maintainer key, never advances a ref, and never creates a local tag by itself — those are three
separate, explicit acts (seal, adopt-tag), each signed locally. A sender’s block and the
receiver’s sealed block for the same patches are different objects; a sender’s tag and the receiver’s
adopted tag are different objects too, sharing the same patch set but not the same identity — expect
the ids to differ, since that is nothing arriving pre-trusted, working as intended.
Divergence is reported, not treated as damage. If an accepted patch does not apply to the receiver’s current tip, that means the two histories have moved differently since they last agreed — ordinary, not corruption.
Limits
- No remote-tracking, no named remotes, no discovery. Every step names a file explicitly; prikk remembers nothing about who you last synced with. Each round starts from nothing.
- Each exchange costs O(history), not O(change). A have-list is 32 bytes per patch reachable from the ref — at 100,000 patches, roughly 3 MB, sent on every exchange regardless of how small the actual delta turns out to be.
- Adopting a tag resolves by scanning local blocks, and that scan is superlinear. Measured at roughly 12.6 ms over 500 blocks and 86 ms over 2000 in a single long branch. Do not expect it to stay fast as history grows.
- The summary covers
heads/*only.remotes/*never appears in it — received, unsealed history is a separate namespace. Tags are not listed in the summary, but they do travel in the build artifact and are adopted separately (RFC 117) — a repository can receive and adopt tags even thoughcomparenever mentions them.
Out of scope
Transport, remote-tracking, and tag deletion are not built. Nothing here documents them as forthcoming.
Claim-to-Source Anchors
| Claim | Source anchors |
|---|---|
sync has nine subcommands — summary, compare, have, build, accept, pending, seal, tags, adopt-tag — each reading and writing local files only. | sync.rs |
summary lists every heads/* ref with its patch-set digest and count; remotes/* never appears, and tags/* is filtered out deliberately. | summary.rs |
compare reports each ref as in-sync, differs, remote-only, or local-only. | summary.rs |
A have-list carries 32 bytes (one ObjectId) per patch reachable from the ref. | have_list.rs |
build computes the delta against a received have-list, signs one recognition claim per block the delta touches, and includes every local tag whose target block lies within the synced ref’s ancestry — even when the patch delta itself is empty. | sender.rs |
build prints a confidentiality notice — the artifact contains repository content in the clear and prikk does not encrypt it — whenever it writes one. | sync.rs |
The exchange artifact (PEXCH002) carries patches, blobs, public AUTHOR key material, recognition claims, and Tag objects; author key material is {key_id, public_key} only, never a secret key or seed. | artifact.rs, author_key_index.rs |
accept verifies and stores everything the artifact carries and reports signature outcomes; it adopts no key, advances no ref, and creates no local tag by itself. | accept.rs |
pending lists accepted patches not yet reachable from any local block. | patch_exchange.rs |
seal turns accepted patches named by claim ids into sealed blocks under the receiver’s own maintainer key, ordered by each claim’s own signed parent relationships. | seal_from_accepted.rs, recognition_claim.rs |
tags lists every received-but-unadopted tag with a live signature outcome and current resolution state (Resolved, NotHeld, or ambiguous); adopt-tag resolves one by patch set and creates a local tag under the receiver’s own key, refusing when the patch set is not yet held or resolves ambiguously. | tag_travel.rs |
| Tag resolution by patch set is a full local-block scan, measured superlinear (~12.6 ms at 500 blocks, ~86 ms at 2000, one long branch). | patch_set_digest.rs, RFC 117 |
Negotiation-as-artifacts is the ruled next increment; prikk stays off the network and prikk-store stays bytes-in/bytes-out by design. | RFC 116 |
| A received Tag object is stored and reportable; sync never mints a local tag — adoption is a separate, explicit, receiver-signed act. | RFC 117, tag_travel.rs |
| Transport, remote-tracking, and tag deletion are not implemented. | sync.rs, RFC 116 |
Provenance
This guide covers RFC 116 (stages 1 through 7) and RFC 117 stage 3. It is documentation-only and does not change CLI behavior, artifact format, signing, trust, or repository state.
Release, Versioning, and Compatibility
This page defines Prikk’s current pre-1.0 compatibility and official-release policy. It separates source versioning, Git release identity, external distribution, and evidence so a release does not need later housekeeping to become truthful.
For repository format details, see repository layout and authority. For identity-bearing objects, see the data model. For persistence limits, see durability and crash recovery.
Core Caveats
- Prikk is pre-1.0 experimental software, not a production Git replacement.
- Cargo APIs, CLI behavior, object schemas, and repository formats are not generally stable.
- No migration path, support window, LTS line, or 1.0 schedule is promised.
- The workspace version alone does not identify an official release.
- Current release checks are partly manual.
cargo auditandcargo denyare not configured gates. - The committed release-signer set is empty, so no release currently satisfies the DC-35 signer gate.
- Tags through 0.17.7 predate this policy and must not be reported as passing its signer-authority audit.
- 0.18.0 does not predate the policy. Its tag carries the maintainer’s ordinary OpenPGP signature but no allowlisted release-signer authority, and no authority transaction was performed; its changelog states so explicitly. It must not be reported as passing the signer-authority audit either. Releases made while the signer set is empty state this in their own release notes rather than relying on the pre-policy exemption above.
- DC-35 does not provide SBOMs, provenance attestations, mature key custody/rotation/revocation, or production-readiness evidence. Those remain later DC-43 work.
Compatibility Surfaces
Prikk treats these as separate compatibility surfaces:
- Cargo crate source APIs and feature sets;
- command names, arguments, exit behavior, and human-readable output;
- canonical object schemas, signature preimages, ObjectIds, and identity domains;
- repository format, on-disk layout, and migration/refusal behavior; and
- source archives and published documentation.
Before 1.0, a minor release may intentionally change documented Cargo or CLI surfaces when release notes identify the change. Repository-format changes additionally require an accepted governing RFC with a new format/schema where applicable and explicit directional read, write, migration, and refusal behavior.
SemVer and release notes cannot authorize silent identity mutation. A signature preimage, ObjectId, canonical identity schema, or domain change requires accepted design authority, a new explicit version or domain, refusal or migration behavior, and literal compatibility vectors. Existing identity versions are never reinterpreted.
Format Stability Contract
RFC 114, answering badge criterion 2 — what minimum must never change for a verification claim made today to hold in ten years. Stated as a promise a user can rely on:
Any prikk release can read every object any prior supported release wrote, and verifies it to the same conclusion. Storage may require a migration step, which is documented and tested. Object identity and signatures never require one.
This splits every byte in a repository into two categories, and they have opposite rules.
Frozen forever — verification-bearing, never changes once shipped:
- The object-id preimage:
OBJECT_ID_DOMAIN(b"PRIKK-OBJECT-ID-v1") ‖ type code (u16 BE) ‖schema_version(u32 BE) ‖ payload length (u64 BE) ‖ canonical payload, hashed SHA-256. - The canonical encoding of each
(object_type, schema_version)pair that has ever been written by a shipped release. - The signature preimage, per signer role.
- The algorithm identifiers themselves (Ed25519, SHA-256) — not merely the algorithms.
- The patch-set digest preimage (RFC 115 Stage 1, design-v1.md §5/D4):
PATCH_SET_DIGEST_DOMAIN(b"PRIKK-PATCH-SET-DIGEST-v1") ‖ count (u64 BE) ‖ each patch id, sorted ascending and deduplicated, 32 bytes each, hashed SHA-256. Not itself a stored object or anObjectId— a comparison value, followingMerkleRoot’s shape — but identity-bearing all the same: two prikk versions must produce identical bytes over the same patch set, or the “are these two repositories the same?” comparison it exists for means nothing across an upgrade.
May change, and must carry a tested migration path:
- Repository format version and on-disk directory layout.
- Container framing, the object index, the WAL.
- The bundle exchange format.
Freezing is not “never add a field.” A schema version lives inside the object-id preimage, so a new field means a new schema version and new ids for objects written under it — with no change whatsoever to objects already written under an earlier schema version. The obligation is to keep every version ever shipped decodable forever, hashing exactly the way it did on the day it was written — not to stop evolving.
If SHA-256 or Ed25519 is ever broken: a broken algorithm gets a new algorithm identifier, used by new objects going forward. Every object already written continues to verify under the identifier it was written with, unchanged. An existing identifier is never redefined — doing so would retroactively alter what past objects mean, which is exactly what this contract exists to prevent.
The obligation covers shipped releases, not every commit. A schema version or format that only ever existed in an unreleased build is owed nothing under this contract; what shipped is permanent, what merely existed in git history is not.
Formats 1 through 5 are not supported under this contract (owner decision, RFC 114 §5.3) — prikk has never been used in production, so there is nothing to protect there. See Repository Format Transitions below for what that means for each retired format specifically.
Repository Format Transitions
New repositories use format 6 and schema-2 Blocks with replay-derived clean-state Merkle roots.
Formats 1 through 5 are rejected at open. Each is refused with an error naming the format found. The earlier bounded legacy read-only mode for format 1 no longer exists — it was retired when format 1 was, and there is no read-only fallback for any superseded format.
The format has moved repeatedly and deliberately: 1→2 (state Merkle roots), 2→3 (object containers), 3→4 (ref containers), 4→5 (trust containers, received-ref index, active ref metadata), 5→6 (compaction slots and generation logs). Prikk is early implementation software and has not committed to format stability; each bump is a deliberate decision that every older repository becomes unopenable.
There is no in-place or history-preserving migration between any two formats. To carry work across a
format change, use prikk bundle export on a version that still opens the old repository and
prikk bundle import into a new one. Do not copy .prikk/ or edit FORMAT to simulate migration —
editing the marker does not change the on-disk shape it describes.
Bundle Format Transitions
The bundle exchange artifact (prikk bundle export/import) carries its own magic and version,
independent of the repository format above. Bundles are always exported as PBNDL002 (DC-53
Stage 2), which added an AUTHOR key-material section after the object list — an addition the prior
PBNDL001 format has no room for and cannot be extended into silently, so the bump is fail-closed on
the write side: an older client meeting a newer bundle refuses it outright with its own hardcoded
magic check.
PBNDL001 bundles are still accepted on import (corrected shortly after the PBNDL002 bump
shipped — bundle-v1-import-regression-v1.md). This is read compatibility only, the same asymmetry
every repository-format transition on this page already has: read what an older client wrote, write
only the current format. This is also what keeps the repository-format migration path above actually
usable — an old repository can only be opened by an old prikk build, and that build only ever produces
a PBNDL001 bundle, so refusing to import one would sever that migration in both directions at once.
A PBNDL001 bundle decodes exactly like a PBNDL002 one whose author-key section is empty: the
Patches it carries read Unverifiable, the same outcome DC-53 already defines for any Patch this
repository never recorded AUTHOR key material for.
The workspace’s declared minimum Rust version is exactly 1.85.0. The locked product workspace must check, test, and build on that toolchain:
cargo +1.85.0 check --workspace --all-targets --locked
cargo +1.85.0 test --workspace --locked
cargo +1.85.0 build --workspace --locked
Current-stable quality gates are separate from minimum-version compatibility. In particular, strict Clippy runs on current stable because its lint set changes between compiler releases.
A patch release must not intentionally break a documented surface. An unavoidable correctness or security break uses a minor release unless a committed emergency exception is accepted by maintainer and architect before tagging. The exception cannot waive identity versioning.
Source Version and Release Identity
All workspace crates use one selected version. At release-candidate preparation, every internal
registry dependency must use exact =X.Y.Z resolution for that release. Current development manifests
still use broad version = "0" requirements, so that future RC gate is not yet satisfied.
Outside an exact release tag, the Cargo version is a source compatibility line, not release identity.
An untagged build is a development build even if prikk --version equals the latest release. It must
not be represented as a release; a shared development artifact needs its exact commit and explicit
non-release build/source metadata.
Official Git tags are unprefixed versions such as 0.18.0, not v0.18.0. List them in version order:
git tag --sort=-v:refname
Plain lexical sorting can incorrectly make older 0.9.x tags appear newer than 0.17.x tags.
An official release identity consists of an authorized signed annotated tag object, its peeled commit, and the digest of every distributed payload artifact. A valid signature from an unlisted key is not an authorized Prikk release signature.
Release States
| State | Workspace source line | Latest released | Candidate | Changelog | RFC location | Git identity |
|---|---|---|---|---|---|---|
| Development | last release | last release | none | no target release claim | proposed/accepted | HEAD commit metadata; no release tag at HEAD |
| Release candidate | target | last release | target | candidate entry | accepted | reviewed RC commit; target tag absent |
| Released | target | target | none | final entry | shipped RFCs in done | authorized signed target tag peels to finalization commit |
Accepted RFCs remain in accepted/ through implementation and RC review. They move to done/ only in
the private finalization commit selected as the tag target. That finalization state must not be pushed
without its tag. An abandoned candidate returns every candidate field to development state and creates
no target tag or asset.
The authoritative field inventory is:
| Field | Tracked authority |
|---|---|
| Workspace source line | root Cargo metadata, Cargo.lock, normalized packages, prikk --version |
| Latest release | README and implementation status |
| Current candidate | ROADMAP and implementation status |
| Change state | CHANGELOG |
| RFC lifecycle | RFC status/location, inbound links, rfcs/README.md |
| Release identity/status | Git tag and append-only release evidence snapshots |
Unregistered duplicate release claims are audit failures. The positive and forbidden abstract rows are
tracked in release-state-cases.json.
Run cargo run --locked -p prikk-release-policy -- check from the repository root to execute the
signer, canonical challenge-byte, release-state, and evidence-schema/sequence fixture tables. The Rust
gate asserts date-time formats, rejects unknown schema assertions, and fails when computed validity
differs from a fixture’s expected outcome. It leaves the worktree unchanged.
Required Release Workflow
This workflow is dormant until the project owner explicitly activates preparation for a named release.
Activation requires a reviewed tracked commit that atomically changes the release lane from parked to
active and records the same exact target version in ROADMAP.md, MILESTONES.md, and
rfcs/IMPLEMENTATION-STATUS.md. That commit must land before requesting a fingerprint or preparing a
bootstrap candidate. Discussion, implementation completion, roadmap targets, review recommendations,
and untracked messages do not activate release work. Before bootstrap begins, parking or retargeting
uses the same reviewed three-file transition; after bootstrap begins, the governance and hold rules
below control closure.
Release conditions attach to unshipped accepted increments. If a later version first ships an increment,
it inherits all release gates and lifecycle/status corrections assigned to that increment. Retargeting
must update the three schedule/status authorities and affected RFC target/status text together. Ordinary
design-first development may continue while the release lane is parked; once activated, every applicable
step below remains binding. If the three authorities disagree, the release lane is parked; see
MILESTONES.md under Baseline and release posture.
- Obtain design and implementation acceptance in isolated commits.
- Complete any signer bootstrap/change/recovery as an earlier isolated reviewed transaction. Confirm that no release hold remains active.
- Prepare one RC commit: select the target version; set exact internal requirements; update lockfile, candidate changelog, README, ROADMAP, MILESTONES, RFC indexes/status, mdBook, and implementation status without claiming release.
- Run and record the full applicable RC gates, package inspection, and adversarial RC review.
- After RC acceptance, create one private finalization commit: remove candidate wording, set latest
released, clear the candidate, move shipped RFCs to
done/, and repair every link/status field. - On the clean finalization commit, rerun the complete deterministic gate suite. RC results do not substitute for this run.
- Create an unprefixed signed annotated tag at that commit. Verify the authorized primary signer, signature, tag object, and peeled commit. Generate and inspect staged archive assets once.
- Require a successful atomic-push capability check. Publish branch and tag only with
git push --atomic <remote> <branch> <tag>. Unsupported atomic push aborts; there is no non-atomic fallback. Atomic publication of commit and tag is the release event. - Publish staged immutable assets and crates from the exact clean tagged tree, then record external status. GitHub Release, crates.io, and Pages are asynchronous distribution, not Git release identity.
The finalization and external steps form one controlled transaction. A local failure before atomic push may be corrected or abandoned without publishing false state. After atomic push, the release exists even if distribution is pending or partial. Published identities are preserved; retry only missing outputs or supersede with a new version.
Release Signer Governance
release-signers.toml is the strict
commit-local allowlist. The current empty array authorizes nobody and blocks official release. The file
supports multiple full uppercase OpenPGP primary fingerprints; two active operators are encouraged
when available but are not required at the current project scale.
The signer file is not the ultimate trust root. Reviewed protected-branch governance authorizes signer policy changes, an allowlisted private key authenticates a tag, hosting and registry administrators control publication, and evidence binds those independently administered outputs. Administrator override is an incident, not ordinary authority.
Every bootstrap, addition, replacement, or removal is isolated before RC finalization and approved by two distinct natural persons: one repository maintainer/administrator and one independent architect or security reviewer. A maintainer may approve admission of their own key in the maintainer role but cannot supply the independent approval. Automation supplies neither identity. Existing-signer approval is useful continuity evidence, never a recovery veto.
The release-state audit uses one canonical governance record for the transaction, signer-set effect, proofs, approvals, authority blobs, public record, and hold. Independent records from different transactions cannot be combined to authorize a development-stage authority change.
A new fingerprint requires a fresh, versioned, transaction-bound, expiry-bounded non-secret signed challenge. Proof applicability is derived from normalized old/new fingerprint sets:
| Transaction effect | Required authority proof |
|---|---|
| Bootstrap, addition, replacement | verified for every introduced fingerprint |
| Removal-only | not-applicable with reason |
| Classification-only, unchanged authority | not-applicable with reason |
Authority proof and later release-tag verification are distinct evidence. Strict signer grammar and positive/forbidden cases are defined in the release policy data.
Loss, Compromise, and Disputes
- All-key unavailability/unusability triggers loss recovery.
- Any suspected compromised authorized key triggers compromise containment.
- Any material signer, authority transaction, tag, or release dispute triggers dispute containment.
Each trigger immediately holds new official tags and incomplete/future distribution. Initial bootstrap uses the same controls. The incident opens a durable public record, obtains the two accountable approvals, records transaction-appropriate proof, makes an isolated governance change/record, and keeps publication blocked for at least 72 hours after evidence becomes public. Architect/security re-review must accept containment/classification and explicitly lift the hold.
An active incident snapshot records a null hold end and lift. A later append-only snapshot may fill its
classification, end, and explicit lift after the minimum interval. Filled governance fields cannot be
rewritten, and an active hold cannot coexist with distribution complete.
A disputed published tag is classified by the same two-person process as valid-at-publication,
never-authorized/hostile, or still disputed. Only the first two can receive an explicit hold lift;
disputed remains held. Emergency administrator quarantine is containment, not normative status.
Valid releases are never retagged or replaced. A hostile identity is quarantined with forensic/public
incident evidence, its version/name is burned, and it is never reused.
This governance controls only official upstream Prikk tags, assets, and package namespaces. It does not restrict contributions, reviews, Apache-2.0 forks, downstream builds, or downstream releases under distinct identities.
Archives, Crates, and Completion
The source archive is prikk-vX.Y.Z.tar.gz, including the v that Git tags omit. Tracked files appear
at archive root. Generation uses deterministic gzip metadata and a new no-clobber staging directory.
The checksum asset is prikk-vX.Y.Z.tar.gz.sha256; it contains lowercase SHA-256, two ASCII spaces, the
archive basename, and LF. The digest covers compressed bytes. Published names and bytes are immutable.
The package graph is derived from normalized manifests. Its current publication levels are:
prikk-error,prikk-hashprikk-crypto,prikk-objectprikk-replayprikk-storeprikk
Before tagging, staged packages must build against an isolated local registry without path overrides.
External publication waits for each predecessor to become registry-visible. For every crate, staged
.crate SHA-256, registry-index checksum, and fetched-byte SHA-256 must match. A mismatch is partial,
blocks dependents, preserves the published version, and requires a superseding version.
Distribution is pending, partial, complete, or superseded. complete requires:
- archive and checksum attached under fixed names with matching recorded bytes;
- every expected crate visible with equal staged, index, and fetched checksums;
- the release page published rather than draft; and
- Pages deployed at the peeled commit, or a reasoned pre-publication review ruling it inapplicable.
Configured Pages failure or delay remains pending/partial and cannot be waived afterward.
Evidence Snapshots
Release evidence snapshots are immutable assets named
prikk-X.Y.Z-release-evidence-NNN.json. Sequence starts at 001, remains contiguous, and links each
predecessor by name and the SHA-256 of its exact observed published bytes. Whitespace, key order, and
final newline are part of that immutable asset identity; re-serialized JSON is not substituted. The
highest valid snapshot is authoritative; absence means pending. Snapshots preserve cumulative attempts,
including failed evidence attachments, and every successor adds at least one newly sequenced attempt.
They never change prior identities or observed tag verification. Each parsed snapshot is validated
against the JSON value decoded from the same exact bytes whose digest participates in the chain.
Tag verification is coherent in every distribution state: not-observed has no detail, verified has
all signer/authority/verifier detail, and failed has authority and verifier detail with an optional
fingerprint when extraction was ambiguous.
The strict structural schema is
release-evidence-v1.schema.json.
Semantic checks additionally enforce predecessor continuity, immutable identity, normalized signer-set
effects, two-person approvals, proof applicability, cumulative attempt history, crate checksum equality,
and exact completion outputs.
Source archives and .crate files are payload artifacts whose digests bind release identity. Checksum
files and evidence snapshots are integrity/status metadata, avoiding a self-referential digest rule
while keeping published names and bytes immutable.
Gates and Evidence Honesty
Applicable deterministic release gates include:
cargo fmt --all -- --check
cargo check --workspace --all-targets --locked
cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
cargo test --workspace --locked
cargo build --workspace --locked
mdbook build docs
git diff --check
Release review also checks clean commit/tag identity, exact internal requirements, normalized packages, isolated-registry builds, RFC/status/link consistency, signer governance, archive/checksum grammar, release-state fixtures, and evidence sequence/completion rules.
A policy list is not passing evidence. Every review/release record must state commands actually
observed, unavailable or inapplicable checks, and environment limits. Never report cargo audit,
cargo deny, crash/reboot testing, registry publication, GitHub Release publication, or Pages
deployment as passed unless observed for that exact release.
Claim-to-Source Anchors
| Claim | Source anchor |
|---|---|
| Compatibility, state, signer-governance, and distribution rules | DC-35 |
| Identity changes require new explicit version/domain authority | DC-34 |
| Format-1/format-2 compatibility and refusal boundary | DC-40 |
| RFC lifecycle and same-release transition | RFC-000 |
| Strict signer/evidence policy data and fixtures | release policy data |
| Released change history | CHANGELOG |
Provenance
This reference implements DC-35’s policy/documentation surface. It does not authorize a signer, change Cargo requirements, create a release candidate, or claim that any release transaction or external distribution gate has passed.
Platform Support
This page is the authoritative current-state reference for which platforms Prikk runs on and, concretely, which commands are read-only versus repository mutation. It exists because that boundary had never been enumerated anywhere — DC-71 traced it once, here, so it does not have to be re-derived from source on demand and cannot drift silently again (a CI job builds every listed non-Linux target on every change; see Non-Linux CI conformance below).
The boundary
Repository mutation requires Linux, macOS, or Windows. crates/prikk-store’s anchored
filesystem primitives use no-follow, nonblocking, atomic-rename, and no-clobber-install capabilities
(durability and crash recovery) with a reviewed implementation on each of
those three platforms — LinuxDurability, MacosDurability (DC-81/DC-82; G3 uses
fcntl_fullfsync in place of fsync, measured ~180x slower on the GitHub macOS runner and recorded
in FINDINGS.md), and WindowsDurability (DC-87 Stage 2) — and no reviewed equivalent on any other
platform yet
(DC-37,
superseded for Linux/macOS/Windows by DC-87).
Every mutation function’s signature compiles on every platform; only its body has a real
implementor on Linux, macOS, and Windows, and a caller on any other platform receives a clean runtime
error rather than a build failure or a silent no-op.
What Windows actually guarantees and does not, for path resolution (G1). This is stated here
rather than left to be discovered, because it is a real difference and not a coverage gap. Anchored
resolution on Linux and macOS opens each path component with openat(dirfd, name, O_NOFOLLOW), so the
handle for a component is bound to the object that was checked — the next open is scoped to that
handle, not to a re-walked path string. Windows has no equivalent: no Win32 primitive takes a
directory handle as a resolution root for opening a child by name, so the walk itself is always a
re-walked path string on Windows, by construction.
Windows’ actual implementation (crates/prikk-store/src/fsutil/anchored/windows.rs) refuses a reparse
point at each component as it is opened (FILE_FLAG_OPEN_REPARSE_POINT plus a post-open attribute
check), which defeats a symlink or junction that is already in place. It does not close the window
between checking a component and opening the next one. So a concurrent local process that
substitutes a reparse point mid-walk, timed into that window, is not provably defeated on Windows,
while it is on Linux and macOS. A passive, already-planted reparse point is caught on every platform.
This mid-walk window is unchanged by anything below — DC-96 verifies the anchor a walk starts
from, not each intermediate component of the walk itself.
Prikk does not claim otherwise. This gap was accepted, once, on the condition that it be stated rather
than elided (prerequisite-ruling-v1.md §4.1) — this section is that statement.
Anchor replacement (DC-96 Windows Anchor Identity). DC-87 Stage 2’s own CI job demonstrated a
second, wider gap: renaming a repository’s root (or .prikk specifically) aside and creating a fresh
directory at that path redirected both reads and writes — including objects, refs, and the WAL, not
only the worktree — into the impostor, silently, with prikk reporting success. This was not the G1
mid-walk race above; it needed no reparse point at all, and the disclosure as it stood would have led a
reader to conclude it was already defended. It was not.
Fixed, as prevention, not merely detection. An earlier version of this fix stored only a path
string plus an identity value and refused on mismatch — detection, and wrong: it could satisfy only
half of each acceptance test, since the tests require operations to keep working correctly against
the retained directory after a replacement, not merely refuse
(.git-exclude/reviewed/DC-96-implementation-ruling-v1.md §2-§4). WindowsAuthority
(crates/prikk-store/src/fsutil/anchored/windows_authority.rs) instead retains the directory handle
it was bound to. Windows has no openat-equivalent to resolve a child by name against that handle,
but a retained handle still follows its object across a rename — GetFinalPathNameByHandle returns
its current path. Every walk re-derives that current path from the retained handle first, confirms
via identity (GetFileInformationByHandle’s (volume serial number, file index) pair) that the
object found there is still the one that was bound, and only then walks forward from it. Both Win32
calls go through prikk-ffi — crates/prikk-ffi, the one workspace crate permitted unsafe per
DC-90. Three residual properties, stated precisely rather than left to be inferred:
- Anchor replacement between operations: prevented while a repository is open, in two different
ways. For
.prikk(repository_mutation), the retained handle follows a rename that does succeed — the gap the CI job demonstrated, now closed by continuing correctly against the retained directory rather than merely refusing. For the worktree root (worktree_mutation), the outcome is stronger still: NTFS refuses the rename outright, becauseRepositoryLayoutretains a nested handle on.prikkinside it — see the next paragraph for what this means operationally. - Anchor replacement racing a single operation — swapped between the post-open identity check
and the open that immediately follows it — is still possible. The window is narrowed from “any
time before the next operation” to one check-then-open pair; it is not closed, because Windows still
offers no
openat-equivalent to close it by construction the way Linux and macOS do. DC-99 Stage 2 found the confirmation step guarding this window unexercised by any test: a negative control neutralizedWindowsAuthority::verified_anchor_path’s identity comparison and the full suite stayed green, 936/936, identical to the unmodified branch — neither DC-96 acceptance test constructs the narrow race this comparison exists for. RFC 106 built the failpoint barrier this needed (TestBarrier::AnchorVerification, mirroring DC-98’swait_at_directory_create) and constructed the race directly: a test holds the window open, installs a replacement at the anchor’s own path, and confirms the operation is refused with the specific “Windows anchor replaced” diagnostic — passing on real Windows CI (run32002318009). Repeating DC-99’s negative control against this new test reproduced the same failure shape deliberately: with the comparison neutralized, that one test failed and was the only failure in the suite (run32002319494). The comparison now has a control that depends on it. - Intermediate path components are unchanged — this is exactly the G1 mid-walk window above, and DC-96 does not touch it.
A fourth property, user-facing rather than adversarial: while a prikk command holds a repository
open on Windows, that repository’s directory — and any directory containing it — cannot be renamed or
moved by any process, prikk included. NTFS refuses to rename a directory that contains an open
handle anywhere within it, unconditionally, and prikk retains one on .prikk for the duration of a
command (RepositoryLayout::init/open, crates/prikk-store/src/fsutil/anchored/ windows_authority.rs). This is not a bug report waiting to happen; it is the mechanism above,
observed from the other side. It is bounded to a single command’s execution — prikk has no daemon —
so the window is as long as one invocation takes, not a whole working session.
The 64-bit file index is not reliable on every filesystem — identity is the secondary check, not
the sole mechanism, which is why this does not weaken the fix. Per Microsoft’s own documentation
for BY_HANDLE_FILE_INFORMATION (nFileIndexHigh/nFileIndexLow): “The ReFS file system… includes
128-bit file identifiers… The 64-bit identifier [nFileIndexHigh/nFileIndexLow] is not guaranteed
to be unique on ReFS” — ReFS callers needing a reliable id are directed to GetFileInformationByHandleEx
with FileIdInfo instead. Windows 11’s Dev Drive, Microsoft’s own recommended location for source
repositories, is ReFS. This matters less than it would have under the detection-only design: the
primary mechanism here is the retained handle following the renamed object via
GetFinalPathNameByHandle, which does not depend on the file index at all; identity is only the
confirmation that what was found at the re-derived path is the same object, not what determines where
the walk goes. A coincidental file-index collision on ReFS would need to land on the object the walk
already, independently, arrived at correctly — not redirect it. FILE_ID_INFO is not used here; if a
future increment needs a stronger per-filesystem guarantee, that is its own design question.
The nine DurabilityContract guarantees on Windows
| Method | Windows guarantee |
|---|---|
durable_append | Held. Content durability on an existing name is what Windows provides. |
durable_truncate / durable_truncate_to_empty | Held. |
create_exclusive | Held at init only. The new directory entry it creates is not itself durably confirmed — see the init-time exemption below. |
ensure_directory | Held at init only, same caveat. |
remove_if_present | Held, conditional on every open in the Windows backend requesting FILE_SHARE_DELETE — enforced in one place (open_no_follow), not per call site. |
atomic_replace | Weaker. std::fs::rename over the destination, with no durability lever asserted for the rename itself (MOVEFILE_WRITE_THROUGH’s same-volume guarantee was investigated to three independent primary sources and found genuinely undeterminable). Acceptable only because its remaining callers are two rebuildable caches. |
set_permission_bits | Vacuous — a documented no-op. NTFS has no POSIX execute bit; prikk’s own recorded mode is never derived from the filesystem, so a round-trip checkout on Linux restores the node’s recorded mode faithfully regardless of what this method does on Windows. |
durable_directory_entry | Vacuous — a documented no-op. FlushFileBuffers’s own documentation covers file, communications-device, named-pipe, and volume handles and says nothing about a directory handle — there is no contract to implement against. Safe because both production callers sit inside the worktree unclean-shutdown marker’s bracket (worktree_marker.rs): a crash between this call and the entry becoming durable leaves the marker dirty, and commit-authoring refuses to infer deletion until the worktree is re-verified. |
promote and publish_immutable retired (DC-98 Stage 1). Both rows above are gone, not merely
weakened. promote (no named guarantee of its own, orphaned by RFC 102 Stage 4’s ref-pointer
rewire) and publish_immutable (G5, race-safe no-clobber publication) both had zero production
callers left; the ruling that had kept publish_immutable (design-v1.md §12.3) named its own
discharge condition — Stages 4-5 shipping and showing no loose-file use remained — which DC-98’s
RFC confirmed. DurabilityContract goes from eleven methods to nine.
The init-time exemption. create_exclusive and ensure_directory create names, and Windows
cannot make a new directory entry durable. Both are reachable only during init. This is tolerated
because an interrupted init has nothing to lose — no user history exists yet, and FORMAT is written
last — so an incomplete init is detectable and a re-run completes it idempotently. That argument
depends on ordering, not on a durability primitive, so it holds on Windows unchanged.
DC-76’s negative controls, per guarantee, on Windows (DC-97; G5 retired in DC-98). Stage 2
shipped with none of DC-76’s original nine demonstrated there; DC-97 classified each individually
rather than leaving one blanket statement, since the honest answer differs guarantee by guarantee.
G5 (race-safe no-clobber publication) is no longer one of the guarantees to classify at all —
retired in DC-98 along with publish_immutable, its only method, once both had zero production
callers left. Eight guarantees remain:
| Guarantee | Windows control | Why |
|---|---|---|
| G1 (root-anchored, no-follow) | Yes — windows::tests::a_reparse_point_substituted_for_a_directory_component_is_refused | Substantiated by a negative control watched to fail, not merely reasoned about: the test’s original bare is_err() did not distinguish which of validate_directory_not_reparse_point’s two checks fired, so it passed even with the first check (is_reparse_point) disabled — a directory symlink’s no-follow handle independently reports is_dir=false, so the second check (!metadata.is_dir()) caught it too. A follow-up probe found the same is true for a junction (mount-point reparse point): its no-follow attributes do carry FILE_ATTRIBUTE_REPARSE_POINT, but is_dir() still reports false, because std’s FileType::is_dir excludes reparse points by construction on Windows for any reparse tag. So check 2 is not incidental coverage of one reparse-point shape, it is a std-semantic backstop for all of them — and check 1 is not dead code either: it is insurance against that std semantic ever changing, since if is_dir() ever stopped excluding reparse points, check 1 would become the sole defense. The assertion was tightened to require the error name the reparse point specifically (check 1’s own message), not merely occur for any reason; watched to pass against real production code and fail with is_reparse_point stubbed out, both observed on CI, not assumed |
| G2 (atomic content replacement) | Yes — conformance::create_exclusive_refuses_an_already_occupied_path’s sibling shape, atomic_replace_overwrites_existing_content | Same shared-assertion shape Linux/macOS use for the exclusive-creation case; the replace case is windows::tests’ own test |
| G3 (durable-after-return) | Yes — windows::tests::durable_truncate_sync_failure_is_retryable_and_idempotent | DC-98 wired the failpoint injection mechanism into windows.rs (nine boundaries, DurabilityContract methods carrying one to two calls each) — this control specifically injects at durable_truncate’s sync boundary, satisfying the RFC’s own named minimum bar. Watched to pass against real production code and fail with required_file_sync swallowed, both observed on CI (run 31983187612), not assumed |
| G4 (exclusive creation) | Yes — conformance::create_exclusive_refuses_an_already_occupied_path, &WindowsDurability | Same shared assertion body Linux/macOS use — no Windows-specific test needed, the file’s own architecture already covers a new platform |
| G6 (regular-file validation) | No — no Windows analogue exists, not merely unbuilt | Linux/macOS evidence uses a FIFO, an ordinary-path filesystem object with no Windows equivalent reachable the same way: Windows named pipes live in a separate \\.\pipe\ namespace, not placeable inside an anchored directory tree. Windows’ own reserved-device-name special files (CON, NUL, …) are already refused one layer up, at RepoPath::parse, before ever reaching this guarantee’s own code path |
| G7 (non-blocking opens) | No, same reason as G6 | |
| G8 (concurrent-safe directory creation) | Yes — windows::tests::concurrent_required_directory_creation_is_idempotent | windows::tests::ensure_directory_is_idempotent_under_a_concurrent_creator_shape (still present) only calls the same operation twice sequentially in one thread — idempotency, not a proven race. This is the real control: eight threads barrier-synchronized (set_directory_create_barrier_for_test, the same mechanism Linux/macOS’s own G8 control uses) to reach ensure_directory_component_no_follow’s create syscall together, so at least one is guaranteed to observe AlreadyExists. The guarantee under test is that arm’s tolerance, not the barrier itself — confirmed by removing the tolerance arm and watching the concurrent test fail while the barrier stayed in place (CI run 31985261745), isolating the property from the synchronization scaffolding that makes it observable |
| G9 (mode-bit isolation) | Yes, as a documented no-op — windows::tests::set_permission_bits_is_a_documented_noop | Two independent reasons this is not negatively controllable further, not one: NTFS has no execute bit to mask (Windows), and fchmod already masks non-permission bits at the kernel level regardless of what this code does (Linux) — conformance.rs’s own shared assertion function reads back POSIX mode bits and so was never given a Windows wrapper; the no-op’s own, differently-shaped test is the right coverage instead |
Reported rather than silently left implicit, per DC-76’s own precedent (two of its original nine also could not be cleanly demonstrated on the platforms it shipped on, and were reported rather than dropped).
The same gap exists on the read path today, in the shipped read-only configuration. All four non-Unix
fallback read functions resolve a whole path in one operating-system call, so reparse points at
intermediate components are followed — there is no component-by-component walk on that path at all. One
of them, read_file_if_exists, additionally does not refuse a symlink at the final component, unlike
its three siblings in the same module, which use a no-follow stat. That last one is an asymmetry inside
one file rather than a platform limitation, and it is stated here rather than left implicit because the
guarantee is otherwise described per-function.
prikk unlock’s PID liveness check now has a real primitive on all three platforms (DC-99 Stage
1) — the same shape as set_permission_bits/durable_directory_entry above, outside the
DurabilityContract table because it lives in a different module
(crates/prikk-store/src/unlock.rs). Linux/macOS use kill(pid, 0) via
rustix::process::test_kill_process; Windows uses OpenProcess/WaitForSingleObject
(prikk_ffi::process_liveness) — OpenProcess failing with ERROR_INVALID_PARAMETER and a
successfully-opened handle’s WaitForSingleObject reporting it signaled both mean the process is
gone; ERROR_ACCESS_DENIED on open means it exists but this caller cannot query it further, the
same reasoning Linux/macOS’s EPERM branch already applies. Both platforms trust a positive result
(AppearsRunning) and never a negative or unknown one — PidLiveness’s own advisory contract is
unchanged; a real primitive makes the refusal better informed, it does not make clearing safe. One
platform-specific guard: PID 0 names the Windows System Idle Process, a real but never
lock-file-legitimate PID, and is rejected before the OS call so a malformed pid=0 lock body
produces Unknown on Windows the same way rustix::process::Pid::from_raw(0) already makes it
produce Unknown on Linux/macOS.
Read-only commands build and run everywhere. They never reach a mutation primitive — verified by
tracing every command’s call graph to crates/prikk-store/src/fsutil’s mutation set (ensure_root,
write_file_atomically, write_worktree_file_atomically, append_file_required,
truncate_existing_file_required, truncate_file_empty_required, create_new_file_required,
remove_file_required/remove_file_if_present_required/remove_worktree_file_required,
promote_file_required, publish_immutable_file, ensure_directory_required,
sync_directory_required), not merely by a command’s name suggesting it.
The command set
| Command | Boundary |
|---|---|
verify | Read-only |
log | Read-only |
status | Read-only |
doctor (no repair flags) | Read-only |
doctor --repair-wal-tail / --repair-main-ref | Mutation |
checkout --plan-only | Read-only |
checkout --snapshot-plan | Read-only |
checkout --snapshot-materialize | Mutation (writes the worktree) |
checkout --patch-plan | Read-only |
checkout --patch-materialize | Mutation (writes the worktree) |
checkout --patch-delete-plan | Read-only |
checkout --patch-materialize-delete | Mutation (writes and deletes worktree files) |
merge-evidence | Read-only |
merge-plan | Read-only |
inverse-plan | Read-only |
rollback-preview | Read-only |
rollback-draft | Mutation (appends to the active WAL) |
rollback-draft-verify | Read-only |
worktree-status | Read-only, but see the note below — currently unreachable against an ordinarily-authored repository |
branch / branch list | Read-only |
branch create / branch close | Mutation |
tag / tag list | Read-only |
tag create | Mutation |
trust maintainer add | Mutation |
init | Mutation (creates .prikk/) |
commit | Mutation |
seal | Mutation |
Traced 2026-08-04 (DC-71) by following each command’s implementation to whichever of the mutation
functions above it does or does not reach, including transitively — rollback-draft, for instance,
calls no mutation primitive directly in its own file, but reaches one through Wal::append_patch.
A name suggesting “plan” or “preview” is a hint, not proof; every row above was traced, not assumed.
worktree-status is read-only by the same trace, but no CLI command produces the state it
requires: worktree_status (crates/prikk-store/src/worktree_status.rs:88) calls
prepare_snapshot_checkout_plan, which errors unless the target block carries a snapshot blob
(checkout.rs:94-97). Nothing in the CLI’s commit/seal path — the only way an ordinary
repository is built — ever sets one; only a test-internal helper does
(worktree_status/tests.rs:94, publish_snapshot_block). This is a capability gap, not a
mutation/read-only classification error, recorded in MILESTONES.md and out of DC-71’s scope to fix.
Non-Linux CI conformance
.github/workflows/ci.yml’s non-linux-build and non-linux-verify jobs run on GitHub’s native
windows-latest and macos-latest runners on every push and pull request, so a regression in this
boundary — the exact defect DC-71 fixed, which shipped undetected because nothing built a non-Linux
target — fails CI immediately rather than being found by a user or the next trial build.
non-linux-verify additionally runs the read-only command set (minus worktree-status, per the note
above) against a fixture repository authored on Linux, so this is a demonstrated property, not merely
a successful compile.
macos-mutation and windows-mutation (DC-81, DC-87 Stage 2) run the full workspace test suite
natively on macos-latest and windows-latest, since neither developer nor architect can run either
platform locally as part of this project’s own environment — the CI job existing and being green is
the verification for each backend, not a supplement to one done elsewhere.
windows-mutate → linux-mutate-reference → verify-cross-platform-history (DC-87 Stage 2
criterion 7) close the one property none of the jobs above can: that repository authored on Linux,
mutated on Windows, and verified on Linux produces identical object ids and a clean verify. The
Linux-built fixture is mutated identically on both platforms with the same deterministic signing seeds
fixture already uses; the Windows-mutated repository is then handed to a Linux job, which runs
prikk verify against it directly and diffs its recorded object ids against the independently-computed
Linux reference. Every other job in this workflow is one platform verifying itself — this is the only
one where a different platform checks Windows’ output.
What is not covered here
- Prebuilt non-Linux binaries are not published. Building from source (
cargo build/cargo install) is the only non-Linux install path today; see the README’s install section. - DC-76’s negative controls are only partly demonstrated on Windows, for the eight guarantees
that remain (G5 retired in DC-98) — see “The nine
DurabilityContractguarantees on Windows” above for the per-guarantee table and reasons. G1, G2, G3, G4, G8, and G9 are demonstrated; only G6 and G7 are not, and both for the same reason — no Windows analogue exists to demonstrate at all (Windows named pipes live in a separate\\.\pipe\namespace, not reachable the way a FIFO is on Linux/macOS), unrelated to the failpoint injection mechanism DC-98 wired for the other six. macos-latestis Apple Silicon (aarch64-apple-darwin), not x86_64 — GitHub’s default since the macOS 14 runner image.windows-latestis x86_64. Neither the x86_64 macOS nor the arm64 Windows variant is separately CI-gated, and Windows arm64 is untested entirely; nothing in the Windows backend is architecture-specific (it is#[cfg(target_os = ...)], not target-triple-specific), so this is a coverage gap in CI breadth, not a known or suspected difference in behavior.- File mode / executable-bit authoring on Windows, or any platform with no observable POSIX mode
(DC-87 §3.3/§4.3): worktree authoring never derives a node’s recorded mode from such a platform’s
filesystem — an existing node’s already-recorded mode is always carried forward untouched, and a
brand-new file is created non-executable by default, since there is no existing recorded mode to
inherit and no observed signal to use.
set_permission_bitsis correspondingly a documented no-op on Windows (see the guarantee table above) — this is a missing capability (an executable file’s initial creation cannot be authored from such a worktree), not data loss — a previously-recorded executable bit is never silently dropped from sealed history by this platform difference.
System Architecture
An overview of how Prikk is put together: which crate owns what, which way dependencies point, and where the boundaries that matter are enforced.
For the objects themselves and how they change over time, see Data Model Relationships and Lifecycle.
Crate graph
Seven published crates. Dependencies point strictly downward — there are no cycles, and each crate depends only on layers beneath it.
graph TD
CLI["<b>prikk</b><br/>CLI surface"]
STORE["<b>prikk-store</b><br/>repository, WAL, refs, verify, merge"]
REPLAY["<b>prikk-replay</b><br/>node lifecycle state"]
OBJECT["<b>prikk-object</b><br/>canonical encoding, object identity"]
CRYPTO["<b>prikk-crypto</b><br/>Ed25519"]
HASH["<b>prikk-hash</b><br/>SHA-256"]
ERROR["<b>prikk-error</b><br/>error taxonomy"]
CLI --> STORE
CLI --> OBJECT
CLI --> HASH
CLI --> ERROR
STORE --> REPLAY
STORE --> CRYPTO
STORE --> OBJECT
STORE --> HASH
STORE --> ERROR
REPLAY --> OBJECT
REPLAY --> HASH
REPLAY --> ERROR
CRYPTO --> ERROR
OBJECT --> HASH
OBJECT --> ERROR
| Crate | Owns | Does not own |
|---|---|---|
prikk-error | The error taxonomy every layer returns | Anything else — it has no dependencies |
prikk-hash | SHA-256, first-party since DC-55 | Object identity rules |
prikk-crypto | Ed25519 signing and verification | Who is trusted, or when to sign |
prikk-object | Canonical encoding, ObjectId derivation, payload shapes and their validation | Storage, I/O, policy |
prikk-replay | Node lifecycle state — what exists, what is tombstoned | Where state is stored |
prikk-store | The repository: object store, WAL, refs, verify, patch algebra, merge, filesystem durability | Command-line parsing and presentation |
prikk | CLI surface, argument parsing, output | Any rule — it delegates every decision downward |
Dependency boundary, enforced not documented
prikk-store may depend on exactly getrandom and rustix; prikk-crypto on ed25519-dalek
and getrandom; prikk-hash on sha2. Every other product crate has no third-party
dependencies at all.
This is not a convention. It is checked by prikk-release-policy boundary-check, which resolves the
real package graph from the root manifest and fails the build on any addition. Adding a dependency to a
product crate is therefore a reviewed decision, not an implementation detail.
The mutation pipeline
Every change to sealed history follows the same path. Each stage is separately durable, and the repository is consistent if the process stops between any two of them.
flowchart LR
WT["Worktree<br/><i>ordinary files</i>"]
WAL["Active WAL<br/><i>uncommitted patches</i>"]
OBJ["Object store<br/><i>content-addressed</i>"]
REF["Ref<br/><i>published tip</i>"]
WT -- "commit<br/>author signs" --> WAL
WAL -- "seal<br/>maintainer signs" --> OBJ
OBJ -- "publish<br/>compare-and-swap" --> REF
- commit turns worktree differences into a signed patch in the active WAL. The author signs.
- seal persists the WAL’s patches as objects and builds a block over them. The maintainer signs the block. Multiple commits may be queued and sealed together.
- publish advances the ref by compare-and-swap against its expected previous state, so a concurrent writer cannot be silently overwritten.
The two signatures are separate roles by construction: an author cannot seal, and a maintainer sealing another author’s work never re-signs that author’s patches.
Repository layout
Under .prikk/:
| Directory | Holds | Trust |
|---|---|---|
objects/ | Content-addressed objects, named by ObjectId | Authoritative |
refs/containers/ | Every ref’s own pointer entry and ref-log records, in shared append-only containers | Authoritative |
trust/ | Maintainer trust store — which keys may seal | Authoritative |
cache/ | Rebuildable derived state | Never a root of trust |
The last row is a requirement, not an observation: NFR-PERF-04 states that caches are rebuildable
and never roots of trust. BlockSummaryCache uses the canonical codec for reproducibility but is
explicitly excluded from block identity.
Where the platform boundary sits
Read-only commands run on Linux, macOS, and Windows, verified continuously in CI. Mutation runs on all
three as of 0.21.0 — each platform’s durability implementor lives behind one gated dispatch point
(ACTIVE_DURABILITY, DC-82), so adding Windows was one more arm there rather than a rewrite of the
mutation layer, which is what the seam was drawn for.
Windows is not a straight equivalent, and the differences are named rather than implied: it has no
openat, so anchored resolution is a validated path walk with the anchor’s identity confirmed against a
retained handle, and four residual properties are stated in
platform support. The mutation suite runs on all three platforms in CI, and a
repository authored on Linux, mutated on Windows, and verified on Linux is required to produce
byte-identical object ids.
That is deliberate. DC-37 requires anchored opens that refuse symlink traversal, atomic replacement, and
explicit file and directory durability; those guarantees were implemented against Linux primitives
first (LinuxDurability), macOS second (MacosDurability, DC-81), and Windows third
(WindowsDurability, DC-87 Stage 2) — with no reviewed equivalent on any other platform. See
Platform Support for the per-platform residual gaps, including Windows’
weaker anchoring guarantee in one stated way.
Where the unsafe-code boundary sits
Every crate in the workspace carries #![forbid(unsafe_code)], applied uniformly through the root
Cargo.toml’s [workspace.lints.rust] table (unsafe_code = "forbid") and each member’s own
[lints] / workspace = true. The owner’s ruling (DC-90) permits at most one workspace crate to be
named as an exception — never inferred from what a crate happens to do — and no crate is named today:
prikk writes no unsafe code of its own yet, even though it already runs some (rustix’s own
internal FFI on Linux and macOS, which forbid(unsafe_code) governs code prikk writes, not code it
depends on).
The boundary is a gate, not a convention. release-policy boundary-check
(tools/release-policy/src/boundary/unsafe_boundary.rs) fails the build if a second crate is ever
named exempt, if any non-exempt crate drops workspace lint inheritance, or — the rule that makes an
eventual exemption self-guarding — if the one exempt crate opts out of inheritance without locally
re-declaring clippy::undocumented_unsafe_blocks = "deny" in its own manifest. That lint is enabled
once, at the workspace root, specifically because the crate permitted to write unsafe is also the
one crate that could otherwise switch its own SAFETY-comment requirement off by deleting a line.
What the gate cannot see, and the review obligation that covers it instead, is documented in full
in unsafe_boundary.rs’s own module doc — read that before relying on a green boundary-check as
proof of anything it doesn’t test. In short: FFI-ABI correctness (whether a foreign function
declaration actually matches the real platform ABI) and SAFETY: comment content are both human
review judgments, not machine-checkable properties, and comment staleness — a comment that no longer
justifies the code beneath it after an edit — degrades silently behind a gate that stays green either
way.
Verification is the trust boundary
prikk verify re-derives rather than trusts: object ids are recomputed from canonical bytes, block state
roots are re-derived from lineage, and a merge block’s recorded baseline is re-checked as a genuine
common ancestor of both parents.
Two limits are worth stating plainly, because they define what verification means here:
- Verification confirms structural and cryptographic validity. It does not re-derive that a change was semantically the right change — that rests on the maintainer’s signature, uniformly, for merges exactly as for ordinary commits.
verifyenforces repository-wide author verification (DC-53): every reachable Patch’s AUTHOR signature is cryptographically checked against recorded key material. This remains trust-on-first-use continuity, not first-contact authenticity — there is no independent repository-wide AUTHOR trust policy (allowlist or revocation) the way MAINTAINER keys have one; see trust and threat model.
Known architectural costs
| Cost | Status |
|---|---|
prikk verify is roughly O(N³) in sealed block count — 34 s at 160 blocks | Tracked, unowned |
| Node lifecycle state grows with cumulative history, not the current tree | Tracked; the project has no theory of forgetting yet |
Windows mutation’s anchored path resolution cannot close the inter-component TOCTOU window openat closes on Linux/macOS | Accepted, documented (platform support) — requires a concurrent local attacker to matter |
| DC-76’s negative controls are only partly demonstrated on Windows, for the eight guarantees that remain (G5 retired in DC-98) — see platform support for the per-guarantee table | Reported per DC-76’s own precedent, unowned |
| Commit cost is not yet bounded independently of repository size (NFR-PERF-01) | Reduced, still missed |
| Merge complexity scoped to active block size (NFR-PERF-03) is argued, not benchmarked | Unowned |
These are recorded in FINDINGS.md in the repository rather than left implicit.
What the block design trades, and what it does not
Patch-theoretic systems have a known failure mode: Darcs’s exponential merge, which arises because its patches are context-dependent. Reordering two of them requires commuting one into an equivalent that applies in the other’s context, and resolving conflicts means searching those orderings.
Prikk cannot have that failure mode, by construction. Its operations are context-free — every
operation names a stable NodeId, and EditText identifies its span by content anchors with
presentation_hint_line explicitly excluded from algebraic identity. A patch transports between
lineages without transformation, which is also why a merge can adopt patches byte-identically with
their author signatures intact. There is no commutation search to explode.
The second half is deliberate refusal rather than cleverness: the patch algebra proves confluence only for a conservative subset it can prove, and returns a typed conflict witness for everything else. Cost is bounded by refusing hard cases, not by exploring them. Sealing history into immutable blocks then keeps that reasoning confined to the active working set, which is itself capped (NFR-PERF-02).
But the trade is real, and it is worth stating plainly rather than leaving for someone to discover:
The mechanism that bounds patch cost is the one that creates prikk’s actual cost. History is sealed into a chain carrying state roots, and
verifyre-derives that chain from genesis, for every block — which is exactly the O(N³) term above.
Prikk did not inherit Darcs’s problem. It has a different one, and it lives in the verification path rather than the merge path. That distinction matters strategically: verification is this project’s central claim in a way that merge throughput is not, so the cubic cost is a dependency of the claim rather than a performance ticket beside it.
The fix is known and does not require a design change — memoize the lineage walk and reuse the accumulated state across the per-block loop.
Repository Layout and Authority
This page is the authoritative current-state reference for Prikk’s on-disk repository layout and storage authority boundaries. It describes the current implementation and is grounded in the code, released RFCs, and implementation status records listed in the anchor table at the foot of the page.
For logical object concepts, see the data model. For repository path and worktree write-safety rules, see path and worktree safety. For local persistence and recovery behavior, see durability and crash recovery. For trust and signature scope, see the trust and threat model. For lock and ref compare-and-swap behavior, see concurrency and locking. For format stability, migration limits, and release identity, see release, versioning, and compatibility.
Core Caveats
- Prikk is early implementation software and is not a production Git replacement.
.prikk/is Prikk’s native repository format and is not Git-compatible storage..prikk/FORMATis a current format-version gate, not a stable-format or migration guarantee.- Ref files are mutable pointers for convenience and recovery, not roots of trust.
- Cache-like directories are initialized today, but are not current roots of trust; the quarantine directory is retired and no longer initialized.
- Durability and recovery claims are supported by current unit and integration tests, not by a completed crash-matrix or fuzzing campaign.
- Repository mutation is exercised by project gates on Linux, macOS, and Windows (DC-87 Stage 2). Windows’ anchoring guarantee is weaker than Linux/macOS in one stated way — see platform support for the exact gap and which of the nine durability guarantees are held, weaker, or documented no-ops there. Read-only commands are CI-gated on macOS and Windows too — see platform support.
- Stable repository-format migration, garbage collection, quarantine enforcement, cache rebuilding,
hosted forge trust, and remote-tracking remain deferred.
prikk sync(RFC 116, RFC 117) andprikk merge(DC-74) have since shipped — see the sync and merge guides.
Initialized Layout
A fresh prikk init creates the repository directory, the initialized directories below, and the
format marker file. It does not create runtime leaf files such as the active WAL or active ref
metadata. Ref, received-ref, and trust storage are the exception: the ref-pointer index, ref-log,
received-ref index, and trust containers are all fixed, named files allocated by init itself, empty
until first use — there is no per-ref, per-received-ref, or per-key-id file or directory created
later, since none of those names exist at init time and a per-name file would have to be.
.prikk/
FORMAT
worktree.marker
containers/
patch/{a,b}.container
block/{a,b}.container
ref-state/{a,b}.container
tag/{a,b}.container
attestation/{a,b}.container
blob/{a,b}.container
index.container
generations.log
active/
default/
queue.wal
ref-name
refs/
containers/
log-{a,b}.container
pointer-index-{a,b}.container
pointer-index-generation.log
received-index-{a,b}.container
received-index-generation.log
locks/
trust/
keys.container
policy-{a,b}.container
policy-generation.log
cache/
Every file above is created by init and is empty until first use. No name under .prikk/ is created
after init — that is a design invariant, not an implementation detail, and it is what makes the
repository durable on filesystems that cannot make a new directory entry durable.
init also creates refs/tmp/, which is allocated but never written to. It is a remnant of a
retired candidate-publication mechanism, not authority for anything, but its absence is an error:
verify scans it on every run, so repository open validates it is present.
Ten further directories that were previously allocated at init for the same reason — objects/ and
its six object-type subdirectories, quarantine/, refs/by-id/, refs/logs/ — are no longer created.
Nothing has ever written into any of them (object and ref storage moved into containers years earlier),
and nothing validates their presence at open, so a repository initialized before this change keeps them
harmlessly and a newly initialized one simply lacks them. One dormant diagnostic still reads the
objects/ tree if present — see Object Store below.
New repositories contain 6 in FORMAT. Formats 1 through 5 are rejected at open, with an error
naming the format found and directing migration through prikk bundle export on a version that still
supports it. There is no dual-layout bridge and no in-place migration: the format marker is a gate, and
a repository whose on-disk shape does not match what the code expects is refused rather than opened and
left to fail later.
cache/ holds rebuildable, non-authoritative state — a corrupt or absent cache file is never an error,
and any operation’s result is identical whether the cache is warm, cold, or missing.
There is no initialized gc/ directory, and prikk performs no garbage collection: no object is ever
deleted or superseded once written.
Object Store
Objects are not stored one-file-per-object. Every persistent object envelope is a checksum-framed
record appended into a shared, per-type container allocated at init:
containers/<object-type>/a.container
with a paired b.container allocated at init and permanently unused — object containers have no data
model to compact against (see Compaction below) — and one containers/index.container
mapping each object id to the container, slot, offset and length where its record lives.
Six object types are persisted this way — patch, block, ref-state, tag, attestation, blob.
RefUpdate is stored inline in ref logs, not as an object record.
Why containers rather than files. An object’s identity is its content hash, so every
one-file-per-object write created a new directory entry. Making a new name durable requires an fsync on
the parent directory, which POSIX provides and Windows does not — there is no documented or undocumented
Windows primitive that makes a new directory entry durable. Appending to a file that already has a name
needs only content durability, which every supported platform provides. Moving objects into fixed,
init-allocated containers is what makes prikk’s durability guarantee statable as a property rather
than as a list of platforms that happen to pass.
Reading is isolate-and-continue. A damaged record is named at its byte offset and the scan continues to the next intact one, so corruption is confined to the records it actually damaged rather than failing the whole container. A record is authority only when its frame checksum verifies, its envelope decodes and validates, it has the expected type, and its computed content-addressed id matches the requested id.
The index is rebuildable and off the durability path. An object record is appended and made durable before its index entry is appended, so a crash between the two leaves an object present but unindexed — recoverable by rebuilding the index from a container scan. The reverse ordering would let a reader see a valid index entry pointing at bytes that are not there, so the ordering is load-bearing.
Nothing supersedes or deletes an object. Writing an id that already exists with identical bytes is a no-op; writing one with different bytes is an error. Containers therefore only ever grow.
objects/ and its six type subdirectories are a retired one-file-per-object layout, replaced by the
containers above; nothing in a format-3 repository writes into it, and init no longer creates it. A
repository initialized before this change may still have it on disk — its presence or absence is not
validated at open — and if so, one dormant diagnostic (scan_loose_file_temp_debris, kept for
.pobj.tmp. debris a format-3 repository can no longer produce) still reads it during verify.
Refs and Ref Logs
Ref pointer and ref-log storage is shared, not per-ref: every ref’s own pointer entry and log records
live in containers allocated once at init, not in a file named for that ref. A ref name does not
exist until branch create/tag create mints it, well after init — a per-ref file could never be
one of init’s own fixed names, so pointers and logs for every ref instead interleave inside:
refs/containers/pointer-index-{a,b}.container
refs/containers/log-a.container
refs/containers/log-b.container
refs/locks/<ref-name-storage-key>.lock
Locks remain per-ref files, one per ref name actually in use, unaffected by this: the storage key is the hex SHA-256 digest of the ref name bytes, the same digest used internally to attribute each container entry to its own ref.
pointer-index-{a,b}.container is an append-only, checksum-framed sequence of pointer entries; each
entry records one ref’s human-readable name, its published RefState object id, and the SHA-256 key
derived from that name. A ref’s current pointer is its last matching entry in the live slot —
republishing a ref appends a new entry rather than rewriting the old one, and which slot is live is
named by this container’s own generation log, not always a (see Compaction below).
It is useful for lookup and recovery, but is not trusted alone: verification checks pointer content
against RefState objects and ref-log evidence, the same as before.
received-index-{a,b}.container holds the same shape of entry for received refs — pointers
imported by prikk bundle import under the separate remotes/<name> namespace (DC-78 §D4), never
refs/by-id/: an imported RefState object keeps the origin repository’s own embedded ref name, which
could never agree with a locally renamed pointer, so received refs get their own container and key
space rather than a special case in the ordinary one. Last-entry-wins, the same as the ref-pointer
index, and compacted the same way. This index is never consulted by verify_repository directly —
every object a received pointer leads to is checked by the ordinary object-store scan regardless of
how it was discovered.
log-a.container/log-b.container hold every ref’s RefUpdate log records, interleaved by append
order; a reader filters to one ref’s own subsequence by that same per-record key. Each record carries
a signed RefUpdate envelope inline with frame magic, versioning, length, and checksum. Ref logs are
publication evidence when their record chain, referenced RefState objects, target Blocks, signatures,
and trust policy checks all hold — unchanged in meaning, only in storage shape. Slot b is allocated
alongside slot a at init and permanently unused: the ref log is DC-38/DC-69’s audit trail and must
never be compacted, so writes always target slot a (see Compaction below).
refs/locks/*.lock files are local synchronization files for ref-specific publication and repair, not
a root of trust. There is no longer a temporary-candidate mechanism: an append-only pointer entry has
no candidate value to stage before becoming durable, so a publish that used to write, sync, and
promote a candidate now durably appends the pointer entry directly, in one step.
refs/by-id/ and refs/logs/ are no longer initialized directories — nothing has read or written
either since ref publication moved into containers, so init no longer creates them, and a repository
missing them behaves identically to one that still has them from before this change (their presence or
absence is not validated at open).
refs/tmp/ is different and is genuinely required. init still allocates it, and verify lists it
on every run, so a repository missing it fails verification with directory is absent: refs/tmp.
Nothing has written into it since ref publication moved into containers, so the scan can only ever find
nothing — but the directory must exist for the scan to succeed.
Active Session
The default active-session paths are:
active/default/queue.wal
active/default/active.lock
active/default/ref-name
These files are runtime-written, not guaranteed members of a bare initialized repository.
queue.wal stores exact signed Patch envelopes before sealing. WAL records are load-bearing local
session state: they are the pending changes that seal replays and publishes, but they are not sealed
history until publication succeeds.
ref-name records which local branch ref owns a non-empty active WAL. Missing or malformed active ref
metadata on a non-empty WAL is an integrity issue because seal must not guess the publication target.
active.lock is a local synchronization file. It prevents concurrent active-session writers, but it is
not evidence of repository history or trust. Stale lock cleanup after a crash is manual today; see
concurrency and locking for the current operator boundary.
Trust Store
The repository-local MAINTAINER trust containers are:
trust/keys.container
trust/policy-{a,b}.container
Both are allocated empty at init, then appended to by prikk trust maintainer add/remove — no
name is created after init.
keys.container holds one append-only entry per adopted key id: the key id and its Ed25519 public key.
It has no slot pair and is never compacted — TOFU history must persist across key removal (see
Compaction below).
policy-{a,b}.container holds a sequence of complete policy snapshots in its live slot (named by
this container’s own generation log, not always a) — each add or remove appends the entire
current adopted-key-id list, not an incremental change; readers take the last complete snapshot, with
required = 1 meaning any one adopted key’s signature suffices (never stored — it is a constant, not
configurable). Seal checks the configured MAINTAINER signer against this repository-local policy
before publication, and verify checks publication envelopes against the same local trust boundary.
This trust store is authority for current publication-trust checks. It is not remote trust, global
identity, key rotation, hosted forge policy, or a multi-maintainer threshold system. Key revocation is
supported (prikk trust maintainer remove): a removed key’s material is retained internally (so a
different key presented later under the same id is still refused), but it no longer counts toward the
adopted set or reserves its case-folded id.
Compaction
Three containers accumulate entries that are superseded the moment a newer one lands:
pointer-index-{a,b}.container, received-index-{a,b}.container, and policy-{a,b}.container. A
ref’s or received ref’s pointer is only ever its last matching entry — everything earlier for the
same key is dead weight from the moment it is superseded — and a trust policy snapshot supersedes every
earlier snapshot outright, since each one already carries the complete adopted-key-id list.
Each of these three containers is paired with its own generation log
(pointer-index-generation.log, received-index-generation.log, policy-generation.log, all under
the same directory as the container they belong to) recording which slot — a or b — is currently
live. An empty generation log means no compaction has ever run for that container, and the live slot
is a. A reader resolves the live slot by reading the last complete record in the generation log; it
never assumes a.
prikk compact --pointer-index|--received-index|--trust-policy|--all reclaims the dead entries for
one or all three: it reads the currently-live slot, keeps only what is still current (the last entry
per key for the two indexes, the last snapshot for the trust policy), writes that reduced set to the
other slot, makes it durable, and only then appends a generation record naming the new slot live —
so a crash at any point before that append leaves the previous generation fully authoritative, and
retrying the compaction from scratch is always safe. prikk compact ... --plan-only reports what a
real run would reclaim without writing anything. Compaction never runs automatically; nothing else in
Prikk invokes it.
Compacting one of these containers takes the same per-container lock its own writers take (see
concurrency and locking), so a compaction run and an ordinary write to the
same container cannot interleave, and a real run and a --plan-only preview never report stale
numbers against each other.
Two container families never compact, deliberately — not because rotation is merely pending.
Object containers (containers/<type>/{a,b}.container) have no data model to compact against: nothing
is ever superseded or deleted (see Object Store above), so there is nothing for a
second slot to reclaim. The ref log (refs/containers/log-{a,b}.container) is DC-38/DC-69’s audit
trail, which must never be compacted. Both keep their b slot allocated at init and permanently
unused. The trust key container (trust/keys.container) has no slot pair at all — TOFU history must
persist across key removal, so it stays a single append-only file, never compacted.
Authority Model
| Path or data | Classification | Current meaning |
|---|---|---|
.prikk/FORMAT | Format gate | Required by repository open; 6 is the current format. Formats 1-5 are rejected at open, with no bridge and no in-place migration. |
containers/<type>/a.container | Content-addressed object authority | One checksum-framed record per object. Authority when the frame checksum verifies, the envelope validates, and its computed id/type match the requested object. objects/ is a retired remnant no longer created by init; authority for nothing. |
refs/containers/log-a.container (log-b.container reserved) | Publication evidence | Shared, append-only signed RefUpdate records for every ref, interleaved; authoritative only with valid chain, object, signature, and trust checks. |
trust/policy-{a,b}.container and trust/keys.container | Repository-local trust authority | Current local MAINTAINER trust input for seal and verify publication-trust checks. policy-{a,b} is compacted by prikk compact --trust-policy (the live slot is named by its own generation log); keys.container has no slot pair and is never compacted — TOFU history must persist across key removal. |
active/default/queue.wal | Local active-session state | Pending signed Patch envelopes before seal; load-bearing for the active session, not sealed history. |
active/default/ref-name | Local active-session metadata | Identifies which ref owns a non-empty active WAL; not sealed history. |
refs/containers/pointer-index-{a,b}.container | Mutable convenience pointer | Shared, append-only, last-entry-wins RefState pointer for every ref; fast current-state lookup and recovery target; not trusted without RefState/ref-log checks. Compacted by prikk compact --pointer-index; the live slot is named by its own generation log, not always a — see Compaction. |
refs/containers/received-index-{a,b}.container | Mutable convenience pointer | Same shape as the ref-pointer index, for imported remotes/<name> refs (prikk bundle import); not consulted by verify_repository directly. Compacted by prikk compact --received-index. |
*-generation.log (pointer-index-, received-index-, policy-) | Compaction state | Records which slot (a/b) is currently live for its own container; empty means a. Not history or trust evidence — see Compaction. |
active/default/active.lock, refs/locks/*.lock, and the four container locks (pointer-index.lock, log.lock, received-index.lock, policy.lock, all under refs/containers/ or trust/) | Local synchronization | Prevent concurrent writers, and (for the container locks) a concurrent prikk compact run; not history or trust evidence. Recoverable after a crash via prikk unlock — see concurrency and locking. |
cache/ | Initialized, rebuildable, non-root | Never authority; a corrupt or absent cache file is not an error and does not change any result. |
refs/tmp/ | Initialized, unwritten, required | init still allocates it; nothing writes into it since ref publication moved into containers, but verify lists it on every run, so its absence fails verification. Not authority for anything. |
objects/ (and its six type subdirectories), quarantine/, refs/by-id/, refs/logs/ | Retired, no longer initialized | init no longer creates these; nothing has written into any of them since object and ref publication state moved into containers. Not validated at open, so a repository initialized before this change keeps them harmlessly. Not authority for anything. objects/ alone still has one dormant reader — see Object Store. |
gc/ | Deferred/not present | No current initialized directory or released behavior. |
Deferred and Not Stable
Prikk does not provide in-place or history-preserving migration between any two formats. The
documented writable path is a newly initialized format-2 repository followed by deliberate worktree
re-authoring, which creates new NodeIds, objects, signatures, and history. Copying .prikk/ data or
editing FORMAT is not migration. This explicit transition does not promise general format stability.
prikk sync (RFC 116, RFC 117) and prikk merge (DC-74) have since shipped — see the
sync and merge guides. Still deferred: garbage collection,
cache rebuild semantics, quarantine enforcement, stable repository-format migration, backup/restore
workflows, remote trust, hosted forge semantics, complete branch management, remote-tracking, and
full cross-platform filesystem validation.
Claim-to-Source Anchors
| Claim | Source anchors |
|---|---|
Repository initialization creates the listed directories and writes .prikk/FORMAT. | layout.rs, DC-31 |
.prikk/FORMAT selects current format 6; formats 1-5 are rejected at open. | layout.rs, DC-40 |
Persistent objects are checksum-framed records appended into per-type containers allocated at init. | layout.rs, object_store.rs |
Six object types currently have initialized persistent object directories; RefUpdate is inline-only in ref logs. | layout.rs, object_store.rs, refs/container.rs |
| Ref storage keys are SHA-256 hex digests of human-readable ref names, shared by the pointer index, the log container, and per-ref lock files. | layout.rs, refs.rs |
| The ref-pointer index is a shared, append-only, last-entry-wins container holding every ref’s own current-pointer entry (name, RefState id, storage key). | refs/pointer_index.rs, refs.rs, data model |
| The ref-log container is a shared, append-only sequence holding every ref’s own signed RefUpdate envelopes, interleaved, with frame magic, checksums, and per-ref replay semantics. | refs/container.rs, refs.rs, durability and crash recovery |
| Active WAL and active ref metadata are runtime active-session state, not fresh-init files. | active.rs, wal.rs, durability and crash recovery |
| Trust policy and maintainer public-key files are written by the trust command and define current repository-local MAINTAINER trust. | trust.rs, layout.rs, security and signing setup |
| Verification checks object placement, ref pointer/log consistency, active WAL state, and publication trust within current limits. | verify.rs, integrity and recovery diagnostics, trust and threat model |
cache/ is initialized but not a root of trust; quarantine/ is retired and no longer initialized, and gc/ is not an initialized directory. | layout.rs, DC-31 |
The received-ref index is a shared, append-only, last-entry-wins container for imported remotes/<name> pointers, kept separate from refs/by-id/ because an imported RefState’s own embedded ref name can never agree with a locally renamed pointer. | received.rs, received_index.rs |
Three containers (ref-pointer index, received-ref index, trust policy) each have a generation log naming which slot is live, defaulting to a when empty; prikk compact reads the live slot, writes the reduced set to the other slot durably, then appends a generation record naming it live; --plan-only performs the same read with no write. Object containers and the ref log allocate an unused b slot and never compact; the trust key container has no slot pair at all. | generation.rs, compact.rs, lock.rs |
Provenance
This reference implements DC-31 as a documentation-only extension of the DC-24 current-state reference series. It adds no code, schema, CLI behavior, repository behavior, trust behavior, verification behavior, repair behavior, or repository-format stability guarantee.
Concurrency and Locking
This page is the authoritative current-state reference for Prikk’s local concurrency and locking model. It explains what the current lock files protect, how active-session and ref publication writes are serialized, how ref compare-and-swap checks fail, and where stale-lock recovery remains manual.
For physical paths and authority boundaries, see repository layout and authority. For local persistence and crash-recovery behavior, see durability and crash recovery. For verification and doctor diagnostics, see integrity and recovery diagnostics. For trust and signing boundaries, see the trust and threat model and the security and signing setup guide.
Core Caveats
- Prikk is early implementation software and is not a production Git replacement.
- Current locks are local lock files. They are not distributed locks, remote coordination, hosted-forge locks, or filesystem leases.
- There is no global repository lock today.
- Lock conflicts and stale-baseline ref publication conflicts both surface as
LockConflict, but they have different causes and operator responses. - Stale lock cleanup after a crash is manual today, through
prikk unlock— not automatic, not a doctor repair, and not gated on the tool’s own PID check, which is advisory only. There is no lock timeout or automatic lock stealing. - The active-session model uses one default active WAL and active ref metadata. It is not a multi-active-session model.
- Durability and recovery claims are supported by current unit and integration tests, not by a completed crash-matrix or fuzzing campaign.
- Repository mutation is exercised by project gates on Linux, macOS, and Windows (DC-87 Stage 2). Windows’ anchoring guarantee is weaker than Linux/macOS in one stated way — see platform support for the exact gap and which of the eight remaining durability guarantees (G5 retired in DC-98) are held, weaker, or documented no-ops there. Read-only commands are CI-gated on macOS and Windows too — see platform support.
.prikk/is not a stable repository format and there is no stable migration policy yet.
Lock Files and Scope
Prikk currently uses six lock types: the active-session lock, one per-ref lock, and four container locks (RFC 102 Stage 6 Step 2) — one per compacting container plus the ref log:
active/default/active.lock
refs/locks/<ref-name-storage-key>.lock
refs/containers/pointer-index.lock
refs/containers/log.lock
refs/containers/received-index.lock
trust/policy.lock
There is no temporary-candidate mechanism: ref publication writes into a shared, append-only pointer container directly, and an append-only record has no candidate value to stage before becoming durable.
The lock primitive is the same for every lock kind above. The store creates the lock file with
exclusive file creation. If the file already exists, acquisition fails with LockConflict. When
acquisition succeeds, the file body records the current process id, lock kind, and a note that stale
lock stealing is not implemented — see Stale Locks and Manual Cleanup
below for what that note no longer fully describes. The lock file and parent directory are
required-synced before acquisition succeeds. A post-create sync failure returns failure and
deliberately retains the lock as an actionable stale-lock state.
Lock release is best-effort file removal when the lock guard is dropped. If a process exits normally, that usually removes the lock. If a process dies while holding the lock, the file can remain and later commands fail closed instead of guessing whether the repository is safe to mutate.
The four container locks are acquired by whichever operation is touching that container — the writer
(ref publication, trust add/remove, bundle import) or prikk compact/prikk compact --plan-only — and
held for that operation’s whole critical section, never just the final write. Multi-container
operations (ref publication touches the pointer-index and ref-log locks together; trust add touches
only the policy lock, since the key container is not lockable) acquire their whole set through one
internal helper that sorts it into a single fixed order first, so no call site can express an inverted
acquisition order.
These lock files are local synchronization state. They are not history, trust evidence, publication evidence, or object identity.
Active Session Locking
The default active session stores pending Patch envelopes before seal:
active/default/queue.wal
active/default/ref-name
active/default/active.lock
The active lock protects writes to this active-session state. Current command paths acquire
active.lock before mutating or sealing the default active WAL:
- worktree patch authoring holds the active lock across the active-WAL emptiness/ref-owner guard, patch authoring boundary, and final WAL append;
- rollback-draft append acquires the active lock before appending rollback-draft state;
- the active-session append helper acquires the active lock before appending a signed Patch envelope;
- doctor WAL-tail repair acquires the active lock before its final publication guard and holds it through verification, truncation, and the post-repair report;
- seal acquires the active lock before replaying the WAL, checking active ref metadata, publishing the ref, and draining active state after successful publication.
The active WAL is paired with active ref metadata. A non-empty active WAL must have valid metadata identifying the local branch ref that owns those pending records. Missing, malformed, or mismatched metadata fails closed; seal does not guess the publication target.
Current worktree authoring is single active-commit-before-seal for the default active WAL. A second commit before seal either loses the active lock or, after the first commit releases the lock, sees the non-empty WAL and fails with guidance to seal first.
Ref Publication Locking and CAS
Ref publication uses a ref-specific lock and repeated expected-current checks. These are related but distinct mechanisms:
- the per-ref lock serializes Prikk publications and signer-backed completion for the same ref;
- the expected-current checks reject stale-baseline publication when the caller’s expected previous RefState no longer matches the current pointer.
A lock conflict such as active lock already exists or ref lock already exists means another process
may still be holding a local lock, or a stale lock file may remain after a crash. A conflict such as
ref CAS mismatch means the ref’s current pointer did not match the publication’s expected previous
RefState. That is not fixed by deleting a lock file; the caller must re-read the current ref state and
rebuild or retry the publication from the new baseline.
Current ref publication is scoped to one ref:
- Validate the publication inputs.
- Acquire
refs/locks/<ref-name-storage-key>.lock. - Persist the signed RefState object into its container.
- Validate pointer/log state, including the empty state required for unborn-ref creation.
- Check the current ref pointer against
expected_previous_ref_state_id. - Append and required-sync the new pointer entry to the shared pointer-index container — the publication commit point. An append-only record has no candidate value to stage first, so this one durable append is both the check-then-write step and the promotion step the pre-container design needed two for.
- Append and required-sync exactly one signed RefUpdate record to the shared log container.
- Confirm pointer/log agreement before active state is removed.
Those checks prevent silent overwrite when the on-disk ref pointer has moved away from the caller’s expected baseline. They are not a global repository transaction, a distributed consensus protocol, or a proof that every crash point has been exhaustively tested.
Seal takes active.lock first and then enters ref publication, which acquires the ref lock. Current
code does not acquire those locks in the reverse order.
Interrupted Publication Locking
The pointer-index append is the publication commit point. If interruption leaves the pointer exactly
one transition ahead of the log, only signer-backed seal retry may finish publication. It takes the
active lock and the same ref-specific lock, revalidates retained WAL, RefState, Block, sequence,
old/new ids, and maintainer trust, then appends the exact deterministic RefUpdate. A structurally
incomplete final log frame may be truncated only by that path after the complete prefix verifies; the
shared log container has no pre-append refusal on an existing incomplete tail (unlike the pointer-first
check above), since a torn tail belonging to one ref never enters any other ref’s own filtered
subsequence and so cannot block a different ref’s publish.
Doctor diagnoses interrupted publication but does not sign, append, promote, or reconstruct a missing pointer. The former format-1 missing-pointer repair is refused in 0.18.0. The sole bounded legacy mutation is signer-backed seal completion of one exact format-1 log-ahead transition with matching retained active state.
Container Locking and Compaction
Four containers — the ref-pointer index, the ref log, the received-ref index, and the trust policy
container — each have their own lock, held for the whole critical section by whichever operation is
touching that container: an ordinary writer (ref publication, trust add/remove, bundle import) or
prikk compact/prikk compact --plan-only. This excludes a compaction run and an ordinary write from
interleaving; it is not about protecting the container’s content the way CAS protects a ref’s
baseline, but about protecting which physical slot is currently authoritative while it is being
read, written, or switched. For what a container lock actually protects against and how compaction
itself works, see repository layout — Compaction.
Ref publication acquires the ref-pointer-index and ref-log container locks together, in that order, in
addition to (not instead of) the per-ref lock above. Trust add/remove acquires the trust-policy
container lock in addition to active.lock. Bundle import acquires only the received-ref-index
container lock — it previously acquired no lock at all for this write, which is what surfaced the
container-locking work in the first place.
Object Container Writes Are Not Among the Four Locked Containers
The content-addressed object containers (write_object_to_container, index.rs) are not one of
the four containers above — there is no LockableContainer variant for them, and no dedicated lock
file protects a write into one. An object write is safe under concurrency only when something else
already holds a lock across it:
seal(crates/prikk-cli/src/seal.rs) acquiresActiveLockbefore persisting the WAL’s Patch envelopes and building a Block, and holds it for the whole operation — object writes there are incidentally serialized against any other session by the same lock that serializes everything elsesealdoes.import_bundle(bundle.rs) does not. Its object-writing loop runs before any lock is acquired at all; the received-ref-index container lock above is taken only afterward, to protect the received-pointer write, and covers none of the object writes that already happened.
This matters because write_object_to_container’s own write path (index.rs) reads the target
container’s current length, then appends at that offset and records it in the index — a
read-then-append that is not atomic across two unsynchronized writers. Content-addressed
idempotency (RFC 102 Stage 3’s same-id-same-bytes no-op, preserving the old publish_immutable
contract) makes writing the same object twice from two racing writers safe, but does not cover
two different concurrent unprotected appends into the same container computing offsets against
the same stale length.
Known and accepted, not fixed here. This predates DC-98 — it was previously recorded only as a
comment pointing at FINDINGS.md and a DurabilityContract::publish_immutable row, both since
removed, leaving it unregistered anywhere. Re-registered here in its own terms rather than by
reference to either. The mechanism that would close it (a dedicated object-container lock, or
extending import_bundle to hold ActiveLock across its object writes) is out of scope for this
page to design; it is deferred, tracked follow-up scope, not a defect this document is claiming is
fixed.
Stale Locks and Manual Cleanup
If a process dies while holding any lock — active.lock, a ref lock, or one of the four container
locks — the lock file can remain. Current Prikk does not steal stale locks, expire them, or use doctor
to clear them, and this is deliberate, not merely unimplemented: automatically clearing a lock whose
process turns out to still be running would let two writers hold the same container simultaneously,
the exact race locking exists to prevent.
prikk unlock is the supported recovery path. A bare invocation lists every currently held lock, its
recorded process id, and a best-effort advisory on whether that process still appears to be running
(checked with kill(pid, 0) on Linux and macOS; unknown on other platforms). This check is
asymmetric on purpose: a positive result — the process still appears to be running — is reliable
evidence to refuse, because the check actually found it. A negative or unknown result is not
evidence the lock is safe to clear, since PID reuse after a reboot or PID-namespace isolation inside a
container can both make a genuinely running process appear absent. prikk unlock --lock <path> clears
one specific lock, after printing its details, and requires typing yes at an interactive prompt
unless --yes is passed for scripting — the tool never decides a lock is stale on its own; it reports
what it can check and lets the operator supply the fact it cannot.
Manual cleanup remains an operator decision, now made through a supported command rather than deleting
the file directly. It is only safe after confirming that no Prikk process is still writing the
repository. If the active WAL is non-empty, preserve the repository state and use verify / doctor
diagnostics before deciding whether clearing any lock is appropriate. Do not clear a lock to work
around a ref CAS mismatch; that error means the publication baseline is stale, not that a lock file
is blocking progress.
Concurrent Operations Supported Today
The current model is conservative:
- one writer can hold the default active-session lock;
- one writer can hold the lock for a specific ref;
- different ref locks are separate files, so current storage code does not serialize all refs through a single global lock — but ref publication to any ref also acquires the shared ref-pointer-index and ref-log container locks, so two publications to different refs still serialize against each other on those, even though their per-ref locks differ;
- one writer or one
prikk compactrun can hold each of the four container locks; - the default active WAL still serializes public command flows that author then seal active state;
- read-only verification, doctor analysis, history inspection, checkout planning, merge evidence, and merge planning do not create these lock files, though they still read mutable repository state.
This does not mean Prikk supports multi-user concurrent repository mutation, branch transactions, remote synchronization, or race-free behavior under arbitrary concurrent filesystem modification.
Deferred and Not Promised
Still deferred: multi-active sessions, distributed locking, remote sync, hosted-forge lock semantics,
branch transactions, lock expiry, automatic stale-lock recovery, broad active-session recovery,
complete crash-matrix testing, filesystem fault injection, fuzzing for WAL/ref-log recovery, macOS and
Windows filesystem validation, stable repository-format migration, backup/restore tooling, and
production-readiness claims. A best-effort, advisory PID check now exists (prikk unlock, see
Stale Locks and Manual Cleanup above) — what remains deferred is
automatic recovery, not the check itself; the tool still requires an explicit operator decision for
every lock it clears.
Claim-to-Source Anchors
| Claim | Source anchors |
|---|---|
| Active and ref locks use exclusive anchored file creation, required-sync the lock file and parent directory, retain a stale lock on acquisition-sync failure, and attempt best-effort removal on drop. | lock.rs, DC-37 |
Existing lock files fail closed as LockConflict, and current locks have no stale-lock stealing. | lock.rs, durability and crash recovery |
Active-session append holds active.lock before appending to the active WAL. | active.rs, wal.rs |
Worktree patch authoring holds active.lock across the active-WAL guard and final WAL append, enforcing the current seal-before-second-commit behavior. | node_authoring.rs, DC-15 |
Rollback-draft append acquires active.lock before appending rollback-draft state. | rollback_draft.rs, DC-10 |
Seal acquires active.lock, validates active ref metadata, publishes through the ref store, then drains active state after successful publication. | seal.rs, refs.rs, durability and crash recovery |
| Non-empty active WALs require valid active ref metadata; missing or malformed metadata is an integrity issue. | active.rs, verify.rs, integrity and recovery diagnostics |
| Ref publication uses a per-ref lock, expected-current checks, signed RefState persistence, a durable pointer-index append as the commit point, then exactly one signed RefUpdate append to the shared log container. | refs/publication.rs, refs/pointer_index.rs, refs/container.rs, DC-38 |
Ref CAS mismatch returns LockConflict and is distinct from an existing lock-file conflict. | refs.rs, lock.rs |
| Unborn ref publication is allowed only when the pointer is absent and the ref log is empty with no trailing partial bytes. | refs.rs, seal.rs, DC-13 |
| Doctor refuses format-1 missing-pointer reconstruction; exact interrupted publication completion requires signer-backed seal under the active and ref locks. | seal.rs, doctor.rs, DC-38 |
| Doctor repairs are opt-in and do not clear unsafe active sessions or define stale-lock cleanup. | doctor.rs, integrity and recovery diagnostics, DC-29 |
Four container locks (ref-pointer index, ref log, received-ref index, trust policy) are acquired by writers and prikk compact alike, sorted into one fixed order by a single acquisition helper before any lock is taken. | lock.rs, compact.rs |
prikk unlock lists every held lock with an advisory (not authoritative) liveness check of its recorded process id, and clears one named lock only after explicit confirmation or --yes. | unlock.rs, prikk-cli/src/unlock.rs |
| Repository path and durability claims for mutation remain limited by current test evidence and gates exercised on Linux, macOS, and Windows (DC-87 Stage 2); read-only commands are CI-gated cross-platform as of DC-71. | durability and crash recovery, path and worktree safety, platform support, DC-28, DC-32 |
Provenance
This reference implements DC-33 as a documentation-only extension of the DC-24 current-state reference series. It adds no code, schema, CLI behavior, lock behavior, commit behavior, seal behavior, verification behavior, doctor behavior, trust behavior, repository behavior, release semantics, or repository-format stability guarantee.
Path and Worktree Safety
This page is the authoritative current-state reference for Prikk’s repository path validation and worktree write-safety boundaries. It describes the current implementation through 0.17.6 and is grounded in the code, released RFCs, and implementation status records listed in the anchor table at the foot of the page.
For physical repository layout and .prikk/ authority boundaries, see
repository layout and authority. For trust and threat boundaries, see the
trust and threat model. For local lock and stale-lock behavior, see
concurrency and locking.
Core Caveats
- Prikk is early implementation software and is not a production Git replacement.
- Repository paths currently use a conservative ASCII-only subset.
- Unicode NFC normalization is not implemented; non-ASCII repository paths are rejected.
- Cross-platform conservative checks are enforced even on Unix, including Windows reserved names and case-insensitive collision rejection.
- The collision-rejection rule is ASCII case-folding only (
to_ascii_lowercase), applied uniformly to repository paths, branch ref names, tag ref names, and maintainer trust key ids (DC-72). It is not Unicode normalization: an NFC-composed and NFD-decomposed spelling of the same visible name are different byte sequences and are not folded together. Repository paths cannot reach this case today because non-ASCII repository paths are rejected outright (previous bullet); branch and tag ref names have no such ASCII restriction, so an NFC/NFD pair there is a live, recorded, un-rejected collision. Locale-dependent case rules (Turkishİ/i, Germanß/SS) are outside ASCII folding for the same reason. Closing this needs a normalization dependency prikk-store’s dependency allowlist does not currently permit (tools/release-policy/src/boundary/placement.rs). - Repository-path collisions are rejected at
seal, not atcommit(DC-72) —commitrecords a case-colliding pair into the active WAL without error;sealcomputes the full state root over all live paths and rejects there. Nothing enters sealed, verifiable history either way, but the rejection surfaces later than the action that introduced it. Recorded as a known ergonomic gap, not fixed — moving the check earlier is a separate change to the commit path. - Branch and tag ref-name collisions, and maintainer trust key id collisions, are rejected only when the name is first created (no prior published state for that exact name) — an ordinary pointer update to an already-published ref does not re-scan every other ref.
- Symlink authoring and symlink materialization are deferred.
- Current materialization safety is check-then-write. It is not an
openat/O_NOFOLLOWdesign, not a canonical realpath proof, and not a race-free guarantee under concurrent worktree modification. - Repository mutation is exercised by project gates on Linux, macOS, and Windows (DC-87 Stage 2). Windows’ anchoring guarantee is weaker than Linux/macOS in one stated way — see platform support for the exact gap and which of the nine durability guarantees are held, weaker, or documented no-ops there. Read-only commands are CI-gated on macOS and Windows too — see platform support.
- Stable path-format policy, path-policy configuration, Git path compatibility, stable repository-format migration, and complete checkout semantics remain deferred.
Repository Path Shape
Prikk’s RepoPath is a validated repository-relative path string. The current accepted shape is:
- non-empty;
- ASCII only;
- repository-relative, with no leading
/; - slash-separated with
/; - made of non-empty components;
- not targeting the top-level
.prikkmetadata directory; - free of the rejected component forms listed below.
RepoPath is a logical repository path, not a host path. Store code joins it to the repository root
only after validation.
Rejected Path Forms
The current validator rejects:
- empty paths;
- absolute paths that start with
/; - backslashes;
- colon characters;
- non-ASCII bytes;
- control bytes
0x00through0x1Fand0x7F; - empty path components;
.and..components;.prikkas the first component, case-insensitively;- components ending in a space or dot;
- Windows reserved component basenames:
CON,PRN,AUX,NUL,COM1throughCOM9, andLPT1throughLPT9; - duplicate paths in a path set; and
- case-insensitive collisions in a path set.
The Windows reserved-name check is matched on the component basename before the first .. It is not a
complete Windows path policy and does not include COM0 or LPT0.
The .prikk rejection applies to the first component only. A later .prikk component is not rejected
by that specific validator rule. Worktree authoring separately skips the top-level .prikk/ directory.
Snapshot Manifest Paths
Snapshot manifests decode path bytes as UTF-8 text, parse each path through RepoPath, and then
validate path ordering and collisions. Manifest entries must be sorted by repository path. Duplicate
paths and case-insensitive collisions are rejected.
Snapshot entries also carry length-framed content bytes. The path-safety check does not inspect file content; it validates where the content may be represented or materialized.
Materialization Safety
Snapshot materialization is opt-in through prikk checkout --snapshot-materialize. It writes files only
from a validated snapshot manifest. Patch materialization is opt-in through
prikk checkout --patch-materialize and writes the supported patch replay result through the same
shared materializer.
For each materialized file, the current implementation:
- joins the validated
RepoPathto the repository root; - checks that the joined path lexically starts with the repository root;
- checks each existing parent directory with symlink-aware metadata and refuses symlink parents;
- refuses non-directory parent paths;
- checks an existing final target with symlink-aware metadata;
- refuses symlink targets;
- refuses non-file targets;
- leaves existing files unchanged when bytes already match;
- refuses to overwrite existing files with different bytes;
- writes new file bytes through the current atomic file-write helper; and
- never removes extra worktree files during ordinary snapshot or patch materialization.
This is intentionally conservative, but it is not complete symlink-escape protection. The containment check is lexical rather than canonicalized realpath proof. Parent and target checks happen before the write. A concurrent process that mutates the worktree between checks and writes is outside the current guarantee.
Deletion Safety
Patch deletion is a separate opt-in path:
prikk checkout --patch-materialize-delete [path] [--ref REF]
The command removes only files that the replayed supported patch chain explicitly removed with a
DeleteFile operation. Before removal, Prikk checks the current worktree target with symlink-aware
metadata, refuses symlink targets, refuses non-regular targets, and requires the current bytes to match
the deleted file’s old Blob precondition bytes.
Already-absent deletion targets are counted separately. Arbitrary untracked files are never deleted, and general checkout pruning remains deferred.
Worktree Authoring Safety
prikk commit enumerates regular worktree files, skips the top-level .prikk/ metadata directory, and
validates identity-bearing paths through RepoPath.
The current authoring path:
- rejects symlink entries because symlink authoring is out of scope;
- rejects non-regular entries;
- rejects non-UTF-8 host paths before they can become repository paths;
- validates each repository-relative path through
RepoPath; - normalizes regular file modes into Prikk’s supported mode representation; and
- rejects snapshot-only published baselines as worktree-authoring identity authority.
Worktree authoring does not infer renames. A move is represented as a deletion plus a creation in the current supported authoring model.
Ref and Tag Name Safety
prikk branch create, prikk tag create, and a branch’s first seal (the moment that publishes a
ref with no prior published state) reject a new ref name that ASCII-case-folds to an existing ref
name other than itself. An ordinary pointer update to an already-published ref does not re-run this
check.
Branch names (heads/...) and tag names (tags/...) are folded and compared only within their own
namespace: validate_local_branch_ref/validate_local_tag_ref require the exact, case-sensitive
heads//tags/ prefix, so the two namespaces never fold into each other. heads/Main colliding with
heads/main is rejected; tags/Main alongside heads/main is not a collision.
Ref names have no non-ASCII restriction — unlike repository paths, a branch or tag literally named
café is accepted. Because the fold is ASCII-only, an NFC-composed and an NFD-decomposed spelling of
the same name are not recognized as colliding; see the ASCII-folding caveat above.
Maintainer Trust Key Id Safety
A maintainer key id is a field inside a trust-container record, not a filesystem path component — RFC
102 Stage 5 replaced the earlier {key_id}.pub-under-.prikk/trust/keys/maintainer/ storage, which is
what originally motivated checking it the same way a repository path is checked. The same checks are
still applied, now for their own reasons rather than a filesystem hazard:
- storage-safe character allowlist (ASCII alphanumeric,
-,_) — kept as a conservative constraint on the id shape, independent of any current storage mechanism; - Windows reserved device stem rejected regardless of host OS (
CON,PRN,AUX,NUL,COM1throughCOM9,LPT1throughLPT9) — this is the same checkRepoPathuses, shared rather than duplicated; - case-insensitive collision against every other currently-adopted key id is rejected — a semantic guard against operator confusion and ambiguous audit trails now, not a case-insensitive-filesystem workaround. A key id removed from the active policy no longer reserves its case-folded name.
trust maintainer add adds a new key id, confirms idempotently if it already matches, and refuses if it
conflicts with a different key under the same id (DC-78’s TOFU enforcement) — re-adding an unchanged
key_id is not treated as colliding with itself. trust maintainer remove revokes a key id from the
active policy; the key’s own material is retained internally, so a different key presented later under
the same id is still refused even after removal.
Deferred and Not Promised
Still deferred: Unicode NFC normalization, non-ASCII repository paths, symlink authoring, symlink materialization, full platform path matrix, Git path compatibility, path-policy configuration, stable repository-format migration, complete checkout pruning, complete branch switching, production merge execution, and race-free worktree mutation hardening.
Current checks are deliberately strict so future path policy can expand from a conservative baseline.
Claim-to-Source Anchors
| Claim | Source anchors |
|---|---|
RepoPath accepts only the current ASCII, repository-relative, slash-separated subset. | path.rs, path/tests.rs, DC-32 |
The .prikk validator rule applies to the first component case-insensitively. | path.rs, repository layout |
| Duplicate paths and case-insensitive collisions are rejected. | path.rs, snapshot.rs |
Snapshot manifests decode UTF-8 path bytes, parse RepoPath, enforce sorted paths, and length-frame content bytes. | snapshot.rs, snapshot checkout guide |
| Snapshot materialization writes only validated snapshot entries and uses the shared safe materializer. | worktree.rs, snapshot materialization guide |
| Patch materialization writes supported replay results through the shared safe materializer. | patch_checkout.rs, patch materialization guide |
| Materialization checks lexical root-containment and refuses symlink parents, symlink targets, non-file targets, and conflicting existing files. | worktree.rs, path.rs store adapter |
| Materialization writes use the current atomic file-write helper. | fsutil.rs, worktree.rs |
| Patch deletion is opt-in, deletes only explicit replay deletions, and requires current bytes to match old Blob precondition bytes. | patch_checkout.rs, patch deletions guide |
Worktree authoring skips top-level .prikk/, rejects symlinks/non-regular entries, rejects non-UTF-8 paths, validates through RepoPath, and rejects snapshot-only baselines. | node_authoring.rs, worktree patch guide |
Current trust/threat docs treat .prikk private paths and absolute host paths as sensitive diagnostics material. | trust and threat model, patch algebra reference |
Repository-path collisions are rejected at seal (state-root derivation), not at commit. | state_root.rs, seal.rs |
| Branch and tag ref names reject a case-insensitive collision against another ref in the same namespace, checked only at first publication. | refs/publication.rs, refs.rs, dc72_path_safety_collisions.rs |
| Maintainer trust key ids reject a Windows-reserved stem and a case-insensitive collision against another stored key id. | layout.rs, trust.rs, dc72_path_safety_collisions.rs |
Provenance
This reference implements DC-32 as a documentation-only extension of the current-state reference series. It adds no code, schema, CLI behavior, checkout behavior, materialization behavior, worktree authoring behavior, repository behavior, trust behavior, verification behavior, release semantics, or stable path policy guarantee.
DC-72 (NFR-SEC-03 path-safety conformance) added the ref/tag-name and maintainer-key-id collision and reserved-name checks this page now documents, and the ASCII-folding/seal-timing caveats above. That work was code, not documentation-only; this page’s provenance note is scoped to what DC-32 originally contributed, not to every later increment that changed the behavior it describes.
Data Model
This page is the authoritative current-state reference for Prikk’s data model. It describes what has
shipped on main — not necessarily the latest tagged release, see README.md’s Current
Status for that boundary — and
is grounded in the code, released RFCs, and implementation status records listed in the anchor table at
the foot of the page.
Core Caveats
- Prikk is early implementation software and is not a production Git replacement.
.prikk/is Prikk’s native repository format and is not Git-compatible storage.- Ref pointers are mutable, for convenience and recovery, not roots of trust.
- Durability and recovery claims are supported by current unit and integration tests, not by a completed crash-matrix or fuzzing campaign.
- Repository mutation is exercised by project gates on Linux, macOS, and Windows (DC-87 Stage 2). Windows’ anchoring guarantee is weaker than Linux/macOS in one stated way — see platform support for the exact gap and which of the nine durability guarantees are held, weaker, or documented no-ops there. Read-only commands are CI-gated on macOS and Windows too — see platform support.
- Stable repository-format migration, complete branch management, remote-tracking, hosted forge
trust, and plugin execution remain deferred.
prikk sync(RFC 116) and tag travel/adoption (RFC 117) andprikk merge(DC-74) have since shipped — see the sync and merge guides.
Trust, signature, and threat-boundary caveats live in the
trust and threat model. The local persistence and crash-recovery boundary
lives in the durability and crash recovery reference. The physical
.prikk/ layout and authority-vs-pointer/cache boundary lives in the
repository layout and authority reference. Local lock and ref
compare-and-swap behavior lives in the concurrency and locking reference.
Object Identity
Prikk objects are typed, versioned envelopes. An object id is SHA-256 over a domain-separated preimage containing the object type, schema version, payload length, and unsigned canonical payload bytes. Signatures live outside that identity preimage, so adding or sorting signatures does not change the object id.
New envelope serialization and repository writes require a strict signature sequence. Ed25519 signatures must be 64 bytes, duplicate signature tuples are rejected, and signatures are ordered by key-id bytes, signer-role code, algorithm code, then signature bytes. Advisory signature timestamps do not affect that order. Format-1 verification preserves older structurally readable bytes and reports malformed shape, duplicate, or non-canonical ordering as warnings instead of rewriting them.
The current object model includes persistent Patch, Block, RefState, Blob, Tag, and RecognitionClaim
object directories. Tag objects are produced by public command surfaces — prikk tag create and
sync adopt-tag both create them (RFC 117) — this page’s claim otherwise is stale and corrected here.
Attestation remains genuinely unconstructed: the object type and directory are defined, but no
production code path builds one. RefUpdate is an object-envelope type stored inline in ref logs rather
than as a persistent object-store directory. BlockSummaryCache and RecoveryNote are explicitly not
roots of trust.
Patch and Operation Model
A Patch is the identity-bearing unit of logical change. Its payload contains one or more ordered
operations, sorted parent Patch ids, optional intent, optional preconditions, and an identity-bearing
purpose. PatchPurpose::Normal is the default by omission. PatchPurpose::RollbackDraft is encoded
explicitly and survives WAL-to-object persistence for rollback classification.
Current production authoring creates node-addressed patches from the worktree. It derives the baseline from authoritative replay of the published branch tip, or from an empty genesis baseline for an unborn branch ref. It rejects snapshot-only baselines without node identity for worktree authoring.
Blocks
A Block is an immutable sealed history unit. Its payload records sorted parent Block ids, Block kind,
Patch ids in canonical Block order, a state Merkle root, and an optional snapshot Blob reference.
Seal creates schema-2 Root Blocks with zero parents for unborn refs and schema-2 Normal Blocks with
exactly one schema-2 parent for refs with an existing published tip. Merge Blocks with exactly two
parents are supported (DC-75) and record both parents, a mainline pointer, and the merge baseline,
which verify re-derives rather than trusts. Repair and Import Blocks remain unauthorized until a
later design defines their state derivation.
The state root commits to the complete replay-derived live-node set in canonical path order. Each leaf binds the exact repository path, nonzero NodeId, node kind, normalized mode, and either the file Blob ObjectId or opaque UTF-8 symlink target. Binary Merkle reduction promotes an odd final hash unchanged. Patch ids, tombstones, implicit directories, snapshots, and caches are not state entries. Verification replays every Block from empty state or its one parent and rejects a root mismatch, missing evidence, invalid path/mode/kind/content state, or mixed Block schema lineage. Snapshots and caches may be used only as checked auxiliary data; they cannot override replay.
Tags
A Tag is a named, signed pointer into history, created by prikk tag create or sync adopt-tag
(RFC 117). Its payload carries a local pointer half and a portable identity half, and the
distinction is the point of the object:
target_block_id— the local pointer: a Block this repository can resolve directly.patch_set_digest— the digest oftarget_block_id’s own patch closure (compute_patch_set_digest_from_block), computed at creation time. Two repositories holding the same patches produce the samepatch_set_digestindependently, by construction — this is the value a tag’s portability across repositories depends on, sincetarget_block_iditself does not survive a move: blocks diverge between repositories by design even when the underlying history is identical.patch_count— the number of distinct patch ids in the closurepatch_set_digestcovers. Not new information (the digest’s own preimage already hashesDOMAIN ‖ count ‖ sorted ids), exposed as a separate field so a resolver can prune a candidate by size before hashing it. A hint that narrows, never an authority — a wrongpatch_countcan only cause a right candidate to be skipped or extra candidates hashed; it can never produce a wrong resolution, because the digest still has to match.
TagPayload also carries name, an optional message, the same no-clock created_at sentinel every
other current-write payload uses, and author_key_id. All seven fields are admitted at schema_version
1 — the owner ruled (2026-08-23) that Tag’s schema window stays closed rather than minting a schema 2
for patch_set_digest/patch_count, on the standing premise that no production repository holds a tag
yet. This is a two-way, permanent incompatibility: a Tag written before patch_set_digest/
patch_count existed will not decode against the current 7-field reader, and the reverse is also true —
see the 0.23.0 CHANGELOG entry for the
full consequence, since it reaches prikk verify, not only prikk tag list.
Recognition Claims
A RecognitionClaim is a signed assertion, under the signer’s key, that specific patches were sealed
into a specific block — nothing more. It exists so a sender can tell a receiver what a block contains
before the receiver holds that block: the claim is deliberately never existence-checked against the
objects it names, unlike every other reference in this data model. Its payload is minimal by design
(RFC 115 Stage 2 D3): block_id, patch_ids (the block’s own order, verbatim — not sorted, not
deduplicated, non-empty), and parent_block_ids (the block’s own parents, verbatim — not sorted, not
deduplicated, may be empty). It carries no signer key_id (the signature preimage already binds it), no
timestamp, and no project/genesis binding — each omission is deliberate, not an oversight. See
Recognition claims and sync relations
for how it relates to Block and Patch.
Refs and Publication
RefState is the content-addressed state for a branch or tag ref. A ref pointer entry, in a shared
append-only container holding every ref’s current pointer, stores the current RefState id for
convenience and recovery, but the pointer is not itself the root of trust. RefUpdate records are
signed envelope entries in a shared append-only ref-log container and link old and new RefState ids,
target Block id, update sequence, a schema-1 no-clock sentinel, and maintainer key id. The created_at
field is exactly zero for current writes and is not a trusted creation or event timestamp.
Publication is guarded by ref-specific locking and compare-and-swap checks. The concurrency and locking reference owns the detailed lock/CAS behavior. Seal persists WAL Patch envelopes, creates a signed Block and RefState, durably appends the authoritative ref pointer as the publication commit point, appends exactly one signed RefUpdate log entry, confirms pointer/log agreement, then drains the active WAL and active ref metadata.
Received Namespace
Imported history (bundle import, sync accept) lands under a received pointer, always named
remotes/<origin ref name> — a distinct index from refs/by-id/’s ordinary ref-pointer container, kept
in its own small append-only format. A received RefState keeps its origin’s own embedded ref_name
(rewriting it would invalidate the object’s content-addressed identity and signature), so a pointer
declared as remotes/heads/main could never agree with a payload that still says heads/main under the
ordinary pointer container’s own consistency check — storing received refs separately sidesteps that
conflict rather than special-casing the check to allow it.
Import never advances a local ref. A received pointer is discoverable by name and nothing more;
turning received history into local history is an ordinary merge, using machinery that already
exists. The received-pointer index is never read by verify_repository — every object a received
pointer leads to (RefState, Block, Patch, Blob, Attestation) is an ordinary object-store entry, checked
exactly like any other by the existing type-based object scan, so accepting received history adds no
new verification path, only a new way to discover a receiver’s own object graph by name.
Sync and Exchange Artifacts
Three wire formats move information between repositories. None is a persistent object type or a root of trust — each is representational, not frozen (RFC 114 §3): every object it carries has identity already frozen elsewhere, and the artifact itself carries none of its own.
PSYNCSU1(sync summary) — one message per repository: every localheads/*ref, each with its own patch-set digest and patch count. Answers “are we the same?” without moving a single patch id. Branches only;remotes/*andtags/*are excluded deliberately, not by oversight.PSYNCHV1(have-list) — one ref, its declared patch-set digest, and the full patch-id list the digest is over. Sent receiver → sender so the sender can compute the delta. The digest is always recomputed over the decoded list and checked, never trusted from the wire.PEXCH002(exchange artifact) — the patch-level payload itself, built bysync buildand consumed bysync accept. Six sections in order: the declared patch-set digest; the ordered Patch list in the sender’s own application order; every Blob any carried patch references; author key material (continuity only, never a trust decision); recognition claims (may be empty); and Tag objects (may be empty — every tag whosetarget_block_idlies within the synced ref’s ancestry). A carried Tag is reported on accept, never adopted — adoption issync adopt-tag, a separate, explicit, receiver-signed act.PEXCH002supersededPEXCH001(RFC 117 stage 3, adding the Tag section) as a format revision, not a migration: aPEXCH001byte stream is refused outright on read, since the artifact is transient in-flight data that never becomes repository history, so there is nothing to preserve across the bump.
Every declared count in PEXCH002 (patches, blobs, author keys, claims, tags) is checked against a
caller-supplied ceiling at the moment it is read, before that section’s loop runs — not after decoding
everything and counting.
Active WAL and Recovery Boundary
The active WAL stores exact signed Patch envelopes before sealing. WAL append requires a Patch envelope with at least one signature, writes a checksummed record, and fsyncs the WAL file. WAL replay reads valid records from the start and reports incomplete trailing bytes separately from checksum failures.
The detailed persistence, seal-publication, and recovery framing lives in the durability and crash recovery reference.
The current active-session model is single-commit-per-active-WAL. Active ref metadata records which branch ref owns a non-empty active WAL. Missing or malformed active ref metadata on a non-empty WAL is an integrity issue; stale metadata on an empty WAL is local debris.
Doctor repair is intentionally narrow. It can truncate an incomplete trailing active-WAL record after
the preceding records verify. It does not reconstruct missing ref pointers, sign or append RefUpdates,
synthesize missing objects, repair malformed logs, or prove crash behavior beyond current test
evidence. Exact interrupted ref publication completion belongs to signer-backed seal retry.
Replay, Checkout, Verify, and Doctor
Replay and lifecycle semantics live in the internally scoped prikk-replay crate, while prikk-store
remains the repository integration crate for layout, refs, WAL, active sessions, object storage,
verification, doctor, and worktree integration. prikk-replay is not a stable external Rust API.
Repository verification is read-only. It checks object placement, envelope decoding, object identity, Block references, ref pointer and log consistency, active WAL checksums, active WAL metadata health, sealed rollback Patch classification, and publication trust for publication envelopes. Doctor converts verification results into actionable diagnostics and exposes only the narrow repairs described above. The diagnostic catalog lives in the integrity and recovery diagnostics reference.
Deferred
prikk sync (RFC 116, RFC 117) and prikk merge (DC-74) have since shipped — see the
sync and merge guides. Still deferred: stable
repository-format migration, complete branch management, remote-tracking, hosted forge trust,
audit/plugin execution, persisted proof or witness objects, general rollback authorization,
multi-maintainer publication policy, and full cross-platform filesystem validation.
Claim-to-Source Anchors
| Claim | Source anchors |
|---|---|
| Object ids derive from type, schema version, payload length, and unsigned canonical payload. | id.rs, envelope.rs, DC-09 |
| Signatures are outside object identity; strict new envelopes enforce Ed25519 shape, tuple uniqueness, and canonical order. | envelope.rs, signature.rs, DC-39 |
| Current persistent object directories exclude RefUpdate. | layout.rs, id.rs |
| Patch payloads require non-empty contiguous operations and carry identity-bearing purpose. | patch.rs, DC-10 |
| Worktree authoring derives baselines from authoritative replay or valid genesis. | node_authoring.rs, DC-13, implementation status |
| Blocks contain parent ids, kind, Patch ids, state root, and optional snapshot Blob ref. | block.rs, seal.rs |
Tag carries target_block_id (local) and patch_set_digest/patch_count (portable), both fields amended in place at schema 1, no schema 2. | payload/tag.rs, tag.rs, RFC 117 |
RecognitionClaim carries a block’s own patch_ids/parent_block_ids verbatim and is never existence-checked against them at decode time. | payload/recognition_claim.rs, RFC 115 Stage 2 (D3), RFC 116 (N3) |
| RefState is content-addressed state and ref pointers are mutable entries in a shared container. | refs.rs payload, refs/pointer_index.rs, DC-11 |
| RefUpdate is append-only publication evidence stored inline in a shared ref-log container; schema-1 writes use zero as a no-clock sentinel. | refs.rs payload, refs/container.rs, seal.rs, DC-39 |
Received refs are stored under remotes/<name> in their own index, never read by verify_repository; import never advances a local ref. | received.rs, received_index.rs, DC-78 §D4 |
PSYNCSU1/PSYNCHV1 negotiate; PEXCH002 (formerly PEXCH001) carries patches, blobs, author keys, claims, and tags — representational, not frozen. | sync_negotiation/summary.rs, sync_negotiation/have_list.rs, patch_exchange/artifact.rs, RFC 116, RFC 117 stage 3 |
| Active WAL records exact signed Patch envelopes and detects trailing partial bytes. | wal.rs, verify.rs, DC-15 |
prikk compact reclaims dead records from the ref-pointer, received, and trust-key containers only; never the ref log or sealed objects. | compact.rs, RFC 102 Stage 6 Step 2 |
| Verification is read-only and bounded to structural, WAL, ref, rollback, and publication-trust checks. | verify.rs, doctor.rs, implementation status |
prikk-replay is internally scoped and not a stable external API. | DC-19, DC-20, implementation status |
| Durability and platform claims remain limited by current test evidence. | DC-24 baseline recap, DC-24 |
Provenance
This reference consolidates released records through DC-23 and DC-24. It uses
baseline-recap.md
only as a tracked recap of older non-VCS baseline inputs; current code, released RFCs, and
IMPLEMENTATION-STATUS.md
remain the durable authorities. DC-26 moved this current-state reference from rfcs/fdds/ into the
published book without changing code, schema, trust, or CLI behavior.
Data Model Relationships and Lifecycle
How Prikk’s objects relate to one another, and how each moves through its states.
This is the relationship and lifecycle view. For the per-object field contracts, identity rules, and their source anchors, see Data Model. For the crate layering these objects live in, see System Architecture.
The object taxonomy
Eleven object types. The type code is part of object identity, so an object of one type can never collide with another type’s id.
| Code | Type | Role | Stored in |
|---|---|---|---|
0x01 | Patch | An authored change: an ordered list of operations | objects/ |
0x02 | Block | A sealed group of patches, linked into lineage | objects/ |
0x03 | RefState | A ref’s state at one point: which block it names | objects/ |
0x04 | RefUpdate | The event advancing a ref from one state to the next | refs/containers/ |
0x05 | Tag | A named, signed pointer into history | objects/ |
0x06 | Attestation | A policy/plugin scan result about one block | objects/ |
0x07 | Blob | File content, addressed by hash | objects/ |
0x08 | BlockSummaryCache | Rebuildable derived summary — never a root of trust | cache/ |
0x09 | RecoveryNote | A signed doctor-repair note; never a RefUpdate substitute | refs/recovery/ |
0x0A | ProjectGenesis | Project identity anchor; its id is the project_id | objects/ |
0x0B | RecognitionClaim | A signed claim: named patches were sealed into a named block, under the signer’s key | objects/ |
How they relate
graph TD
REF["<b>Ref</b><br/>heads/main"]
RS["<b>RefState</b><br/>update_seq, closed"]
RU["<b>RefUpdate</b><br/>old → new, author_key_id"]
BLK["<b>Block</b><br/>kind, state_merkle_root"]
PATCH["<b>Patch</b><br/>operations, purpose"]
BLOB["<b>Blob</b><br/>content"]
TAG["<b>Tag</b><br/>patch_set_digest, patch_count"]
ATT["<b>Attestation</b>"]
RCLAIM["<b>RecognitionClaim</b><br/>patch_ids, parent_block_ids"]
REF -->|"names current"| RS
RS -->|"previous_ref_state_id"| RS
RS -->|"target_object_id"| BLK
RU -.->|"records the transition"| RS
BLK -->|"parent_block_ids"| BLK
BLK -->|"patch_ids"| PATCH
PATCH -->|"operations reference"| BLOB
TAG -->|"target_block_id"| BLK
TAG -.->|"patch_set_digest resolves to"| PATCH
ATT -->|"target_block_id"| BLK
RCLAIM -->|"block_id"| BLK
RCLAIM -.->|"patch_ids (block's own order, verbatim)"| PATCH
Three edges deserve comment:
RefState → RefStateis a backward chain viaprevious_ref_state_id, with a monotonicupdate_seq. Publication is compare-and-swap against the expected previous state.Patch → Patchexists in the format asparent_patch_idsbut is inert: every construction site sets it empty, including the authoring path, and nothing reads it. There is no patch DAG. Merge provenance is carried by block parentage instead. RFC 115’saccept_exchange_artifactgoes further than merely not populating it: an incoming Patch carrying a non-emptyparent_patch_idsis refused outright (patch_exchange/accept.rs), not merely ignored.Tag → Patchis a digest, not a pointer, and resolving it is a search. A tag’starget_block_idis the local pointer half of its identity;patch_set_digest(RFC 117 T1) is the portable half — the digest oftarget_block_id’s own patch closure, the same value two repositories holding the same patches produce independently. A receiver with no local block matchingtarget_block_idresolves the tag by searching local blocks for one whose own patch closure produces the same digest, pruned by the accompanyingpatch_count(T7) before any hashing — never by looking the digest up in an index. See Recognition claims and sync relations below.
Block lineage
A block’s kind determines its parent arity, and the shape validator enforces it.
| Kind | Parents | Extra required fields | Status |
|---|---|---|---|
Root | 0 | — | In use |
Normal | exactly 1 | — | In use |
Merge | exactly 2 | mainline_parent_id, merge_baseline_block_id | In use since 0.19.0 |
Repair | — | — | Not authorized — rejected outright |
Import | — | — | Not authorized — rejected outright |
gitGraph
commit id: "Root"
commit id: "Normal"
branch topic
commit id: "topic work"
checkout main
commit id: "main work"
merge topic id: "Merge"
commit id: "Normal"
parent_block_ids is stored sorted by object id, which is why a merge cannot express “which parent
is mainline” positionally — mainline_parent_id names it explicitly instead. merge_baseline_block_id
records the baseline confluence was proven against, and verify re-derives that it is a genuine common
ancestor of both parents rather than trusting it.
Patch and operations
A patch carries an ordered list of operations with contiguous op_seq from 1. Operations name what
they change by stable identity, never where by position — this is what lets a patch be adopted by a
merge without transformation, keeping its bytes and its author’s signature intact.
| Operation | Identifies its target by | Authorable today |
|---|---|---|
CreateFile | node_id, path | Yes |
DeleteNode | node_id | Yes |
EditText | node_id + left_anchor_hash / right_anchor_hash | Yes |
ReplaceBinary | node_id | Yes |
ChangePerm | node_id | Yes |
RenamePath | node_id | No — no authoring path |
CreateSymlink | node_id | No — symlink authoring is out of scope |
EditText also carries presentation_hint_line, which is explicitly not part of algebraic identity
— it is a display hint and never affects commutation.
A patch’s purpose is either Normal or RollbackDraft; the latter survives WAL-to-object persistence
so a rollback draft stays classifiable.
Recognition claims and sync relations
RFC 115/116/117 shipped a new object type, a namespace, and a resolution relation that don’t fit anywhere above. None of the three objects here are Blocks — they are how one repository tells another what it holds, or records what it received.
Claim → Block. A RecognitionClaim is a signed assertion that specific patches were sealed into a
specific block, under the signer’s key — nothing more. It is never existence-checked against the
block or patches it names at decode time: that is the entire reason it is a claim object and not a
Block, and it is what lets a claim be verified with none of its referenced objects present. Two fields
carry a block’s own data verbatim, not independently chosen:
patch_ids— the block’s ownpatch_ids, in the block’s own order (design-v1.md §11, D6).Block.patch_idshas no sorted-or-unique invariant; it is a free sequence consumed in order, and the claim mirrors it exactly. Order is load-bearing here — the receiver applies patches in this sequence — so sequence equality, not set equality, is what a consistency check against a held block must test.parent_block_ids— the block’s ownparent_block_ids, verbatim (RFC 116 design-v1.md §3, N3). This is what lets a batch of claims spanning a multi-block delta be sorted into sealing order without the receiver needing to have any of the blocks yet.
A claim that contradicts a block the receiver does hold is a detected lie, refused loudly by a separate consistency check — the claim payload itself has no object-store access and cannot perform that check.
Tag → patch set digest → the patches that resolve it. See the taxonomy diagram’s comment above:
patch_set_digest is the identity that survives a tag moving between repositories, because two
repositories holding the same patches produce the same digest independently, while target_block_id
does not survive — blocks diverge by design even between repositories with identical history. A
receiver resolving a travelled tag has no direct pointer to follow; it searches its own local blocks
for one whose own patch closure hashes to the declared digest, using patch_count to prune candidates
by size before ever hashing one. This is a plausibility-tried relation, not a lookup — the search can
find NotHeld (not enough history synced yet) or Ambiguous (two local candidates match), and either
outcome refuses adoption rather than guessing.
Received objects → remotes/ → local refs, and import never advances a local ref. Imported history
(via bundle import or sync accept) is recorded under a received pointer, always named
remotes/<origin ref name> — a distinct namespace from refs/by-id/’s ordinary pointers, because a
received RefState’s embedded ref_name still names the origin’s own ref (rewriting it would
invalidate the object’s content-addressed identity and signature). Turning received history into local
history is an ordinary merge, using machinery that already exists — receiving is never itself a “pull”
that advances anything. The received-pointer index is its own small append-only container, never read
by verify_repository: every object a received pointer leads to (RefState, Block, Patch, Blob,
Attestation) is an ordinary object-store entry, checked exactly like any other by the existing
type-based object scan, so there is no new verification path — only a new way to discover a receiver’s
own object graph by name.
Lifecycle: content, from worktree to sealed history
stateDiagram-v2
[*] --> Worktree
Worktree --> ActiveWAL: commit (author signs)
ActiveWAL --> ActiveWAL: further commits queue
ActiveWAL --> Sealed: seal (maintainer signs)
Sealed --> Published: publish (compare-and-swap)
Published --> [*]
Sealed history is append-only. Nothing above removes or rewrites a sealed object; a rollback is a new patch that inverts an earlier one, not an erasure.
Lifecycle: a node
Nodes — files and their identities — have their own state machine, enforced by prikk-replay.
stateDiagram-v2
[*] --> Live: CreateFile
Live --> Live: EditText / ReplaceBinary / ChangePerm
Live --> Tombstoned: DeleteNode
Tombstoned --> Live: CreateFile (restoration-equivalent only)
The last transition is the constrained one. Once a node_id has been seen, re-creating it requires
restoration-equivalence to that node’s latest tombstone — you cannot silently reuse a node identity
for different content. Every node_id ever seen is retained for exactly this check, which is why
lifecycle state grows with cumulative history rather than with the current tree.
Lifecycle: a ref
stateDiagram-v2
[*] --> Open: branch create
Open --> Open: seal + publish (update_seq + 1)
Open --> Closed: branch close
Closed --> Open: ordinary CAS publish
Closure is a published ref state carrying closed, not a deletion — history and every object stay.
Reopening is permitted and is an ordinary compare-and-swap update, though no branch reopen verb exists
today.
Note that neither seal nor merge inspects closed, so advancing a closed branch reopens it silently.
This is consistent with closure being advisory rather than a lock, but it is unreported by those
commands.
Lifecycle: a repository
Before init, there is nothing: no .prikk/ directory, no lifecycle to describe. init creates the
full layout in one pass and writes FORMAT last — every other required file or empty container is
created first, all through idempotent, retryable primitives. An interrupted init therefore leaves
FORMAT absent, which is itself the detectable, safe state: a re-run of init skips straight past the
already-initialized guard (which only fires once FORMAT exists) and completes whichever names are
still missing. This is tolerated only because an interrupted init has nothing to lose — no user
history exists yet.
Every other command opens an existing repository through RepositoryLayout::open, which reads FORMAT
and refuses outright — no migration offered — if it names anything but the current format (format 6,
per RFC 114’s ruling that formats 1-5 are out of scope). There is no format-migration verb; a repository
is either format 6 or it is refused by every command, including init itself against an
already-initialized non-format-6 directory (its own, terser refusal).
There is no decommission or deletion lifecycle for a repository as a whole — closure exists only at the branch-ref level (see Lifecycle: a ref above), with no repository-level equivalent.
doctor and unlock are not lifecycle stages; they operate on whatever state a repository is already
in, regardless of how it got there. doctor’s only supported repair is truncating an incomplete
trailing active-WAL record (--repair-wal-tail); --repair-main-ref is a recognized input that
performs no repair and is always refused — see the
integrity and recovery diagnostics reference. unlock reports, and on
request clears, stale lock files. Neither doctor nor unlock advances a repository through a stage
the way init/commit/seal/publish do.
Lifecycle: compaction
Everything above is append-only: nothing is ever removed from a container, only new records added and
superseded. Three containers accumulate dead records this way — the ref-pointer index, the received
(imported-ref) index, and the trust-key/policy container — and prikk compact is the only operation
that reclaims any of it.
What it rewrites. Each of the three targets is compacted independently
(compact_ref_pointer_index, compact_received_index, compact_trust_policy; --all runs all three).
Compaction reads a container’s currently-live slot, corruption-checked in full, reduces it to the same
“keep only the last record per key” rule its own reader already applies at read time, writes that
reduction to the container’s currently-retired slot, and only then durably switches the generation log
to name the new slot live. The old slot’s bytes are never touched until the switch to the new one is
already durable.
What it preserves. The reduction persists exactly what a reader would compute anyway — verify’s
output is unchanged by construction, not merely by inspection, because compaction writes the same
per-key “last entry wins” result its own read path already resolves at query time. --plan-only computes
and reports the same before/after record counts without writing anything, so an operator can preview the
effect before committing to it. There is no confirmation prompt, unlike unlock: the container lock
already excludes concurrent writers, the corruption check already covers every record, and the reduction
already persists exactly what every reader independently resolves — there is no unresolved fact left for
a prompt to gate on.
What guarantees hold across it. Compaction refuses outright on any known-corrupt record, not only
the latest one — stricter than an ordinary read, because compaction is destructive to the retired slot in
a way a read never is: a naive compactor that silently dropped a corrupt record while abandoning the old
slot would turn corruption into permanent deletion, through the very mechanism built to survive it. The
container’s lock is held for the whole operation (resolve, read, reduce, and — for a real run —
truncate/write/switch), excluding every other writer of that container for its duration: publish,
trust changes, and bundle/sync import cannot observe a torn state, because they cannot run at all while
compaction holds the lock.
What it never touches. The ref log (refs/containers/, DC-38/DC-69’s audit trail) and the
RecoveryNote container are never compaction targets — there is no function for either. Compaction never
touches sealed objects (objects/) at all; only the three pointer/policy containers above ever
accumulate reclaimable dead records.
What the model does not currently record
Stated here so it is not inferred from silence:
- No patch DAG.
parent_patch_idsis inert — every construction site sets it empty, and an incoming Patch carrying a non-empty one is refused outright on import. - A ref’s
required_attestation_idsare cleared by every ordinary seal, while branch closure preserves them. RenamePathandCreateSymlinkdecode and validate but cannot be authored.- Merge-base discovery is manual —
--baseline-blockis always explicit. ProjectGenesisis a reserved type code with no payload module. It names itself"project-genesis"and has a test vector, butvalidate_format2_schemarefuses it outright in a format-2 identity position — there is no project-genesis lifecycle, and none is implied by the code existing to reject it.Attestationis defined but never constructed. No production code path builds one; the object type and directory exist, and nothing populates them.- A tag’s deletion and movement do not travel.
sync/bundlemove a tag’s creation and its adoption; a tag deleted or repointed locally has no mechanism to propagate that change to a repository that already received it. - There is no ref deletion at all, of any kind, anywhere in
prikk-store— the onlyremove_ref_pointer_entry-shaped function is test-only support, never reachable from a command. A branch can be closed (above); nothing can be removed. This is also why a tag written by an older schema cannot be cleared to make room for a new one under a later schema — there is no supported way to remove the ref standing in the way.
Trust and Threat Model
This page is the authoritative current-state reference for Prikk’s trust and threat model. It
describes the implementation on main as of 2026-08-18 (released through 0.22.1) and is grounded in
the code, released RFCs, and implementation status records listed in the anchor table at the foot of the
page. Refreshed 2026-08-18 after DC-53 completed; before that refresh this page still described 0.16.0
and stated several AUTHOR claims that DC-53 had falsified.
Core Caveats
- Prikk is early implementation software and is not a production Git replacement.
.prikk/is Prikk’s native repository format and is not Git-compatible storage.- Ref pointers are mutable, not roots of trust.
- Maintainer trust is repository-local with the current minimal
required = 1policy. verifyis not a global trust proof.- MAINTAINER key revocation exists (
prikk trust maintainer remove); there is no key rotation, hardware signing, remote trust, sync trust, or stable migration policy yet, and no AUTHOR-identity revocation. For AUTHOR keys specifically (DC-53 Stage 2): onekey_idis permanently bound to the first public key ever recorded for it in a given repository; attempting to sign under the samekey_idwith a different key — whether from a genuine rotation attempt or an impersonation attempt — is refused identically, and is indistinguishable as the reason for the refusal. - Durability and recovery claims are supported by current unit and integration tests, not by a completed crash-matrix or fuzzing campaign.
- Repository mutation is exercised by project gates on Linux, macOS, and Windows (DC-87 Stage 2). Windows’ anchoring guarantee is weaker than Linux/macOS in one stated way — see platform support for the exact gap and which of the nine durability guarantees are held, weaker, or documented no-ops there. Read-only commands are CI-gated on macOS and Windows too — see platform support.
Changes that alter trust, threat, verification, signature, key-management, durability,
platform-support, or production-readiness claims require architect review or accepted RFC/DC coverage.
The local persistence and crash-recovery boundary is covered by the
durability and crash recovery reference. The current verify / doctor
diagnostic catalog is covered by the
integrity and recovery diagnostics reference. Current operator setup for
environment key input and repository-local maintainer trust is covered by the
security and signing setup guide. Physical trust-store paths and other
.prikk/ authority boundaries are covered by the
repository layout and authority reference. Repository path validation and
worktree write-safety limits are covered by the
path and worktree safety reference.
Trust Roots and Roles
Current signing uses role-bound Ed25519 signatures. The signature preimage binds the algorithm, object
type, object id, signer role, and key id. Signer roles include AUTHOR and MAINTAINER. Ed25519 signing
and strict verification live in prikk-crypto; trust stores, key persistence, rotation, revocation,
and policy are outside that crate.
AUTHOR signatures identify the key used by the authoring path for Patch envelopes. Production commit
and rollback-draft authoring use real Ed25519 AUTHOR signatures. Since DC-53 (2026-08-18) Prikk does
maintain a repository-local AUTHOR key-material store and verify checks every reachable Patch’s AUTHOR
signature against it. Prikk still implements no AUTHOR revocation, rotation, expiration, or
identity policy.
What AUTHOR key material proves, stated precisely (DC-53 Stage 2). A repository records each
key_id’s public key the first time it observes a Patch signed under that name — trust-on-first-use,
and that first observation is not itself verified against anything. Every subsequent appearance of
the same key_id is checked against the key recorded at first contact, and one key_id is permanently
bound to one public key for the life of that repository. What this proves is “the same key_id has
always signed under this name here” — not “this author’s claimed identity is genuine.” A reader must
be able to tell “prikk verified this author” apart from “prikk verified this author is the same one
as last time”, because only the second is true.
When AUTHOR key material travels in a bundle (prikk bundle export/import, DC-53 Stage 2), the same
limit applies with one further step: a transported key is supplied by the sender. A signature that
verifies against a key which arrived in the same bundle proves only that the two are internally
consistent — an attacker who re-signs a Patch with their own key and ships that key in the bundle
produces a bundle that verifies perfectly. Import records transported material under the same
first-contact rule as local material; it performs no additional check of who actually holds the key.
Transport does not weaken the maintainer signature’s own role in DC-78’s exchange claim — a receiver
still relies on that signature for the decision to include imported patches at all; AUTHOR verification
adds continuity of authorship on top of it, and does not replace it.
MAINTAINER signatures identify publication objects. Seal uses real role-bound Ed25519 MAINTAINER signatures for Block, RefState, and RefUpdate envelopes and verifies the signer against the local maintainer trust policy before publishing.
Key Input and Local Trust Store
Current key input is intentionally minimal. The CLI reads AUTHOR key material from
PRIKK_AUTHOR_KEY_ID and PRIKK_AUTHOR_SEED, and MAINTAINER key material from
PRIKK_MAINTAINER_KEY_ID and PRIKK_MAINTAINER_SEED. The seed values are caller-provided 32-byte
Ed25519 secret seeds encoded as 64 hex characters. Prikk does not provide local secret storage, key
generation, or public-key derivation. For the current setup workflow and seed-handling warnings, see
the security and signing setup guide.
The local maintainer trust store supports a set of repository-local adopted MAINTAINER keys, with
required = 1 continuing to mean any one adopted key’s signature suffices. prikk trust maintainer add
adds a new key id to the set, or idempotently confirms an already-adopted id’s matching key; it refuses
to replace an adopted id’s key with a different one. This refusal is a trust-on-first-use rule: the
first public key seen for a key id is the one trusted for that id, permanently, until an operator
removes it out-of-band. There is no remote trust distribution.
What Seal Checks
Seal requires --allow-no-audit, a valid local branch ref, a non-empty active WAL, valid active ref
metadata matching the requested ref, and no trailing partial WAL bytes. It verifies that the configured
MAINTAINER signer matches the repository-local trust policy before publication. It then persists Patch
objects, signs and writes the Block and RefState, durably appends the ref pointer as the commit point, appends
exactly one signed RefUpdate, confirms pointer/log agreement, and clears active state. Signer-backed
retry is also the only authority that may finish an exact interrupted publication.
Current seal does not run audit plugins, evaluate attestation policy, perform semantic merge, publish multi-parent merge Blocks, or provide remote trust distribution.
What Verify Checks
prikk verify is read-only. It checks persisted object placement and identity, envelope decoding,
Block references, ref pointer/log consistency, active WAL records, active WAL metadata health,
rollback-draft structure for active and sealed rollback-marked Patches, and publication trust for
Block, RefState, and RefUpdate envelopes against the repository-local maintainer trust policy.
verify does not prove that a repository is globally trustworthy. It does check every reachable
Patch’s AUTHOR signature against recorded key material (DC-53), and fails when one does not verify or
when a key_id’s recorded material contradicts itself — but that is continuity, not identity. It does
not enforce historical PKI semantics, AUTHOR revocation, rotation, expiration, threshold policy beyond
required = 1, remote policy, hosted identity, or complete crash-proof durability.
Rollback-Draft Boundary
Rollback drafts are Patch objects whose payload purpose is PatchPurpose::RollbackDraft. Active
rollback-draft verification requires exactly one active WAL record, rejects trailing partial WAL bytes,
requires a rollback-draft Patch purpose, requires an AUTHOR Ed25519 signature, rejects the legacy
placeholder marker key id, requires 64-byte signature payloads, and compares the active payload with
the inverse Patch derived from the current ref.
This is structural and semantic validation for the supported rollback subset. It is not rollback
authorization and does not publish rollback refs. Repository-wide AUTHOR verification is performed by
verify, not here (DC-53); this path’s own checks are unchanged.
Threat Boundaries
Current protections target local repository corruption, malformed persisted data, wrong object
placement, ref pointer/log drift, active-WAL ownership drift, unsigned or untrusted publication
objects, and legacy rollback marker signatures. Diagnostics should avoid raw text spans, replacement
text, blob bytes, absolute host paths, .prikk private paths, signer secrets, key material, and
arbitrary object debug dumps.
Current non-goals include global identity trust, remote trust, hosted forge semantics, key lifecycle management, hardware signing, multi-maintainer thresholds, production audit policy, plugin execution, and stable repository-format migration.
Claim-to-Source Anchors
| Claim | Source anchors |
|---|---|
| Ed25519 is the only current signing and verification algorithm. | prikk-crypto, signature.rs |
| Signature preimages bind algorithm, object type, object id, signer role, and key id. | signature.rs, author_signing.rs, maintainer_signing.rs |
| AUTHOR signing is real Ed25519 on Patch envelopes, not a placeholder. | author_signing.rs, node_authoring.rs, DC-10 |
| AUTHOR private key material comes from environment variables and is never persisted by Prikk. The public half is persisted in the repository-local author-key container, recorded at authoring time (DC-53), because an Ed25519 signature cannot be verified without it. | main.rs, author_signing.rs, implementation status |
| MAINTAINER publication signing is real Ed25519 and role-bound. | maintainer_signing.rs, seal.rs, DC-11 |
Maintainer trust is repository-local, held as a set of adopted keys, with required = 1 meaning any one adopted key’s signature suffices. | trust.rs, layout.rs, DC-11 FDD-04 handoff |
| Seal validates the maintainer signer against local trust before publication. | seal.rs, trust.rs |
| Verify checks publication trust for Block, RefState, and RefUpdate envelopes. | verify.rs, trust.rs |
Verify enforces repository-wide AUTHOR verification (DC-53): every reachable Patch’s AUTHOR signature is checked against recorded key material, one key_id binds to one public key, and material travels with a PBNDL002 bundle. It remains trust-on-first-use — continuity, not identity. | verify.rs, rollback_verify.rs, implementation status |
| Rollback-draft verification is structural and semantic for the supported subset only. | rollback_verify.rs, DC-14, DC-14 FDD-04 handoff |
| Active WAL metadata integrity is part of verification and doctor diagnostics. | verify.rs, doctor.rs, DC-15 |
| Durability and platform claims remain limited by current test evidence. | DC-24 baseline recap, DC-24 |
Provenance
This reference consolidates released records through DC-23 and DC-24. It supersedes stale
v0.2.0-era notes that described MAINTAINER signing as deferred; the current released code signs
publication objects with real MAINTAINER Ed25519 signatures and verifies them against local trust.
DC-26 moved this current-state reference from rfcs/fdds/ into the published book without changing
code, schema, trust, or CLI behavior.
Durability and Crash Recovery
This page is the authoritative current-state reference for Prikk’s local persistence and crash-recovery model. It describes the current implementation behavior without adding storage, verification, doctor, or command semantics.
For related concepts, see the repository layout and authority reference, the
data model, the trust and threat model, and the command
guides for verify and doctor through the
integrity and recovery diagnostics reference.
Release-transaction durability, artifact identity, and evidence limits are documented separately in
release, versioning, and compatibility.
Core Caveats
- Prikk is early implementation software and is not a production Git replacement.
- Durability and recovery claims are supported by current unit and integration tests, not by a completed crash-matrix or fuzzing campaign.
- Repository mutation currently requires Linux, macOS, or Windows (DC-87 Stage 2) anchored relative no-follow path resolution, strict regular file sync, and the required install primitives — with one stated exception: Windows’ anchoring is not handle-scoped between path components the way Linux/macOS’s is, so it does not close the same inter-component race (see platform support for the exact gap and the full guarantee-by-guarantee table). Filesystems without any of those proved capabilities remain read-only/diagnostic targets — see platform support for exactly which commands that covers and how it is CI-verified.
.prikk/is not a stable repository format and there is no stable migration policy yet.- The ref pointer is a mutable convenience pointer (an entry in a shared, append-only container, not a file of its own), not a root of trust.
doctorrepairs are opt-in and narrow; they do not synthesize missing objects, signatures, trust policy, or key material.- Stale
active.lockcleanup after a crash is manual today; the current lock/CAS boundary is covered by the concurrency and locking reference.
Commit Persistence Boundary
A successful commit appends an exact signed Patch envelope to the active WAL. The WAL append path
rejects non-Patch envelopes and unsigned Patch envelopes, writes a checksummed record, required-syncs
the WAL file, and required-syncs the parent directory after every append. Any required
file or directory sync failure returns an operation failure while retaining written state for replay.
That is the active-session persistence boundary. It does not mean the Patch is sealed into a Block, a
RefState has been published, a ref pointer moved, or the active WAL has been drained. Sealed history is
created later by seal.
WAL Replay and Tail Handling
WAL replay reads valid records from the start of the file. Each complete record carries magic, version, sequence, body length, checksum, and the encoded signed envelope bytes.
Incomplete trailing bytes are reported separately as trailing partial bytes. They represent the only
current WAL truncation case that doctor --repair-wal-tail handles. A complete record with a checksum
mismatch, malformed header, unsupported version, or malformed envelope is an integrity failure and is
not a safe automatic truncation candidate.
Active Ref Metadata
The active WAL is paired with active ref metadata that records which local branch ref owns the non-empty WAL. A non-empty active WAL with missing or malformed active ref metadata is an active-session integrity issue. Seal refuses that state rather than guessing which ref should receive the WAL records.
An empty active WAL with leftover active ref metadata is local debris. Verification and doctor report that distinction so empty-WAL cleanup does not get confused with sealed-history corruption.
Seal Publication Flow
seal --allow-no-audit publishes the active WAL through the repository storage layers in a fixed
order:
- Acquire the active lock.
- Replay the active WAL and reject trailing partial bytes.
- Reject an empty active WAL, or clean empty-WAL metadata debris where the command path permits it.
- Require active ref metadata to match the requested local branch ref.
- Verify the configured MAINTAINER signer against the repository-local trust policy.
- Persist the signed Patch envelopes from the WAL into the object store.
- Create a signed Block envelope.
- Create a signed RefState envelope.
- Construct the deterministic signed RefUpdate.
- Append and required-sync the new pointer entry to the shared pointer-index container — the publication commit point. An append-only entry has no candidate value to stage first.
- Append and required-sync exactly one signed RefUpdate record to the shared log container.
- Confirm pointer/log agreement, then drain the active WAL and remove active ref metadata.
The implementation is designed so interruption recovery lands on a checkable previous ref state or a
checkable new published state. That statement is bounded by the current evidence: unit/integration
tests, no completed crash-matrix or fuzzing campaign, and gates exercised on Linux, macOS, and Windows
(the macOS mutation test suite and Windows mutation test suite CI jobs run the full suite on
macos-latest/windows-latest) — with the caveat that DC-76’s negative controls (eight remain; G5
retired in DC-98) are only partly demonstrated on Windows: G1, G2, G4, and G9 are, but G3 and G8
still rely on a failpoint injection mechanism that exists only on Linux/macOS, and G6/G7 have no
Windows analogue at all. See platform support for the per-guarantee table.
If the active WAL’s Patch IDs already match the current published tip, seal reconstructs the expected no-clock RefUpdate and finishes any exact one-record pointer lead before cleanup. An existing complete matching record is not duplicated. If the already-published transition cannot be checked exactly, seal fails closed.
Required Filesystem Boundaries
Authoritative directories are traversed through anchored no-follow handles on the supported Linux and macOS mutation paths. Missing directories are created one component at a time, and each new name is established by syncing its parent before descent. Retry also re-syncs the parent of an observed component instead of treating presence as proof of earlier durability.
Reads, metadata checks, and directory listings that authorize a mutation use the same retained root
as the mutation. Replacing the visible worktree or .prikk path therefore cannot redirect a
check-then-mutate workflow to a different tree. Append retries classify an exact retained complete
record without duplicating it and re-sync the file and parent; required removal re-syncs its retained
parent even when the final entry is already absent.
Mutable metadata publication uses a unique same-directory exclusive temp, complete file sync, atomic replace rename, and required parent sync. An error after rename leaves the final name in place and returns failure for verification or retry; it does not blindly roll back visible state.
Immutable object publication uses a separate no-clobber operation. It syncs a unique same-shard temp, installs the final name without replacement, syncs the shard, removes only its invocation-owned temp, and syncs the shard again. If another publisher wins, success requires same-handle validation and exact persisted-byte equality; malformed, wrong-identity, wrong-type, or byte-different winners fail without replacement. Crash-left object temps are warning-only debris and are never object authority.
Worktree writes and removals use separately named strict operations. Their errors propagate and may leave partial worktree effects, but the worktree does not become repository authority. Lock removal from a guard destructor remains explicitly best-effort because destruction cannot return an error.
Ref Pointer and Ref Log Recovery
Ref publication uses a signed RefState object, a signed inline RefUpdate log record, and a mutable ref pointer entry in a shared, append-only container. The pointer is useful for fast lookup, but it is not trusted by itself.
The ref store validates branch ref names, holds a ref-specific lock, rechecks the expected current RefState ID, then durably appends the new pointer entry — the publication commit point — before appending the committed log record. An append-only entry has no candidate value to stage first, so there is no separate write-then-promote step and no candidate-cleanup diagnostic anymore: the append either lands durably or it does not.
Verification jointly classifies pointer and log state. A pointer leading the log by exactly one
expected transition is an interrupted publication and makes verify fail. Signer-backed seal retry
may append the exact deterministic RefUpdate after revalidating retained WAL and trust. If the final
log frame is structurally incomplete, that same path may truncate and sync only the container’s own
trailing incomplete suffix before the append; a torn tail belonging to one ref never enters any other
ref’s own filtered record sequence, so it cannot block a different ref’s own publish or repair. Fully
framed checksum-invalid or malformed records are never truncation-safe.
For released format-1 repositories, one exact already-signed log-ahead transition may be completed by signer-backed seal without another append when retained active state proves the transition. Other ahead-log states fail closed. A missing format-1 pointer with log history is diagnosed but is not reconstructed by doctor in 0.18.0; preserve the repository and restore from backup or retain it for later migration/recovery tooling.
Pointer/log agreement with the matching active WAL and metadata still retained is incomplete cleanup, not a healthy repository state. Verification returns non-zero and unrelated mutation remains blocked until signer-backed seal revalidates the transition, appends nothing, and removes active state.
Doctor Repair Boundary
The current doctor mutation is doctor --repair-wal-tail, which acquires the active lock and truncates
incomplete trailing active-WAL bytes after an under-lock publication guard and verification have
accepted the preceding WAL prefix. Doctor diagnoses ref-publication
states but does not sign, append, promote, or reconstruct ref authority.
The integrity and recovery diagnostics reference owns the full diagnostic
catalog: verification checks, DoctorIssue codes, severities, and diagnostic interpretation. This
page intentionally does not duplicate that catalog.
Doctor repair refuses to modify the repository when verification has error-severity issues. It also does not auto-trust keys, repair signatures, repair checksum mismatches, rebuild missing objects, recover missing key material, or clear unsafe active sessions.
Stale Locks and Manual Repair
active.lock is acquired with exclusive file creation. If a process dies while holding it, stale lock
cleanup is manual today. DC-28 does not define lock stealing, lock expiry, process ownership checks, or
automatic stale-lock repair. The current lock and compare-and-swap behavior is covered by the
concurrency and locking reference.
Deferred Work
Still deferred: the broad crash-matrix campaign, fuzzing for WAL/ref-log recovery, macOS and Windows filesystem validation, stale-lock policy, broad active-session recovery, ref-log repair, missing-object recovery, object quarantine or garbage collection, backup/restore tooling, stable repository-format migration, and production-readiness claims.
Claim-to-Source Anchors
| Claim | Source anchors |
|---|---|
| Commit persistence appends exact signed Patch envelopes to the active WAL, required-syncs the WAL file, and required-syncs the parent directory after every append. | wal.rs, DC-37 |
| WAL replay reports incomplete trailing bytes separately from complete-record checksum failures. | wal.rs, PR-004, PR-006 |
| WAL-tail repair truncates only incomplete trailing bytes and refuses complete-record integrity failures. | wal.rs, doctor.rs, PR-012 |
| Non-empty active WALs require valid active-ref ownership metadata; empty-WAL metadata debris is separate local debris. | verify.rs, doctor.rs, DC-15 |
| Seal rejects trailing partial WAL bytes, missing/malformed active ref metadata, and mismatched active ref ownership before publication. | seal.rs, DC-15 |
| Seal persists WAL Patches, creates signed Block and RefState objects, durably appends the pointer commit point, appends exactly one signed RefUpdate, confirms agreement, then drains active state. | seal.rs, refs/publication.rs, DC-38 |
| Seal verifies the configured MAINTAINER signer against repository-local trust before publication. | seal.rs, trust.rs, DC-11 |
| Ref publication uses ref-specific locking, compare-and-swap checks, signed RefState/RefUpdate envelopes, pointer-first commit, and an idempotent exact log append. | refs/publication.rs, refs/pointer_index.rs, refs/container.rs, DC-38 |
| Immutable object publication never replaces an existing final name; existing or concurrent winners require valid identity/type and exact persisted-byte equality, while recognized crash-left temps remain warning-only debris. | object_store.rs, immutable.rs, DC-36 |
| Doctor refuses format-1 missing-pointer reconstruction; exact interrupted ref publication completion requires retained active evidence and a trusted signer. | doctor.rs, seal.rs, DC-38 |
| Doctor began as read-only diagnostics, and current mutating repairs remain opt-in and narrow. | doctor.rs, PR-011, PR-012, PR-013 |
| Ref pointers are mutable, not roots of trust. | refs.rs, refs/pointer_index.rs, data model |
| Durability/platform claims remain limited by current test evidence and gates exercised on Linux, macOS, and Windows. | DC-24 baseline recap, DC-24, DC-28 |
Provenance
This reference follows the DC-26 documentation-home model: current-state references live in the
published mdBook, not under rfcs/fdds/. Its required-sync and ref-publication sections are updated
with the DC-37 and DC-38 implementations and remain subject to the combined 0.18.0 implementation and
release reviews.
Integrity and Recovery Diagnostics
This page is the authoritative current-state reference for Prikk’s repository verification and doctor
diagnostics. It describes what prikk verify checks, what it does not prove, how prikk doctor
interprets verification results, and which repair boundaries are intentionally narrow.
For the storage recovery mechanics behind WAL-tail truncation and signer-backed ref completion, see the durability and crash recovery reference. For trust scope, see the trust and threat model. For operator key input and local maintainer trust setup, see the security and signing setup guide.
Core Caveats
- Prikk is early implementation software and is not a production Git replacement.
prikk verifyis read-only.verifychecks structural integrity and current repository-local publication trust for publication objects; it is not a global trust proof.- There is no repository-wide AUTHOR trust enforcement.
- MAINTAINER key revocation exists (
prikk trust maintainer remove), but there is no historical PKI (temporal/point-in-time revocation semantics), key rotation, hardware signing, remote trust, sync trust, or stable migration policy yet. prikk doctorrepairs are opt-in and narrow.- Doctor recommendations are human guidance, not an automated recovery policy.
- Output fields, counters, severity labels, and issue-code names are current CLI vocabulary, not a stable machine-readable schema.
Verify Scope
prikk verify calls the repository verification layer and prints a read-only report. Current
verification covers:
- persisted object placement by object type directory and canonical object path;
- object envelope decoding and recomputed object identity;
- Block payload decoding and references to parent Blocks, Patch objects, and optional snapshot Blobs;
- joint ref pointer, RefState-chain, and ref-log-chain consistency;
- signed RefUpdate log record decoding;
- warning-level format-1 signature-envelope diagnosis for malformed Ed25519 shape, duplicate tuples, and non-canonical order;
- active WAL replay, including trailing partial WAL byte reporting;
- whether active WAL Patch records already exist as persisted Patch objects;
- active WAL ref metadata health;
- active rollback-draft WAL record classification;
- sealed rollback Block and sealed rollback Patch classification;
- repository-local publication trust for Block, RefState, and RefUpdate envelopes.
Object enumeration, Block/RefState reads, active-WAL replay, active metadata, ref pointers, and ref logs all use the same retained repository-root authority. Publication trust consumes the exact Block, RefState, and RefUpdate envelopes returned by those anchored structural scans; it does not reopen publication paths in a separate trust phase.
Publication trust, format-1 signature-envelope warnings, and recognized ref-publication state issues are collected separately from hard structural verification errors. This lets the command preserve and diagnose legacy bytes while still returning command failure when trust is invalid or a blocking interrupted-publication state exists.
What Verify Does Not Prove
verify does not prove that a repository is globally trustworthy. It does not enforce
repository-wide AUTHOR trust, historical PKI semantics (temporal/point-in-time revocation tracking –
verify only ever checks against the current adopted-key snapshot), key rotation, remote identity,
remote trust, hosted forge policy, or thresholds beyond the current repository-local required = 1
maintainer policy.
verify also does not prove production readiness, stable repository-format migration, complete
cross-platform filesystem behavior, merge execution safety, semantic conflict resolution, backup
coverage, or successful recovery from every crash shape.
verify does not read the received-ref index. Ref pointers imported by prikk bundle import live
in refs/containers/received-index-{a,b}.container and are outside the verification surface entirely.
They are also not rebuildable: the origin ref name exists only inside the imported bundle, which may be
gone. So nothing detects their loss or corruption — a gap in verify’s own scope, independent of
platform.
Verify Output and Exit Behavior
The current CLI prints counters for checked objects, Blocks, rollback Blocks, sealed rollback Patches, WAL records, persisted WAL Patches, refs, ref-log records, rollback draft WAL records, publication trust records, publication trust issues, ref-publication issues, and trailing partial WAL bytes. It also prints signature-envelope warnings and the active WAL metadata state.
Signature-envelope warnings use at most one issue per code per envelope, in malformed, duplicate,
then non-canonical-order sequence. Object findings are ordered by numeric object type and raw ObjectId
bytes, followed by active WAL sequence, then unsigned UTF-8 ref-name bytes and ref-log sequence. These
warnings do not independently make verify fail and never authorize normalization or mutation of the
legacy envelope.
The command exits with failure when:
- structural verification returns an error before a report can be produced;
- the report has a non-empty active WAL with missing or malformed active ref metadata; or
- the report has publication-trust issues; or
- the report has a blocking ref-publication issue such as a one-transition pointer lead, a bounded format-1 log lead, matching active state retained after completed publication, a missing format-1 pointer with log history, or an unproved pointer/log divergence.
Trailing partial WAL bytes are printed as a warning in the report. The recovery mechanics and safe truncation boundary are covered by the durability and crash recovery reference.
Active WAL Metadata States
ActiveWalMetadataStatus currently has six states:
| State | CLI meaning | Doctor issue |
|---|---|---|
MissingForEmptyWal | Empty active WAL with no metadata. | Healthy; no issue by itself. |
ValidForEmptyWal | Empty active WAL with stale but valid metadata. | Warning: PRIKK-DOCTOR-ACTIVE-REF-METADATA-DEBRIS. |
InvalidForEmptyWal | Empty active WAL with malformed metadata. | Warning: PRIKK-DOCTOR-ACTIVE-REF-METADATA-MALFORMED-DEBRIS. |
ValidForNonEmptyWal | Non-empty active WAL with valid ownership metadata. | Healthy; no issue by itself. |
MissingForNonEmptyWal | Non-empty active WAL without ownership metadata. | Error: PRIKK-DOCTOR-ACTIVE-REF-METADATA-MISSING. |
InvalidForNonEmptyWal | Non-empty active WAL with malformed ownership metadata. | Error: PRIKK-DOCTOR-ACTIVE-REF-METADATA-MALFORMED. |
Only the non-empty missing/malformed states are active-session integrity issues. Empty-WAL metadata states are local debris warnings because no WAL records need ownership for publication.
Doctor Scope
prikk doctor is an actionable diagnostic layer over repository verification. When verification
completes, doctor prints the verification report, emits issue lines with severity, code, message, and
recommendation, then prints an issue summary.
When verification fails before a report can be produced, doctor emits a verification-error issue and recommends preserving the repository before attempting repair.
Doctor output is intended for human diagnostics. The issue-code strings and severity labels are current CLI vocabulary, not a stable JSON/API contract.
Doctor Issue Catalog
Current doctor severities are info, warning, and error.
| Code | Severity | Meaning |
|---|---|---|
PRIKK-DOCTOR-VERIFY-OK | info | The structural verification scan completed; later issue lines still determine health. |
PRIKK-DOCTOR-WAL-TRAILING-PARTIAL | warning | Active WAL has trailing bytes that look like an incomplete final record. |
PRIKK-DOCTOR-ACTIVE-REF-METADATA-MISSING | error | Active WAL has records but active ref metadata is missing. |
PRIKK-DOCTOR-ACTIVE-REF-METADATA-MALFORMED | error | Active WAL has records but active ref metadata is malformed. |
PRIKK-DOCTOR-ACTIVE-REF-METADATA-DEBRIS | warning | Active WAL is empty but stale valid ref metadata remains. |
PRIKK-DOCTOR-ACTIVE-REF-METADATA-MALFORMED-DEBRIS | warning | Active WAL is empty but malformed ref metadata remains. |
PRIKK-DOCTOR-VERIFY-ERROR | error | Repository verification failed before doctor could produce a healthy report. |
Publication-trust issues can also appear in doctor output as error-severity diagnostics using the
trust issue code and message from publication-trust verification. Ref-publication diagnostics use
their verification codes: pointer lead, legacy log lead, retained active cleanup, missing pointer,
and unproved divergence are errors; candidate debris and non-canonical legacy timestamps are warnings.
Signature-envelope diagnostics appear as warnings using
PRIKK-VERIFY-SIGNATURE-MALFORMED, PRIKK-VERIFY-SIGNATURE-DUPLICATE, and
PRIKK-VERIFY-SIGNATURE-NONCANONICAL-ORDER. Doctor does not rewrite those envelopes.
MissingForEmptyWal and ValidForNonEmptyWal are healthy metadata states and do not produce doctor
issues by themselves.
Doctor Repair Boundary
Doctor’s supported repair switch is --repair-wal-tail. The former --repair-main-ref input is
retained only to return an explicit format-1 compatibility refusal in 0.18.0; it performs no repair.
Repair refuses to run when repository health has error-severity issues. The detailed recovery mechanics and safety preconditions for those repairs live in the durability and crash recovery reference. Local lock conflicts, stale-lock limits, and ref compare-and-swap conflicts are covered by the concurrency and locking reference.
Doctor does not synthesize missing objects, repair malformed ref logs, repair checksum mismatches,
repair signatures, auto-trust keys, reconstruct trust policy, recover key material, reconstruct ref
pointers, clear unsafe active sessions, or define stale-lock cleanup. Exact interrupted-publication
completion requires signer-backed seal with matching retained active state.
Relationship to Rollback Verification
Repository verify counts active rollback-draft WAL records after classifying and decoding
rollback-marked Patch payloads under the supported replay subset. It also counts sealed rollback
Blocks and sealed rollback Patch references.
prikk rollback-draft-verify is a stronger selected-ref pre-seal check for one active rollback draft.
It verifies that the active WAL contains exactly one rollback draft and that the draft payload matches
the inverse Patch derived from the selected ref. See the
rollback draft verification guide for the command-level
boundary.
Deferred Work
Still deferred: broader repair policy, stale-lock policy, missing-object recovery, malformed-log
repair, checksum-mismatch repair, object quarantine and garbage collection, repository-wide AUTHOR
trust policy, key rotation, hardware signing, remote trust, hosted identity, JSON output, stable
diagnostic schema, backup/restore tooling, stable repository-format migration, and production
readiness. (MAINTAINER key revocation is no longer deferred — prikk trust maintainer remove.)
Claim-to-Source Anchors
| Claim | Source anchors |
|---|---|
| Repository verification reports counters for objects, WAL records, Blocks, refs, ref logs, rollback material, publication trust, signature-envelope warnings, ref-publication issues, trailing partial WAL bytes, and active WAL metadata state. | verify.rs, signature_diagnostics.rs, verification.rs, DC-39 |
| Verification checks object placement, envelope decoding, object identity, Block references, ref pointer/log consistency, WAL replay, rollback classification, and publication trust. | verify.rs, refs.rs, data model |
| Publication trust checks Block, RefState, and RefUpdate envelopes against repository-local maintainer trust and reports issues separately. | verify.rs, trust.rs, DC-11, trust and threat model |
verify command failure occurs for active-WAL metadata integrity issues, publication-trust issues, or blocking ref-publication issues after printing the report. | main.rs, verify.rs, DC-38 |
| Active WAL metadata has six states, with two healthy states, two empty-WAL warning states, and two non-empty-WAL integrity states. | verify.rs, doctor.rs, DC-15 |
| Doctor is a diagnostic layer over verification with issue severities, issue codes, messages, recommendations, and an issue summary. | doctor.rs, output.rs, PR-011 |
| Doctor surfaces publication-trust and ref-publication issue codes in addition to doctor-owned diagnostics. | doctor.rs, trust.rs, refs/verify.rs |
| Doctor mutation is opt-in and limited to active-WAL tail truncation; format-1 missing-pointer repair is explicitly refused. | doctor.rs, args.rs, DC-38, durability and crash recovery |
Repository verification classifies rollback draft WAL records and sealed rollback material, while rollback-draft-verify performs a stronger selected-ref check. | verify.rs, rollback_verify.rs, PR-029, PR-030, rollback draft verification guide |
| Verify/doctor output is current CLI vocabulary, not a stable machine-readable schema. | output.rs, DC-29 |
Provenance
This reference consolidates current behavior through the DC-39 implementation candidate. It follows
the DC-26 documentation-home model: current-state references live in the published mdBook, not under
rfcs/fdds/. DC-38 documents pointer-first publication diagnostics and the narrower doctor boundary;
DC-39 adds strict new-envelope admission and byte-preserving format-1 signature diagnostics.
Patch Algebra and Merge Evidence
This page is the authoritative current-state reference for Prikk’s patch algebra and merge-evidence concepts. It describes the current implementation through 0.17.1 and is grounded in the code, released RFCs, and implementation status records listed in the anchor table at the foot of the page.
For command syntax and examples, see the merge evidence and merge plan guides.
Core Caveats
- Prikk is early implementation software and is not a production Git replacement.
- Patch algebra and merge evidence are currently read-only analysis surfaces.
prikk merge-evidenceandprikk merge-planrequire explicit baseline, left target, and right target inputs. They do not infer merge bases or branch merge intent.- Current confluence results apply only to the supported operation subset and the selected explicit candidate sequences.
ConfluentandConfluentSubsetalone do not create a merge commit —prikk merge(DC-74) is the separate, explicit command that executes a confluent merge; see the merge guide.- Active-WAL merge drafts, worktree conflict materialization, conflict-resolution UI, persisted proof/witness/plan objects, JSON output, same-node text operational transforms, path-scoped analysis, and public stable Rust APIs remain deferred.
Patch Operations and Ordering
A Patch contains ordered operations. The evidence displays use op_seq to show the one-based operation
sequence recorded by a Patch operation, while bracketed indexes such as left[0] and right[0] show
the zero-based position in the derived left or right candidate sequence.
The current evidence model summarizes operation kind, optional node id, and a safe repository-relative path when one is available. It does not expose raw operation payloads. Preconditions and evidence facts are checked through the store-backed patch-algebra evidence boundary; malformed required sealed evidence is an evidence failure, not ordinary unsupported algebra.
Pair Classification
Internal pair classification currently uses four categories:
| Pair class | Meaning |
|---|---|
Independent | The classifier sees no ordering or conflict relation for the pair, subject to later replay proof. |
OrderedDependency | The pair has a required order, such as create-after-delete relations that can only be considered in one direction. |
Conflict | The pair has a concrete conflict witness, such as same-path creation, live-state mismatch, mode/blob mismatch, or delete/mutation conflict. |
Unknown | The relation cannot be safely classified, either because the operation/relation is unsupported, evidence is insufficient, or the design is intentionally deferred. |
These Rust categories are implementation details, not stable public API. Public commands surface the separate merge-evidence outcomes described below.
Intent metadata is advisory. It does not override replay, lifecycle, preimage, evidence, or commutation proof requirements.
Commutation
Prikk treats a pair as commuting only when both conditions hold:
- the classifier reports
Independent; and - replaying the pair in both orders produces the same lifecycle state.
If the classifier reports an ordered dependency or conflict, the pair does not commute. If required evidence is missing or malformed, the analysis fails closed as an evidence problem. If a relation is not supported or is intentionally deferred, it remains unknown rather than being treated as safe.
Flat Confluence
Current confluence is flat and explicit-input. The analysis receives a sealed baseline state plus two candidate operation sequences derived from explicit left and right targets.
The current check requires:
- each candidate sequence to replay validly enough for the supported subset;
- cross-pairs between left and right to commute;
- replay of left-then-right and right-then-left to succeed; and
- final lifecycle states to be equal.
This is not automatic branch merge semantics. It does not choose a merge base, publish a result, materialize a worktree, create a merge commit, or create multi-parent Blocks.
Evidence Outcomes
prikk merge-evidence prints the public DC-21/DC-23 outcome vocabulary:
| Outcome | Meaning |
|---|---|
Confluent | The selected sequences are proven confluent under the current supported analysis. This is scoped evidence, not execution readiness. |
Conflict | A concrete conflict witness was found. |
OrderedDependency | A relation requires ordering policy that the current public merge surface does not execute. |
Unsupported | The operation kind or relation is outside the supported algebra subset. |
Deferred | The relation is known but intentionally deferred, such as same-node text transforms or sequence-internal dependency handling. |
NotConfluent | Replay or final-state comparison failed after otherwise supported analysis. |
EvidenceFailure | Required sealed evidence is missing, malformed, unreadable, wrong-type, or identity-invalid. |
InvalidCandidate | Candidate input is malformed or insufficient before analysis can produce usable evidence. |
EvidenceFailure is distinct from Unsupported or Deferred: required sealed evidence failures must
not be hidden as unknown algebra.
Reason Codes and Proof Phases
Evidence output also prints reason: and item-level phase: fields. Reason codes explain why an
outcome was produced; phases say which proof stage produced the item.
Current public reason-code names include:
| Reason code | Meaning |
|---|---|
proven_confluent | The selected pair or sequence passed the current confluence proof. |
pair_conflict | A cross-side pair produced a conflict witness. |
ordered_dependency | A cross-side pair requires a specific order. |
unsupported_operation | The operation or relation is outside the current supported subset. |
same_node_text_transform_deferred | Same-node text operational transforms are intentionally deferred. |
sequence_internal_dependency_deferred | A sequence-internal dependency blocks flat confluence analysis. |
pair_replay_failed | Replaying a pair in both orders did not prove commutation. |
final_state_mismatch | Final lifecycle states differed after composed replay. |
missing_required_evidence | Required sealed evidence was absent. |
malformed_required_evidence | Required sealed evidence was present but malformed. |
wrong_type_required_evidence | Required sealed evidence had the wrong object kind. |
unreadable_required_evidence | Required sealed evidence could not be read. |
invalid_unsealed_candidate | Optional unsealed candidate evidence was malformed. |
insufficient_unsealed_candidate_evidence | Optional unsealed candidate evidence was insufficient for analysis. |
Current public proof phases include:
| Phase | Meaning |
|---|---|
classification | Pair classification or evidence validation produced the item. |
replay-both-orders | Pair replay in both operation orders produced the item. |
flatness | Candidate-sequence flatness checks produced the item. |
final-state-comparison | Final lifecycle-state comparison produced the item. |
composed-replay exists only behind test-only display code and is not a current public phase.
Merge Plan Mapping
prikk merge-plan preserves the underlying evidence outcome and maps it to a non-executable planning
status:
| Evidence outcome | Plan status | Action |
|---|---|---|
Confluent | ConfluentSubset | Review the evidence, then run prikk merge (DC-74) to execute. |
Conflict | BlockedConflict | Inspect evidence; conflict resolution is not implemented. |
OrderedDependency | BlockedOrderedDependency | Inspect ordering evidence; execution ordering policy is not implemented. |
Unsupported | BlockedUnsupported | Inspect unsupported operation evidence. |
Deferred | BlockedDeferred | Inspect deferred design evidence. |
NotConfluent | BlockedNotConfluent | Inspect replay/final-state mismatch evidence. |
EvidenceFailure | BlockedEvidenceFailure | Repair or verify repository evidence before planning. |
InvalidCandidate | BlockedInvalidCandidate | Select valid sealed candidates before planning. |
ConfluentSubset is intentionally narrow. It means the selected candidates are proven confluent only
for the currently supported subset. It is not a whole-merge guarantee and does not mean Prikk can
create a merge commit.
Privacy and Output Limits
Evidence and plan output are intended for human diagnostics, not as durable machine-readable schema.
The current display model avoids raw replacement text, raw text spans, blob bytes, absolute host
paths, .prikk private paths, signer secrets, key material, arbitrary object debug dumps, and raw
operation payloads. Displayed paths are repository-relative when available and safe.
Deferred Work
prikk merge (DC-74) executes confluent merges — see the merge guide. Still
deferred: automatic merge-base discovery, branch merge semantics beyond a two-sided confluent merge,
conflict resolution, active-WAL merge drafts, worktree conflict materialization, conflict-resolution
UI, persisted proof/witness/merge-evidence/merge-plan objects, same-node text operational transforms,
path-scoped analysis, display-path filtering, JSON output, patch-algebra crate extraction, and public
stable Rust APIs for replay, patch algebra, merge evidence, or merge planning internals.
Claim-to-Source Anchors
| Claim | Source anchors |
|---|---|
Pair classification uses Independent, OrderedDependency, Conflict, and Unknown. | types.rs, classify.rs, DC-16 |
| Commutation requires classifier independence plus replay-both-orders proof. | commutation.rs, DC-18 |
| Flat confluence checks individual sequence validity, cross-pair commutation, composed replay, and final lifecycle-state equality. | commutation.rs, analysis.rs, DC-18 |
| Required sealed evidence failures are reported separately from ordinary unsupported algebra. | evidence.rs, error.rs, DC-17 |
Merge-evidence public outcomes are Confluent, Conflict, OrderedDependency, Unsupported, Deferred, NotConfluent, EvidenceFailure, and InvalidCandidate. | types.rs, display.rs, DC-21 |
| Reason-code and proof-phase strings are display vocabulary, not persisted object schema. | display.rs, mapping.rs, DC-21 |
merge-evidence is read-only and requires explicit baseline plus left/right targets. | merge_evidence.rs, DC-22, merge evidence guide |
merge-plan maps evidence outcomes to ConfluentSubset and Blocked* statuses without adding merge execution. | merge_plan.rs, DC-25, merge plan guide |
| Evidence and plan output avoid raw text spans, replacement text, blob bytes, absolute host paths, and arbitrary object debug dumps. | display.rs, DC-21, DC-23 |
| Patch algebra, merge evidence, and merge plan internals are not public stable Rust APIs. | DC-20, DC-25, implementation status |
Provenance
This reference consolidates released records through DC-25 and follows the DC-26 documentation-home model: current-state references live in the published mdBook, while RFCs retain design history and gating material. It does not change code, schema, CLI behavior, merge semantics, or public API stability.
Development
The implementation follows the design-first sequence:
- Requirements and RFCs.
- External design.
- Foundational Design Documents.
- Program design.
- Implementation.
- Testing and evidence.
Release preparation follows the separate release, versioning, and compatibility policy. A listed gate is not passing evidence unless it was observed for the exact commit or release under review.
Run the standard checks before submitting a source drop:
cargo fmt --all -- --check
cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
cargo test --workspace --locked
cargo run --locked -p prikk-release-policy -- check
Building the documentation
The book uses Mermaid diagrams, which are rendered by the mdbook-mermaid preprocessor. Both tools are
needed to build it:
cargo install mdbook --no-default-features --features search --vers "^0.5" --locked
cargo install mdbook-mermaid --vers "^0.17" --locked
mdbook build docs
mdbook build fails with a clear message if the preprocessor is missing, so a stale toolchain cannot
silently produce diagrams as code blocks. The Mermaid assets are vendored under docs/, so the built
book renders offline and fetches nothing.
The workspace declares Rust 1.85 as its minimum supported version. Verify that contract with the exact minimum toolchain and locked dependency graph:
cargo +1.85.0 check --workspace --all-targets --locked
cargo +1.85.0 test --workspace --locked
cargo +1.85.0 build --workspace --locked
Strict Clippy remains a current-stable quality gate. It is not an MSRV gate because Clippy’s lint set changes with the toolchain.