Prikk Documentation
Prikk is an experimental, design-first distributed version control system built around block-oriented patch theory. This documentation now covers a full guide and reference section, and continues to grow as new 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.
New here? Install, then the Tutorial — a first repository from an empty directory to a sealed, verified commit, key setup included.
For the current architecture and trust boundaries, start with:
Install
See the root README for what to run: the shell
installer below, cargo binstall prikk, a direct download from the release
page, or cargo install prikk from crates.io. This
page covers what comes after — the steps a new user usually gets stuck on.
The shell installer
curl -fsSL https://github.com/prikk-vcs/prikk/releases/latest/download/install.sh | sh
What it does, exactly: detects your OS and CPU architecture (uname -s/uname -m); downloads
that platform’s release archive and its .sha256 file from the same release page URLs the manual
path uses; verifies the checksum with sha256sum -c (or shasum -a 256 -c on macOS, which has no
sha256sum by default) and installs nothing if verification fails; extracts the prikk binary
to ~/.local/bin (chmod +x); and, if that directory is not already on PATH, appends one clearly
marked block to your shell’s startup file so a later uninstall can find and remove exactly that
block, and nothing else.
Generated, not hand-maintained. install.sh/uninstall.sh are produced by
prikk-release-policy generate-installer and attached as release assets at publish time, the same
way the tarballs and their checksums are — never committed to the repository as tracked files.
Their source template lives in tools/release-policy/templates/, reviewed like any other change to
this project.
What it claims, and what it does not. A passing checksum proves the file you received matches
what the release page published; it does not prove who published it.
In v0 one maintainer key signs every release tag; release-signers.toml is empty because no
multi-signer policy exists yet — see
how prikk releases. The script prints this
same statement when it finishes, rather than only stating it here.
Supported today: Linux (x86_64, aarch64) and macOS (Apple Silicon only — there is no
prebuilt binary for Intel Macs). On Windows, this script refuses — it is a POSIX shell script,
unusable there — and points at the Windows installer below, rather than
silently doing nothing or guessing.
Version selection: defaults to the latest release. Pin one with sh install.sh --version X.Y.Z
or PRIKK_INSTALL_VERSION=X.Y.Z — the form a CI pipeline typically wants, so a workflow does not
silently pick up a new release mid-pipeline. Override the install directory with --prefix DIR or
PRIKK_INSTALL_DIR.
Read it before you run it, if you prefer: curl -fsSL <url> -o install.sh, open it in an
editor, then sh install.sh. It is a single self-contained file — nothing it does is hidden behind
a second download.
Re-running is safe. It overwrites the binary with the same (or a newly pinned) version and does
not duplicate the marked PATH block if one already exists.
Uninstalling:
curl -fsSL https://github.com/prikk-vcs/prikk/releases/latest/download/uninstall.sh | sh
Removes the binary from its install directory and the marked PATH block from whichever shell
startup file has it, and nothing else — an unrelated file in the same directory, or unrelated lines
in the same startup file, are left untouched. If you installed with a custom PRIKK_INSTALL_DIR,
pass the same value to the uninstaller too, the same way: PRIKK_INSTALL_DIR=/custom/path curl ....
Neither script touches any repository’s own .prikk directory — delete those yourself if you no
longer want that history tracked.
One real caveat about the PATH edit: macOS Terminal starts a login shell, which reads
~/.bash_profile, not ~/.bashrc, unless your own ~/.bash_profile already sources it. The
installer still writes to ~/.bashrc (or ~/.zshrc for zsh) — the file an interactive non-login
shell reads on Linux, and the common case this script optimizes for — and says so when it runs; if
your ~/.bash_profile does not source ~/.bashrc, add the PATH line manually, or add
. ~/.bashrc to ~/.bash_profile yourself.
The Windows installer
irm https://github.com/prikk-vcs/prikk/releases/latest/download/install.ps1 | iex
What it does, exactly: decides the architecture from Windows’ own processor environment
variables ($env:PROCESSOR_ARCHITEW6432 when set, else $env:PROCESSOR_ARCHITECTURE — never a
.NET API); downloads that platform’s release .zip and its .sha256 from the same release
page URLs the manual path uses; verifies the checksum with Get-FileHash and installs nothing if
verification fails; extracts prikk.exe to %LOCALAPPDATA%\Programs\prikk; and, if that
directory is not already on your user PATH, adds it — see The PATH change, below.
Generated, not hand-maintained. Exactly like install.sh/uninstall.sh above,
install.ps1/uninstall.ps1 are produced by prikk-release-policy generate-installer and attached
as release assets at publish time — never committed to the repository as tracked files. Their
source template lives beside install.sh’s own, in tools/release-policy/templates/.
What it claims, and what it does not. The same statement as the shell installer’s: a passing
checksum proves the file you received matches what the release page published; it does not prove
who published it. The script prints this itself when it finishes, the same as install.sh does.
prikk.exe is not code-signed, and Windows may warn or block it.
Supported today: x64 Windows installs the x64 build. ARM64 Windows installs the x64 build
too, which runs under Windows’ own x64 emulation. Any other architecture refuses with a pointer to
the manual .zip download or cargo install prikk, the same way the shell installer refuses an
Intel Mac.
Version and install-directory overrides: environment variables only —
PRIKK_INSTALL_VERSION=X.Y.Z and PRIKK_INSTALL_DIR=<path>, set before the one-liner above. There
are no --version/--prefix flags here, unlike install.sh: the one-line irm | iex form
evaluates the downloaded script as if it were typed at the prompt, with no way to forward
positional arguments to it the way sh -s -- --version X.Y.Z can.
The PATH change. The installer adds its install directory to your user PATH (never the
machine PATH), without asking first — the same no-prompt rule install.sh follows for its own
PATH edit. It records in a marker file, inside the install directory, that it made the change;
uninstall.ps1 removes the PATH entry only if that marker says this installer added it, so a
directory you had already put on PATH yourself beforehand is left alone.
Execution policy. The one-line form above is unaffected by execution policy: code piped into
Invoke-Expression runs as if typed at the prompt, which is not the code path execution policy
gates. If you instead save the file first (irm <url> -o install.ps1, the same “read it before you
run it” option the shell installer offers) and try to run that file directly, PowerShell’s default
policy will likely block it — either Unblock-File .\install.ps1 first, or run it as
powershell -ExecutionPolicy Bypass -File install.ps1.
Re-running is safe. It overwrites prikk.exe with the same (or a newly pinned) version and
does not add a duplicate PATH entry if one is already there.
Uninstalling:
irm https://github.com/prikk-vcs/prikk/releases/latest/download/uninstall.ps1 | iex
Removes prikk.exe, the marker file, and the install directory itself if it is then empty, and the
PATH entry — but only if the marker says this installer’s own run added it. If you installed with
a custom PRIKK_INSTALL_DIR, set the same value before running the uninstaller too.
Arch Linux (AUR)
Two packages, prikk and
prikk-bin, are generated and pushed to the AUR by
the release workflow (the aur-build and aur-push jobs) for every tagged release — not
hand-maintained. This differs from install.sh/install.ps1 above: those are attached as release
assets, while these are pushed straight into each package’s own AUR git repository.
prikk-bin installs the prebuilt binary and needs no key. Its PKGBUILD carries no
validpgpkeys — the checksum is all it verifies. As elsewhere on this page: a passing checksum
proves the package you received matches what the release published; it does not prove who
published it. Before building, paru -G prikk-bin downloads its PKGBUILD for reading: check that
its source_x86_64/source_aarch64 URLs point at
github.com/prikk-vcs/prikk/releases/download/.
paru -S prikk-bin # or: yay -S prikk-bin
prikk builds from the signed source tag instead, and needs one step first that prikk-bin
does not: import the release key, using the same command Verify the
tag below already uses.
curl -fsSL https://github.com/nabbisen.gpg | gpg --import
This step is not optional. AUR helpers cannot fetch the key for you: it is on no public
keyserver — neither keys.openpgp.org nor keyserver.ubuntu.com has it. Without the import,
makepkg stops with “unknown public key”. Once the key is imported, read the PKGBUILD before building:
paru -G prikk # downloads the PKGBUILD (and .SRCINFO) for reading; builds nothing
and check that validpgpkeys names 25757DA6CBF7022C4E14CCAC1B3066B87DB99A34 and that the
source= line ends in ?signed. Anyone with AUR push access to the prikk package controls its
text, and could delete validpgpkeys and the ?signed suffix on source= entirely, leaving no
signature check at all. The signature protects the source only for as long as the PKGBUILD is the
one the release workflow itself generated and pushed — so check those two fields yourself before
every build.
paru -S prikk # or: yay -S prikk
makepkg verifies the tagged source against the imported key before building; a tag whose signature
does not verify never reaches cargo build. That check accepts a signature only from a fingerprint
actually listed in the PKGBUILD’s own validpgpkeys — importing the wrong key cannot pass it.
Verify the checksum
Every downloaded release archive ships beside a .sha256 file recording its SHA-256 digest —
for example, prikk-x86_64-unknown-linux-gnu.tar.gz ships beside
prikk-x86_64-unknown-linux-gnu.tar.gz.sha256. Download both into the same directory, then verify
from inside it, substituting the archive name for your platform.
Linux:
sha256sum -c prikk-x86_64-unknown-linux-gnu.tar.gz.sha256
macOS (no sha256sum by default; shasum reads the identical file format):
shasum -a 256 -c prikk-aarch64-apple-darwin.tar.gz.sha256
Both print <archive name>: OK on success and exit non-zero on a mismatch.
Windows (PowerShell):
$expected = (Get-Content prikk-x86_64-pc-windows-msvc.zip.sha256).Split(' ')[0]
$actual = (Get-FileHash prikk-x86_64-pc-windows-msvc.zip -Algorithm SHA256).Hash.ToLower()
$expected -eq $actual
This should print True. Unverified on a Windows machine — this command was derived from how
the release build produces the checksum file (a lowercase hex digest, matching sha256sum’s own
format), not confirmed by running it.
A passing checksum means the download was not damaged in transit. It does not show that the file is genuine: whoever can replace an asset can replace its checksum too. To check that a release came from the project’s key, see Verify the tag below. The wider caveats are in Release, Versioning, and Compatibility.
Verify the tag
Release tags are signed with the project’s one maintainer key. Its fingerprint is:
25757DA6CBF7022C4E14CCAC1B3066B87DB99A34
Fetch the key, then check the tag of the version you installed. The check runs in a clone of the repository, so you need one (an installed binary has none):
curl -fsSL https://github.com/nabbisen.gpg | gpg --import
git clone https://github.com/prikk-vcs/prikk && cd prikk
git verify-tag --raw 0.48.0 2>&1 | grep VALIDSIG
git verify-tag --raw prints the VALIDSIG line on stderr, hence 2>&1. Its last field must equal the fingerprint
above; that is the check. The warning “This key is not certified with a trusted
signature” is expected unless you have certified the key yourself (gpg --lsign-key). What matters is the fingerprint
match.
What the tag covers: the source at the tagged commit. The release binaries are built from that tag by the release workflow and are not signed, so the tag does not cover them. A checksum (above) detects a damaged download, not a substituted one.
Build from source
Prebuilt archives cover four targets: x86_64 and aarch64 Linux, aarch64 macOS, and x86_64
Windows. Anywhere else, build it yourself — there is no separate porting step:
cargo install prikk # from crates.io, and puts it on PATH for you
or from a clone, which is also what you want when working on Prikk itself:
git clone https://github.com/prikk-vcs/prikk
cd prikk
cargo build -p prikk --release --locked # binary at target/release/prikk
Other Linux architectures
Fully supported. Nothing in Prikk is gated on CPU architecture — only on the operating system — so any architecture Rust targets on Linux builds and runs with no reduction in capability.
FreeBSD, OpenBSD, and other platforms
They build, but they are read-only. Prikk compiles for FreeBSD, and nothing in it is OpenBSD-specific, so the same applies there — but repository mutation is refused at runtime on any platform other than Linux, macOS, and Windows:
repository mutation requires Linux, macOS, or Windows root-scoped filesystem capabilities
So init, commit, and seal will not work. Reading an existing repository does — verify, log,
status, doctor, and the other read-only commands.
This is a review boundary rather than a technical one. FreeBSD has the filesystem primitives Prikk’s POSIX path uses; what it does not have is a durability implementation anyone has reviewed, or CI that exercises one. Prikk refuses rather than writing history through a path nobody has audited. Support is recorded as a future theme and is not scheduled. Platform Support states what each supported platform guarantees, including two narrower guarantees on Windows.
Put the binary on PATH
cargo binstall/cargo install place the binary in Cargo’s own bin directory (~/.cargo/bin on
Linux/macOS), which is normally already on PATH once Rust is installed.
For a direct download, move the extracted prikk (or prikk.exe on Windows) into a directory
already on your PATH — ~/.local/bin is a common choice on Linux/macOS if it’s already there —
or add the directory you placed it in to PATH yourself.
Confirm it worked
prikk --version
prints the version you installed, in the form:
prikk <version>
Check that <version> matches the release you downloaded — that match is the actual
verification, not any particular number this page could show.
If the shell reports “command not found” instead, the binary is not on PATH yet.
Next: Tutorial
Installing the binary configures no signing keys. Continue with the Tutorial — it walks through your first commit and seal, key setup included, using disposable public example keys. Security and Signing Setup covers real key handling once you are past that.
Uninstalling
Prikk places nothing outside its own binary and, inside each repository it manages, a .prikk
directory. Delete the binary, and remove .prikk from any repository whose history you no longer
want tracked — there is nothing else to clean up.
First Run: Keys and Setup
Read this before Install’s next step, Tutorial — this page is about getting your own signing keys, not the shared example seed the tutorial deliberately reuses so its own walkthrough is reproducible.
Every prikk commit and every prikk seal needs a real Ed25519 key. Before prikk key and prikk setup existed, obtaining one meant inventing 32 bytes by hand and then having no way at all to derive
the matching public key — reproduced first-hand while preparing this project’s own release testing,
by copying a public key out of a CI configuration file rather than deriving it. That gap is what this
page closes.
The fast path: prikk setup
prikk setup composes init, key generation for both roles, and trust maintainer add into one
command:
prikk setup ./my-repo
initialized Prikk repository at ./my-repo/.prikk
trusted maintainer key: ed25519-cd1bc694acba34fe
adopted maintainer keys: 1
your keys are in /home/you/.config/prikk
every new shell finds them -- nothing to export
next steps:
prikk commit -m "<message>"
prikk seal --allow-no-audit # no audit trust policy is configured yet; see `prikk seal --help`
There is nothing to export and nothing to copy. Both seeds are written as files in your key
directory, and every later prikk run finds them there — this shell, tomorrow’s shell, after a
reboot. Commit and seal directly:
echo "hello prikk" > ./my-repo/readme.txt
(cd ./my-repo && prikk commit --from-worktree -m "genesis")
(cd ./my-repo && prikk seal --allow-no-audit)
Each new key gets its own key id. A key id is the name a signature carries.
setup names each key it creates ed25519- followed by the first 16 hex characters of its public key,
and writes that id beside the seed. Your keys’ ids are therefore yours alone: two people who each ran
setup can exchange history, and each adopts the other’s maintainer key under its own id. The id is a
name, not a proof. What makes it an identity is that a repository binds one id to one public key.
prikk key status shows the id in effect and where it came from.
Neither command names a branch. Every command that takes --ref defaults to the branch named in
.prikk/current-branch, which init and setup set to heads/main; --ref given explicitly
always wins.
Where your keys live
One directory, per platform, and prikk resolves it itself:
| platform | key directory |
|---|---|
| Linux, macOS, BSD | $XDG_CONFIG_HOME/prikk, or $HOME/.config/prikk when XDG_CONFIG_HOME is unset |
| Windows | %APPDATA%\prikk |
It holds author.seed and maintainer.seed, plus author.key-id and maintainer.key-id, each
holding the id of the seed beside it. A key-id file must belong to its seed. If the file does not
hold the id derived from the seed in use (a replaced seed, a copied file, or hand-edited content),
signing is refused, and prikk key status reports the key unusable, naming the file and both ids.
To sign under an id of your choosing, set PRIKK_AUTHOR_KEY_ID / PRIKK_MAINTAINER_KEY_ID; never edit
the file. On Unix the directory is mode 0700
and each file 0600; prikk refuses to read a seed file that group or others can read, naming the
mode and the chmod that fixes it. On Windows there are no mode bits: the directory relies on
%APPDATA% being per-user by platform ACL, which is stated here rather than assumed — it is the one
location where prikk accepts inherited permissions, because they are the right ones and they are
known.
--author-seed-out <path> / --maintainer-seed-out <path> override the location. A seed written
somewhere else is not found automatically, so setup prints the PRIKK_AUTHOR_SEED_FILE /
PRIKK_MAINTAINER_SEED_FILE line that points at it, and the key id. The id lives in <path>.key-id,
beside the seed, and is found from there. It is printed, not exported: an exported
PRIKK_*_KEY_ID would override the file.
The seeds are not recoverable. There is no copy in the repository, no keyring, no escrow. Lose the AUTHOR seed and you can no longer sign patches with that identity; lose the MAINTAINER seed and you can no longer seal to a repository that already trusts it. Back up the key directory the way you back up anything else you cannot regenerate.
The trust decision is always shown, never performed silently. trusted maintainer key: <id>
is the same line prikk trust maintainer add itself prints — registering a maintainer key is a trust
act, and composing the steps removes the typing, never the seeing.
After a reboot
Nothing to do. Your keys are files in the key directory, so a new shell, a new terminal, a reboot —
prikk commit and prikk seal keep working with no setup at all. This section used to explain how
to re-export two variables after every reboot; the variables are gone.
If you moved a seed with --*-seed-out, that one is the exception: prikk does not look there on
its own, so export its PRIKK_*_SEED_FILE in your shell profile —
export PRIKK_AUTHOR_SEED_FILE="$HOME/keys/author.seed"
— or move the file into the key directory and drop the variable.
PRIKK_AUTHOR_SEED and PRIKK_MAINTAINER_SEED are no longer read
Before prikk 0.40 a seed was passed as an environment variable. That channel is removed: a seed in the environment leaks into process listings, shell history, and any child process, and it silently survived in profiles long after the key had changed.
A stale export is now simply ignored. prikk 0.40 refused while either variable was set — one release wide, so that no automation could quietly start signing with a different key than it thought it was using — and 0.41 stopped doing even that. Either variable may sit in your shell profile indefinitely; prikk does not read it, and does not object to it.
You can still remove it, and should, because it is a secret sitting in a file for no reason. To see which key is actually in effect before you commit anything:
prikk key status
That names the file each role resolves to and whether it is usable — see Can I Sign Here?.
The commands setup composes — and when you’d use them directly
setup is not the only way in. Each step is a first-class, documented command in its own right, and
understanding them is what lets you reason about what setup actually did.
prikk key generate — a fresh seed
prikk key generate
seed: ...
note: this seed is now in your terminal scrollback -- treat it as a secret
key id: ed25519-2d353d98420bc75c
public key: ...
next steps:
recommended: re-run with --out <path>, which writes the seed and its key-id file
prikk trust maintainer add --key-id ed25519-2d353d98420bc75c --public-key ...
or save by hand -- both files, mode 0600:
/home/you/.config/prikk/maintainer.seed (the seed above and a newline)
/home/you/.config/prikk/maintainer.key-id (exactly ed25519-2d353d98420bc75c and a newline)
note: a seed saved without its key-id file signs under the shared legacy id `maintainer`, which other installations may also use
note: the same seed works as an AUTHOR key instead -- name the files author.seed and author.key-id and skip the trust step
Saving by hand needs both files. The key id travels with the seed only through its key-id file; a seed saved alone signs under the legacy id every installation made before 0.45.0 shares.
--out <path> writes the seed instead of printing it — and then it is never printed at all, only
the public key and the next steps are:
prikk key generate --out ./maintainer.seed
wrote seed to ./maintainer.seed (mode 0600)
key id: ed25519-2d353d98420bc75c (in ./maintainer.seed.key-id)
public key: ...
...
--out also writes the key’s id to <path>.key-id, and the id is found there whichever role uses the
seed. --out refuses to overwrite an existing file (the seed or its key-id file), and refuses any path with a .prikk component — prikk
never invents a secret’s location and never manages its lifecycle (writing it once, where you asked,
is the entire commitment). On Windows, --out currently refuses outright: Unix file permissions
(mode 0600) have no portable equivalent here without unsafe code or a new dependency, and writing a
secret at whatever permissions the filesystem happens to inherit, silently, is not acceptable. Print
and place the seed yourself instead.
prikk key public — derive a public key you already have
With no arguments it reads your key directory’s author.seed; --role maintainer reads the other
one:
prikk key public --role maintainer
public key: ...
--seed-file <path> reads any other seed file instead — one you moved with --*-seed-out, or one
from elsewhere entirely.
The seed is read from a file, never from an argument. A --seed <hex> flag would put key
material into /proc/<pid>/cmdline (world-readable on Linux) and into shell history; a path is not a
secret, and the seed itself never appears on the command line. This replaces the older
--seed-env <NAME>, which took the name of an environment variable holding the seed — there is no
longer an environment variable to name.
prikk trust maintainer add — the trust act itself
prikk trust maintainer add --key-id <the key id key generate printed> --public-key <the hex key generate printed>
Registering a maintainer key is what lets prikk seal publish — see
Security and Signing Setup for the full trust model, revocation, and what is and
is not enforced today.
Which key is which
Two independent roles, two independent seeds:
- AUTHOR signs the Patch a
commitqueues. No trust registration needed for it at all. - MAINTAINER signs the Block, RefState, and RefUpdate a
sealpublishes, and must be registered withtrust maintainer addfirst.
Trust is per-repository. The trust policy a maintainer key is adopted into belongs to one repository, so the same maintainer key must be trusted again in every repository you seal in — adoption is a trust act about this repository, not a machine-wide or account-wide setting. An AUTHOR key has no such step anywhere, which is why it travels for free.
setup generates one of each, but a single seed works as either role — key generate prints the
maintainer framing because that is the one role requiring a visible trust step, but the same seed
saved as author.seed works too, with no trust step at all.
A second project
prikk setup ./second-project
initialized Prikk repository at ./second-project/.prikk
trusted maintainer key: ed25519-cd1bc694acba34fe
adopted maintainer keys: 1
using your keys in /home/you/.config/prikk
every new shell finds them -- nothing to export
using rather than your keys are in is the whole difference. setup found the seeds already
in your key directory, so it minted nothing and wrote nothing — it initialized the repository and
adopted the maintainer key you already have, which is the one step a new repository genuinely needs.
Both projects sign with the same identity, which is usually what you want when it is the same person.
Pointed at a directory that already holds a repository, setup refuses before doing anything at
all:
error: precondition not met: ./second-project already holds a repository; to use your existing keys
here run `prikk trust maintainer add` (see `prikk key public`), or pick a different directory for a
new project
If you want the second project to have its own identity, give the new seeds their own paths:
prikk setup ./second-project \
--author-seed-out ~/keys/second-author.seed \
--maintainer-seed-out ~/keys/second-maintainer.seed
setup prints the PRIKK_*_SEED_FILE lines for them, since prikk will not find them on its own.
The same thing by hand
setup composes it; these are the steps if you would rather run them yourself:
cd ./second-project
prikk init .
prikk key status --role maintainer # the key id line names the id to adopt
prikk trust maintainer add --key-id <that key id> --public-key "$(prikk key public --role maintainer | sed 's/^public key: //')"
prikk commit needs nothing extra — an AUTHOR key is registered nowhere, so it needs nothing from
any repository. Only seal needs the trust act above; without it:
error: precondition not met: no maintainer key is adopted in this repository yet; run `prikk trust maintainer add` (a trust policy container that replays empty reads the same way -- run `prikk doctor` if a key was adopted here before)
That is the state every new repository starts in, not a fault. Adopt the key, deriving the public half from the seed you already hold:
prikk key public --role maintainer
public key: 27b081593fa86489f9356ef4bc0cbf5f4a5a5b708aa1a10f1a8187fd56a34801
prikk trust maintainer add --key-id ed25519-27b081593fa86489 --public-key 27b081593fa86489f9356ef4bc0cbf5f4a5a5b708aa1a10f1a8187fd56a34801
prikk seal --allow-no-audit
trusted maintainer key: ed25519-27b081593fa86489
adopted maintainer keys: 1
sealed active WAL into block
The --key-id must be the id seal signs under, which prikk key status --role maintainer reports;
seal checks the signer against what this repository trusts under that id.
Keys made before 0.45.0
A seed with no key-id file beside it signs under the legacy id: author or maintainer, the role
word. Keys made by setup before 0.45.0 look like that, and keep that id. Nothing about an existing
installation changes, and its repositories keep committing, sealing and verifying. setup reusing such
a key says so:
note: your author key uses the shared legacy key id `author`, which other installations may also use; for a distinct one, set PRIKK_AUTHOR_KEY_ID and adopt the key under that id in each repository
Every such installation shares those ids, so two of them collide. One key id binds to one public key in a repository, so:
- history one installation signed under
authorcannot be imported into a repository whereauthoris already bound to the other’s key; - a maintainer key cannot be adopted under
maintainerbeside a different one.
Both refusals name the route that works: sign new history under a distinct id, either a key made by
prikk key generate --out <path> (pointed at by PRIKK_*_SEED_FILE) or an id set with
PRIKK_*_KEY_ID, and adopt the maintainer key under that id. History already signed under the shared
id keeps it; there is no command that re-signs it.
Claim-to-Source Anchors
| Claim | Source anchors |
|---|---|
prikk key generate/prikk key public/prikk setup exist and compose init, key generation, and trust maintainer add. | key.rs, setup.rs, commands.rs |
A generated seed draws from the OS CSPRNG and is never accepted on argv; key public reads it from a named environment variable. | prikk-crypto/src/lib.rs (generate_seed), key.rs |
--out writes the seed at mode 0600, refuses to overwrite, and refuses a path inside .prikk/; it refuses outright on Windows. | key.rs (write_seed_to_path) |
setup shows the trust decision it makes, and prints nothing that reproduces without your own OS entropy. | setup.rs |
Maintainer trust is per-repository: seal checks the exported signer against the trust policy of the repository being sealed, so the same key must be adopted again in each one. | trust.rs (verify_signer_trusted, which resolves the policy from the RepositoryLayout it is given) |
Provenance
Written for RFC 135
§9, which measured the unfamiliar-step count to a first sealed commit at eleven, with the third step
(deriving a maintainer public key) impossible before this page’s own commands existed. See
Git → prikk for how prikk’s commands relate to Git’s, including
git config’s row, which is where this page is cross-linked from.
Tutorial
This is the first thing to read after Install — one continuous path from an empty
directory to a sealed, verified repository: create it, make a commit, seal it, then look at what you
built. It confronts key setup directly, because commit and seal both need one and a tutorial that
hides that fails at your second command.
Prikk is early implementation software and is not a production Git replacement. Work through this tutorial with throwaway content — nothing here is a claim that Prikk is ready to hold history you care about yet.
Create a repository
mkdir sample-repo && cd sample-repo
prikk init
initialized Prikk repository at /path/to/sample-repo/.prikk
.prikk is the only thing Prikk ever creates. There is no separate config step.
Your first commit — and why it needs a key
Add a file and commit it:
echo "hello prikk" > readme.txt
prikk commit -m "genesis"
error: author signing is required: no seed at /home/you/.config/prikk/author.seed. Create one with `prikk key generate --out /home/you/.config/prikk/author.seed`, run `prikk setup` in a new project directory, or set PRIKK_AUTHOR_SEED_FILE to an existing seed file
This refusal is the point, not a mistake in this tutorial. Every commit in Prikk produces a signed Patch — there is no unsigned-commit mode to fall back to. The refusal names the file prikk looked for; create it and try again:
prikk key generate --out ~/.config/prikk/author.seed
prikk commit -m "genesis"
recorded worktree patch in active WAL
baseline ref: heads/main
patch id: ...
WAL sequence: 1
operations: 1
referenced blobs: 1
create-file readme.txt
The seed above is a public example, shown in this documentation and in the project’s README — never use it, or any seed you have shown anyone else, for real signing. See First Run for how to generate your own, and Security and Signing Setup for how real key material is meant to be handled; this tutorial only needs you to get past the refusal, not to understand key management yet.
The commit is recorded, but nothing has been published — readme.txt’s patch sits in the active
write-ahead log until you seal it. You can commit again before sealing; Prikk queues patches and
seals them together. This tutorial seals right away.
Sealing — and the second key
prikk seal --allow-no-audit
error: maintainer signing is required: no seed at /home/you/.config/prikk/maintainer.seed. Create one with `prikk key generate --out /home/you/.config/prikk/maintainer.seed`, run `prikk setup` in a new project directory, or set PRIKK_MAINTAINER_SEED_FILE to an existing seed file
A different role, a different key. commit signs as the author of a change; seal signs as
the maintainer who publishes it — Prikk keeps the two separate even when one person holds both
roles, because the trust boundary they express is different (see
Trust and Threat Model for why). Set the maintainer key and try
again:
prikk key generate --out ~/.config/prikk/maintainer.seed
prikk seal --allow-no-audit
error: precondition not met: no maintainer key is adopted in this repository yet; run `prikk trust maintainer add` (a trust policy container that replays empty reads the same way -- run `prikk doctor` if a key was adopted here before)
One more refusal, and it is also expected. Having a maintainer key configured is not the same as Prikk trusting it — a repository only publishes under keys it has been told to trust, explicitly, once:
prikk trust maintainer add \
--key-id "dev-maintainer" \
--public-key "a00899dfd3357aee69729405913f9324dfc033cec04a2215239eda64ae6d9d91"
trusted maintainer key: dev-maintainer
adopted maintainer keys: 1
The public key above is the matching half of the example seed — trust-on-first-use means whichever public key you adopt for a key id here is the one Prikk holds that id to from now on. Now seal succeeds:
prikk seal --allow-no-audit
sealed active WAL into block
patches: 1
block id: ...
heads/main RefState: ...
Your commit is now published: a sealed Block on heads/main, with a RefState recording exactly
which block that ref points at.
Look at what you built
prikk log
ref: heads/main
block ...
update-seq: 1
kind: Root
parents: 0
patches: 1
Root means this is the first block on this ref — nothing came before it.
Check its integrity
prikk verify
verified repository: /path/to/sample-repo/.prikk
...
checked blocks: 1
...
issue summary: errors=0, warnings=0, info=1
verify re-derives every check from the objects on disk — signatures, structural links, WAL
replay — rather than trusting any cached summary. A clean repository still prints one info line;
that is not a problem, only a note.
Doctor
prikk doctor
doctor repository: /path/to/sample-repo/.prikk
...
issue summary: errors=0, warnings=0, info=1
doctor runs the same structural checks as verify and adds repair-oriented diagnostics — this is
the command to reach for when something looks wrong, not verify again. On a healthy repository the
two report the same thing.
Where to go next
- Hit a refusal this tutorial didn’t cover? See Troubleshooting.
- Wondering why Prikk works this way rather than how to make it work? See the FAQ.
- Ready for the real key-handling story — not the public example seeds above? See Security and Signing Setup.
- Want the full command reference? The rest of this guide is organized one page per command.
This tutorial’s own command sequence is exercised by an automated test
(crates/prikk-cli/tests/installers_and_help/beginners_tutorial.rs), which runs every command above, in order, against
the compiled binary, and asserts on the same output lines quoted here — including both refusals. A
change to the CLI that alters any of them fails that test, not just this page.
Troubleshooting
Refusals a newcomer actually hits, in the Tutorial’s own sequence, each quoted verbatim from the CLI. If a message here reads as confusing, that is worth reporting — this page explains what exists today; it does not change any wording.
For the three key-material refusals below — a missing seed, a seed others can read, and an
undecodable one — prikk key status reports the same state without needing a commit to provoke it,
and exits 0 while doing so. It names the file in effect, whether it is usable, and why not. See
Can I Sign Here?.
error: author signing is required: no seed at <path>
prikk looked for your AUTHOR seed and did not find it. Every commit produces a signed Patch; there is
no unsigned path. The message names the exact file and three ways to get one — create it with
prikk key generate --out <that path>, run prikk setup in a new project directory, or point
PRIKK_AUTHOR_SEED_FILE at a seed file you already have. seal says the same for the MAINTAINER
seed. See First Run for where the key directory is on each platform.
prikk key status --role author shows which of the two inputs it is resolving and to which path —
useful when the file you created is not the file it is looking for. An override that is set and
points at nothing reports reason: override-missing, which distinguishes “you have no key” from
“your PRIKK_AUTHOR_SEED_FILE is wrong”.
prikk 0.40 also reported error: precondition not met: PRIKK_AUTHOR_SEED is no longer read when a
seed was still exported as an environment variable. That refusal was deliberately one release wide
and is gone as of 0.41: a stale PRIKK_AUTHOR_SEED or PRIKK_MAINTAINER_SEED is now simply unread,
and prikk key status is where you check which key is in effect instead.
error: <path> is readable by group or other (mode 0644); run chmod 600 ``
A seed file anyone but you can read is refused before signing. Run the chmod the message names.
(Unix only — on Windows the key directory relies on %APPDATA%’s per-user ACL instead.)
prikk key status reports the same file as usable: false with the mode in its reason, and
running it again is how you confirm the chmod took.
error: <path> must be 64 hex characters, got 8
A seed file must hold exactly 64 lowercase hex characters (32 raw bytes) — the number after got
reports how many your file actually had, so it will differ from the 8 shown here. A shorter,
longer, or non-hex value is rejected before anything is signed; nothing is written to the repository
when this fires. prikk key status reports such a file as reason: undecodable.
error: invalid name: worktree has no node-addressed changes to commit
You ran commit with nothing changed since the last commit or seal — no new, edited, or deleted
files for Prikk to record. Change something first.
error: maintainer signing is required: no seed at <path>
seal needs a maintainer key, distinct from the author key commit used — see
why the tutorial needs a second key. Same three routes as
the AUTHOR case above, with maintainer.seed in place of author.seed.
error: precondition not met: no maintainer key is adopted in this repository yet
Your maintainer key is configured but not yet trusted by this repository. Run
prikk trust maintainer add --key-id ID --public-key HEX with your maintainer key’s id and public half
(prikk key status --role maintainer reports the id; prikk key public --role maintainer derives the
public key), then seal again. This is a repository-local, trust-on-first-use step — every fresh repository
needs it once, even with a key you have used elsewhere.
The message’s parenthetical names the one other way this refusal can be reached: a trust policy
container damaged so badly it replays as empty is indistinguishable, at this read, from one never
written. If you know a key was adopted here before, run prikk doctor.
Earlier releases reported this same refusal as error: integrity error: publication trust policy is missing or unreadable, which sent readers looking for corruption that was not there.
error: precondition not met: <path>: worktree symlink authoring is out of scope
commit found a symlink where it needs a regular file — an untracked symlink in the worktree, or a
tracked file that has been replaced by one, including a dangling symlink whose target does not
exist. Symlink authoring is not implemented yet,
so the commit refuses rather than authoring something it cannot faithfully replay. Replace the
symlink with a regular file, or move it outside the repository (or add it to .prikkignore if it is
not meant to be tracked), then commit again.
You do not have to run commit to find these. prikk worktree-status reports the same paths, with
the same reason, under refused paths: — see Worktree Status.
Earlier releases reported this as error: integrity error: worktree authoring: unsupported symlink authoring: ..., which sent readers looking for repository corruption. Nothing is damaged.
error: precondition not met: <path>: existing TextFile cannot accept non-UTF-8 content
A path tracked as a text node now holds bytes that are not valid UTF-8. Changing a node’s kind from text to binary (or back) is out of scope, so the commit refuses rather than silently changing what the node is. Restore valid UTF-8 content at that path, or delete the path and add it again as a new node, then commit again.
Earlier releases reported this as error: integrity error: worktree authoring: unsupported kind transition: .... Same refusal, correct class: it is a state you fix, not damage.
error: precondition not met: --target names a tag (tags/<t>); pass the block id it points at, or a branch ref
You ran prikk tag create --target against another tag. A tag ref is one hop from its Block — ref →
tag object → block — and a tag of a tag is outside that model, so Prikk refuses rather than quietly
tagging the Block instead: that would make “tag this tag” and “tag this block” produce identical
history. Pass the block id (prikk tag list shows each tag’s target block) or a branch ref.
prikk branch create --from is the other way round: it does dereference a tag, because --from
means “at the block this ref names”.
Earlier releases reported this as error: object type mismatch: expected block, got tag, which read
as repository damage rather than as a correctable argument.
error: precondition not met: <path> already holds a repository
You ran prikk setup on a directory that already has a .prikk in it. setup is the first-run
command: it initializes a repository and mints a fresh key pair, so there is nothing sensible for
it to do on a repository that already has adopted keys. It refuses before doing anything — no
init, no keys minted, no seed file written, even when you passed --author-seed-out or
--maintainer-seed-out.
Two ways on, both named in the message: to work in the existing repository with keys you already
hold, run prikk trust maintainer add --key-id ID --public-key HEX (prikk key status --role maintainer reports the id, and prikk key public --role maintainer derives the public half); to start a new project, point setup at a different
directory.
Earlier releases got further before failing: they printed initialized Prikk repository at …, minted
two keys, wrote any --*-seed-out file, and only then reported maintainer key id maintainer is already adopted with a different public key. If you hit that version, the seed files it left behind
belong to keys no repository adopted — they are not your keys, and can be deleted.
error: integrity error: index entry for <id> resolves to an envelope with computed id <other>
The object index points at the wrong record: the entry claims an object lives at a location that actually holds a different one. Your objects are intact — the containers are self-describing and carry every byte needed — it is only the index that is wrong. Run
prikk doctor --repair-index
which rebuilds the index by scanning the containers, touches no container bytes, and reports how many
entries it moved. Then prikk verify again.
This happened when two commands that both write objects ran at the same moment — for example a
prikk commit and a prikk tag create. Since prikk 0.40 that is prevented: the second command is
refused with lock conflict instead, so a repository cannot reach this state any more. The repair
exists for repositories damaged before that.
error: integrity error: active WAL has a damaged record (damaged record at byte offset N; K sound record(s) follow it); …
A record in the queue of unsealed commits is damaged, and K intact records stand behind it. prikk verify and prikk doctor name
it and exit non-zero. prikk doctor --repair-wal-tail will refuse and leave the file exactly as it is: that switch truncates only a
torn tail, the incomplete last record left by an interrupted commit, and this is not one (a sound record follows the damage). Keep the
repository as it is and copy .prikk/active/ aside before doing anything else: the intact records can still be read. A torn tail, by
contrast, shows as trailing partial WAL bytes: N and a PRIKK-DOCTOR-WAL-TRAILING-PARTIAL warning, and --repair-wal-tail is the
right answer to it. (Before 0.48.0 a damaged length was mistaken for a torn tail and the repair deleted the intact records after it.)
error: ... the bytes after byte offset N look like many frame headers; …
0.49.0 (RFC 167). A torn or invalid frame’s own resynchronisation scan — checking whether a sound frame is hidden somewhere later in the file, before concluding this is an ordinary crash tail — gave up after reading 8 times the file’s own size without finishing (the message’s own words: this binary “stopped checking … and treats this as damage, not a torn tail”). This is not a torn tail: the scan could not rule one out, and ambiguity always resolves to damage, never silently to a tail. In practice this needs a file deliberately packed with fake frame headers; an honest crash does not produce it (random bytes following an accidental magic match almost never also claim a plausible length).
A scan this long is damage, not a torn tail, and the tail repairs refuse damage. For a WAL, prikk doctor reports
a failed record and recommends “preserve the repository and inspect the failing WAL record before attempting repair”
(doctor.rs, PRIKK-DOCTOR-VERIFY-WAL-RECORD-INCOMPLETE). No command repairs this in 0.49.0, so restore the
repository from a backup or a clone. For the other files, the diagnosis is again prikk doctor:
- the pointer index: diagnose with
prikk doctor;--repair-pointer-index-tailrefuses damage; - trust policy or the received index: diagnose with
prikk doctor;--repair-tailsrefuses damage; - an object container or the ref log container: no automated repair. If
doctoralso reports it as an unreferenced remnant, nothing needs the frame and no action is required; otherwise the way out is a copy of a sound repository, the same as any other damaged record in these two files.
error: N queued commit(s) have lost the record of which branch they belong to
Affects 0.20.0 through 0.48.0 (the stranding itself); 0.49.0 adds the way out. A crash during a
commit after the first one in a session could leave .prikk/active/default/ref-name empty while the
queue of unsealed commits (the active WAL) still held one or more records — because every commit, not
only the first, used to rewrite this file by a durable truncate then a durable append, and a crash
landing between the two left it empty. 0.49.0 also closes the same condition reached a second way: the
file present but naming a different ref than the session’s own commit witness does (RFC 166 D6) — both
read identically, since a session cannot have two owners. Either way, the commit itself is not lost (its
bytes are durably queued), but nothing can tell which ref it belongs to, so the way through is blocked:
-
prikk statusshowsqueued patches: N targeting <missing metadata>, with a warning naming the exact text below; -
prikk verifyandprikk doctorboth exit1; -
prikk commit,prikk sealandprikk rollback-draftall refuse with the same text, naming a concrete command to run. When this session’s own commit record (its “witness”) is still readable, it names the branch that record remembers:error: 2 queued commits have lost the record of which branch they belong to. This session's own commit record names heads/main. Check with: `prikk doctor --restore-queue-target --ref heads/main --plan-only`Without one (the record itself is gone, or this is a session a pre-0.49.0 binary left behind), it names your current branch instead, as a plain assumption:
error: 1 queued commit has lost the record of which branch it belongs to. Your current branch is heads/main. Check with: `prikk doctor --restore-queue-target --ref heads/main --plan-only`(
commit’s own text is prefixedintegrity error:; the others are not, otherwise identical.) -
prikk doctor --repair-wal-tailandprikk doctor --repair-tailsdo not help: there is no torn tail here for either of them to repair.
The ref always comes from you — --restore-queue-target never reads it from the witness, even when
the refusal shows one as a hint. With a readable commit record, --ref must match it — a mismatch
refuses, rather than silently preferring one source over the other. Without one, --ref must be your
current branch (prikk status’s own current branch: <ref> line, which reads a different, unaffected
file and so stays right even while ref-name itself is empty or disagrees) unless you pass
--not-current-branch — for example, if you made these commits with commit --ref <other> while
sitting on a different branch. A current branch that cannot be resolved at all needs the same flag,
fail closed, since there is then nothing to compare --ref against.
The way out, run end to end against a real killed repository:
# If a previous run was interrupted mid-write, its lock may still be on disk:
prikk unlock --lock .prikk/active/default/active.lock --yes
# See what this would do first, without writing anything -- the plan shows each queued commit's own
# message and the paths it touches, and the target branch's latest sealed commit, never a bare hash:
prikk doctor --restore-queue-target --ref heads/main --plan-only
# Then do it:
prikk doctor --restore-queue-target --ref heads/main
prikk verify # now exits 0
prikk seal --allow-no-audit # the queued commit(s) seal normally, as the restore's own output says
$ prikk doctor --restore-queue-target --ref heads/main --plan-only
doctor repository: /path/to/.prikk
restoring 2 queued commits to heads/main:
1. add a (a.txt)
2. add b (b.txt)
heads/main has never been published -- these would be its first commits
plan only -- nothing written
$ prikk doctor --restore-queue-target --ref heads/main
doctor repository: /path/to/.prikk
restoring 2 queued commits to heads/main:
1. add a (a.txt)
2. add b (b.txt)
heads/main has never been published -- these would be its first commits
the 2 queued commits now belong to heads/main; publish them with `prikk seal --allow-no-audit`
When there is no commit record to defer to, the plan says so once, honestly, rather than guessing
silently: “prikk cannot tell which branch these commits were made on; your current branch is assumed.
If you made them with --ref, restore to that branch.”
Restoring to a branch other than your current one needs --not-current-branch, and the refusal
without it names both branches:
$ prikk doctor --restore-queue-target --ref heads/other
error: your current branch is heads/main, but you asked to restore to heads/other; if these commits
were made with `commit --ref heads/other` (or `rollback-draft` on it), pass --not-current-branch to
confirm that; otherwise restore to heads/main instead
Nothing about the queued commits’ own content is at risk at any point in this sequence — only the small metadata file naming which ref owns it, rewritten in place (truncated, then appended). 0.49.0 also writes this file once per session instead of once per commit, closing the window for every commit going forward; see current limitations for the status of that fix.
A restored owner is final for this verb: once ownership is present, a second restore refuses the
same way a healthy session does, even to a different ref and even with --not-current-branch — a wrong
restore is undone by prikk doctor --recovery-restore <run id>, the run its output names, not by a second call.
error: a queued commit you were told had succeeded disagrees with the WAL in a way the WAL's own sound prefix cannot explain (RFC 166)
Affects every prikk before 0.49.0, which could not report this at all. A single flipped byte in the
body of a queued commit’s own last frame reads, by itself, exactly like an interrupted write: the
checksum it carries no longer matches, and nothing past it decodes either, so the reader cannot tell “a
process was killed mid-write” from “this byte changed after the commit already finished.” Before 0.49.0
that ambiguity resolved in favor of the crash reading — doctor --repair-wal-tail treated it as a torn
tail and deleted it, silently, even though the commit had already succeeded.
0.49.0 keeps a small acknowledgment record (one per active session, .prikk/active/<name>/witness) and
checks it against the queue before answering. When the queue it names no longer matches, it says so
instead of guessing:
$ prikk verify
...
trailing partial WAL bytes: 713
...
acknowledged commits: sequence 1 is damaged
...
error: a queued commit you were told had succeeded disagrees with the WAL in a way the WAL's own sound
prefix cannot explain (RFC 166)
(exit 1; before 0.49.0 this exited 0, with trailing partial WAL bytes: 713 the only hint, read
as a harmless tail.)
$ prikk doctor
...
error [PRIKK-DOCTOR-COMMIT-WITNESS-ACKNOWLEDGED-DAMAGE]: a queued commit you were told had succeeded
(sequence 1) is damaged
recommendation: run `prikk doctor --discard-damaged-commits` to remove it; it was already
acknowledged, so it cannot be removed as a crash leftover
...
error: doctor found repository health errors
$ prikk doctor --repair-wal-tail
...
active session "default": skipped -- this active session has its own blocking issue: a queued commit
you were told had succeeded (sequence 1) is damaged
...
error: doctor repair skipped one or more active sessions; see the per-active outcomes above for which
and why
The way out, run end to end against a real killed repository:
# See what this would remove first, without writing anything:
prikk doctor --discard-damaged-commits --plan-only
# Then do it -- the removed bytes are saved to the recovery log first, all-or-nothing:
prikk doctor --discard-damaged-commits
$ prikk doctor --discard-damaged-commits --plan-only
doctor repository: /path/to/.prikk
acknowledged commit at sequence 1 (patch 01157f0c...)
340 bytes saved to recovery/log (planned, nothing written) before truncation
plan only -- nothing written
$ prikk doctor --discard-damaged-commits
doctor repository: /path/to/.prikk
acknowledged commit at sequence 1 (patch 01157f0c...)
340 bytes saved to recovery/log (planned, nothing written) before truncation
damaged commit discarded
After it, prikk verify exits 0 again. The removed bytes are kept until prikk doctor --recovery-clear removes them.
They are saved under the run the output names. prikk doctor --recovery-list groups the saved entries by run, and
prikk doctor --recovery-restore <run id> undoes the whole discard when its conditions hold: the WAL and the commit
witness come back byte-for-byte, and the run’s files are checked first, so a later repair that changed one of them is
named (restore run <later id> first). The content is read back by restoring the run (the queued commit’s own
content, not just its presence, since the WAL body is the signed Patch envelope itself). Rows 5 (the record no longer present at all) and 7 (the acknowledgment history
itself unreadable) are the same verb’s job too, with no sequence or Patch id to name in row 7’s case —
the plan still says so, honestly, rather than guessing one.
If the acknowledgment record itself is what’s damaged, not the queue: doctor reports a warning
(PRIKK-DOCTOR-COMMIT-WITNESS-DAMAGED) instead of an error, and does not refuse — nothing acknowledged
is at risk when the queue itself is sound. doctor --repair-tails rebuilds it from the sound queue.
error: integrity error: the ref pointer index has an incomplete tail at byte offset N (M byte(s) follow); …
seal, branch create, tag create and merge each refuse before appending to the pointer index
itself — the file this refusal protects is always untouched by it. From 0.49.0 (RFC 164 Rule D) they
also refuse before writing any object of their own: seal, tag create and merge run this check at
the start of the command, before their patch, block or tag object is written, and branch create writes
no content object before its own pointer-index append. A refusal therefore leaves nothing new behind.
(A refused attempt made by a binary older than 0.49.0 may have left ordinary, content-addressed objects
that nothing references; verify still exits 0, and a retry after the repair below reuses them.)
prikk compact --pointer-index refuses the identical way, before touching anything (RFC 164 round
2, carried to 0.49.0 release prep): the live slot it is about to read and reduce ending in a torn tail
is the same unclean-tail refusal, the same advice, just reached from a different writer — --plan-only
is unaffected, since a plan-only run never writes behind the tail it would otherwise refuse over.
This file also has its own narrower doctor verb, which repairs only this file. The entries below use
prikk doctor --repair-tails, which covers this file too (args.rs:101):
prikk doctor --repair-pointer-index-tail
which truncates the incomplete trailing record under the pointer-index lock, saving the removed bytes to
recovery/log, the same way --repair-wal-tail does for the WAL. After it, prikk verify should exit
0, and the publication that refused can be retried.
error: integrity error: the trust key container has an incomplete tail at byte offset N (M byte(s) follow); …
trust maintainer add refuses before writing anything, but only when adding this key would actually
append to the file — a new key id, or an existing one under a different (conflicting) public key. Both
have to write to .prikk/trust/keys.container, and its own last write was interrupted (a crash
mid-append); this command would otherwise append behind that torn tail, blind. Re-adding a key id
already adopted with the same public key is unaffected: it appends nothing, so it succeeds regardless
of the tail. Fixed in 0.49.0 (RFC 164 Rule A): the tail is defined by position here now, the same way it
already was for the WAL and the pointer index — a torn prefix, zeros, or garbage all count, whatever the
shape. Run:
prikk doctor --repair-tails
which truncates the incomplete trailing record under this file’s own lock, saving the removed bytes to
recovery/log first. After it, prikk verify should exit 0, and trust maintainer add can be
retried.
error: integrity error: the trust policy container has an incomplete tail at byte offset N (M byte(s) follow); …
The same refusal as the trust-key one above, for the trust-policy container instead
(.prikk/trust/policy-a.container or -b.container, whichever prikk verify’s own report names as
live) — trust maintainer add and trust maintainer remove both read this container, and both refuse
here only when they are about to append their own new snapshot: add of a key id not yet in the
policy, or remove of one that is (and is not the last one). Re-adding an already-adopted key, or
removing one that was never adopted, appends nothing and is unaffected by a tail here. The way out is
the same: prikk doctor --repair-tails, then retry.
error: integrity error: the author key container has an incomplete tail at byte offset N (M byte(s) follow); …
A commit, a rollback draft, or a bundle import/sync accept refuses here only for an author key id
this repository has not recorded material for yet — recording it would append to
.prikk/trust/author-keys.container, and its own last write was interrupted (a crash mid-append); the
same shape as the two trust-store refusals above, for the file that records AUTHOR (not MAINTAINER) key
material. A commit (or import) by an author key id this repository has already recorded material for
is unaffected: it appends nothing to this file, so it succeeds regardless of the tail. For a
commit, this check runs before any of the commit’s own writes (RFC 164 Rule D, 0.49.0): it sits
ahead of the blob, object-index and commit-index writes, so a refused commit by a new author key leaves
none of them behind. The check runs again immediately before the WAL append that would queue the commit.
(A refusal made by a binary older than 0.49.0 may have left an unreferenced blob and index entry; they
are ordinary content-addressed objects, and a retry after prikk doctor --repair-tails reuses them.)
error: integrity error: the received index has an incomplete tail at byte offset N (M byte(s) follow); …
bundle import refuses before writing anything at all — not only leaving the received-ref index’s
own live slot (.prikk/refs/containers/received-index-a.container or -b.container) untouched, but
also the bundle’s own objects and any author-key material it carries: the check runs before the first
object write, the same as every other decision an import makes. (sync accept never writes to this
file; a received/remotes/* ref is only ever created by bundle import.) Run prikk doctor --repair-tails, then retry the import.
error: integrity error: the <container>'s generation log has an incomplete tail at byte offset N (M byte(s) follow); …
prikk compact refuses before writing anything at all — before the retired slot is truncated, not
only before the new generation record — when the container’s own generation log (<container> is “ref
pointer index”, “received index”, or “trust policy container”, whichever compact flag you ran) ends in
a torn tail from an interrupted compact. This is the same rule as the four container refusals above,
one layer up: compact already reads the generation log to pick the live slot, and this check rides that
same read rather than performing a new one. compact --plan-only is unaffected: a plan-only run
writes nothing, so it never refuses over a tail it would never write behind. Run prikk doctor --repair-tails, then retry compact.
error: integrity error: <container> has a damaged entry; … (seen for a damaged generation-log record, not a slot)
Seen from any command that reads a generation log’s live slot — status, log, branch list, seal,
commit, verify, doctor, and compact itself for that container — for either of two reasons (RFC
164 Rule A, §9 and §9.2): a sound record follows damaged bytes further into the file, or the last
record is itself complete — its checksum verifies, recomputed with this format’s own real magic and
version, over either the claimed length or the length to the end of the file — but its envelope still
fails, or its checksum itself is wrong. Neither is a tail: a tail is what a crash leaves, and a crash can
only leave something incomplete — a torn prefix, or zeros/garbage with nothing sound after them at the
very end. A complete record was fully written, whatever its own stored magic, version, or length field
says (one of those three can itself be the single corrupted byte, with the checksum and body untouched
— §9.2); a failing checksum or envelope on it is corruption after the fact, not a crash, so RFC 164 §9/
§9.2 treats it exactly like interior damage, never as a tail to be repaired away (repairing it away would
silently revert whichever slot it names to the previous one — measured, and closed, on a real build; a
corrupted header field did so even before any repair ran, until §9.2 closed that too).
On the ref pointer index’s own generation log, this stops every command that resolves a ref, since
every one of them reads it; on the received-index and trust-policy generation logs, only compact is
affected. Trailing bytes that do not form a complete record (a torn tail) are truncated by --repair-tails,
as for every covered file; the removed bytes are saved to recovery/log first. Damage in the middle of the file,
or a complete but corrupt last record, has no repair: --repair-tails refuses it, rather than guessing which
bytes are safe to remove, or silently undoing the decision the damaged record carried. This entry gives no truncation advice: whether the
damage sits before a sound record or is the complete-but-corrupt last record itself, no offset here is
one a repair can safely remove.
Handoff 165 Q1b: doctor has nothing of its own for a damaged generation-log record (unlike a
damaged slot, generation logs are not among the files check_appended_file_tails reports), so the
message names the way out directly instead of sending you to doctor: the ref pointer index names the
rebuild (prikk doctor --rebuild-pointer-index --plan-only, then the real run — the remedy is the same
as a damaged slot’s, since the rebuild reads neither); the received index and the trust policy
container name a copy of this repository’s whole .prikk/ directory from a backup taken before the
damage (the trust policy text also says to re-apply every trust change made since).
warning: <container>'s generation log names no live slot; slot <X> was deduced from the entries (…)
0.50.0 step 1 Part E4, corrected by handoff 165 (019 §5.7; 020 §3.1). Seen from prikk verify or
prikk doctor, when a compacting container’s generation log reads as empty or absent — not damaged,
not a tail, genuinely nothing — while the other slot (b) holds real data. Slot b is never
written except alongside the one generation record that names it live, so this shape can only mean a
compaction (or, for the ref pointer index, prikk doctor --rebuild-pointer-index’s own rebuild)
genuinely happened and the record of it was lost afterward — an accidental deletion, or a partial
restore from backup.
This is a warning, not a refusal, when content can decide: every ordinary command already
resolves it correctly. Content is tested in both directions (handoff 165 corrected Part E4’s
own rule, which tested only one): slot a is stale beside b — or b stale beside a — when
either slot equals, or is a prefix of, a compaction of some earlier prefix of the other’s own history
(a crash before a switch was recorded, or a write cut short), or when the other slot starts with a
full compaction of it and simply kept taking writes after. Whichever one direction holds names the
live slot — covering a crash between a compaction’s own new-slot write and its generation record, a
partly written slot, and a crash that leaves a slot live but still taking ordinary writes afterward (a
new branch, a revocation), without losing those writes or reinstating what they revoked the way
comparing only against the stale slot’s current content (an earlier version of this rule) could.
Both directions holding and the two slots folding to the same entries means either reading is
correct (reported as “both slots agree”). Only when neither direction holds, or both hold while the
folds disagree, does the deduction genuinely fail — see the next entry; this is distinct from one
of the two slots being damaged, which refuses too but for an unrelated reason (the container’s own
damage text, not this one). A restore from the recovery log refuses outright in this ambiguous-or-
damaged state instead of deducing, since it is a deliberate writer and a stale meaning file could
otherwise compare unchanged and pass.
The way out, named in the warning itself. For the received index and the trust policy container:
run the exact command it names — prikk compact --received-index or --trust-policy. It resolves
the identical way, saves the slot it is about to overwrite and the generation log as one recovery
run first (both saves landing before either file is written), then writes a fresh generation
record, ending the deduced state for good. For the ref pointer index, compact --pointer-index
instead refuses outright here (Q2 review): the warning names prikk doctor --rebuild-pointer-index --plan-only, then the real run, which re-derives the whole index from the
ref log directly, without reading either slot as live — this is the pointer index’s only way out of
this state now, not merely one option among others.
error: ambiguous generation log: <container>'s generation log is lost, and its two slots fit two different histories; …
Handoff 165 Q1b (020 §3.1, Q1 review). Seen from the same commands as the warning above, when
content genuinely cannot decide: either neither of the two slots relates to the other at all (not
a shape any real compaction sequence produces), or both relate and their folds disagree. The Q1
review proved this is not a gap in the rule but a real property of the state: two equally honest
command histories — add a key, compact, remove it; or compact again, then add the key back — can
leave byte-identical slot files with opposite answers on whether that key is trusted. Once the
generation log that would have told them apart is lost, no rule that reads only the slots can be
right in both histories, so refusing is the only honest answer. This is a distinct, typed error
(PrikkError::AmbiguousGenerationLog, not Integrity) — nothing here is corrupt. doctor raises
PRIKK-DOCTOR-GENERATION-LOG-AMBIGUOUS, printed first, before the current-branch warning and any
PRIKK-DOCTOR-VERIFY-STAGE-INCOMPLETE error the same ambiguity also causes.
The way out, named in the message itself: for the ref pointer index, run prikk doctor --rebuild-pointer-index --plan-only, then the real run — it re-derives the index from the ref log,
never reading either slot, so the ambiguity does not touch it. For the received index and the trust
policy container, 0.51.0 adds a second way out, named first: prikk compact --received-index --keep-slot a|b --plan-only (or --trust-policy) prints each slot’s own entry count and folded
state — for the trust policy, the keys trusted in one slot and not the other, by name — and which
slot content would deduce, or “ambiguous: prikk will not choose,” before anything is written. The
user knows which history is theirs; running the real command (without --plan-only) saves the
slot it overwrites and the generation log, as one recovery run, before writing anything. A whole-
.prikk/ backup taken before the generation log was lost stays the second way out (the trust
policy text also says to re-apply every trust change made since that backup) — and the only one
left for either container once the generation log is lost and the chosen slot does not decode
cleanly (--keep-slot refuses on a damaged chosen slot, naming that fact, rather than guessing).
error: integrity error: the ref pointer index's live slot is not recorded; run \prikk doctor –rebuild-pointer-index –plan-only`, then `prikk doctor –rebuild-pointer-index` first`
0.50.0 step 1 Part E3/E4 (019 §5.7), corrected by Q2b (H1 ruling item 2). Seen from prikk doctor --recovery-restore (plan or run), when the entry being restored names a meaning file in the ref
pointer index or the ref log, and the pointer index’s own generation log names no live slot
(deduced or ambiguous alike). Unlike an ordinary read, a restore does not deduce here: it is a
deliberate writer, checking that a meaning file’s state still matches what the entry recorded
before trusting it, and a stale meaning file in this exact state could compare unchanged and let a
restore through that should not proceed. The message used to name prikk compact --pointer-index
as the way out; that command now refuses outright in this exact state, so it would just hand back a
second refusal — the rebuild is named instead, for either sub-case. Run it, as the message says,
then retry the restore.
error: integrity error: <container> has a damaged entry; run doctor before reading
Seen from trust maintainer add or a commit, naming the trust-key or author-key container, for
either of two reasons (RFC 164 Rule A, §9 and §9.2): a sound record follows damaged bytes further
into the file, or the last record is itself complete (its checksum verifies against this
format’s own real magic and version, whatever its own stored header fields say) but its envelope or
checksum still fails. Neither is a tail — see the generation-log entry above for why a complete
record is never one, whatever its position; a genuine tail (trailing zeros, garbage, or a torn prefix
at the end of the file, with nothing sound after it, and not itself a complete record) reads as one
of the four “incomplete tail” entries above instead, repaired by prikk doctor --repair-tails.
doctor has nothing that repairs interior damage in either container — --repair-tails refuses
on it, the same way every other covered file’s own repair does, rather than guessing which bytes are
safe to remove, or silently reverting the decision (a trust adoption, a revocation) the damaged
record carried. This entry gives no truncation advice: there is no offset here promised to be safe to
remove. If you believe the bytes after some offset really are nothing but trailing garbage from an
interrupted append (not a complete, corrupted record), run prikk verify first and read its own
report carefully before deciding to truncate anything by hand; when in doubt, back the file up and
ask before changing it.
error: integrity error: ref pointer index has a damaged entry; run \prikk doctor –rebuild-pointer-index –plan-only`, then `prikk doctor –rebuild-pointer-index``
0.50.0 P3c (019 §2.2, §4.4). Seen from verify, doctor, branch list, or anything that resolves
the current branch, for the identical two reasons the entry above names, but for the ref pointer
index specifically — which, unlike the trust-key and author-key containers, does have a way out:
the plan, then the rebuild for real, re-derives the index from the ref log directly, never reading
the damaged slot at all. Before 0.50.0 the message said only “run doctor before reading,” which
reads this exact container the exact same way and so answers with the identical failure again — the
rebuild is what actually clears it. doctor’s own PRIKK-DOCTOR-POINTER-INDEX-INTERIOR-DAMAGE
issue prints first, before the current-branch warning and any PRIKK-DOCTOR-VERIFY-STAGE-INCOMPLETE
error the identical damage also causes — each of those now names this issue instead of the generic
“inspect the failing stage.”
error: integrity error: received-ref index has a damaged entry; no repair exists -- preserve the repository; the way out is a copy of this repository's own \.prikk/` directory from a backup taken before the damage` (or the identical shape for the trust policy container, which adds re-applying every trust change made since)
0.50.0 P3c (019 §2.2, §4.4). Seen from a bundle import, sync accept, or trust maintainer add/remove, for the identical two reasons as the entry above. Unlike the ref pointer index, there
is no rebuild for either of these: neither container is re-derivable from anything else this
repository holds (the received index is the only record of what was received; the trust policy
container’s own history is not reconstructable from the keys alone). The message says this plainly
and names a copy of the whole .prikk/ directory, never a single file, as the way out: the two
slots and the generation log are one set, and restoring the slots alone could leave a log naming a
slot the restored copy disagrees with. For the trust policy container specifically, an older copy
could re-trust a key that was revoked since — the message also says to re-apply every trust change
made since that backup. Both replace the same circular “run doctor before reading” the pointer index
used to share with them, and doctor’s own dedicated issue for each prints first, the same as the
pointer index’s own.
error: integrity error: object <id> references missing <role> <id>
Seen from verify (and anything that calls it, such as doctor) after a bundle import was
interrupted by a crash partway through, when the object making the dangling reference is itself
reachable from committed state (an unreachable one is a harmless remnant instead — see
PRIKK-DOCTOR-UNREFERENCED-REMNANT below, which now uses the same canonical form). Exact wording
varies with what was missing when the crash landed, but the shape is always object <owner> references missing <role> <id>:
object <id> references missing snapshot blob <id>object <id> references missing block patch <id>object <id> references missing parent block <id>snapshot of Block <id> names Blob <id> for <path>, which is missinglifecycle replay: blob <id> required for a state effect is missing
An unreachable owner producing the identical dangling reference is not an error at all, just a warning naming the same two objects in the same form:
warning [PRIKK-DOCTOR-UNREFERENCED-REMNANT]: object <owner> references missing <role> <id>
In 0.48.0 and earlier, bundle import wrote the objects it carried in the bundle’s own order, not
in dependency order — across object types, and, within the same type, a child block could be listed
before its own parent — so a crash partway through could leave a Block durable while something it
names is not. None of doctor’s three repairs (--repair-wal-tail, --repair-index,
--repair-pointer-index-tail) clears this — they do not know this shape. The way out is to run
the same bundle import again, with the same input: every object it carries is content-addressed,
so the retry only writes what is still missing, and this cleared the state in every case reproduced
(25 of 25). Fixed in 0.49.0: bundle import now writes objects in full dependency order (across and
within kinds), so an interrupted write can no longer produce this shape. sync accept is ordered the
same way now, for the same reason.
0.49.0: this error now fires only when the Block making the dangling reference is itself reachable from committed state — a ref, a received pointer, a queued patch, or a sealed block reached from them (RFC 164 Rule E). A Block nothing reaches this way is an unreferenced remnant, reported as a warning instead — see the next entry.
If this came from prikk show on a sealed block, under 0.52.0 or 0.53.0, specifically on the
second (or a later) consecutive EditText of the same file — lifecycle replay: blob <id> required for a state effect is missing, from show alone, in either its text or --format json
output — this is not the interrupted-import damage the rest of this entry describes. It is a read
bug in show’s own sealed-block fold, fixed in 0.54.0 (CHANGELOG, ### Fixed): the stored history
is sound, prikk verify exits 0 the whole time, and no repair is needed or possible for it. The
way out is to upgrade to 0.54.0 or later; running any of this entry’s repair steps on a repository
that only has this symptom fixes nothing, because nothing is actually damaged.
warning: <owner type> <id> references missing <missing type> <id> (<role>) -- re-run the import if you still have the bundle; otherwise it is harmless
Seen from verify and doctor (as PRIKK-DOCTOR-UNREFERENCED-REMNANT) for a stored object’s own
dangling reference when nothing committed still needs it — not reached by any ref, received pointer,
queued patch, or sealed block reached from them (RFC 164 Rule E, 0.49.0). The same underlying shape as
the previous entry’s error, told apart by reachability: a Block left behind by an interrupted bundle import/sync accept that nothing in this repository was ever going to use is harmless debris, not
damage, and verify exits 0 over it. No command removes a remnant in 0.49.0; if the bundle that
produced it is still available, re-running the same import is still the way to make it whole, exactly
as the previous entry describes — this warning simply means doing so is optional, not required for
verify to pass. A remnant that later becomes reachable (a new branch created over it, say) is
reclassified as damage — the previous entry’s error — on the very next run: reachability is always
recomputed fresh, never cached.
error: repository has interrupted or divergent ref publication state
Seen from verify after branch create, tag create, sync adopt-tag, merge, or seal was
interrupted by a crash partway through publishing a ref: the ref’s pointer names a RefState the
ref log has not yet confirmed (a “pointer lead”). --format json and doctor name one of two codes,
and they mean different things.
PRIKK-VERIFY-REF-POINTER-LEADS-LOG — the lead is completable (RFC 165 R4): the leading
RefState verifies under the current trust policy, chains cleanly to the log’s own tip, names a
target that exists, and, if the interrupted publication consumed the active WAL (seal/sync seal),
the retained WAL evidence still matches it. verify’s own detail line names the way out directly
(019 §5.2: verify prints only its own message, never a separate recommendation the way doctor’s
issues do), with and without a tail on the ref log container itself:
ref-publication [PRIKK-VERIFY-REF-POINTER-LEADS-LOG]: authoritative pointer leads committed ref log by one transition; run `prikk ref complete <ref>`
ref-publication [PRIKK-VERIFY-REF-POINTER-LEADS-LOG]: authoritative pointer leads ref log by one transition with <N> incomplete trailing byte(s); run `prikk ref complete <ref>`
doctor recommends the same thing, prikk ref complete <ref>. Retrying the same command does not finish it:
branch create/tag create still answer “already exists” (the ref was durably created — unless
the pointer itself also reports a completable lead, in which case the refusal itself now names
ref complete), and merge refuses before gathering evidence rather than building on a pointer its
own log has not caught up to.
PRIKK-VERIFY-REF-DIVERGENCE — the lead fails one of those conditions (an untrusted or revoked
signer, a broken chain, a missing or wrong-kind target, mismatched WAL evidence, or damage elsewhere
in the ref log or pointer index). Three distinct shapes land on this one code, each with its own
detail line:
ref-publication [PRIKK-VERIFY-REF-DIVERGENCE]: format-2 ref log leads the authoritative pointer
ref-publication [PRIKK-VERIFY-REF-DIVERGENCE]: format-2 ref pointer is missing while committed log history exists
ref-publication [PRIKK-VERIFY-REF-DIVERGENCE]: ref-log chain or sequence diverges for <ref>
The first two are legacy format-2 shapes (format 6/7 repositories cannot produce them through any
normal command — pointer-then-log write order makes them unreachable); the third is a genuinely
broken chain. doctor recommends manual recovery only; preserve the repository and ask before
changing anything.
prikk ref complete <ref> finishes a completable lead: it appends one more signed RefUpdate
record to the ref log, matching the state the pointer already carries, through the exact same write
seal’s own DC-38 retry uses (never a new code path for the append). Any adopted maintainer key
may complete it, not only the one that started it — the leading RefState is already signed by an
adopted maintainer; the completer’s own key only signs the log record, so a different operator can
finish a crash another operator’s command left behind. --plan-only prints what it would do (the
leading state, its target, the log sequence it would append at, and which key would sign it) without
writing anything. seal’s own retry of the same ref is unaffected — DC-38’s natural-retry mechanism
still completes a seal-shaped crash through its own existing path, and ref complete refuses a ref
that is not actually a pending completion (already caught up, or not published at all) the same way.
Measured for merge: 10 of 300 kills on 0.48.0, 16 of 300 on the build carrying 0.49.0’s own
dependency-order fix (the two are unrelated defects reached through different commands); every one of
those crash states is now either completable (ref complete) or named as a genuine divergence.
A complete damaged ref-log record (never a tail, RFC 164 §9.2) refuses any of seal/branch create/
tag create/sync adopt-tag/merge/ref complete/the rebuild below, identically:
error: integrity error: the ref log has a damaged record at byte offset <N>: <checksum/decode detail>; this is not an incomplete publication and no seal retry resolves it -- the way out is a copy of a sound repository, not a repair
prikk doctor --rebuild-pointer-index (RFC 165 R5)
The other way out: re-derives the ref-pointer index from the ref log directly, structural and never
trust-filtered (no signature is re-checked for a record already durable in the log — only a current
lead re-enters trust, exactly as above). Where ref complete finishes one ref’s own pending
transition, the rebuild recovers the whole pointer index when it — not the ref log — is what is
damaged or untrustworthy: a flipped byte in a pointer record, a stale fallback behind the log, or a
lead that turns out not to verify at all.
--plan-only prints the same plan a real run writes from (K1), per ref: its state before and after,
and every lead it would drop or restore:
heads/main: <before> -> <after>
restored from the log: heads/main (RefState <id> was stale, behind the log)
dropped lead: heads/topic (RefState <id>) -- the leading RefState's signature does not verify under the current trust policy: <detail>
“Restored” and “dropped lead” are never the same claim. A pointer behind the log — commonly a damaged newest pointer-index record, read as an older, already-log-confirmed entry instead — is restored: nothing authorized is discarded, the ref simply ends where the log already soundly confirms. A pointer genuinely ahead of the log that fails RFC 165 R4’s own rule is a dropped lead: a real, signed transition the rebuild refuses to carry forward, because it cannot verify.
It refuses, writing nothing, over the same ref-log damage/tail shown above, or over any completable lead anywhere in the repository (complete it first — overwriting it would drop an authorized transition, the one thing this verb must never do):
error: precondition not met: <N> ref(s) have a completable lead; run `prikk ref complete <ref>` first -- a rebuild would drop an authorized transition: <ref list>
A damaged pointer-index record is not a refusal reason — serving past it is the rebuild’s own purpose. Structural, no signing: a history entirely signed by a key later revoked is unaffected (the rebuild moves no ref on trust grounds alone), but a lead signed by a revoked key is dropped, named, same as any other failed condition.
error: precondition not met: checkout target for <ref> is not a checkpoint, so it carries no snapshot …
The block you asked to check out has no snapshot, which is the normal state of most blocks: seal
writes one only at a ref’s first block and every 64 blocks after it. Use the patch-replay route the
message names, which does not need a snapshot:
prikk checkout --patch-plan --ref <ref>
Releases before 0.43 reported this as checkout target for <ref> does not contain a snapshot blob, and
earlier ones still under error: integrity error:, which read as damage. Nothing is damaged, and nothing
was ever missing.
error: precondition not met: no prikk repository at <path>
The directory you ran the command in — or the path you passed — has no .prikk directory. Run the
command inside a repository, pass the repository root as the path argument, or create one with
prikk setup <path>.
Before 0.45 this was error: i/o error: No such file or directory (os error 2), which did not say which
file, or that the repository itself was absent. A real I/O failure inside a repository is still
error: i/o error:.
error: precondition not met: ref <ref> does not exist in this repository
A command that reads or advances a ref was given one that is not there — usually a typo, or a branch not
created yet (prikk branch list shows what exists). If the message goes on to say remotes/<ref> exists, the name you gave belongs to a received ref: pass it as remotes/<ref> to a command that
reads received refs (log, merge-evidence, merge-plan, bundle preview), or take it into a local
branch with prikk merge --from remotes/<ref>.
Before 0.45, prikk log --ref on an absent ref reported an empty history with exit 0 and prikk worktree-status --ref reported changes; other commands said ref <ref> is not published (as error: integrity error: or error: invalid name:), does not exist, nothing to export, does not resolve to a published ref, or a false “not a checkpoint”. Scripts that treated an empty log as “no history yet” now
see exit 1.
This refusal never applies to the repository’s own current branch (prikk status names it),
published or not: log, worktree-status, tree, cat --path, a bare diff/diff --from (the
worktree’s baseline), and checkout --plan-only answer for it exactly the same way whether it is named
with --ref/--from or left off. 0.45.0 and 0.46.0 refused a fresh repository’s own current branch when
it was named explicitly — the one ref this message was never meant to name — which is fixed in 0.47.0;
see the CHANGELOG. Every other name, published or not, still refuses exactly as this section describes
— including the current branch named as one side of diff --from … --to ….
error: precondition not met: remotes/<ref> is a received ref, and this command does not accept received refs; …
Received refs (remotes/…, from prikk bundle import or prikk sync accept) are untrusted pointers:
they can be read and merged from, but checkout, inverse-plan, rollback-preview, bundle export,
branch create --from and tag create --target do not accept them. The rest of the message names the commands that do read them. To work on the received
history, merge it into a local branch with prikk merge --from remotes/<ref>.
error: integrity error: existing container record for <id> differs from candidate -- this repository is format 6, …
Someone sent you an object you already hold, carrying signatures yours does not — a second signer of the same patch or block. A format-6 repository holds one record per object, so it cannot keep both; format 7 merges the signatures into one object. Check that every prikk that opens this repository is 0.45 or newer (older binaries refuse a format-7 repository at open), then:
prikk format upgrade
and repeat the import or accept. format upgrade verifies the repository first and changes only the
FORMAT marker; running it on a format-7 repository changes nothing.
error: precondition not met: <type> <id> would carry at least 5 counted signatures, above the limit of 4 per object …
An import or accept would put more than four counted signatures on one object. The operation is refused whole and nothing is written. A MAINTAINER signature by a key adopted in this repository is not counted, so the limit only bites on signatures this repository has no reason to trust. Ask the sender which signatures the object is meant to carry.
error: author signing refused: <path> holds key id <id>, but the seed at <path> derives <other> …
The key-id file beside your seed (author.key-id or maintainer.key-id) names a different key than the
seed derives: the seed was replaced, or the file was copied or edited. Restore the seed the file belongs
to, or remove the file so the id is derived again. A custom key id is set with PRIKK_AUTHOR_KEY_ID or
PRIKK_MAINTAINER_KEY_ID, never by editing the file. prikk key status reports this state as
reason: key-id-file-mismatch; the same refusal reads maintainer signing refused: for the maintainer
key.
error: precondition not met: the worktree was materialized from the snapshot of Block <block> on <ref> and is not replay-verified; run prikk verify …
You ran checkout --snapshot-materialize, and the worktree it wrote is the block’s signed state rather
than a replay of its history. Until the repository has been replayed, commit, mv, seal, merge,
sync accept, sync seal, rollback-draft --append-inverse and branch switch refuse, so nothing
derived from an unverified worktree becomes history. Run:
prikk verify
When verify finds nothing about objects, the WAL, refs or block state, it prints provisional worktree: replay-verified; marker cleared and the commands work again. If it prints provisional worktree: kept,
fix what verify reported first. See snapshot materialization.
error: precondition not met: <old> -> <new>: the source is present in the worktree again, …
A prikk mv declaration says <old> became <new>, and the worktree says otherwise — most often
because the move was undone with a shell mv rather than with prikk mv. The commit refuses rather
than guess which one is the tracked node, and the worktree still reads clean: nothing is
missing, modified or untracked, so worktree-status reports it as refused declarations: 1 instead.
The refusal names the way out for the state you are actually in, and each command was measured:
prikk mv <new> <old> # drop the declaration: it nets to no move and is removed
prikk mv <old> <new> # or make the move again, and commit authors the rename
A shell mv back is not one of them: it recreates the state that produced this refusal.
If both paths exist, prikk mv refuses in either direction until one copy is set aside — delete
<old> and commit to author the rename, or delete <new> and then run prikk mv <new> <old>. If the
destination is another tracked node this commit does not also move or delete, prikk mv <new> <old>
drops the declaration; <old> keeps its content and <new> stays deleted, so the commit authors that
deletion. See Declared Moves and Worktree Status.
warning: the snapshot of Block <block> failed validation (…); this report replayed the whole history instead -- run prikk verify``
checkout --patch-plan, --patch-delete-plan or bundle preview tried to start at a snapshot, and the
snapshot did not validate. The report did not use it: it replayed the whole history, and its output and
exit code are what they would have been with no snapshot. The warning is a sign of a damaged or
tampered snapshot object. Run prikk verify, which names the block and the finding.
error: active WAL has no patch records to seal
You ran seal with nothing queued — every commit since the last seal has already been published.
There is nothing to fix; commit something before sealing again.
error: invalid name: ref topic is not a local branch ref; expected heads/<name>
You passed a bare name (topic) where Prikk expects a fully-qualified ref
(heads/topic). branch create, branch close, tag create, and any --ref flag all take the
qualified form — see why refs are fully qualified for
the reasoning. Re-run the same command with heads/ (or tags/ for a tag) in front of the name.
error: i/o error: repository mutation requires Linux, macOS, or Windows root-scoped filesystem capabilities
You are running init, commit, or seal on a platform other than Linux, macOS, or Windows.
Reading commands (verify, log, status, doctor) work anywhere Prikk builds; mutation does
not, by design — see Platform Support.
error: precondition not met: heads/<name> does not exist; run prikk branch create heads/ first
firstprikk branch switch only moves to a branch that already exists. Create it from the branch you are
on with prikk branch create heads/<name>, then switch. The same command says heads/<name> is closed for a closed branch: prikk branch list shows the open ones.
error: precondition not met: the active WAL holds unsealed work for heads/<name>; seal it …
You committed on one branch and have not sealed, and are switching to another. Unsealed work belongs
to the branch it was committed on, so the switch refuses rather than leave it stranded. Seal it
(prikk seal --allow-no-audit --ref heads/<name>, exactly as the message prints), then switch.
error: precondition not met: the worktree is not clean against heads/<name>: <path> (modified), …
The switch replaces the worktree’s tracked files with the other branch’s, so it refuses while any of
them differs from the branch you are on — it would otherwise overwrite or delete your edits. The
message lists each path and whether it is modified, missing or an unsupported-path;
prikk worktree-status shows the same list. Commit the changes, or restore the files, and switch
again. Untracked files do not count and are never touched.
After a power loss on Windows, following a completed branch switch or checkout (RFC 168 residual (a)). The rewritten files
can hold their old contents, deleted files can come back, and created files can be lost. Nothing in the repository is damaged.
prikk worktree-status lists what differs. The route, run in this order:
- Move each file it lists as modified out of the worktree. The moved copy is yours: keep it or delete it.
- For a file it lists as missing, do nothing: the checkout writes it.
- For a file it lists as untracked that you deleted on the branch, a file that came back, delete it.
- Run
prikk checkout --patch-materialize --ref <the current branch>. It writes the branch’s files into the paths that are now free.
prikk worktree-status should then list nothing for those paths.
error: precondition not met: refusing to switch to heads/<name>: <n> in the way of its files: …
A file the other branch has would land on a path that already holds something that is not part of the branch you are on — usually an untracked file with the same name. Nothing was written. Move the listed paths aside and switch again.
If a switch was interrupted (a crash, a full disk), no file is torn and the current branch is still
the old one; commit then refuses with worktree materialization was interrupted (below). Run the same
prikk branch switch heads/<name> again: it recognises the half-switched files and completes.
error: precondition not met: refusing to materialize: <n> path(s) in the way: …
checkout --patch-materialize, --patch-materialize-delete or --snapshot-materialize found files
that would have to be overwritten: each is listed with why, such as an existing file with different content or not a regular file. Nothing was written. This is the ordinary result of checking out
another branch’s files over your own: move the listed paths aside, or commit them, and run the checkout
again. Earlier releases wrote every file before the first conflict, left the worktree part-written and
reported integrity error: refusing to overwrite existing file with different content.
error: precondition not met: refusing checkout deletion because <n> candidate(s) are unsafe: …
--patch-materialize-delete would delete files that no longer match what history deleted; each is named
with the reason. Nothing was written. Earlier releases reported this as an integrity error: without the
paths.
error: precondition not met: <path> changed during the checkout, so the worktree is partly written; …
A file the checkout had checked changed before it was written, usually because another program wrote it at the same moment. The checkout stopped part-way and the worktree is marked as interrupted. Move that file aside, then run one of the two routes below.
error: precondition not met: worktree materialization was interrupted, …
A checkout or branch switch stopped part-way: a crash, or the change above. prikk status prints
interrupted materialization: … naming your current branch, status --format json carries
interrupted_materialization with the two routes, and prikk doctor warns
PRIKK-DOCTOR-INTERRUPTED-MATERIALIZATION. Until it is cleared, commit refuses, because it cannot tell
a file you deleted from one the stopped checkout never wrote. Move aside any file the stopped checkout
named, then run either
prikk checkout --patch-materialize --ref heads/<current branch> or
prikk branch switch heads/<current branch>; both write the branch’s files again and clear it. A file
the stopped checkout had already written stays in the worktree as an untracked file, and commit authors
the whole worktree as it always does. Earlier releases reported this as an integrity error: whose
suggested route could refuse again.
error: precondition not met: no object <id> in the object store or the active WAL; …
prikk show was given an id that names nothing here — usually a typo, or an id from another
repository. Sealed patch and block ids are listed by prikk log; queued (committed, not yet sealed)
patch ids by prikk status --format json, and show accepts both. Earlier releases reported this as
error: integrity error: no object <id>, which read as repository damage; nothing is damaged.
error: precondition not met: .prikk/current-branch names heads/<name>, which does not exist; …
A command whose --ref you did not give defaults to the current branch, and .prikk/current-branch
names a branch that is not there. The same message says which is closed for a closed branch, and
.prikk/current-branch is malformed (…) when the file does not hold exactly one heads/<name> line.
Nothing is damaged, and both prikk doctor (the warning PRIKK-DOCTOR-CURRENT-BRANCH) and prikk verify report it, as a warning that never changes either command’s exit status. Either run prikk branch switch heads/<name> to an existing, open branch — with an unusable pointer the switch writes
only what is absent and deletes nothing — or prikk branch create the branch the file names. Any
command still works with --ref given explicitly meanwhile.
error: invalid name: backslashes are not allowed in repository paths
A file in the worktree has a name no repository path can hold — here a backslash; a name that is not
UTF-8 says worktree path is not valid UTF-8: <name> instead. prikk commit refuses the whole commit
rather than skip the file silently. prikk worktree-status lists it as an unsupported-path entry with
this same reason. Rename the file, or exclude it with a .prikkignore rule
(Ignoring Worktree Paths).
Something not listed here
Run prikk doctor — it is the diagnostic-first command, and its recommendation
lines usually name the next step. If it does not, the message is real (Prikk does not invent
placeholder diagnostics) but this page has not caught up with it yet.
FAQ
The conceptual questions the Tutorial raises but does not stop to answer, and the ones that decide whether a first repository becomes a second one.
What do I run first?
prikk init, then work through the Tutorial — it is the shortest path from an empty
directory to a sealed, verified repository, and it is exercised by an automated test, so it will not
silently go stale.
What is sealing, and do I have to do it?
Yes, if you want the commit to count as history. commit only appends a signed Patch to a local,
per-session write-ahead log — nothing is published yet, and nothing else can see it. seal is the
step that turns everything queued in that log into a signed Block, publishes a RefState pointing
at it, and makes it part of the ref’s real history. You can commit many times before sealing (they
queue), but until you seal, there is no history to check out, verify against, or share — only a
pending queue status can show you.
There is no auto-seal. A workflow that only commits and never seals is a workflow that never publishes anything.
How many commits can I queue before sealing?
Two environment variables bound the queue, both fail-closed on a malformed value:
PRIKK_ACTIVE_PATCH_WARN— warn at this many queued patches (default 800)PRIKK_ACTIVE_PATCH_LIMIT— refuse further commits at this many (default 1000)
The limit is checked before any write, so a refused commit leaves no partial state. Committing and sealing one-for-one still works exactly as before; nothing forces accumulation.
Why does this need keys at all?
Every commit and every seal in Prikk is a signed object, not a plain record — there is no unsigned mode. Two different roles sign two different things:
- Author — signs the Patch a
commitproduces. Identifies who authored a change. - Maintainer — signs the
Block/RefStateasealproduces, and must be a key the repository has been told to trust first (trust maintainer add). Identifies who published it.
Prikk keeps these separate even for a single-person repository, because the two signatures answer different questions later: who wrote this content and who decided it should become history. See Trust and Threat Model for what each signature is actually checked against, and Security and Signing Setup for how key material is currently supplied — environment variables, not a keychain or config file, and intentionally minimal at this stage.
Why is it heads/topic and not topic?
Every ref name is fully qualified: branches are heads/<name>, tags are tags/<name>. A bare name
is refused:
error: invalid name: ref topic is not a local branch ref; expected heads/<name>
The qualification exists so a ref’s kind is legible from its own name, without needing to look
anywhere else to know whether v1 names a branch or a tag. There is currently no default or implicit
prefix — every command that names a ref (branch create, tag create, --ref) takes the qualified
form.
How do I switch branches?
prikk branch switch heads/<name>
.prikk/current-branch names the current branch (heads/main in a new repository), and every
command that takes --ref defaults to it. branch switch changes the worktree to the other branch’s
files and then moves the pointer. It refuses, and writes nothing, when the branch does not exist or is
closed, when unsealed work belongs to another branch (seal first), or when the worktree has changes
against the branch you are on (prikk worktree-status lists them). Untracked files are never
touched; a file of the old branch is deleted only when it is unchanged. Switching to the branch you
are already on is always a no-op — including when it has never been sealed, which just says so rather
than claiming the branch does not exist.
An explicit --ref still works on every command without switching. prikk branch list shows what
exists and marks the current branch with *. A fresh repository’s current branch, before its first
seal, is not one of those rows — it has no published history to list — but is still named: prose
prints one line below no branches (current branch heads/main has no published history yet; the first prikk seal publishes it), and --format json carries it as a top-level
"unpublished_current_branch" field, null once it has been sealed.
Is my data safe in a Prikk repository right now?
Prikk is early implementation software and is not a production Git replacement. Nothing in this tutorial, FAQ, or troubleshooting page is a claim otherwise — work through them with content you can afford to lose, and see Platform Support and Non-Goals for what is and is not currently guaranteed.
Something else?
If a real refusal sent you here and this page does not answer it, check Troubleshooting first — it is organized by the exact message you saw, not by concept.
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.
-
Key input is a seed file per role — a path you name, or the role’s file in your key directory — and intentionally minimal.
prikk key statusreports what is in effect. -
Seeds are secret key material. Prikk does not store, encrypt, rotate, revoke, expire, or back up private keys —
prikk key generate --outwrites one, once, to a path you name, and never reads it back or manages it afterward (see First Run). -
prikk key generatedraws a fresh seed from the OS CSPRNG;prikk key publicderives a public key from a seed you already hold;prikk setupcomposes both roles plustrust maintainer addinto one command. External tooling is no longer required, though nothing stops you from bringing your own matched seed/public-key pair instead. -
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, and there is no AUTHOR trust policy: no list of adopted author keys, and nothing to adopt one into. A patch signed by an author key this repository has never seen commits, seals, and passes
verify— verified by doing it, not inferred. -
What is enforced for AUTHOR keys is one key id, one public key. Committing under a key id the repository has already recorded, with different key material, is refused:
error: integrity error: author key_id alice already has a different recorded public key (…); one key_id binds to one public key -- this looks like a key-rotation attempt, which is not supported and is indistinguishable from impersonationWhen the id is the legacy default
author, which every installation made before 0.45.0 shares, the refusal says that instead. It explains that history signed underauthorby another key cannot enter this repository, and names the route: sign new history under a distinct id, from a key made byprikk key generate --outor withPRIKK_AUTHOR_KEY_ID.That is an impersonation guard, not a trust decision: it stops one identity being silently reused by a different key, and says nothing about whether that identity is trusted.
verifyreportsunverifiable author signatures: 0when every author signature checks out against the material recorded for its key id. -
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 each role’s seed from a file, in exactly two places and no third:
PRIKK_AUTHOR_SEED_FILE/PRIKK_MAINTAINER_SEED_FILE, if set — a path;- otherwise
<key directory>/author.seed/<key directory>/maintainer.seed.
The key directory is $XDG_CONFIG_HOME/prikk (else $HOME/.config/prikk) on Unix and
%APPDATA%\prikk on Windows. The key id recorded in signatures is resolved in one place, in this
order:
PRIKK_AUTHOR_KEY_ID/PRIKK_MAINTAINER_KEY_ID, if set;- the key-id file beside the seed:
author.key-id/maintainer.key-idin the key directory, or<seed path>.key-id, whichprikk setupandprikk key generate --outwrite when they create a seed (the id ised25519-and the first 16 hex characters of the public key); - otherwise the legacy role word,
author/maintainer, for a seed made before 0.45.0.
A key-id file must hold the id derived from its seed; otherwise signing is refused.
A seed never travels through the environment. PRIKK_AUTHOR_SEED and PRIKK_MAINTAINER_SEED
carried one until prikk 0.40. An environment variable is readable by every child process, survives in
shell profiles long after the key changed, and lands in process listings — none of which is true of a
file the operator places.
0.40 refused while either variable was set, for exactly one release, so that no automation could
quietly start signing with a different key than it thought it was using; 0.41 removed that refusal
and the variables are now simply unread. prikk key status answers the question the refusal was
standing in for — which key will actually sign here — without needing a signing attempt to provoke
it.
Each seed file holds a 32-byte Ed25519 secret seed as 64 hex characters. A missing file, a file readable by group or others (Unix), an empty key id, wrong-length seed hex, and non-hex bytes all fail closed before signing.
prikk key public --role maintainer derives the matching public key directly — see
First Run. Nothing computes it automatically; deriving it is a separate, explicit
step.
Can I Sign Here?
prikk key status answers that question without signing anything, and without printing a seed:
prikk key status [path] [--role author|maintainer] [--format json]
It reports, per role, which of the two inputs above is in effect (source), the file it resolves to,
whether that file is usable and if not why (reason — missing, an override that names a file which is
not there, readable by group or others, undecodable, or key-id-file-mismatch for a key-id file that
does not belong to its seed, with the file and both ids named), the key id and where it came from
(key_id_source: environment, key-file or default), and how that key id relates to
what this repository has already recorded (binding): unrecorded, matches, mismatch, or — for
MAINTAINER — not-adopted.
It answers the same question commit and seal answer when they refuse, from the same computation, so
a usable: true here is a prediction of what the signing path will do rather than a second opinion.
Not-ready is an answer, not a failure: every state above exits 0. A non-zero exit is reserved for
a repository that cannot be read at all.
prikk key status --role author --format json
--format json emits a key-status-v1 document and is the form front-ends should read; it carries
public material only.
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 be the key id the MAINTAINER signer signs under, which prikk key status --role maintainer
reports. If ID is already adopted with a different public key, add is refused. Adopting under
another local id would not help, because the id travels in every signature: the other maintainer must
sign under a distinct id. HEX must be the lowercase
64-hex-character Ed25519 public key that matches the MAINTAINER seed file.
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
The shortest path is one command — see First Run for the full walkthrough and output:
prikk setup ./sample-repo
setup prints the exports to run next, having already run init, generated both an AUTHOR and a
MAINTAINER seed, and registered the MAINTAINER key. If you would rather bring your own matched
seed/public-key pair instead of a generated one, or understand each step separately, the same result
composed by hand:
prikk init ./sample-repo
export PRIKK_AUTHOR_KEY_ID="author-key-id"
export PRIKK_AUTHOR_SEED_FILE="$AUTHOR_SEED_FILE"
export PRIKK_MAINTAINER_KEY_ID="maintainer-key-id"
export PRIKK_MAINTAINER_SEED_FILE="$MAINTAINER_SEED_FILE"
(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)
AUTHOR_SEED_FILE and MAINTAINER_SEED_FILE are paths to mode-0600 seed files you supply, and
MAINTAINER_PUBLIC_KEY_64_HEX a value you supply — prikk key generate --out <path> and
prikk key public --seed-file <path> produce them if you do not already have your own. Leave both
_FILE variables unset to use the key directory, which is what prikk setup writes.
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
A missing AUTHOR seed file prevents commands that need AUTHOR signing from creating signed Patch envelopes; the refusal names the path it looked for and the command that creates it.
A missing MAINTAINER seed file prevents seal from creating signed publication objects, the same way.
A seed file that group or others can read is refused before signing, naming the mode and the chmod
that fixes it.
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
Key generation and public-key derivation shipped — see First Run. Still deferred: a
general configuration command and every durable, non-secret policy setting (no beneficiary yet — a
first real adopter is the named trigger); a credential-helper boundary (refused deliberately, git/ssh-style, in
favor of the write-once-where-you-name-it model key generate --out already uses); local secret
storage beyond that single named write, 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 exposes trust maintainer add, key generation (key generate), and public-key derivation (key public) — since RFC 135. | key.rs, commands.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] [--format json]
The command follows the current RefState chain from newest to oldest and validates that each entry resolves to a persisted Block object.
--ref takes any published ref, not only a branch. A tag ref resolves through its tag object to the
Block that object names — so prikk log --ref tags/v1 reports the tagged Block, which need not be
the branch tip.
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.
Repository Status
prikk status
Unlike most other commands, status takes no path or --ref argument: it always reports on the
repository rooted at the current working directory, using its default active-session WAL.
For each invocation, the CLI reports:
- the repository’s
.prikkdirectory path; - the number of active (unsealed) WAL records, and any trailing partial bytes from an interrupted append;
heads/main’s currentRefStateID, specifically — not whichever ref the active queue is targeting, which is reported separately;- the queued-patch count and the ref it targets, when the queue is non-empty;
- when the worktree was written by
checkout --snapshot-materializeandprikk verifyhas not replayed the repository since,provisional worktree: materialized from the snapshot of <block> on <ref>; not replay-verified — run prikk verify(provisional_worktreein--format json,nullotherwise; see snapshot materialization); - when a checkout or
branch switchstopped part-way (a crash, or a file changed during it),interrupted materialization: a checkout or branch switch stopped part-way; move aside any file it named, then run prikk checkout --patch-materialize --ref <branch> or prikk branch switch <branch>, naming the current branch (interrupted_materializationin--format json, holding those tworoutes,nullotherwise; see troubleshooting); - a warning once the queued-patch count reaches the recommended threshold or the configured hard
limit (
PRIKK_ACTIVE_PATCH_WARN/PRIKK_ACTIVE_PATCH_LIMIT; see NFR-PERF-02), the same thresholdsprikk commitenforces on the write path. - when the next
commitorsealwould refuse,warning: the next commit or seal will refuse: <reason>, where the reason is the one that write would give; - otherwise, when a queued commit was written durably but never confirmed (a previous command was
interrupted after its own write, or an older prikk wrote it),
note: a queued commit was already written but not confirmed; the two cases cannot be told apart (crates/prikk-cli/src/main.rs:624-641).
status does not run verify’s integrity checks, and does not
report the worktree’s own divergence from the replay baseline — that is
worktree-status. Multi-operation text diff minimization and audit plugins are
not yet implemented; the command’s own trailing line notes this.
Worktree Status
prikk worktree-status reports read-only worktree status against the same replay-derived baseline
prikk commit would author against (RFC 122, replay-baseline-handoff-v1.md): the sealed
node-addressed lineage for the selected ref, with any already-queued (unsealed) patches folded on
top. It answers “what would the next commit author?”, not merely “what differs from the last seal.”
prikk worktree-status [path] [--ref REF] [--format json]
It reports missing, modified, untracked, and unsupported paths. It does not write the worktree.
Patch generation is handled separately by prikk commit --from-worktree. To see the content of what
commit would author, use prikk diff: a file this command calls untracked is an added
entry there.
What the next commit would refuse
Answering “what would the next commit author?” includes the paths it would not author. Every
change entry carries an authoring verdict alongside its change kind, and a refused entry carries
the reason prikk commit would print for it:
modified files: 1
unsupported paths: 0
refused paths: 1
worktree: changed against baseline
modified a.txt — tracked path is not a regular file [refused: precondition not met: a.txt: worktree symlink authoring is out of scope]
Three things about that line are deliberate:
refusedis not a change kind. The entry ismodifiedand refused. The refusal is a bracketed suffix, so the first word of an entry line is still the change kind — a reader that splits on it sees exactly what it saw before this field existed.refused pathsis notunsupported paths. An unsupported path is an unrepresentable name; a refused path is a representable name whose worktree entry cannot be authored. The example above is one without being the other, which is why the two counters sit side by side.- The reason is
commit’s own. Both commands ask one shared classifier, so the string here is the stringprikk commitprints, not a paraphrase of it.
In --format json the same facts are a top-level refused_count and, on each change,
"authoring": "authored" | "refused" with "refusal": null or the reason string. The fields are
additive within worktree-status-report-v1: a consumer written against the older field set reads
these documents unchanged.
A dangling symlink — one whose target does not exist — is refused here too. Presence is checked
without following the link, so the path reports as modified and refused rather than as missing,
which is what prikk commit has always seen. A path that is genuinely absent is still missing,
with no refusal.
An unrepresentable name is refused too. A file whose name no repository path can hold — a
backslash in it, or bytes that are not UTF-8 — stays an unsupported-path entry, and carries the
refusal prikk commit gives that name. In --format json, path is repo-relative for every kind
except unsupported-path, where it is the file’s OS name relative to the worktree root, rendered
lossily (an invalid byte becomes U+FFFD): no repository path exists for it, and the absolute path
on this machine does not belong in a repository-scoped report.
Every live declaration says what the commit will do with it. A prikk mv declaration is not a
property of any single path’s entry, so it is reported on its own line, with the resolution the next
prikk commit will reach:
live rename declarations: 1
notes.md -> docs/notes.md [refused: notes.md -> docs/notes.md: the source is present in the worktree again, so the declared move is not what the worktree holds. Run `prikk mv docs/notes.md notes.md` to drop the declaration, or `prikk mv notes.md docs/notes.md` to make the move again]
refused declarations: 1
The resolution is one of rename (authored as a rename, and the line says whether content or mode
also changed), deletion (the destination is gone, or a directory stands there — either way it is
no longer a file, so the source is authored as a deletion), deletion-ignored (.prikkignore excludes
the destination — decided from the rules, whatever kind of entry is there), never-tracked (the
source was never a node, so the declaration is dropped), or refused (the whole commit is refused,
with that message). A rename line also says (content also changes) or (mode also changes) when
the destination differs from the source node’s baseline, because commit authors those beside the
rename.
In --format json each declaration carries resolution, refusal, content_changed and
mode_changed, and the top level
carries refused_declaration_count. These are additions within worktree-status-report-v1: against
0.42.0’s output for the same repository, the only differences are the new keys — no field was
removed, renamed, or given a different value.
content_changed and mode_changed are booleans only when the destination is a regular file. They
are null when the resolution is not a rename, and when the destination is a symlink, a FIFO or a
socket: such a destination is never opened — reading a FIFO would wait forever — so there is no
difference to report, and prikk commit refuses over that path anyway (worktree symlink authoring is out of scope, or worktree entry is not a regular file). A symlink is not followed to compare its
target.
refused_count and refused_declaration_count are separate on purpose, and clean can be true
while a declaration is refused. A worktree whose declared move was undone with a shell mv matches
its baseline byte for byte — nothing is missing, modified or untracked — and prikk commit still
refuses, because the declaration contradicts what the worktree holds. So read the commit’s prospects
as “refused paths: 0 and refused declarations: 0”; either one alone is only half the answer.
The resolution comes from the same classifier prikk commit obeys, so the two commands cannot
disagree about a declaration — see Declared Moves.
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 in the worktree. Under
.prikk/, the baseline derivation this command shares withprikk commitrefreshes the rebuildable cache (cache/lifecycle-state.v1), which is best-effort and never authoritative.prikk diffuses the same derivation with that refresh switched off, and writes nothing at all.
For the exact repository path validator rules, see
path and worktree safety. A path matched by a .prikkignore rule at
the repository root never appears in the untracked list at all — see
Ignoring Worktree Paths.
Claim-to-Source Anchors
| Claim | Source anchors |
|---|---|
worktree-status compares the worktree against the replay-derived baseline commit shares — the sealed lineage with any already-queued patches folded on top — not a stored snapshot Blob. | worktree_status.rs, patch_replay.rs |
It writes nothing in the worktree, may refresh the rebuildable baseline cache under .prikk/cache/, and reports missing, modified, untracked, and unsupported-path changes. | worktree_status.rs |
Each entry’s authoring verdict and refusal reason come from the same classifier prikk commit refuses with, so the two commands cannot disagree about a path. | node_authoring.rs, worktree_status.rs |
A contradicted rename declaration is refused by commit and is reported as a refused declaration, not a refused path — and can sit in a clean worktree. | rfc147_authoring_refusal_field.rs, rfc147_declaration_resolution.rs |
Each declaration’s resolution is what commit then does, and a refusal’s text is commit’s own message; every prikk mv a refusal names is run in the state that produced it. | declaration_resolution.rs, rfc147_declaration_resolution.rs |
Provenance
This guide covers RFC 122’s rewire onto the replay baseline, RFC 147 §2e’s per-entry authoring verdict, and RFC 147 §2f’s per-declaration resolution. It does not change repository state, signing, trust, or the bundle/sync formats.
Ignoring Worktree Paths
.prikkignore (since 0.29.0) is a plain file at the repository root. Each non-blank line is one
rule: a literal repository-relative path prefix.
target
build/output
target matches target itself and everything under target/ — never target2 or targetfoo.
Matching is by whole path component, not by string prefix, so a rule can never accidentally cover a
differently-named sibling.
What the syntax deliberately does not have
No globbing, no negation, no comments, no per-directory files. *.log does not work. This is a
stated limit, not a first step toward gitignore compatibility — RFC 124’s own reasoning is that a
syntax which nearly matches gitignore’s semantics would be worse than one that obviously does not
attempt to.
Where it binds
.prikkignore is consulted in exactly three places: prikk commit’s
worktree walk, prikk worktree-status’s untracked-path scan, and the worktree
side of prikk diff, which reads the worktree the way commit does. A rule that matches
keeps a path out of discovery in all three.
Nowhere else. Applying a patch, replaying history, verifying a repository, and materializing a
checkout all ignore .prikkignore entirely. This is deliberate: if those steps consulted it, two
repositories holding the same signed history but different .prikkignore files could disagree about
what that history means — the ignore file governs what gets proposed for authoring, never what
already-sealed history is.
A rule can never hide an already-tracked path, or a path lying under one. Adding a line to
.prikkignore cannot make commit read an existing file as missing and author a deletion for it —
only untracked, undiscovered paths are affected.
One practical consequence worth stating: an ignored directory is skipped without being opened or
descended into. That is not only a speed benefit. commit’s own worktree walk fails closed on any
symlink or unsupported entry it encounters, and a real node_modules/-style directory is typically
full of exactly those — without skipping the whole directory rather than each file inside it,
ignoring it would not actually let such a worktree commit at all.
A malformed file refuses; an absent one does not
A .prikkignore that is not valid UTF-8, or contains a line that is not a valid repository-relative
path once a trailing / is stripped, is refused outright — commit and worktree-status both fail
closed rather than silently treating it as empty. No .prikkignore at all is not malformed: it means
no rules, and every repository created before 0.29.0 behaves exactly as it always has.
It is an ordinary tracked file, not configuration
There is no separate configuration mechanism for it. prikk config (since 0.47.0) exists, but its
one key bounds the size of incoming objects and has nothing to do with exclusion; a general
configuration file remains a deliberately deferred, future increment. .prikkignore is committed and signed like any other
worktree file, and it travels through bundle export/import and sync the same way. A repository
you receive carries the sender’s own rules, and those rules shape your future commits from that
point on — they say nothing about, and change nothing in, the patches you already received.
Limits
- No globbing, negation, or per-directory files. One flat list of literal path prefixes at the repository root is the whole mechanism.
- A file swept into history by mistake still cannot be removed later.
.prikkignoreprevents future capture; it does nothing about what a worktree already committed before the rule existed.
Claim-to-Source Anchors
| Claim | Source anchors |
|---|---|
.prikkignore rules are literal repository-relative path prefixes, matched by whole path component, with no globbing or negation. | ignore.rs |
The rule set binds only at commit’s worktree walk and worktree-status’s untracked scan — nowhere else. | ignore.rs, worktree_files.rs, worktree_status.rs |
| An already-tracked path, or one lying under an already-tracked path, is never hidden by a rule. | ignore.rs |
A malformed .prikkignore is refused; an absent one means no rules and no behavior change for an existing repository. | ignore.rs |
.prikkignore is an ordinary tracked, signed worktree file and travels through bundle export/import and sync like any other. | ignore.rs |
Provenance
This guide documents RFC 124’s worktree ignore mechanism
(rfcs/done/124-worktree-ignore-mechanism.md), shipped in 0.29.0. It is documentation-only and adds
no behavior: .prikkignore’s syntax, binding sites, and limits are exactly as ignore.rs’s own
module doc records them.
Show
prikk show reports what one sealed block or one patch changed: each operation, its kind, the
path(s) it affects, and the content it changed (RFC 142).
prikk show <block-id|patch-id> [--format json]
A patch is self-describing — the content is already in it. EditText carries the exact bytes
replaced and their replacement; CreateFile/DeleteNode carry (or point to) the file’s content;
RenamePath/ChangePerm/CreateSymlink carry everything they need inline. Nothing is replayed to
find what changed.
A RenamePath also reports the AUTHOR key id that signed the patch asserting it — the signer is
recoverable in the same answer as the rename itself, never a fact a caller has to look up
separately. This is structural, not a rendering choice: show’s own read type cannot represent a
rename without it, so a patch whose AUTHOR signature is missing fails the command rather than
rendering a rename with no signer.
Three operation kinds — EditText, ChangePerm, ReplaceBinary — are node-addressed rather than
path-addressed, so finding where they apply needs one replay of some lifecycle state — which state
depends on what the target is (below). show pays that cost once per invocation, not once per
operation, and there is no flag to skip it: a surface that named a 32-byte node id where a path
belongs would have failed at the one thing it exists to do. If the state a node-addressed operation
resolves against no longer has that node live — one patch that edits a node and then deletes it
— the edit’s own path reports as unresolved rather than failing the command; every other operation
still renders.
Which forms resolve a node-addressed path:
| Target | Resolves against | Node-addressed paths resolve? |
|---|---|---|
| A patch in a sealed block | that patch’s own state: the block’s starting state (its parent’s, or empty for the first block), folded with the block’s own patches in canonical order up to and including that patch | Yes |
| A queued (committed, unsealed) patch id | the folded baseline — the sealed tip with the queue folded on top, truncated at that patch — exactly the state a seal right now would give it | Yes |
| A bare sealed patch id | nothing — a patch id alone carries no block context, and searching for a block that happens to contain it would make the answer depend on what else is sealed | No — reports unresolved_node_id |
A block’s output is the union of its patches, in the block’s own canonical order.
Truncation matters, for a block’s own patches too, not only a queue. Every patch resolves
against the state at that patch, not its block’s end: if a later patch in the same block renames a
path an earlier one edited, or deletes the node an earlier one edited, the earlier patch’s own
edit-text still names the path (or node) it had at the time — never the later name, and never
unresolved_node_id just because a later patch in the same block went on to delete it. The block
form folds a block’s own patches onto its parent’s state, and the queued form folds the active
queue onto the sealed tip, through two different drivers — but both apply the same per-operation
fold to resolve a path, so the answer is identical before and after seal by construction: sealing
does not change what came before a given patch, only what comes after it.
show renders EditText’s own before/after span — it does not synthesize a unified diff. prikk’s
edits are content-anchored spans, not line ranges, and a line-oriented rendering would assert a
structure the format does not have.
ReplaceBinary and any binary-kind blob report their object id and declared size, never content —
nobody has asked to see binary bytes in a terminal.
A content reference is an id, not a guarantee. CreateFile, DeleteNode’s file preimage, and
both sides of ReplaceBinary name a blob by content-addressed id — and once a text file has been
edited, its pre-edit identity is deliberately never written back to the object store (the content
is reconstructed by replay instead). The exception is a checkpoint — a ref’s first sealed block, and every 64th block
after it along its history — which stores the content of every file live at that block, edited or
not, so content that was live at a checkpoint reports normally. If a referenced blob is simply absent from the object
store, that one piece of content reports as unavailable — a named state carrying the blob id,
distinct from real empty content — rather than failing the command. Every other operation in the
block still renders, and the command still exits 0. This is not evidence of a damaged repository:
the overwhelmingly common cause is a deleted file whose most recent edit made its old identity
unbacked by design, and show cannot tell that specific case apart from any other blob that simply
never made it into the store; a doctor-level surface wanting to would need to check independently
whether the node’s own history includes an edit since that blob was last live.
Damage is different, and fails the command. If the object store reports something more specific
than absence for a referenced blob — a content-hash mismatch, a stored type disagreement, a
malformed payload, or a SNAPSHOT-kind blob named where file content belongs — that is the object
store affirmatively reporting a problem, not an ordinary, by-design gap. show propagates it: the
command fails (exit 1) with a message naming the integrity problem, rather than rendering the
affected content as unavailable and exiting 0. Absence is ambiguous enough to degrade; a reported
error is not.
show never fails because there was nothing notable to report: exit 0 covers every case the
target resolves to, whether or not anything is unresolved or an absent content reference reports as
unavailable. A malformed or missing id is a usage error (exit 2), a well-formed id naming no real
block or patch is a failure (exit 1), and so is a referenced object the store reports as damaged
rather than merely absent — an unavailable content reference never softens any of those, which
stay what they were.
A queued patch can be shown before it is sealed. An id prikk status --format json lists under
the queue — committed, not yet sealed — is looked up in the active WAL when the object store does not
hold it, and renders exactly as it will once sealed — node-addressed paths resolved, from the folded
baseline truncated at that patch (the table above) — plus a queued: yes line in prose and
"queued": true on the patch in JSON (false for everything read from the object store, block or
sealed patch alike). An id found in neither place is a precondition (exit 1) that names both places
and where each kind of id is listed; it is not reported as damage.
Three things are easy to conflate, and show only ever speaks to the first: queued here means
committed to this repository’s own local active WAL — not yet sealed, and never anything received
from elsewhere; sync pending means a patch sync accept has already verified and stored from an
exchange artifact, but not yet sealed into a block; and log means sealed, permanent
history. All three are distinct states a patch passes through, never the same thing twice.
A rename names every signer. A patch may carry several AUTHOR signatures (format 7: another
signer’s copy of the same patch merges in). A rename reports every one: prose lists them all on its
asserted by line, and JSON carries author_key_ids, every signer in canonical order (key id bytes).
author_key_id keeps its meaning — the first signer in that order — so a patch with one signer reports
exactly as before.
--format json carries the same content as the prose form, as show-report-v1. It settles the
format for show and nothing else.
Comparing two arbitrary points in history — not just one block or patch — means reconstructing both
and comparing them, a materially more expensive operation show’s own cost model does not cover.
That is a separate command: prikk diff replays both points (or one point and the worktree)
and shows the difference as unified hunks. show still answers “what did this block or patch change”,
from the patch’s own record.
Claim-to-Source Anchors
| Claim | Source anchors |
|---|---|
| Content comes from the patch payload directly; only node-addressed operations need a replay, once per invocation, against that patch’s own state – its block’s starting state folded with the block’s own patches up to that patch, or (for a queued patch) the folded baseline truncated at that patch. | show.rs, patch_replay.rs |
| An unresolved node-addressed operation is reported, not fatal; every other operation still renders. A bare sealed patch id resolves nothing; a queued patch id resolves against its own truncated folded baseline. | show.rs |
EditText renders its own before/after span; ReplaceBinary and binary blobs report id and size only, never content. | show.rs, output/show.rs |
An absent blob reference degrades that one piece of content to a named “unavailable” state rather than failing the command; every other operation still renders and the command still exits 0. A blob the object store reports as damaged (hash mismatch, type mismatch, malformed payload, or SNAPSHOT-kind) propagates instead — the command fails rather than rendering it unavailable. | show.rs, object_store.rs |
Exit 0 whichever way the target resolves; a malformed or missing id is a usage error, exit 2; a well-formed but nonexistent target id, or a referenced object the store reports as damaged, is a failure, exit 1. | main.rs, args.rs |
RenamePath cannot be rendered without its asserting AUTHOR key id – the read type has no construction that omits it, and a patch with no AUTHOR signature fails the command. | show.rs, author_signing.rs |
Provenance
This guide covers RFC 142’s show command. Comparing two arbitrary points in history, or a point with the
worktree, is RFC 153’s prikk diff, a separate command with a materially different cost.
Reading a Point
A point is a place in history: a ref (heads/main, tags/v1, remotes/heads/main) or a bare block
id, 64 lowercase hex characters, as prikk log prints it. Any block the repository holds is a point,
including an older block on a branch and a block only received history reaches. Reading a point adopts
nothing and trusts nothing (RFC 157).
prikk tree — the files at a point
prikk tree [path] [--ref <ref|block-id>] [--prefix <p>] [--format json]
tree lists every file present at the point, in canonical path order (byte order of the path). There
are no directory entries: prikk records paths, not directories, so a directory is just the shared
leading components of the paths under it.
tree repository: /path/to/repo/.prikk
ref: heads/main
target block: 7c1e…
entries: 3
100755 text 18 run.sh
100644 binary 240128 src/big.bin
100644 text 1834 src/main.rs
Each line is the file’s mode (octal), its encoding, its exact size in bytes, and its path. With a block id
the header reads block: <id> instead of ref: <name>.
-
Encoding.
textorbinary, by the same classificationcheckout --patch-plan --format json --content-pathreports. A file is text when it was committed as text. -
The mode is one number, shown two ways. The prose prints it octal, as
ls -landgit ls-treedo (100644,100755); the JSON prints the same number in decimal, ascheckoutalready reports it (33188,33261).100644octal is33188decimal — the two forms never disagree:file prose JSON modean ordinary file 10064433188an executable 10075533261On Windows, expect
100644for files committed there. Windows has no executable bit, so prikk cannot observe one: a file first committed on Windows is recorded100644, and committing on Windows never changes a mode that history already records. That is a platform limit, not lost data. History committed on Linux or macOS still carries100755for its executables, andtreeshows every recorded mode on every platform, so a Windows reader of that history sees100755where it was recorded. See Platform Support. -
Size is exact: a text file’s content is reconstructed by replay, and its length is counted, not estimated.
-
--prefix <p>keeps the paths at or underp, by whole path components:--prefix src/(orsrc) listssrc/main.rsand neversrcx/a.txtorsrc.rs. The replay is whole-tree either way, so a prefix shortens the output, not the work. -
Without
--refit lists the current branch. In a fresh repository whose branch has never been sealed, that is an empty listing with exit0— and naming that same branch explicitly with--refanswers exactly the same way, since the current branch is never absent, published or not. An explicit--refnaming any other absent name refuses withref <name> does not exist in this repository. -
A received ref (
remotes/…) lists like any other. -
Cost. One replay of the point, anchored at its nearest checkpoint, like
checkout --patch-plan.
--format json: tree-listing-v1
{
"schema_version": "tree-listing-v1",
"point": "heads/main",
"target_block_id": "7c1e…",
"prefix": "src/",
"entries": [
{"path": "src/big.bin", "kind": "file", "encoding": "binary", "mode": 33188, "size": 240128, "content_id": "9f02…"},
{"path": "src/main.rs", "kind": "file", "encoding": "text", "mode": 33188, "size": 1834}
]
}
pointis the point exactly as given;target_block_idis the block it resolved to, so a consumer can cache by block. For an unpublished current branch it isnull, with no entries.prefixis--prefixexactly as given, ornull.kindisfile. (symlinkis reserved for when replay supports symlinks; until then a history holding a symlink operation fails the whole call rather than listing part of the tree.)modeis the full file mode, the same numbercheckoutreports (33188is100644,33261is100755).content_idis present only for a binary file: its stored blob id. A text file never has one, even when a checkpoint happens to store its content, so a listing does not change with checkpoint cadence. Treat it as a cache hint, not as a way to fetch content.
prikk cat — one file’s bytes at a point
prikk cat --path <p> [--ref <ref|block-id>] [--output <file> [--force]] [--max-bytes <N>] [--format json]
cat writes the file’s bytes at the point — text and binary alike — to stdout, or to --output. It is the
one command whose job is bytes: diff never prints binary content, and checkout --content-path reports
text inside a plan.
prikk cat --path src/main.rs # to stdout
prikk cat --path src/big.bin --output ./big.bin # to a file
prikk cat --path src/main.rs --ref <block-id> # at an older block
- All or nothing. The content is resolved in full before one byte is written. A failure anywhere — an unsupported operation, damage, a bound — writes nothing at all.
--max-bytes <N>refuses content larger than N, naming the size and the bound, and writes nothing, not even a partial file. Read exactly what it promises:- It bounds what is written — exactly, and all or nothing. When it refuses, nothing reaches stdout and no file is created or replaced.
- It does not bound memory.
catreads the point through the same replaycheckout --patch-planuses, which holds every file’s content while it builds the tree, so a large file is already in memory when the bound is compared. Peak memory is that replay’s own cost, proportional to the size of the whole tree at the point, and--max-bytesdoes not lower it. A caller bounding hostile input gets a bound on the output, not on the process. A memory bound would need the replay to stream, which is planned separately (RFC 158) and not somethingcatcan add on its own.
--output <file>refuses an existing file unless you pass--force(bundle export’s own rule), and refuses any path inside.prikk/. The write goes to a temporary file beside the destination and is renamed into place, so a failed or interrupted write leaves the destination untouched rather than half written.- Binary content never reaches a terminal. With stdout on a terminal, a binary file refuses and names
--output; nothing is written. History from someone else must not be able to drive your terminal by being printed. Text goes to a terminal as-is, exactly ascheckoutwould materialize it. Redirecting or piping stdout is not a terminal, soprikk cat --path src/big.bin > big.binwrites the bytes. - A path that is not a file at that point — never existed, deleted by then, or a directory prefix like
src— refuses withpath <p> does not exist at <point>.
--format json: path-content-v1
{
"schema_version": "path-content-v1",
"point": "heads/main",
"target_block_id": "7c1e…",
"path": "src/big.bin", "kind": "file", "encoding": "binary", "mode": 33188, "size": 240128,
"content_id": "9f02…"
}
No bytes. The fields are the same as one tree-listing-v1 entry, computed by the same code, so a
consumer can decide between rendering inline and downloading — and show a size — before fetching anything.
When tree and cat refuse
- An absent ref (never the current branch, which is never absent, published or not), a block id the
repository does not hold, or an id naming another kind of object: the point resolver’s preconditions,
exit
1— see Commands. - A
--refvalue that is neither a ref name nor a block id, or a--prefixthat is not a repository-relative path: usage, exit2. - An operation replay does not support, anywhere in the history: the whole call fails, and nothing is listed.
- Damage the store reports:
integrity error. A binary file whose blob is missing or no longer recomputes fails the whole call, fortreeand forcatalike: a state with a hole is not a state, so no entry is listed as unavailable and no partial listing is printed (RFC 157 §5a).
cat refuses the same things, plus a path that is not a file at the point, a --max-bytes bound the content
exceeds, an existing --output without --force, an --output inside .prikk/, and binary content to a
terminal.
Comparing: prikk diff
prikk diff shows what changed between two states, where a state is a point in history (a ref or a
bare block id — see Reading a Point) or the worktree. It shows content: every text
change as unified hunks, the way diff -u prints them.
prikk diff # the worktree against the current branch: what `commit` would author
prikk diff --from <ref|block-id> # the worktree against that point
prikk diff --from <ref|block-id> --to <ref|block-id> # two points
prikk diff --path src/main.rs --path README.md # only these paths in the output
prikk diff --format json # diff-report-v1
There is no fourth shape: a lone --to is a usage error (exit 2), because it names a right side and no left
one. Compare two points with --from and --to, or a point with the worktree with --from alone.
What each side is
- A point is a ref — a received ref (
remotes/…) too, read and not adopted — or a bare block id.--from heads/main --to <block-id>and--from <block-id> --to <block-id>are both fine, and a ref and the block it points at give the same answer. - The worktree is what
prikk commitwould author, and the report is built fromcommit’s own rules so that it cannot disagree with the commit it predicts. Runprikk commitand thenprikk diff: it is empty again.
That has consequences worth knowing:
- A new file is
added, with its content. prikk has no index, andcommitauthors every worktree file.prikkignoredoes not exclude, so there is no separate “untracked” list here. A file thatprikk worktree-statuscalls untracked is anaddedentry inprikk diff. - Ignored paths never appear — as an entry or as anything else — and an ignore rule cannot hide a path that is already tracked: a tracked file that a later rule covers still shows its edits (Ignoring Worktree Paths).
- The left side of a bare
diffis the current branch’s tip with its queued commits folded on top, because that is whatcommitauthors against. Thefrom:line says so when commits are queued and not yet sealed:from: heads/main (block 7c1e…, plus 2 queued commits not yet sealed). In JSON the left side carriesqueued_patches.--from <point>compares against that point exactly, queue or no queue. - A fresh repository — a current branch that has never been sealed — has the empty state on the left:
every file
commitwould author isadded, and the exit code is0(from: heads/main (not published: the empty state)) — the current branch is never absent, published or not, so naming it explicitly with--fromrefuses no more than leaving--fromoff does. A queued, unsealed commit is where the two forms part ways, by the same rule as the previous bullet: the bare left side folds the queue on top of the empty state (a queued file reads as already there, unchanged);--from heads/maincompares against the empty state exactly, queue or no queue, so a queued file reads asaddedthere instead.--fromnaming any other ref that does not exist refuses. - It writes nothing. No lock, no cache, no index, no marker:
prikk diffreads. It works while another process holds the active lock, and while the worktree is provisional (materialized from a snapshot and not yet verified) or an interrupted materialization left its marker — states in whichcommitrefuses. It does not clear either marker. In an interrupted-materialization state, a file the interrupted write never created reads asdeleted, which is whatcommitwould have signed; that is the reasoncommitrefuses there.
Statuses
Each differing path has one status, in canonical path order. A path that is the same on both sides is not listed.
| Status | Meaning |
|---|---|
added | On the right only. |
deleted | On the left only. |
modified | A text file whose content differs. |
renamed | A declared rename: the same file at a different path (below). |
mode | Same content, different mode (the executable bit). |
binary | A binary file whose content differs — ids and sizes, never bytes. |
symlink | In the schema, not produced today: replay applies no symlink operation, so a history holding one fails the call, and a worktree symlink is an unsupported path (below). |
diff repository: /path/to/repo/.prikk
from: heads/main (block 7c1e…)
to: worktree
entries: 3
modified src/main.rs
--- a/src/main.rs
+++ b/src/main.rs
@@ -10,7 +10,7 @@
…three lines of context…
- let old = 1;
+ let new = 2;
…three lines of context…
renamed docs/old.md -> docs/new.md
added notes.txt
--- /dev/null
+++ b/notes.txt
@@ -0,0 +1 @@
+a new file
Hunks, and patch
Text changes are unified hunks with 3 lines of context, headed by the ---/+++ lines patch(1) reads
(/dev/null on the side that has no file). A last line without a newline is marked
\ No newline at end of file, exactly as diff -u marks it, and a line ending in \r\n is a different line from
one ending in \n. Applying an entry’s hunks to its left content reproduces its right content byte for byte, on
Windows as everywhere; patch on Unix applies the printed text.
Renames are declared, never guessed
A renamed entry exists only where history (or a live prikk mv on the worktree side) declares a
rename. Deleting a file and creating another with the same bytes is a deleted and an added, never a rename:
nothing here infers one from similar content. A rename is the same file at a new path, so:
- a chain of renames
a → b → cis onerenamedentry fromatoc, whatever the two points are; - a file deleted and then created again — at its old path or at another — is a different file, so it is
never
renamed: at a new path it isdeletedplusadded, at the same path it ismodified; - a rename that also changes the content shows its hunks.
--path selects a renamed entry by either its old or its new path.
With --from <a point on another branch>, the worktree side takes node identity from the current branch’s
baseline, so a rename is paired only where the two agree: across branches, read a deleted plus an added as
“these two paths were not paired here”, not as “no rename was declared”.
Binary files
A binary entry shows the size of each side and an id — and never a byte. The prose names prikk cat for
reading a side, with the block each side resolved to rather than the name you gave (a ref may move before you
run the command). The worktree side has no block: it is the file itself. The left side of a bare diff with
queued, unsealed commits has no block either — it is the sealed tip plus that queue, a state no block names —
so no --ref line is printed for it; the prose instead says the side “can be read only after prikk seal”. For
a binary file on the worktree side, the id is the one the file would have once committed. content_id exists
for binary files only, exactly as it does in prikk tree: a text file is its own content.
Paths commit refuses
Some worktree entries commit cannot author: a symlink (Unix), a FIFO or socket, a name that is not a safe
repository path, an existing text file whose content is no longer valid UTF-8. prikk diff names each one,
once, with the refusal commit prints for it, and shows no content:
unsupported paths: 1
link: precondition not met: link: worktree symlink authoring is out of scope
A tracked file replaced by a symlink is named here and is not also reported deleted. The rest of the diff is
still shown. unsupported_paths is always empty between two points; only the worktree form fills it.
A live prikk mv that commit would refuse (both paths present, say) refuses the diff too, with commit’s own
message and nothing on stdout: there is no honest answer to what a commit that cannot happen would author.
The cost, and the bound on the line diff
Two points cost two anchored replays, as checkout --patch-plan does. The
worktree costs one read of the baseline plus a read of every worktree file, as worktree-status does, and
materializes the baseline text of the files that changed only. --path filters the output; it does not shrink
the work.
The line diff finds the shortest edit script within a work bound: 45,000,000 search steps for one file (a step is one diagonal visited or one line followed inside the search, so the bound counts work, never time, and is the same on every machine). Almost every diff finishes far inside it. Two large files that share many lines in very different orders — a file with its lines reversed, say — can reach it; the search then stops and the region it had not resolved is shown as deleted and re-added. The hunks are larger than the shortest, and they still apply: applying them reproduces the right side byte for byte.
You are always told. Every entry in --format json has a minimal boolean — true when its script is the
shortest, false when the bound engaged — and the prose puts a note: not the shortest edit script line under
that entry’s header. The same two inputs give the same output on every run and every machine.
--format json: diff-report-v1
{
"schema_version": "diff-report-v1",
"from": {"point": "heads/main", "target_block_id": "7c1e…", "queued_patches": 0},
"to": {"point": "worktree", "target_block_id": "worktree"},
"entries": [
{
"path": "docs/new.md",
"status": "renamed",
"from_path": "docs/old.md",
"from": {"kind": "file", "encoding": "text", "mode": 33188, "size": 812},
"to": {"kind": "file", "encoding": "text", "mode": 33188, "size": 830},
"minimal": true,
"hunks": ["@@ -3 +3 @@\n-old\n+new\n"]
}
],
"unsupported_paths": [{"path": "link", "refusal": "precondition not met: link: worktree symlink authoring is out of scope"}]
}
from/to—pointis how the side was named: a ref, a block id, orworktree.target_block_idis the block it resolved to; for the worktree it is the string"worktree"; for the current branch of a fresh repository (the empty state) it isnull.queued_patchesappears only on the implicit left side of a barediff: how many unsealed commits were folded on top of that block.entries— canonical path order.pathis the right side’s path (the left’s for a deletion);from_pathis present for a rename only.fromandtoare the fields atree-listing-v1entry carries (kind,encoding,mode,size, andcontent_idfor binary), emitted by the same code; the path is the entry’s own. A side is absent when the path is not there (addedhas nofrom,deletednoto).content_idappears for binary sides only.hunksis a list of strings, each starting at its@@line and ending in a newline; empty for a binary, mode-only or content-identical entry.minimalis always present.unsupported_paths— objects withpathandrefusal, in path order; empty between two points.- No
untrackedlist. A new worktree file is anaddedentry.
diff-report-v1 settles the format for diff and nothing else.
When diff refuses
- An absent ref, or a block id the repository does not hold — precondition, exit
1:ref heads/x does not exist in this repository,block <id> is not in this repository. An id naming a patch, not a block, refuses too. The current branch is never absent when it is the worktree’s baseline (--fromalone, or left off); named as one side of two points with--to, it refuses exactly like any other unpublished ref. - A value that is neither a ref name nor a block id, a
--paththat is not a repository-relative path, a lone--to, a repeated flag — usage, exit2. - An operation replay does not support, anywhere in either history — the whole call fails, and nothing is printed.
- A blob that is missing or no longer recomputes — fails the whole call as
integrity error: a comparison with a hole in it is not a comparison, so no entry is shown as unavailable (RFC 157 §5a). - A live rename declaration
commitwould refuse — the same messagecommitprints.
Claim-to-Source Anchors
| Claim | Source anchors |
|---|---|
| The line diff is a shortest edit script within a counted work bound, and marks what it could not finish. | line_diff.rs |
| Each entry is classified by path first, and a rename pairs only the same declared node. | diff.rs |
The worktree side is read with commit’s own baseline, walk, ignore rules, declarations and refusals, and writes nothing. | worktree_read.rs, worktree_files.rs |
Each side of an entry is described by the function tree uses. | point_reading.rs |
The prose and diff-report-v1 are rendered from one report. | diff.rs |
Checkout Planning
PR-017 includes a read-only checkout plan:
prikk checkout --plan-only [path] [--ref heads/main]
The command resolves the current RefState to its Block and reports whether checkout would need snapshot materialization or patch application.
--ref takes any published ref, not only a branch: a tag ref resolves through its tag object to the
Block that object names, so --ref tags/v1 plans against the tagged Block rather than the branch
tip. That applies to --patch-plan and its --content-path reporting too.
The read-only modes — --plan-only, --snapshot-plan, --patch-plan (with or without
--content-path) and --patch-delete-plan — also take a bare block id, as log prints it. Any
block the repository holds resolves, including an older block on a branch, so two blocks on one ref
can be read side by side:
prikk checkout --patch-plan --format json --content-path a.txt --ref <block-id>
A block id replays exactly as a ref naming the same block does, from the same checkpoint. The prose
header then reads block: <id> instead of ref: <name>. An id the repository does not hold refuses
with block <id> is not in this repository; an id naming another kind of object refuses naming its
type, for example object <id> is a patch, not a block. The modes that write the worktree refuse a
block id, because the next commit authors against a branch and a block id names none.
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]
A refused checkout writes nothing
--patch-materialize, --patch-materialize-delete and --snapshot-materialize check every file they
would write before writing any. An existing file with different content, a directory or other non-file
at a path, or a symlinked parent directory is a conflict, and a conflict refuses the whole checkout:
error: precondition not met: refusing to materialize: 2 path(s) in the way: dev-only.txt (an existing file with different content), shared.txt (an existing file with different content); move them aside, or commit them on their own branch, and run the checkout again (nothing was written)
Every conflicting path is named, not only the first. Nothing is written, no marker is set, and commit
keeps working. --patch-materialize-delete likewise refuses, naming each file, when a file it would
delete has changed. See troubleshooting for the one case that still leaves a
worktree part-written.
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|block-id>]
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 and does not remove extra files. Before writing anything it checks every file it would write: an existing file with different content, a non-file at a path, a symlink target or a symlinked parent directory is a conflict, and any conflict refuses the whole checkout with every such path named and nothing written (see checkout). So snapshot checkout cannot be used to write outside the repository worktree.
A snapshot worktree is provisional until prikk verify
A snapshot proves only that its files match the state root its block signs. It does not prove that
replaying the history before that block produces the same files: only replay proves that, and only
prikk verify replays the whole repository. So a worktree written from a snapshot is provisional.
- The marker. Before its first write,
checkout --snapshot-materializerecords the ref and the snapshot’s block in.prikk/worktree.provisional. A crash after that point leaves the marker set. - The derivation gate. While the marker is set, every command that would turn the worktree, or the
repository’s current state, into history refuses as a precondition and names the route:
commit,mv,seal,merge,sync accept,sync seal,rollback-draft --append-inverseandbranch switch. Reading commands,checkout --patch-*,tag create,branch create/close,trustandbundlestay available. - Clearing it. Run
prikk verify. The marker clears when verify finds nothing about objects, the WAL, refs or block state. A publication-trust finding alone does not keep it: who signed a block says nothing about whether replay reproduces the snapshot. A materialization that runs whileverifyis running keeps its marker, and verify says so. - When no marker is written. If this repository has already confirmed the block’s state by replay
and an adopted maintainer signed the block, the snapshot’s files are exactly what replay gives, so
--snapshot-materializewrites no marker and printsprovisional: no. Otherwise it printsprovisional: yesand the marker applies. A block is confirmed whenseal,mergeorsync sealsealed it or replayed it as part of its parent history, or whenprikk verifyreplay-verified it. Receiving a block (bundle import,sync accept) never confirms it. “Adopted” means a signature by a key inprikk trust maintainer listthat verifies: a block signed by a key this repository has not adopted is provisional however the record came to list it. If a recorded block’s manifest fails validation, or its signature does not verify against an adopted key, the command also says so on stderr, naming the block. - The record. Those confirmed block ids live in
.prikk/cache/replay-verified-blocks.v1. It is a cache, never authoritative, and never enough by itself: a damaged, truncated or other-version file reads as empty, deleting it is always safe, and a line written into it by hand makes no snapshot trusted, because a block must also carry a verifying signature by an adopted key. Without it, every snapshot materialization is provisional and every worktree write replays from genesis, untilsealorverifyrecords the blocks again.verifyadds every block its own replay confirmed to this record regardless of what else that run found — even a run that exits1on a publication-trust failure still records what it replayed. So adopting the maintainer key afterward is enough on its own: the record already has the block, the adopted-key check beside it is re-evaluated fresh every time rather than frozen into what was recorded, and no secondverifyrun is needed before the checkpoint becomes usable. The record is keyed to the repository’s own relative layout, not to an absolute path, so moving the repository’s directory leaves it intact. A fresh receive or a freshly restored repository has simply never hadsealorverifyrun against it locally, so the file starts empty rather than resetting — neither operation clears it. - Where it shows.
prikk statusprintsprovisional worktree: materialized from the snapshot of <block> on <ref>; not replay-verified — run prikk verify,status --format jsoncarriesprovisional_worktree, andprikk doctorlistsPRIKK-DOCTOR-PROVISIONAL-WORKTREE.
Reports that start at a snapshot
checkout --patch-plan (prose and --format json, including --content-path),
checkout --patch-delete-plan and bundle preview only read, so they may start at the nearest valid
snapshot on the ref’s history instead of genesis. Their output is byte-identical
either way: the file manifest comes from the snapshot, and everything about history — patch and
operation counts, coverage, files history deleted — is still computed over the whole chain. A snapshot
that fails validation is never used: the report replays from genesis, prints the same output, exits 0,
and adds one line on stderr naming the block and prikk verify.
Commands that write the worktree — checkout --patch-materialize, --patch-materialize-delete and
branch switch — start at a snapshot only when its block is in the record above, carries a maintainer
signature by an adopted key that verifies, is at most 63 blocks back from the tip, and the snapshot passes
validation; otherwise they replay the whole history. A received block’s snapshot is never a starting
point for a worktree write. The written files are identical either way. rollback-preview always
replays the whole history.
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_FILE="<path to a 0600 seed file>" # or leave unset to use ~/.config/prikk/author.seed
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 queues commits until seal, up to the queue limit described in the FAQ. The
active WAL records the one ref it belongs to, and a commit for a different ref is refused while the queue is
non-empty, so seal --ref heads/main cannot publish a patch authored for heads/topic
(crates/prikk-store/src/commit_boundary/active.rs:301-314).
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. A path matched by a .prikkignore rule
at the repository root is skipped by this scan entirely — never authored, never reported — see
Ignoring Worktree Paths.
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
Declared Move and Rename Authoring
prikk mv <old> <new> is the first prikk command that authors a RenamePath operation. Renames are
never inferred from content — comparing bytes to guess “this looks like that file, renamed” is not
something prikk does. A rename only ever comes from an explicit declaration.
prikk mv old-name.txt new-name.txt
This both performs the move on disk and records a declaration. The declaration is durable, kept
alongside the active queue, and is authored into the next prikk commit — not immediately.
The four worktree states
prikk mv keys its behavior on existence only, never on content:
| Worktree state | Behavior |
|---|---|
<old> exists, <new> does not | Perform the rename, record the declaration |
<old> is gone, <new> exists | Record the declaration only — no bytes are touched |
| Both exist | Refused — two files exist, and which one is the tracked node is not prikk’s to guess |
| Neither exists | Refused — there is nothing to move |
The second row is what makes the command useful for the ordinary flow of running a shell mv and
only afterward remembering to tell prikk about it:
mv old-name.txt new-name.txt # already moved by hand
prikk mv old-name.txt new-name.txt # records the declaration; touches no bytes
Reviewing a live declaration before committing
A declaration made and forgotten becomes permanent history at the next commit. prikk worktree-status
shows every live declaration, in prose and in --format json:
prikk worktree-status
live rename declarations: 1
old-name.txt -> new-name.txt [rename]
refused declarations: 0
Each line says what the next commit will do with that declaration, from the same classifier
commit obeys — rename, deletion, deletion-ignored, never-tracked, or refused with
commit’s own message. A rename also says when content or mode changes alongside it
([rename] (content also changes)), because commit authors those in the same patch. In
--format json the fields are resolution, refusal, content_changed and mode_changed, plus a
top-level refused_declaration_count. See
Worktree Status.
What a commit does with a live declaration
prikk commit consumes every live declaration against the worktree at commit time:
- Confirmed (old path absent, new path present): authors a
RenamePath. Any simultaneous content or mode change on the same node is authored alongside it in the same patch. - Nets to deletion: if the declared destination is no longer a file at commit time — deleted,
replaced by a directory, or excluded by
.prikkignore— the declaration is dropped and a plain deletion is authored instead, never a rename to nowhere. The disclosure names which:destination is gone,destination is a directory, ordestination is ignored. A directory is only ever reported as ignored when.prikkignorereally excludes it. - Not a regular file: a symlink, FIFO or socket at the destination still resolves
rename— andcommitrefuses over that path, as it does for any such entry in the worktree.worktree-statusnever opens it, socontent_changedandmode_changedarenullfor it. - Never tracked: if the declared source was never a sealed node (an untracked file, moved with
prikk mvbefore its first commit), there is no node to rename. - Contradicted: if the worktree disagrees with the declaration (the source path is back on disk),
the whole commit is refused, naming the declaration. Nothing is silently dropped. The refusal names
the way out for the state the worktree is actually in, and every route it names was measured:
- the source is back and the destination is gone (a shell
mvundid the move):prikk mv <new> <old>nets the declaration to no move and drops it, orprikk mv <old> <new>makes the move again. Undoing it with a shellmvis what produced this state, so repeating that is the one thing that does not help. - both paths exist:
prikk mvitself refuses while both are there, so set one copy aside first — delete the old path and commit to author the rename, or delete the new path and then runprikk mv <new> <old>to drop the declaration. - the destination is another tracked node this commit does not also move:
prikk mv <new> <old>drops the declaration; the old path keeps its content and the destination stays deleted in the worktree, so the commit authors that deletion.
- the source is back and the destination is gone (a shell
Every one of these outcomes is correct on its own — a deletion really is the honest record for a file
moved into an ignored directory, for instance. What would be wrong is doing this silently: a user who
asserted a move deserves to see what actually happened to it. So prikk commit names every declaration
that did not become the rename it asserted, one line each, alongside the operations it authors:
$ prikk mv a.txt build/a.txt # build/ is excluded by .prikkignore
moved a.txt -> build/a.txt
$ prikk commit -m moved
delete-file a.txt
declaration a.txt -> build/a.txt: destination is ignored; recorded as a deletion, not a rename
Renaming a node more than once before a commit collapses to the net move: prikk mv a b followed by
prikk mv b c authors exactly one RenamePath, a -> c — never two hops. A declaration that returns
to its starting point (prikk mv a b then prikk mv b a) is dropped entirely; nothing is authored —
and prikk mv says so immediately, since the round trip is resolved right there, not at the next
commit:
$ prikk mv a.txt b.txt
moved a.txt -> b.txt
note: this declaration is authored into the next `prikk commit`; see `prikk worktree-status` to review it first
$ prikk mv b.txt a.txt
moved b.txt -> a.txt
declaration a.txt -> b.txt -> a.txt: nets to no move, dropped
The commit-time hint — a suggestion about what you didn’t declare
If you move a file with a plain shell mv and never tell prikk, commit sees an ordinary delete
and an ordinary create — nothing tells you identity could have been preserved. commit may print a
hint for that:
$ mv old-name.txt new-name.txt # a shell mv, not prikk mv -- never declared
$ prikk commit -m moved
delete-file old-name.txt
create-file new-name.txt
hint: looks like old-name.txt moved to new-name.txt; `prikk mv` would have preserved its identity
The hint and a disclosure line look deliberately different, on purpose, because they mean opposite things — one is a fact about something you asserted, the other is a guess about something you didn’t:
declaration a.txt -> build/a.txt: destination is ignored; recorded as a deletion, not a rename
hint: looks like old-name.txt moved to new-name.txt; `prikk mv` would have preserved its identity
A disclosure line is indented like the operations above it, past tense, and says what did happen to something you declared. A hint is unindented, conditional (“would have”), and says what might have been true of something you never declared at all — the same shape as this guide’s own “note:” lines, not the operation list.
The hint authors nothing. It costs a line of output, never a fact in history, which is exactly
why it is allowed to be wrong. It never prompts and never blocks, and there is no flag to make it a
default — the machine suggests, you assert, always with prikk mv itself.
The signal is exact content equality only — never similarity scoring, and never anything cleverer. A few cases the signal alone would get wrong are handled directly:
- A declared move never triggers it.
prikk mvalready authored aRenamePath; there is nothing left to suggest. - An ambiguous set prints no hint. Two deleted files and two created files sharing identical content — which moved to which is unknowable, and a wrong guess is worse than silence.
- A mass reorganisation prints one summary line, not a wall of hints, once a commit’s own unambiguous candidates pass a small threshold.
Two nodes can be swapped in one commit by chaining declarations through a temporary name:
prikk mv a.txt tmp.txt
prikk mv b.txt a.txt
prikk mv tmp.txt b.txt
prikk commit --from-worktree -m "swap a.txt and b.txt"
What this does not do (yet)
- No similarity scoring in the commit-time hint above — exact content equality only, and nothing cleverer.
- No conflict witness for two declarations naming the same destination.
- Symlink renaming is out of scope, the same as symlink authoring generally.
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|block-id>]
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)RenamePath, authored bycommitfrom aprikk mvdeclaration; one patch’s consecutive renames replay as one batch
Unsupported operations still fail the plan clearly:
CreateSymlink— 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.
Content at a point (RFC 143)
The replay above already reconstructs every file’s bytes internally, then discards them once the
plan is proven. --format json exposes those bytes instead, for exactly the paths you name:
prikk checkout --patch-plan --format json --content-path <repo-relative path> [--ref <ref|block-id>]
--content-path is repeatable and required for --format json to report anything — there is no
whole-tree default. A requested path that never resolved in the replay degrades to not_found
(not an error); an unsupported operation anywhere in the walked chain still fails the whole call,
never folds into a partial-but-successful response. A bare --format json with no --content-path
reports metadata only — an empty content and not_found, a populated coverage — since there is
nothing to scope content to and nothing to guess at.
patch-plan-content-v1’s coverage field is not a completeness promise: it names which
operation kinds this specific replay actually applied (create-file, delete-file, edit-text,
replace-binary, change-perm) and that the walk was single-parent — the same conservative
subset --patch-plan supports above, not a claim that every
operation kind in history is covered. RenamePath and CreateSymlink still fail the call outright,
exactly as they fail the read-only replay itself.
Binary content is never emitted as bytes: a BinaryFile node reports its current blob id and
declared size only. A path that a snapshot seeded but the replayed chain never subsequently touched
has no way to know its own text-vs-binary kind — snapshots carry no kind field — so it reports as
opaque (size only) rather than guessing.
This surface exists to let another tool compare two replayed points without prikk owning comparison
semantics itself — prikk still has no diff, and this does not add one. See
show for the sibling case: one block or patch’s own recorded content, no replay
needed for path-addressed operations.
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 contentRenamePath(RFC 144 increment 1) — the node’s content is written at its new path; a rename in one patch that swaps two files is applied as one step
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. - Neither mode removes a renamed file’s old path from the worktree:
--patch-materializenever deletes, and--patch-materialize-deleteremoves only files aDeleteFileremoved. - Symlinks, merge conflicts, and full patch algebra remain later increments.
commitrefuses a symlink 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|block-id>]
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.
- The replay this plan reads applies text edits, renames and mode changes, as
--patch-materializedoes, but only aDeleteFilemakes a file eligible for deletion: a renamed file’s old path is never deleted. - 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]
--ref takes any published ref, not only a branch. A tag ref resolves through its tag object to
the Block that object names, so prikk inverse-plan --ref tags/v1 plans the inverse of the tagged
Block’s patch, which need not be the branch tip. The same is true of prikk rollback-preview and
prikk rollback-draft-verify, which reach the same resolution. prikk rollback-draft --append-inverse still requires a branch ref: it appends to that ref’s active WAL, and a tag names
no WAL.
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]
--ref takes any published ref, not only a branch — a tag ref resolves through its tag object to
the Block it names, so --ref tags/v1 previews the rollback of the tagged Block. See
inverse planning, which shares the resolution.
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 empty state before the replayed chain: rollback reaches back to the chain’s first block, and a checkpoint snapshot never moves that target. 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 key material as prikk commit: the AUTHOR seed from your key directory (or
PRIKK_AUTHOR_SEED_FILE), with PRIKK_AUTHOR_KEY_ID naming the key id.
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:
RenamePathinverse —commitauthorsRenamePathfrom aprikk mvdeclaration, but inverse planning for it is not implemented:rollback-previewandrollback-drafton a ref whose tip patch is a rename refuse witherror: unsupported object type: inverse planning for RenamePath awaits a rename authoring path(DC-73)CreateSymlinkinverse — not a node-model gap, an authoring one: symlink authoring is refused outright, so there is nothing in ordinary history for it 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; - when a side changes one file more than once (any mix of edits, permission changes and binary
replacements, optionally ending in a delete, or a new text file followed by edits), the evidence judges
that run by its net effect from the baseline, and uses it only when replaying it reproduces the side
exactly. An operation line for such a run names its first operation and adds
folded-through=left[N] op_seq=Mfor its last; a run whose net effect is a mode change and an edit shows both operations with the same range. Indices and counts are always those of the original sequence; - report-level items render as
report:without a fake operation label; - DC-21 outcome and reason-code names are preserved exactly;
- a
RenamePathoperand’s line also carriesasserted-by=<key-id>— the patch’s AUTHOR signature key id. This is structural, not a rendering choice: the underlying read type cannot represent a rename without its asserting signer, so a patch whose AUTHOR signature is missing fails the command rather than showing a rename with no signer.
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 cannot be merged yet. The confluence proof judges each side’s changes against the baseline. Several changes to one file on one side are judged together by their net effect: any mix of edits, permission changes and binary replacements, optionally ending in a delete, and a new text file followed by edits. A no-op run (a change and its undo) is set aside only when the other side does not touch that file; the merge replays each side’s original patches onto the other side’s tip, so the proof never claims what that replay could not do. Some ordinary histories still refuse:
-
Renames that meet. A rename (
prikk mv) merges like any other change, whether or not either side edits the renamed file: an edit, a permission change or a binary replacement names the file by its identity, not its path, so it lands on the renamed file. What refuses, as a conflict:- the same rename on both sides — both sides changing the same thing is a conflict, as it is for the same edit on both sides;
- one file renamed to two different paths, or two files renamed onto one path;
- a rename on one side and a delete of that file on the other;
- a rename onto a path the other side frees, or a swap or chain split across the two sides. Each side’s rename was made against that side’s history, so one side’s rename cannot rely on a path only the other side vacated.
A swap of two files made in one commit merges only when the other side changes nothing. Beside any change on the other side it is refused as
unsupported_operation: the proof compares the two sides one operation at a time, and half of a swap cannot be replayed alone. The same two renames in separate commits never merge, because between them both files would hold one path. -
A side containing a symlink (created or deleted) is not merged yet. The proof cannot replay a symlink, so it refuses as
unsupported_operationrather than guess. -
A side that creates a file and then deletes it, or changes its mode, and a side that deletes a file and creates another at the same path, refuse as
sequence_internal_dependency_deferred. A file created and deleted on one side held its path for part of that side’s history, so treating it as nothing could hide a clash with the other side. -
A side that changes a word and then restores it, while the other side edits the same file, refuses as
sequence_internal_dependency_deferred.
None of these is damage: the repository verifies clean, and the refusal is a precondition.
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/merge/dc74_merge_execution.rs).
Deferred
- Automatic merge-base discovery —
--baseline-blockstays explicit. - Widening
patch_algebra’s conservative subset. - Merging more than two sides in one command.
Conflict arbitration / resolution is not on this list. “Deferred” means eventually built; automatic conflict resolution is refused by the architecture and will not be built at all — see Conflict Resolution Is Refused By Design.
PatchPayload.parent_patch_ids is not on this list either. It was removed at Patch schema 2
(0.24.0), not left unpopulated — there is no patch DAG because the field no longer exists, not
because it is inert. BlockPayload.parent_block_ids, which DC-75 records, answers a related but
different question: block-level parentage, not a patch DAG.
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> [--force]
prikk sync accept <file> [--claims-out <file>] [--force]
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.
A deletion’s own content travels too. An artifact carries the content each deleted node held, so the
receiver can replay and roll back that deletion. When nothing stored it — a text file whose edits only
ever travelled as spans — sync build derives it by replaying the history and carries the result,
checked against the id the deletion names. Before 0.43.0, building an artifact for such a history
refused.
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 separate,
explicit acts (seal, adopt-tag), each signed locally.
An object’s id is the hash of its payload, and signatures are not part of the payload. So a receiver
that seals the same patches, in the same grouping, onto the same parent produces the sender’s block id
and the sender’s RefState id exactly; only the signatures on them differ. (Measured: a receiver
sealing one accepted claim under its own maintainer key got the sender’s block and heads/main
RefState ids.) Sealing the patches in a different grouping, or onto a different tip, gives different
ids. A sender’s tag and the receiver’s adopted tag are different objects: adopt-tag writes a tag object
and RefState of its own, naming the same target block.
An accept interrupted partway never leaves a dangling reference. accept writes under one
lock, held continuously from the first write to the last, in a fixed order: patches and blobs first
(content-addressed and idempotent, so a replayed accept writes nothing new for anything already
present), then AUTHOR key material, then claims and tags last. Claims and tags are what every other
command treats as “this was accepted,” so if accept is interrupted partway through, part of the
artifact may already be sitting in the object store, but nothing yet points at it as accepted — there
is no reference naming content that was never stored. Running the exact same accept again simply
continues it to completion: the content already written is skipped, not rewritten.
One id, several signers, in format 7. After the receiver has sealed the sender’s patches as above,
importing the sender’s bundle of that ref merges: each shared object keeps one stored copy carrying both
maintainers’ signatures, and the import reports objects gaining signatures: N. The same happens in the
other order, when the receiver imports first and seals afterwards. What may join a stored object is
checked first: every incoming signature must verify, or the whole import or exchange is refused with
nothing written; a MAINTAINER signature is kept only if this repository has adopted its key, and is
otherwise dropped and named on a signature not stored: line; an AUTHOR signature needs key material
under the usual trust-on-first-use rules. Nothing arrives pre-trusted.
An object carries at most 4 counted signatures when an import or exchange would store it. Every
signature counts except a MAINTAINER signature by an adopted key that verifies; above the limit the whole
import or exchange refuses, naming the object, at least 5 counted signatures and limit of 4, and writes
nothing — counting stops there, so the number is not the object’s total. It applies
to new objects and merges alike. Local commands never check it and are never refused. seal signs with
an adopted maintainer key, which does not count; your own AUTHOR signatures do, so on an object whose set
they helped fill, another signer’s copy is refused. See
Trust and Threat Model. A format-6 repository — one
created by 0.44.0 or earlier and not yet upgraded — still holds one signed copy per id and refuses with
existing container record for <id> differs from candidate, naming prikk format upgrade.
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. sync compare,sync buildandsync accepteach refuse an oversized input file before reading it, andsync acceptrefuses any one object inside the artifact over a separate bound you can set (equal to the total by default, so it only bites once you lower it) — see Size bounds on incoming artifacts for the defaults, the refusal wording, and how to raise or lower either one.
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.
Backup and Restore
prikk bundle export/verify/import (DC-78) are what back up and restore a repository today.
This page is the recovery procedure: make a backup, check it later without touching anything, and
restore it — plus, just as important, what that restore does and does not establish about the
history you get back. See Security and Signing Setup for the maintainer key
setup this page’s trust maintainer add step assumes.
Prikk is early implementation software. Work through this page with throwaway content, the same example key material the tutorial uses — never real seeds.
Set up a repository to back up
If you have already worked through the tutorial, skip to Make a backup — this is the identical setup, condensed. Each refusal below is explained there, not repeated here.
mkdir source-repo && cd source-repo
prikk init
export PRIKK_AUTHOR_KEY_ID="dev-author"
export PRIKK_AUTHOR_SEED_FILE="$PWD/author.seed"
export PRIKK_MAINTAINER_KEY_ID="dev-maintainer"
export PRIKK_MAINTAINER_SEED_FILE="$PWD/maintainer.seed"
prikk trust maintainer add \
--key-id "dev-maintainer" \
--public-key "a00899dfd3357aee69729405913f9324dfc033cec04a2215239eda64ae6d9d91"
echo "hello prikk" > readme.txt
prikk commit -m "genesis"
prikk seal --allow-no-audit
initialized Prikk repository at /path/to/source-repo/.prikk
trusted maintainer key: dev-maintainer
adopted maintainer keys: 1
recorded worktree patch in active WAL
baseline ref: heads/main
patch id: ...
WAL sequence: 1
operations: 1
referenced blobs: 1
...
create-file readme.txt
...
sealed active WAL into block
patches: 1
block id: ...
heads/main RefState: ...
...
Make a backup
A backup is one ref’s sealed closure — the ref’s own RefState plus every object reachable from
its target block back to genesis:
Run this from inside source-repo, writing the backup one level up so it survives independently
of the repository it came from:
prikk bundle export --ref heads/main --output ../backup.bundle
exported heads/main
tip block: ...
objects: 5
author key material: 1 included (continuity only, not a trust decision)
repository format: 6
tool version: ...
note: this bundle contains one ref's closure only -- other refs in the source repository, if any, are not included, and this bundle makes no claim about them
wrote ../backup.bundle
Running this again refuses rather than silently replacing the file:
prikk bundle export --ref heads/main --output ../backup.bundle
error: refusing to overwrite existing file at ../backup.bundle (pass --force to overwrite it intentionally)
Pass --force when overwriting is actually what you want.
Only sealed history is backed up — this is the limit that costs the most
commit records a patch in the active write-ahead log; only seal publishes it onto a ref.
bundle export walks the ref’s own published block ancestry — the active WAL is not part of that
walk. If you commit and do not seal before exporting, that commit is not in the bundle, and nothing
about the export command tells you so.
This is not a hypothetical. Still inside source-repo, commit again without sealing:
echo "not yet sealed" > notes.txt
prikk commit -m "notes, not yet sealed"
prikk bundle export --ref heads/main --output ../backup-after-unsealed-commit.bundle
exported heads/main
tip block: ...
objects: 5
The tip block and object count are identical to the first export. The second commit is not an error, not a warning, and not present — the bundle is byte-for-byte a backup of the state before that commit ever happened. Before exporting, check whether anything is queued:
prikk status
...
active WAL records: 1
trailing partial WAL bytes: 0
heads/main RefState: ...
queued patches: 1 targeting heads/main
...
queued patches: 1 is the tell. Seal first, then export — now backup.bundle covers both
generations:
prikk seal --allow-no-audit
prikk bundle export --ref heads/main --output ../backup.bundle --force
sealed active WAL into block
patches: 1
block id: ...
heads/main RefState: ...
...
exported heads/main
tip block: ...
objects: 9
author key material: 1 included (continuity only, not a trust decision)
repository format: 6
tool version: ...
note: this bundle contains one ref's closure only -- other refs in the source repository, if any, are not included, and this bundle makes no claim about them
wrote ../backup.bundle
objects: 9 now (two RefStates, two blocks, two patches, two blobs, and the snapshot the first block carries as a checkpoint) — --force was necessary
because ../backup.bundle already existed from the first export.
A bundle is one ref
bundle export takes exactly one --ref. A repository with several branches needs a bundle per
branch — nothing collects them for you, and the manifest’s own printed note above says so in every
bundle you make, not only here.
For every ref, every object, and the trust material in one file, see
The Archive Format instead: prikk archive export <file> carries
the whole repository, verbatim. prikk archive verify <file> reads one back and reports whether it
is internally consistent, needing no repository and writing nothing. prikk archive import <file> [--adopt] lands every ref the archive carries into a fresh (or existing) repository: by default
under remotes/<ref>, the same received-pointer shape bundle import already produces, or as a
real local branch under --adopt, when it is absent locally, signed by a key this repository has
adopted (trust maintainer add), and verified — every other ref still lands under remotes/, with
the reason named.
With prikk 0.51.0, run archive verify on every export before relying on it (fixed in 0.52.0;
see the limitation for the two shapes and why).
A repository holding any file over 256 MiB cannot be archived. archive export (and
bundle export, for a ref that reaches such a file) refuses up front, naming the over-bound
object, and writes nothing — the same per-object bound archive import/bundle import already
enforce when reading one back. A copy of this repository’s own .prikk/ directory is the real
backup for a repository in this state.
If the archive’s own exported local ref and one of its own carried received pointers would land
at the same remotes/<ref> name — because the source repository itself once bundle imported
something for that same ref — the exporter’s own ref wins the name; the colliding received pointer
is not landed, and the import’s own report names it and the RefState it would have landed (its
objects are still imported either way). A successful --adopt lands the exporter’s own ref as a
real local branch instead, so there is no collision at all in that case.
If archive import is killed or lost partway through, prikk doctor names it
(PRIKK-DOCTOR-INTERRUPTED-IMPORT) and its id, with two ways out: run the same import again (it
resumes from wherever it stopped), or prikk doctor --cancel-import <id> to restore the repository
to what it was before the import ever started. If the process was killed outright — a real
SIGKILL, a lost machine, not a command that returned an error on its own — its lock files are
still on disk (nothing steals a lock automatically: see prikk unlock’s own guide), and doctor’s
own message says so, naming prikk unlock first, ahead of resume or cancel. The sequence is
always the same order: prikk doctor to see what is true, prikk unlock --lock <path> for each
lock it (or prikk unlock on its own) lists, if any are present, and only then the same import
again, or --cancel-import.
Check a backup later, without restoring it
bundle verify reads a bundle file and reports whether it is structurally sound and internally
consistent. It needs no repository and writes nothing — safe to run against a backup sitting on a
shelf, from any directory. Leave source-repo first, to show it really does not need one:
cd ..
prikk bundle verify --input backup.bundle
bundle verifies: heads/main
RefState: ...
tip block: ...
objects: 9
author key material: 1 present (continuity only, not a trust decision)
repository format: 6
tool version: ...
note: this bundle contains one ref's closure only -- other refs in the source repository, if any, are not included, and this bundle makes no claim about them
note: this checks structural and internal consistency only -- no signature is cryptographically verified (a standalone bundle carries no trust material to check one against), and this bundle's own author-key section is recorded here, never independently verified, the same as at import. A verified bundle is not yet a trusted one -- import it and run `prikk verify` for that.
Restore
Restoring means importing into a repository and then checking the result — import alone is deliberately not enough to trust what arrived.
mkdir restored-repo && cd restored-repo
prikk init
prikk bundle import --input ../backup.bundle
initialized Prikk repository at /path/to/restored-repo/.prikk
received remotes/heads/main
RefState: ...
objects: 9
new objects: 9
author key material: 1 recorded (continuity only, not a trust decision)
note: no local ref was created or advanced, and no MAINTAINER key was trusted; this repository now holds the received history at remotes/heads/main, which `prikk log --ref remotes/heads/main` and `prikk show` read, and `prikk verify` checks once `trust maintainer add` trusts the sealing key
Restoring into a repository that already holds some of the history stores nothing twice. An object
it already holds under the same signatures is counted but not written; the same object under an
additional signature — a copy another maintainer sealed — merges into the stored one in a format-7
repository, reported as objects gaining signatures: N, and is refused in a format-6 one (see
Sync).
Import records material; it does not create trust. The imported history lands as
remotes/heads/main, not heads/main — no local ref moved, and the maintainer key that sealed it
is not yet trusted here. Trust it explicitly, the same key id and public key
Security and Signing Setup describes adopting:
prikk trust maintainer add \
--key-id "dev-maintainer" \
--public-key "a00899dfd3357aee69729405913f9324dfc033cec04a2215239eda64ae6d9d91"
trusted maintainer key: dev-maintainer
adopted maintainer keys: 1
Now confirm the restore:
prikk verify
verified repository: /path/to/restored-repo/.prikk
...
object items: 9 scanned, 0 failed
...
publication trust issues: 0
sealed blocks: 2
sealed-block ...: dev-maintainer
sealed-block ...: dev-maintainer
...
received refs: 1
received-ref remotes/heads/main: ...
verify is the same command, unmodified, that checks any locally-authored repository — the bundle
format adds no separate verification path. A restored copy is only as checked as this command says
it is; import’s own report is not a substitute for running it.
What a restore gives, and what it does not. Measured on the binary, in a fresh repository after
bundle import:
- It gives verifiable history at
remotes/heads/main.prikk log --ref remotes/heads/mainlists the received blocks,prikk show <block>reads their patches and content, andprikk verifychecks every object and names the key that sealed each block. With no maintainer key trusted,verifyreports publication-trust issues and exits non-zero; aftertrust maintainer addfor the sealing key, it passes. - It does not give a working branch. None of these turn the received ref into your own
heads/maintoday:prikk branch create heads/main --from remotes/heads/mainandprikk checkout --patch-planor--patch-materialize --ref remotes/heads/mainrefuse (remotes/heads/main is a received ref, and this command does not accept received refs), so no worktree is written; andprikk merge --into heads/main --from remotes/heads/mainrefuses in a fresh repository (ref heads/main does not exist in this repository), even with the sealing key trusted. The refusal names what does read a received ref:log,merge-evidence,merge-planandbundle preview. - It cannot be passed on.
prikk bundle export --ref remotes/heads/mainrefuses, so a restored copy is not itself a backup source.
A bundle import restore is therefore an intact, checkable copy of the history, not a repository you
continue working in that way. prikk archive export <file> writes a whole repository, verbatim, into
one file (RFC 155) — see The Archive Format. prikk archive verify <file> reads one back and checks structure, reference closure, and every signature against the
archive’s own carried key material, reporting internally consistent, never trusted — it needs no
repository and writes nothing. A signature by a key the archive does not carry (for instance, a
received branch the source repository itself never trusted) is named, not refused on: trust is
always the receiving repository’s own decision, never the archive’s. prikk archive import <file> --adopt is the landing step that turns a
carried branch into a working one: absent locally, signed by an adopted key, and verified, it becomes
a real local branch; every other ref still lands under remotes/, exactly like bundle import’s own
received pointer, with the reason named.
What this proves, stated precisely
- Corruption is detectable. Every object’s id is a hash of its own bytes, and
bundle verifyconfirms every reference inside the bundle resolves to an object whose recomputed id matches. Content addressing works the same way a downloaded release’s checksum does: it proves the bytes are intact and internally consistent, not who produced them. - A deletion carries the content it removed. A bundle includes the file content each deleted node
held, so a receiver can read that deletion in full —
prikk showrenders the content rather than<unavailable blob …>. Where nothing stored that content — a text file whose edits only ever travelled as spans — the exporter derives it by replaying the history and carries the result, checked against the id the deletion names. Before 0.43.0, exporting such a history refused outright. Rolling that deletion back is a separate matter:rollback-previewandinverse-planwork on a published local ref, and an imported history lands atremotes/…, so both refuse there (remotes/heads/main is a received ref, and this command does not accept received refs). The content is present for when the history is local. bundle verifyanswers “is this file intact” without a repository or a restore. Nothing is written; nothing needs to exist first.verify_repositoryafter import is what turns a restored copy into a checked one — the same structural and authorship checks any repository gets, run against what was actually written.
What this does not prove
- No signature is cryptographically checked offline. A standalone bundle file carries no trust
material to check a signature against —
bundle verifychecks structure only. Authenticity is checked after import, byverify. - Authorship is trust-on-first-use, not identity. See Trust and Threat Model § Trust Roots and Roles for exactly what that does and does not establish — it is not restated here, so the two cannot drift apart.
- A restored repository has no maintainer trust policy until you give it one.
bundle importadopts no key;prikk verifyreports publication-trust issues untiltrust maintainer addis run for the key that sealed the history you received. - This is not release authority. Releases of
prikkitself are signed by the project’s one maintainer key, andrelease-signers.tomlis empty because no multi-signer policy exists yet (Release, Versioning, and Compatibility § How prikk releases). That is separate from repository-local maintainer trust — trusting a maintainer key here says nothing about who built theprikkbinary you are running.
Limits
- One ref per bundle. Multi-ref export does not exist; back up each branch you care about separately.
- No rehearsed restore across a repository-format change. This page exercises ordinary export
and import on the current format; migrating a repository from an older on-disk format via bundle
export/import, as
layout.rs’s own retired-format messages describe, has not been exercised end to end as part of this page. bundle import,bundle previewandbundle verifyeach refuse a bundle over a size bound before reading it, and refuse any one object inside it over a separate bound you can set (equal to the total by default, so it only bites once you lower it) — see Size bounds on incoming artifacts for the defaults, the refusal wording, and how to raise or lower either one.
Claim-to-Source Anchors
| Claim | Source anchors |
|---|---|
A bundle carries one ref’s own RefState plus every object reachable from its target block back to genesis; the active WAL is not part of that walk. | bundle.rs, worktree_patch.rs |
bundle export refuses an existing destination unless --force is passed, and writes atomically. | bundle.rs, durable_output.rs |
The PBNDL003 manifest states repository format, tool version, and that the bundle’s scope is a single ref. | bundle.rs, Release, Versioning, and Compatibility |
bundle verify checks structure, framing, and closure resolution with no repository and no write; it performs no cryptographic signature check. | bundle.rs |
bundle import records objects and author-key material and creates a remotes/<ref> received pointer; it advances no local ref and adopts no maintainer key. | bundle.rs |
verify_repository, unmodified, is what checks a restored repository — the bundle format adds no separate verification path. | bundle.rs, verify.rs |
| AUTHOR key material transported in a bundle is trust-on-first-use; it proves continuity of authorship, not identity. | Trust and Threat Model |
| Multi-ref export does not exist, and no rehearsed restore across a repository-format change has been performed. | bundle.rs, rfcs/done/DC-44-MIGRATION-BACKUP-RESTORE-EVIDENCE.md |
Provenance
This guide implements DC-44’s last design goal (bundle-manifest-handoff-v1.md’s own follow-up,
backup-restore-page-handoff-v1.md). It is documentation-only and does not change CLI behavior,
bundle format, signing, trust, or repository state.
This page’s own command sequence is exercised by an automated test
(crates/prikk-cli/tests/installers_and_help/dc44_backup_restore_page.rs), which runs every command above, in order,
against the compiled binary, and asserts on the same output lines quoted here, including the
--force refusal and the unsealed-commit demonstration. A change to the CLI that alters any of
them fails that test, not just this page.
Git → prikk: a vocabulary map, not a feature map
If you know Git, prikk’s commands will look familiar and several of them mean something different.
commit does not publish. There is no HEAD and no staging area, though there is a current branch
that branch switch moves. seal has no
Git counterpart at all. Reading prikk’s words as Git’s words will make you wrong about several things
at once, and you will not notice, because nothing will error — the commands will simply do something
other than what you expected.
This page is a vocabulary map for a human, not a migration tool. It does not import Git history and does not promise to; the closest active work on that is RFC 113, which is a design for the import contract, and nothing implements it yet.
Command correspondence
| Git | prikk | What differs |
|---|---|---|
git init | prikk init | Same idea: create the repository layout. |
git clone | — | Missing. prikk bundle import reads a bundle file into an untrusted, unmerged remotes/<ref> pointer — it does not create a worktree or a local branch. There is no network transport at all: bundles and sync artifacts are files you move yourself. |
git fetch | prikk sync have <ref> (you) → prikk sync build (them) → prikk sync accept <file> (you), or prikk bundle import | File-based, and never merged. What arrives is an untrusted remotes/<name> pointer. No sockets: you move the file yourself. |
git pull | Fetch as above, then prikk sync seal <ref> --claim <id>, or prikk merge-evidence → prikk merge-plan → prikk merge | No single verb, by design. sync seal when the received patches extend your ref; the three merge steps when the histories diverged. |
git push | — | No counterpart. You produce the artifact (prikk sync build, prikk bundle export) and the other side accepts it; prikk never writes into another repository. |
git add (staging) | — | Missing, deliberately. There is no index and no staging area. prikk commit --from-worktree always considers the whole worktree (governed by .prikkignore, which is a flat list of literal path prefixes — no globbing, no negation, unlike .gitignore; see Ignoring Worktree Paths). You cannot stage part of a change. |
git mv | prikk mv <old> <new> | A declaration, authored into the next commit. Moves the file and records the rename for the next commit; if <old> is already moved on disk (a shell mv), it records the declaration alone and touches no bytes. |
git commit | prikk commit --from-worktree -m <message> | Two-phase, not one. commit only queues a signed Patch into a local WAL; nothing is published yet. See Commit versus seal below. The message is stored as signed evidence since 0.32.0 — see Messages are stored. |
| — | prikk seal --allow-no-audit | No Git counterpart. Publishes the queued WAL as a signed Block, moving the branch ref. Nothing is visible to log or a peer before this runs. |
git status | prikk status + prikk worktree-status | Split in two: status reports the repository/WAL/queue state; worktree-status reports the worktree’s own drift against the replay baseline commit would author against. Neither is git status’s single combined view. |
git log | prikk log | Block and ref metadata only — target Block ID, RefState ID, kind, parent/Patch counts, rollback classification. No commit message, author, or date is shown, because none is stored yet (see below). |
git show | prikk show <block-id|patch-id> | One block or patch, as its operations. Prints each operation with the content it carries — a created file’s text, an edit’s old and new span — rather than a unified diff. Content comes from the patch itself; a block id additionally resolves edited nodes to their paths by one replay, which a bare patch id cannot. |
git ls-tree -r | prikk tree [--ref <ref|block-id>] [--prefix <p>] | Files only, no directory entries. Lists every file present at a ref or a bare block id, with mode, exact size, and whether it is text or binary; a binary file also shows its blob id. --prefix matches whole path components. --format json is tree-listing-v1. |
git show <rev>:<path> | prikk cat --path <p> [--ref <ref|block-id>] | Bytes, bounded. Writes the file’s content at a point to stdout or --output; --max-bytes refuses anything larger, writing nothing. Binary content refuses a terminal and names --output. --format json (path-content-v1) reports the metadata with no bytes. |
git archive <rev> | tar -x -C <dir> | prikk extract --ref <ref|block-id> <dir> | Only a new directory that does not exist yet — refuses an existing target rather than overwriting it, and never writes history or worktree state into it. Anchors only where a writer would (the same rule checkout uses for a worktree write), never cat/tree’s own weaker anchoring, since this is what a CI job or a forge download trusts; --allow-full-replay reads past the trusted distance bound when no admitted snapshot is in reach. |
git diff A B | prikk diff --from <ref|block-id> --to <ref|block-id> | Compares the state at any two refs or block ids and prints unified hunks (3 lines of context) for each text change; a rename is shown only when history declares one (prikk mv), never guessed from similarity; a binary file shows ids and sizes, and prikk cat reads either side. --format json is diff-report-v1. |
git diff (worktree against HEAD) | prikk diff | What commit would author, with content. Bare prikk diff compares the current branch’s tip (and any queued commits) with the worktree; prikk diff --from <point> compares that point. There is no index, so a new file is an added entry with its content — there is no “untracked” list — and ignored paths never appear. commit then diff is empty again. |
git branch | prikk branch list / branch create heads/<name> | List and create work the same way in spirit. |
git branch -d | prikk branch close heads/<name> | Diverged. Closing marks the branch (a schema-2 closed RefState); nothing is deleted or reclaimed, and there is no reopen verb. |
git checkout <branch> / git switch | prikk branch switch heads/<name> | Stricter. Refuses a dirty worktree and unsealed work for another branch rather than carrying changes across; untracked files are never touched. See A current branch, no HEAD below. |
git checkout -- <path> / git restore | prikk checkout --patch-materialize (and the sibling --snapshot-*/--patch-* flags) | Materializes files for one --ref, plan-first (--plan-only, --snapshot-plan, --patch-plan). No pathspec — it is whole-ref, not a partial restore. |
git tag | prikk tag create tags/<name> --target <ref|block> [-m <message>] | The tag message is persisted — unlike a commit message, TagPayload already carries one. |
git merge | prikk merge-evidence → prikk merge-plan → prikk merge --allow-no-audit | Three explicit steps, not one. merge executes only a merge already proven confluent by evidence; a merge that is not refuses with a witness rather than producing a conflicted worktree. |
git rebase / git cherry-pick | — | Missing. The closest thing to cherry-pick is prikk sync seal --claim <id>, which seals one already-received patch — it cannot take an arbitrary patch from your own history. |
git revert | prikk rollback-preview → prikk rollback-draft --append-inverse -m <message> → prikk rollback-draft-verify → prikk seal | Four explicit steps producing a preimage-exact inverse Patch, rather than one command that also commits. prikk inverse-plan shows the unsigned inverse without writing anything. None of them inverts a prikk mv rename yet: a history containing one is refused. |
git stash / git submodule / git worktree (multiple) | — | Missing. One worktree per repository; nothing analogous to any of the three exists. |
git gc | prikk compact --pointer-index|--received-index|--trust-policy|--all | Diverged, not equivalent. prikk’s object model never deletes an object — compact reclaims stale index and policy records only. There is no object pruning of any kind. |
git branch -d -r / git update-ref -d refs/remotes/<name> | prikk received remove <remotes/…> [--plan-only] | Diverged. Removes only the received pointer; the objects it reached stay, reported by prikk verify as unreached objects. --plan-only previews without writing; a real run is restorable by run id through prikk doctor --recovery-restore as long as nothing has since written to the same slot. |
git fsck | prikk verify [--format json] [--stop-on-first-error] | Broader: object/WAL/ref integrity plus three-valued publication-trust classification in one pass. |
git reflog | — | No user-facing verb. Every ref update is a signed, append-only RefUpdate log entry internally, but nothing surfaces it to a command today. |
git config | prikk config get|set|unset|list | Far narrower. One repository-scoped key exists today, incoming.max-object-bytes (RFC 158 Stage A), at .prikk/config — never global, never per-user. Signing keys are still read from a per-user key directory by default, with PRIKK_*_SEED_FILE / PRIKK_*_KEY_ID environment variables overriding it — see Security and Signing Setup. |
| N/A — no counterpart | prikk key generate/prikk key public/prikk key status | Draws a fresh Ed25519 seed from the OS CSPRNG, derives a public key from a seed already held, or reports whether this machine can sign and with which key, signing nothing. Git has no equivalent — an SSH key or GPG key is generated with a separate tool entirely. |
| N/A — no counterpart | prikk setup | Composes init, key generation for both roles, trust maintainer add, and the export lines into one command. Nothing in Git bootstraps signing identity this way — git init alone reaches a usable repository because Git commits are unsigned by default. |
git blame / git bisect / git grep | — | Missing. None exists in any form. |
.gitignore | .prikkignore | Diverged, and narrower on purpose. Literal repository-relative path prefixes only — no globbing, no negation, no per-directory files, and it binds only at commit’s worktree walk and worktree-status’s scan, never at replay or verification. |
git remote add / remote tracking | — | Missing. No remote registry or URLs of any kind — see File-based distribution below. |
| N/A — no counterpart | prikk doctor | Health diagnostics with two real repairs (--repair-wal-tail, --repair-index); --repair-main-ref is recognized and always refused — there is no implemented repair behind it. |
| N/A — no counterpart | prikk format upgrade | Moves a format-6 repository to format 7 in place, after the same verification prikk verify runs. Git’s own extensions.* config changes are the nearest idea, but prikk’s upgrade is one explicit verb, verified first, with no downgrade. |
| N/A — no counterpart | prikk unlock | Lists or clears a held file lock. Nothing in Git needs this because Git has no equivalent lock. |
| N/A — no counterpart | prikk trust maintainer add/remove | Manages the local MAINTAINER trust set. Git has no signer-trust concept at this layer; the closest analogue is a GPG keyring, and it is not the same model. |
| N/A — no counterpart | prikk ref complete <ref> | Finishes an interrupted branch create/tag create/sync adopt-tag/merge/seal publication that is a completable lead (RFC 165 R4), by any adopted maintainer key, not only the one that started it. --plan-only previews without writing. Git has no equivalent: a crashed Git write is repaired by its own reflog/fsck machinery, not a per-ref completion verb. |
Commit versus seal, a two-phase model
Git’s commit does one thing: it creates a commit object and moves the branch pointer in the same
step. prikk splits that into two commands with nothing in between them optional:
prikk commit --from-worktree -m "message" # queues a signed Patch to the local WAL
prikk seal --allow-no-audit # publishes the queue as a Block, moves the ref
A queued-but-unsealed commit is genuinely not part of history yet — it does not appear in log, and
a peer reading your repository cannot see it. You can queue several commits before sealing them
together. There is no Git operation that maps onto commit alone; it is closer to git add plus
git commit --no-verify into a holding area that Git does not have, and seal is the step that
actually behaves like git commit’s publishing half.
A current branch, no HEAD
There is no HEAD and no detached state, but there is a current branch: .prikk/current-branch
names one local branch (heads/main in a new repository), and every command that takes --ref
defaults to it. prikk branch switch heads/<name> moves the worktree and the pointer together. Unlike
git switch, it never carries uncommitted changes across: a dirty worktree, or unsealed work queued
for another branch, is refused, and you commit or seal first. --ref given explicitly still works on
every command without switching.
Messages are stored; authors and dates are not
prikk commit -m <message> requires a non-empty message and stores it — since 0.32.0 it is a
signed, identity-bearing field on the Patch itself (Patch schema 4), and prikk log prints it
under its block, one line per patch. Changing a message changes the patch id, exactly as changing an
operation does: it is evidence, not an annotation
(RFC 123).
A patch written before 0.32.0 carries no message and shows no message line — absence, not a
placeholder. Those patches stay readable forever; the message was never recorded and cannot be
attached retroactively.
An author display name and a commit date are still not stored, and the date never will be.
created_at is pinned to zero at every signing site so object ids stay reproducible across machines,
which CI proves by mutating a repository on Windows and Linux and diffing the resulting id lists. A
timestamp inside the identity surface would destroy that property. The display name is a separate,
deferred question (RFC 123 §5). A tag’s message is persisted too — the two commands are now
symmetric in this respect.
File-based distribution instead of remotes
There is no remote registry, no URLs, and no network transport of any kind in the binary. Moving history between repositories is always a file you produce and hand to the other side yourself:
prikk bundle export/import/verify— a complete, self-contained history bundle in one file.prikk archive export/verify— a whole repository, every ref and every object, verbatim, in one file (RFC 155).archive importis not built yet.prikk sync’ssummary/compare/have/build/accept/pending/sealsubcommands — an incremental, gap-closing exchange, still file-based, still no sockets.
Either way, what you receive lands as an untrusted remotes/<name> pointer — readable by log,
merge-evidence, and merge, but not automatically merged or trusted the way a Git remote-tracking
branch is once fetched.
Claim-to-Source Anchors
| Claim | Source anchors |
|---|---|
commit only queues a signed Patch to the local WAL; seal publishes it as a Block and moves the branch ref. | node_authoring.rs, seal.rs |
There is no HEAD; --ref defaults to the branch .prikk/current-branch names, and branch switch refuses a dirty worktree or unsealed work for another branch. | current_branch.rs, branch_switch.rs, branch.rs, commands.rs |
commit -m’s message is stored as an identity-bearing Patch field since 0.32.0; tag create -m’s message is persisted too. | main.rs, payload/tag.rs, RFC 123 |
There is no staging area; commit --from-worktree always considers the whole worktree, filtered only by .prikkignore. | worktree_files.rs, ignore.rs |
No remote registry or network transport exists; distribution is bundle export/import/verify or sync, both file-based, landing as an untrusted remotes/<name> pointer. | bundle.rs, sync.rs, received.rs |
| Every command named on this page is a real registered command, and every registered command is named on this page. | commands.rs (COMMANDS), checked mechanically by rules (A) and (C) |
Provenance
Seeded from the external architecture audit’s 2026-08-31 feature-completeness matrix
(audit-2026-08-31-task-1a-design-functionality.md §4), re-verified against main at the time of
writing rather than copied — several rows in that matrix are now stale (worktree-status was broken
then and is fixed; .prikkignore did not exist then and does now). RFC
128
§5 requested this page. It does not cover RFC 113 (history import) or any importer tool — neither
exists yet.
Command Surface
Every command prikk accepts, in one place. This page is an inventory, not a tutorial — each command’s own behaviour, refusals, and worked examples live in the guide, and the Git→prikk mapping explains where the vocabulary differs from Git’s.
Every command accepts --help for its own usage:
prikk <command> --help
prikk init [path]
prikk key generate [--out <path>]
prikk key public [--seed-file <path>] [--role author|maintainer]
prikk key status [path] [--role author|maintainer] [--format json]
prikk setup [repo-path] [--author-seed-out <path>] [--maintainer-seed-out <path>]
prikk trust maintainer add --key-id ID --public-key HEX
prikk trust maintainer remove --key-id ID
prikk trust maintainer list [--format json]
prikk trust maintainer check --key-id ID [--format json]
prikk commit [--from-worktree] [--text-edits] [--ref heads/<branch>] -m <message>
prikk mv <old> <new>
prikk seal --allow-no-audit [--ref heads/<branch>]
prikk status [--format json]
prikk log [path] [--limit N] [--ref REF] [--format json]
prikk checkout --plan-only [path] [--ref <ref|block-id>]
prikk checkout --snapshot-plan [path] [--ref <ref|block-id>]
prikk checkout --snapshot-materialize [path] [--ref REF]
prikk checkout --patch-plan [path] [--ref <ref|block-id>] [--format json [--content-path <path>]...]
prikk checkout --patch-materialize [path] [--ref REF]
prikk checkout --patch-delete-plan [path] [--ref <ref|block-id>]
prikk checkout --patch-materialize-delete [path] [--ref REF]
prikk show <block-id|patch-id> [--format json]
prikk tree [path] [--ref <ref|block-id>] [--prefix <p>] [--format json]
prikk cat --path <p> [--ref <ref|block-id>] [--output <file> [--force]] [--max-bytes N] [--format json]
prikk extract (--ref <ref|block-id>|--claim <claim-id>) <dir> [--allow-full-replay] [--plan-only] [--max-bytes N] [--max-files N] [--format json]
prikk diff [path] [--from <ref|block-id>] [--to <ref|block-id>] [--path <p>]... [--format json]
prikk merge-evidence --baseline-block ID (--left-block ID|--left-ref REF) (--right-block ID|--right-ref REF) [path]
prikk merge-plan --baseline-block ID (--left-block ID|--left-ref REF) (--right-block ID|--right-ref REF) [path]
prikk merge --allow-no-audit --baseline-block ID --into REF --from REF [path]
prikk inverse-plan [path] [--ref REF]
prikk rollback-preview [path] [--ref REF]
prikk rollback-draft --append-inverse [path] [--ref REF] -m <message>
prikk rollback-draft-verify [path] [--ref REF]
prikk branch [list] [--all] [--format json]
prikk branch create heads/<name> [--from REF]
prikk branch close heads/<name>
prikk branch switch heads/<name>
prikk tag [list] [--format json]
prikk tag create tags/<name> --target <ref|block> [-m <message>]
prikk config get <key>
prikk config set <key> <value>
prikk config unset <key>
prikk config list
prikk bundle export --ref REF --output <file> [--force]
prikk bundle import --input <file> [--max-object-bytes N]
prikk bundle preview --input <file> [--ref REF] [--max-object-bytes N] [--format json]
prikk bundle verify --input <file> [--max-object-bytes N]
prikk archive export <file> [--format json]
prikk archive verify <file> [--format json]
prikk archive import <file> [--adopt] [--format json]
prikk sync summary --output <file>
prikk sync compare --summary <file>
prikk sync have <ref> --output <file>
prikk sync build <ref> --have <file> --output <file> [--force]
prikk sync accept <file> [--max-object-bytes N] [--claims-out <file>] [--force]
prikk sync pending
prikk sync seal <ref> --claim <id>
prikk sync seal <ref> --claims <file>
prikk sync tags
prikk sync adopt-tag <name>
prikk worktree-status [path] [--ref REF] [--format json]
prikk verify [path] [--stop-on-first-error] [--format json]
prikk doctor [path]
prikk doctor [path] --repair-wal-tail
prikk doctor [path] --repair-index
prikk doctor [path] --repair-pointer-index-tail
prikk doctor [path] --repair-tails [--plan-only]
prikk doctor [path] --repair-main-ref
prikk doctor [path] --rebuild-pointer-index [--plan-only]
prikk doctor [path] --discard-damaged-commits [--plan-only]
prikk doctor [path] --restore-queue-target --ref <ref> [--not-current-branch] [--plan-only]
prikk doctor [path] --recovery-list
prikk doctor [path] --recovery-restore <run id> [--plan-only]
prikk doctor [path] --recovery-clear [--plan-only]
prikk doctor [path] --cancel-import <id> [--plan-only]
prikk ref complete <ref> [--plan-only]
prikk format upgrade [path]
prikk unlock
prikk unlock --lock <path> [--yes|--force]
prikk compact --pointer-index|--received-index|--trust-policy|--all [--plan-only]
prikk compact --received-index|--trust-policy --keep-slot a|b [--plan-only]
prikk received remove <remotes/…> [--plan-only]
prikk --version
What --from and --target accept. branch create --from REF takes any published ref: a
branch ref names its Block directly, and a tag ref is dereferenced through its tag object, so
--from tags/v1 publishes the new branch at the Block that tag names. tag create --target takes a
block id or a branch ref, and refuses a tag ref — a tag of a tag is outside the model (ref →
tag object → block, one hop), and resolving it silently would make --target tags/v1 and --target
that tag’s own block indistinguishable in history. The refusal names both accepted forms.
Key ids.
setup(when it creates a seed) andkey generate --outwrite the new key’s id,ed25519-<first 16 hex of the public key>, beside the seed, as<role>.key-idin the key directory or<seed path>.key-id.- Every signer,
setupandkey statusresolve the id the same way:PRIKK_<ROLE>_KEY_ID, else that file, else the legacy role word. setupadopts the maintainer key under the resolved id and prints it.key statusreportskey_id_sourceasenvironment,key-fileordefault, and a key-id file that does not hold its seed’s id asreason: key-id-file-mismatch, which refuses signing.
format upgrade is explicit, verified and one-way. A repository created by 0.44.0 or earlier is
format 6; new repositories are format 7, which is format 6 plus “an object id may hold several records,
the last authoritative”. prikk format upgrade takes the writer lock, runs the same verification as
prikk verify and refuses — changing nothing — unless it would exit 0, then overwrites the one byte
of the .prikk/FORMAT marker in place and flushes it (no rename; RFC 168 §3.3, crates/prikk-store/src/format_upgrade.rs:81-88). Nothing stored is rewritten. It is idempotent on a format-7
repository, never runs by itself, and has no inverse: prikk 0.44.0 and earlier refuse a format-7
repository at open. See Release Compatibility.
Refusals about refs and repositories.
- A ref that does not exist. A command that reads an existing ref’s state refuses an absent name
before anything else with
precondition not met: ref <name> does not exist in this repository(exit1), adding a sentence whenremotes/<name>exists. That coverslogandworktree-statusgiven--ref, everycheckoutmode,inverse-plan,rollback-preview,rollback-draft,rollback-draft-verify,merge-evidence,merge-plan,merge --into/--from,bundle export,branch create --from,tag create --target,branch closeandbranch switch, which keeps its own route sentence. - Not refused:
sync have,sync buildandbundle previewanswer an absent ref as a state (“none of it”), andcommit --ref,seal --refandbranch createcreate the ref. In a fresh repository,logandworktree-statuswithout--refstill report the unpublished current branch. - A received ref given to a command that cannot use one (
checkout,inverse-plan,rollback-preview,bundle export,branch create --from,tag create --target) refuses withprecondition not met: <name> is a received ref, naming the commands that read received refs (log,merge-evidence,merge-plan,bundle preview) and the one that takes it into a local branch (merge --from). - A path with no repository refuses as a precondition naming that path (“no prikk repository at
”), decided when the repository is opened, in every command that opens one.
A point: a ref or a bare block id. checkout’s read-only modes (--plan-only, --snapshot-plan,
--patch-plan with or without --content-path, --patch-delete-plan) take --ref as a ref name or a
bare block id, 64 lowercase hex characters, resolved by one resolver.
- A block the repository holds resolves whether or not any ref reaches it: an older block on a branch, or a block only received history holds. Reading it adopts nothing.
block <id> is not in this repositoryandobject <id> is a patch, not a block(naming the type it is) refuse as preconditions, exit1. A--refvalue that is neither a ref name (heads/…,tags/…,remotes/…) nor 64 lowercase hex is a usage error, exit2.- The modes that write the worktree (
--snapshot-materialize,--patch-materialize,--patch-materialize-delete) refuse a block id: writing the worktree needs a branch, because the nextcommitauthors against one. - The prose header reads
block: <id>for a block id andref: <name>otherwise;--plan-onlyprintsref-state: <none>for a block id.patch-plan-content-v1’srefholds the value as given, now possibly a block id; its schema version is unchanged.
Reading a point: tree. tree lists the files present at a point — a ref, or a bare block id — with
each one’s mode, exact size and whether it is text or binary, by the same classification checkout --patch-plan --format json reports. It reads received refs. Without --ref it lists the current branch,
and an unpublished current branch lists nothing, exit 0; an explicit absent ref refuses. --prefix
filters by whole path components; the replay is whole-tree either way. --format json is
tree-listing-v1. See Reading a point.
Reading a point: cat. cat writes one file’s bytes at a point, text or binary, to stdout or to
--output. The content is resolved in full before anything is written, so a refusal writes nothing.
--max-bytes bounds what is written — exactly, and all or nothing — but not memory, which is the shared
replay’s own cost; --output refuses an existing file without --force and any path inside .prikk/,
and is written to a temporary sibling then renamed into place. Binary content refuses a terminal, naming
--output; text is written. A path that is not a file at the point refuses with path <p> does not exist at <point>. --format json is path-content-v1: the same fields as one tree-listing-v1 entry, and no bytes.
Writing a point into a new folder: extract. extract --ref <ref|block-id> <dir> writes every file
at a point into a new directory that does not exist yet, no repository history and no worktree state, the
same shape git archive | tar -x produces. <dir> must resolve outside this repository’s own root
(.prikk/ included) — compared by identity, never by spelling, so a relative path, a .. that lands back
inside, a symlinked parent, or a differently-cased or short-name spelling of the root on Windows all refuse
just as a plain path inside the worktree does; a path outside the repository, including a sibling
directory whose name merely starts with the repository’s own name, is unaffected. The reason is the same
one commit --from-worktree already gives for its own worktree content: a copy left inside the repository
would be picked up as worktree content by the very next commit. Unlike cat/tree, it anchors only
where a writer would:
the same admitted-anchor rule checkout’s own worktree writes use, never the weaker rule that lets cat
and tree display any snapshot that merely decodes correctly. A remotes/... ref under a key this
repository has not adopted therefore always replays fully from its own genesis; past 63 blocks with no
admitted snapshot in reach, extract refuses by name, giving the distance and --allow-full-replay to
read it anyway. Every file whose content has not changed since the admitted anchor streams straight from
its stored blob id, nothing is held resident past the one file’s own size, and a path renamed after the
anchor still streams from that same id, at its new path. The target must not already exist; a leftover
<dir>.prikk-extract-tmp from an interrupted run refuses by name rather than being reused or deleted; the
whole write is atomic (a temporary sibling, then one no-replace rename), and every file and directory is
synced before it, so a crash never leaves a target with missing or partial content. provisional: yes|no
says whether an admitted anchor was actually found and used, or the replay fell back to genesis instead.
--plan-only reports the point’s own file count, total bytes, largest file: N bytes and
provisional, and writes nothing at all — no target, no temporary sibling. --max-bytes N/--max-files N refuse the whole point, by name, before the first byte is written — learning the point’s own total
size this way costs what it costs (each file not already resident is read once to measure it), the same
cost a real write already pays; the pre-walk’s own bound (refusing a point whose nearest admitted
snapshot is unreachably far back) is the separate, cheap guard against unbounded history depth, checked
before a single Patch or Blob is read, never relaxed by these flags. --format json (extract-report-v1) carries file_count, total_bytes, largest_file_bytes (the single
largest entry’s own byte length, always present, the same number the text form’s largest file: N bytes
line gives), provisional, written (whether this call actually wrote the target), point (the ref name or
the block/claim id given, the one fixed key tree’s own tree-listing-v1/cat’s own path-content-v1
both use), point_kind ("ref", "block", or "claim"), target_block_id, and claim_parent_block_id
(always present; null unless point_kind is "claim", in which case it is the claim’s own declared
parent — the block its own patches would be sealed onto, not target_block_id, which is the block the
claim recognizes; no command printed a claim’s parent before this round) — the same hand-rolled,
schema_version-named convention those commands already use. --claim <claim-id> (mutually exclusive
with --ref) previews a pending RecognitionClaim — the same id sync seal --claim takes — as if its
own patches were sealed onto its own declared parent, without sealing anything: useful to inspect what a
claim would produce (its size, or its content) before deciding to accept it. A claim naming more than one
parent (one built from an ancestor Merge block) is refused by name — there is no field in the claim
saying which parent is mainline, so this is refused rather than guessed. --claim takes the identical
pre-walk bound as --ref/a bare block id: past the trusted distance from the claimed parent’s own nearest
admitted checkpoint, it refuses by name unless --allow-full-replay is given. extract’s own peak memory,
whole command, tracks the largest single file it actually reads (roughly twice that file’s own size, not
once — see Current State), not the point’s own total content size; this holds the
same way whether the point is named by --ref, a bare block id, or --claim — --claim’s own own
derivation step (prepare_extract_plan_for_claim) costs a little more than the ref/block-id path’s own
equivalent planning step (about 1.1–1.3x, measured), but that planning cost is small next to the one
shared, dominant cost every path pays: streaming the point’s own largest file once. See
Current State for the measured figures, both the planning-only comparison and the
whole command. See Reading a point.
refusal-v1: a machine-readable refusal document. Under --format json, every refusal extract
exits 1 for — never a usage error, exit 2, which prints no document — writes exactly one JSON document
to stdout (never stderr; stderr’s own text and the exit code are entirely unchanged by this). This is a
structural guarantee at extract’s own command boundary, not merely a property each refusal happens to
have today: a refusal reached by a path this document’s own list does not yet name still gets a document,
classified by a fallback, rather than exiting 1 under --format json with nothing on stdout.
{"schema_version": "refusal-v1", "command": "extract", "refusal": "<id>", "reason": "<id>|null", "message": "<the stderr text>"}
refusal is one of fifteen stable, kebab-case ids: over-max-bytes, over-max-files,
destination-exists, destination-inside-repository, temporary-sibling-exists, point-not-found,
claim-not-found, not-a-claim, claim-has-several-parents, claim-parent-not-sealed, case-collision,
no-checkpoint-in-reach, not-a-repository, damaged-repository, io-failure. claim-parent-not-sealed
means the claim’s own declared parent has not been sealed or received into this repository yet — seal or
receive it first, then extract the claim again; this repository is not damaged. reason is set only for
no-checkpoint-in-reach, naming which of
three conditions the admitted-anchor search found in reach — one cause at a time: the first blocker
the search meets, not necessarily the only one. Fix it and run again; the next refusal names the next
cause, if any. In priority order: signer-not-adopted (a recorded checkpoint whose signer this repository
has not adopted — adopt the key), not-recorded (a checkpoint in reach, never recorded by verify/seal
— run verify), or no-checkpoint (none at all — --allow-full-replay). Every other id carries
reason: null. message is stderr’s own text, without its leading error: prefix — the generic framing
applied to every command’s own error, never part of the refusal’s own text itself; the JSON document never
paraphrases it.
This is this project’s own, reusable form for a JSON refusal — refusal-v1 is extract’s own
document today, but its shape (schema_version, command, refusal, reason, message) is designed as
a precedent other commands adopt when next touched, not a one-off specific to extract.
Removing a received pointer: received remove. received remove <remotes/…> [--plan-only]
removes one received ref’s own pointer from the received-index container — the same container
bundle import/archive import write and cat/tree/checkout/extract read at a bare
remotes/… point. The objects it reached are not removed, only the pointer: a stored Block
or RefState nothing else reaches any more is reported by prikk verify as an unreached
object, a non-blocking warning, not a failure (see verify’s own entry above). When verify also
reports a damaged object, the walk cannot read past it: objects reached only through the damaged
one are counted as unreached, and their publication trust is not checked, so the count can include
objects that are in fact reached. verify still fails on the damage itself
(verify/reachability.rs, verify/objects.rs). --plan-only
reports what would be removed — the pointer’s own current target, or that there is nothing named —
writing nothing, but it still takes the container’s lock for the duration of the read: the same
reason compact’s own --plan-only variants do — an operator acts on what a preview reports, and
the read should not go stale against a concurrent writer before it reaches the terminal; the lock
is released once the read is done. A real run saves the removal to the recovery log
unconditionally, naming the run id so prikk doctor --recovery-restore <id> can undo it, as long
as nothing has since written to the same slot (a later, sound re-import of the identical name makes
the restore refuse, rather than silently discarding it). Refuses a name that does not start with
remotes/, a name with no received pointer, and a damaged received index, the same way compact --received-index already does.
Comparing: diff. diff shows what changed between two states. A bare prikk diff compares the current
branch’s tip (with any queued commits) against the worktree — exactly what commit would author, so commit
then diff is empty again; --from <point> compares that point against the worktree; --from and --to
together compare two points, each a ref (a received ref is read, not adopted) or a bare block id. A lone --to
is a usage error. Each differing path has one status: added, deleted, modified, renamed, mode or
binary; a new worktree file is added, with its content, and ignored paths never appear. Text changes carry
unified hunks with 3 lines of context, and a last line without a newline is marked as diff -u marks it. A
rename is reported only when history (or a live prikk mv) declares one, never inferred from similar
content. A binary file shows ids and sizes and never its bytes; prikk cat reads either side. A worktree path
commit would refuse is named under unsupported_paths with commit’s own refusal and no content. The line
diff is shortest within a work bound, above which the entry says minimal: false and its hunks, still valid,
are larger. --path (repeatable, exact) filters the output, and the work is whole-tree either way. Two states
that are the same give no differences and exit 0; diff writes nothing. An unsupported operation, or a
blob that is missing or does not recompute, in either history fails the whole call. --format json is
diff-report-v1; see Comparing.
Size bounds on incoming artifacts. RFC 158 Stage A: bundle import, bundle preview, bundle verify, sync compare, sync build and sync accept all read a file arriving from outside the
repository, and every one of them refuses before reading it when its size is over the bound —
on the open file’s own metadata, so nothing large is allocated to discover it is large, then
enforced again while it streams, so a lying declared size is still caught. Every refusal names what
was too large and its size, the bound applied (in bytes and the nearest binary unit), where that
bound came from, and how to change it; none of them says “malformed” or a bundle is “persisted”
data, since neither is true of an oversized but otherwise well-formed input.
Four size bounds (three overridable, the have-list fixed) and three count bounds exist — six variables in all (DC-86), none previously documented. The size bounds:
| bound | default | applies to | override |
|---|---|---|---|
| bundle | 256 MiB | bundle import/preview/verify | PRIKK_BUNDLE_MAX_BYTES |
| sync exchange artifact | 256 MiB | sync accept | PRIKK_EXCHANGE_MAX_BYTES |
| sync summary | 16 MiB | sync compare | PRIKK_SYNC_SUMMARY_MAX_BYTES |
| have-list | 64 MiB | sync build --have | fixed — no override exists |
A bundle and a sync exchange artifact also each declare a count of objects they carry (bundles) or
of patches/blobs/author keys/claims (exchange artifacts), bounded by PRIKK_BUNDLE_MAX_OBJECTS and
PRIKK_EXCHANGE_MAX_OBJECTS respectively (default 100,000 each), and the sync summary’s ref count
by PRIKK_SYNC_SUMMARY_MAX_REFS (default 100,000).
The per-object bound applies to bundle import, bundle preview, bundle verify and sync accept — everything that decodes objects from outside, never to a repository’s own commit, which
is not bounded by default. It measures an object’s encoded size as it travels — its content plus
a header — checked on the length prefix before the object’s bytes are copied or decoded. So a
file of exactly the bound you set refuses: the header adds 69 bytes (measured, for text and
binary files alike, and pinned by a test), so a file of N bytes needs a bound of at least N + 69.
The refusal names the size it saw, which is that N + 69. Default: 256 MiB, the same as the total
bundle/exchange bound, so it is inert at the defaults — it only acts once an operator lowers it.
Resolved highest-precedence first: --max-object-bytes N on the command itself (on bundle import,
bundle preview, bundle verify, and sync accept), else the repository’s own
incoming.max-object-bytes in prikk config (not available to bundle verify, which opens no
repository), else the default. The bound is never taken from the input it bounds — nothing inside a
bundle or exchange artifact can raise it, and nothing an import or accept writes can reach
.prikk/config.
What is still true for bundle verify and bundle preview, and for bundle import against a
non-seekable input: an artifact under its bound is still read whole into memory, and decoding
costs a multiple of that — measured on a bundle carrying one 255 MiB blob, at the defaults, three
samples each (before bundle import streamed a regular file): bundle verify peaks at a median of
787,400 KiB (≈3.0× the bundle’s own size), bundle import at 1,570,984 KiB (≈6.0×). A pipe or FIFO
input is read into memory in full, the same as this — a non-seekable input cannot be read twice, so
it must be buffered before anything else happens.
bundle import against a regular file is different: it stays an open file, read and re-read in
place rather than buffered, so its own memory follows the largest single object the bundle carries,
not the whole bundle. bundle verify/bundle preview, and bundle import fed a pipe or FIFO, are
unchanged and still carry the multiplier above.
prikk config is a repository’s own small, durable settings file at .prikk/config — never in
the worktree, so a checked-out or imported file can never set one. One key = value per line; blank
lines and # comments are allowed. An unknown key, a duplicate key, or an invalid value (zero,
negative, non-integer) each refuse, naming the line, never a silent fall back to the default. The
only key today is incoming.max-object-bytes. get/list print the effective value and its source
((default) or `.prikk/config`); set validates then writes atomically; unset restores the
default. set rewrites the whole file and unset removes it, so a comment you wrote by hand — which
the format allows — is dropped without a word: keep notes somewhere else. An older prikk build simply does not read this file — it has no effect on that build’s
own behavior, and no error results from a repository holding one.
Exit codes. 0 — the operation succeeded and did what was asked. 1 — operational failure:
verification findings, an integrity failure, a refusal, a dirty worktree. 2 — usage error: an
unknown argument, a missing required flag, a duplicate flag. Graded verification results are in
prikk verify --format json, not in the exit code.
Current State
What prikk can do today, what it cannot do yet, and the limits worth knowing before you rely on it. This page describes deferrals — things not built yet. Permanent refusals, which are a different thing, are in Non-Goals.
What works today
The local core can initialize a repository, author signed patches, seal them into blocks, inspect history, verify integrity, diagnose common repository issues, perform safe checkout planning and materialization for the supported subset, display merge evidence and merge plans for explicit sealed candidates, and execute a merge when the two sides are proven confluent — refusing cleanly, with no object, WAL, or ref write, when they are not.
Cross-platform history identity is tested, not assumed. Prikk authors, commits, and checks out on Linux, macOS, and Windows, and CI requires a repository authored on Linux, mutated on Windows, and verified back on Linux to produce byte-identical object ids — so the claim that anyone can verify anyone’s history holds across the three.
Known limits worth stating up front: merge-base discovery is manual; conflicts are detected and refused
but never resolved; sync exists between repositories, but prikk does not move the bytes itself —
confidentiality is the user’s channel’s property, not prikk’s — negotiation is branch-scoped (tags
travel and are adopted separately, under the receiver’s own key), and there is no discovery or
remote-tracking; verify cost is linear in history length; verify checks author signatures
repository-wide, but only as trust-on-first-use continuity — it proves the same author signed as last
time, not who that author is on first contact; and verify checks a locally-published tag’s
maintainer signature against this repository’s own trust policy, but a received, not-yet-adopted tag
is deliberately exempt — its signature is the sender’s, under a key this repository has not adopted.
Next increment candidates are tracked in ROADMAP.md.
Not a good fit yet
Prikk is not yet the right tool if you need:
- a production replacement for Git;
- stable repository-format compatibility;
- Git object compatibility or transparent Git interoperability;
- hosted forge workflows, or remotes;
- complete branch management, or semantic merge;
- plugin/audit execution, attestations, or automated publication controls;
- mature key lifecycle features such as revocation, rotation, hardware signing, or thresholds;
- flexible exclusion of generated files —
.prikkignore(since 0.29.0) takes literal repo-relative path prefixes, one per line, with no globbing, no negation, and no per-directory files, so patterns like*.logdo not work; and a file swept into history by mistake still cannot be removed later.
Known limitations, measured
Disclosed here rather than left implicit, each with the figure it was measured at and the release it is planned for.
-
Trust keys, trust policy, author keys, the received index, and the three generation logs now have a tail defined by position, a
verify/doctorline, and a repair (fixed in 0.49.0, RFC 164 Rules A/B/C, then corrected by §9). Before this round (N2’s remainder, N7, N10, M4): a torn prefix at these seven files was a repairable-looking tail, but 100 or more zero or random bytes at the same position was damage under their own pre-0.49.0 shape rule –trust maintainer add, a commit, orbundle importrefused with “<container> has a damaged entry”, a generation log’s own reader refused every command that resolves a ref (on the pointer index’s own log) or justcompact(on the other two), and none of the seven had a repair verb – the way out was a manual backup-and-truncate. Rule A first defined a tail as “everything after the last sound record, whatever its shape”; §9 corrected this the same round, before release, once measurement showed it let a complete last record with a failed checksum be truncated as a tail – for a file whose last record carries a decision (the trust policy’s latest snapshot, a generation log’s live slot, the pointer index’s newest pointer), that silently re-asserted the previous decision. A single flipped body byte in a real trust policy snapshot, followed by the pre-§9--repair-tails, silently re-trusted a key that snapshot had just revoked,verifyexiting 0 throughout — measured on a release build before this was closed. §9 was itself incomplete: it decided “complete” from the header’s own magic, version, and length fields, and any one of those three can itself be the single corrupted byte. A flipped header field rolled every reader back to the state before the last record, before any repair even ran – §9.2 corrects this: completeness is decided by the checksum, recomputed with the format’s own real magic and version constants over either the claimed length or the length to the end of the file, whatever the stored header fields say. Verified exhaustively, not sampled: every single byte offset of a last record, across the trust keys, trust policy, author keys, the received index, the generation logs, and the pointer index, is now asserted (a cheap, decode-only test per format) to never decode as a tail. Now: a tail is an incomplete record or bytes that are not a record header, when nothing sound follows; a complete record (its checksum verifying one of the two ways above) is damage, even when last, however its header got corrupted.verifyanddoctorreport a tail as a warning (never failing on it alone) and interior damage (including a complete, corrupted last record) as a failure;prikk doctor --repair-tailstruncates every genuine tail these seven files (plus the WAL and the pointer index) have, in one run, saving what it removes first, and refuses – unconditionally, changing nothing – if any covered file has interior damage, including a complete corrupted record. What remains: interior damage still has no automatic repair – by design, the same as every other covered file – so the way out for it is still manual (back the file up first, ask before truncating; a complete corrupted record has no offset a repair can safely act on at all). Corruption spanning more than one field of the last record (for example a zeroed sector, touching the checksum and other fields together) can still read as a tail: the checksum-decides rule above only resolves a single field’s own corruption against an otherwise-intact record. Telling that shape apart reliably needs a per-record witness (format 8), not yet built. The object containers’ own short tails (N7) are now reported too (verify/doctorprint a trailing-partial byte count per persisted object type), reporting only, no repair, per the review’s ruling. The ref log’s own tail (M4) and N3’s interrupted publications are now settled (RFC 165, 0.49.0): the ref log is Rule B’s eighth reported file and--repair-tails’s tenth covered one (§9.2 applied to it the same as the other seven); a torn last record with a completable pointer lead is an interrupted publication, completed byprikk ref complete <ref>(any adopted key, not only the one that started it), never truncated as a tail. Disclosure, released versions (no advisory, per the owner’s ruling); code history kept apart from measurement: the code shipped in 0.20.0 for all three files (trust policy2827fab7, the pointer index0550e340, the three generation logsb33d1942), confirmed from history, not measured directly. Measured on 0.46.0, 0.47.0, and 0.48.0 (0.20.0-0.45.0 not independently confirmed), with one reader per file –trust maintainer checkfor the trust policy,branch listfor the pointer index and its generation log, not every reader (round 2’s own exhaustive, all-reader sweep is against the fixed 0.49.0 build, not these historical releases): a length-field flip in the trust policy’s last snapshot read straight through to the previous one on all three measured versions,verifyexiting 0 throughout (0.46.0’s and 0.47.0’s ownverifydoes not read the trust files at all; 0.48.0’s reads them but still exits 0 for this shape). A length-field flip in the pointer index’s or its generation log’s own last record rolledbranch listback to the previous tip or slot on 0.46.0 and 0.47.0,verifyexiting 1; on 0.48.0 the generation log refuses instead (its own one-byte-body rule rejects any length but exactly one), but every byte, header or body, flipped in the pointer index’s own last record rollsbranch listback with no repair involved, and a body-byte flip (checksum mismatch) is separately truncatable bydoctor --repair-pointer-index-tailas a tail, reverting the ref’s tip whileverifystays failed both before and after. A flipped magic or version byte is refused on every measured released version – that rollback shape existed only in this release’s own, unreleased Rule A, never shipped. All fixed in 0.49.0 by §9 and §9.2. -
A
bundle importinterrupted by a crash could leave a block durable while the patch, blob, or parent block it names is not, and no repair cleared it (fixed in 0.49.0). In 0.48.0 and earlier, objects were written in the bundle’s own carried order, not in dependency order – across kinds, and, within one kind, a child block could be listed before its own parent. An interruption partway through could leaveverifyfailing for good with one of: “object <id> references missing snapshot blob <id>” / “… missing block patch <id>” / “… missing parent block <id>”; “snapshot of Block <id> names Blob <id> for <path>, which is missing”; or “lifecycle replay: blob <id> required for a state effect is missing” – while a commit was still accepted. None of the threedoctorrepairs cleared it – re-running the same import cleared it in every case reproduced (25 of 25). Reproduced at 89 of 300 kills on 0.48.0 and 136 of 300 on 0.47.0, with an 80-file bundle; present sincebundle importwas first introduced (0.20.0). Fixed in 0.49.0: every writer that lays down more than one object in a single command –bundle import,sync accept– now writes them in dependency order, both across and within kinds, so a new interrupted write can no longer produce this shape. What remains, after RFC 164 Rule E (0.49.0):verifystill needs the original bundle to make such an object whole, but no longer fails forever over one nothing in the repository actually needs.verify/doctornow classify a stored object’s own dangling reference by reachability from committed state (a ref, a received pointer, a queued patch, or a sealed block reached from them): reachable, it is still the same hard failure named above; unreachable, it is an unreferenced remnant – a warning, naming the object and what it lacks, andverifyexits0. No command removes a remnant in 0.49.0; re-running the samebundle importis still the only way to make a still-needed one whole, exactly as before this round. -
Existence checks on RefState, Tag and Attestation references (0.49.0 step 5, round 2; the Attestation half 0.50.0 step 2 Part B).
verifyrequires each attestation a RefState’srequired_attestation_idsnames to be present as an Attestation object (a typed read), and now also decodes it (AttestationPayload::decode_canonical, added this round) and requires its owntarget_block_idto be present too.bundle importrefuses before any write on either gap: an absent attestation, or one present but naming an absent target block. No producer in this repository writes a non-emptyrequired_attestation_idslist, so an honest repository cannot fail either check. Still disclosed as a gap: a received ref’sprevious_ref_state_idis not required bybundle import.verifydoes check a received tip’s previous state (one read), andbundle importrefuses a bundle whose ref chain is not carried. -
A crash inside
branch createortag createnow has a command that completes it (N3, fixed in 0.49.0). Before this round: the ref log’s last record is torn;verifyfails withPRIKK-VERIFY-REF-DIVERGENCEanddoctorrecommends manual recovery, but retrying the samebranch create/tag createanswers “already exists” rather than finishing the interrupted publication – DC-38’s own retry exists forsealonly. Reached throughmergetoo: a crash mid-publication leaves the samePRIKK-VERIFY-REF-DIVERGENCEstate, and re-running the same merge answers “not confluent” rather than finishing it. Measured formerge: 10 of 300 kills on 0.48.0, 16 of 300 on the 0.49.0 build carrying the dependency-order fix above. Fixed in 0.49.0 (R1-R3): every publication (seal,branch create,branch close,tag create,sync adopt-tag,merge) now refuses before its first write while another ref’s publication is incomplete – the same refusalcommitalready gave – so aseal(or any other publication) can no longer bury an unrelated ref’s torn record behind its own, appended content. A publication’s own retry of its own interrupted state is never blocked by this (seal’s DC-38 retry still completes). Fixed in 0.49.0 step 3 (R4-R6):prikk ref complete <ref>completes an interruptedbranch create/branch close/tag create/sync adopt-tag/mergepublication the same wayseal’s own DC-38 retry always completed its own – by any adopted maintainer key, not only the one that started it. Every condition evaluated before any write (the leadingRefStateverifies under current trust, chains cleanly, names a target that exists, and, for a WAL-consuming publication, retained WAL evidence still matches); a lead that fails is left alone (not completed), and the entry point’s own “already exists”/“not confluent” refusal now namesprikk ref complete <ref>when that is why.sync sealremains locked out of its own interrupted publication: its own precondition cannot yet tell “my own retry” apart from “another ref’s incomplete work,” so it refuses both – a known, disclosed gap, not worked around in this round. A lead that fails R4’s rule entirely (an untrusted or revoked signer, a broken chain, a missing or wrong-kind target, or mismatched WAL evidence) has no automatic completion;prikk doctor --rebuild-pointer-indexre-derives the pointer index from the ref log instead, dropping that one lead (named) while leaving every other ref untouched – it refuses outright if any lead anywhere is completable, since completing it is the correct fix and a rebuild would otherwise discard an authorized transition. Addendum 1: a ref-log tail with no pointer lead (zeros, random bytes, or any torn prefix left for a reason unrelated to a pending write) is not an incomplete publication –commitand every other writer that does not append to the ref log proceed over it; every publication still refuses over it (RFC 164 Rule D: a writer refuses over a tail in a file it appends to), naming the tail’s own offset and byte count; its repair isprikk doctor --repair-tails, the same as the other nine files it covers. -
Fixed in 0.49.0 (N6): a damaged last WAL record is no longer removed uncritically as a tail. RFC 162 rule 3 used to define the WAL’s tail by position, not shape: a last record whose own bytes are all present but whose checksum fails was indistinguishable, once nothing sound followed it, from a genuine crash-torn append – so
--repair-wal-tailremoved it either way, even when it was a commit the user had already been told succeeded. A commit now writes a small local witness alongside the WAL (the count, Patch id and frame hash of the last record it acknowledged);verify,statusand every writer read it before a tail speaks. A genuine, never-acknowledged crash tail is still removed exactly as before (--repair-wal-tail,--repair-tails). An acknowledged commit that is now damaged or altogether missing refuses instead (verifyexits 1;commit/seal/rollback-draftrefuse, naming it) –prikk doctor --discard-damaged-commits [--plan-only]is its own way out, never folded into the tail repair’s single meaning. -
Fixed in 0.49.0: a crash during a commit after the first one in a session could leave
ref-nameempty while the queue still held records (0.20.0-0.48.0). Every commit, not only the first, used to rewrite this small file by a durable truncate then a durable append; a crash between the two left nothing durable naming which ref owned the queue. The file is now written once per session, by the first commit only – a later commit never touches it again, so no commit past the first can tear it.verifyanddoctorexit 1, andcommit/seal/rollback-draftrefuse, over any session still stranded from before this fix (or over the same ref-name-vs-witness disagreement by a different route).prikk doctor --restore-queue-target --ref <ref> [--plan-only]is the way out – the ref always comes from the caller, never from the witness; see the troubleshooting entry for the exact text. -
Fixed in 0.49.0 (R1): a ref publication now reads the whole ref log once, not three times.
classify_state’s own replay is threaded through the append’s idempotency check and the post-write agreement check (a ranged read-back of just the bytes appended, not a fourth whole read). Measured at generations 4, 64 and 1,024: 3 whole reads before, 1 after, at every depth; about 1.8× faster at depth 1,024 on a release test binary. What remains: the write-path precondition (M8, below) used to share this same three-reads shape but is now fixed separately; reconciling this round’s own ~417 B/generation figure against an earlier ~4.3 KB/generation measurement (likely counting more than the ref log alone) was not settled. -
Fixed in 0.49.0 (R2): a commit’s own write-path precondition no longer checks every ref’s whole history.
ensure_no_incomplete_publicationused to run a fullverify_refs, replaying the ref log once per ref – a commit touches no ref, yet paid refs × log size. Now one pass over the pointer index and one over the ref log, comparing each ref’s newest pointer with its newest log record. Measured (release build): 62 µs at 1 ref before and after is noise-level, but 6.3 ms → 108 µs at 100 refs, 74 ms → 417 µs at 400, and 6.8 s → 4.4 ms at 4,000.verify/doctorkeep the fuller check (chain continuity, signature-envelope structure, missing-object detection) this precondition never needed. -
Listing objects by type reads that type’s whole container, including on
sync seal’s own path. Deferred to 0.50.0 or later (ROADMAP.md, the 0.49.0 schedule). -
Fixed in 0.53.0 (F4): a one-file
commitno longer reads every stored blob to learn its kind, exactly at a checkpoint; between checkpoints it follows the content added since the nearest one. Before 0.53.0, a cold baseline — a branch switch, a step the cache’s own one-block fold cannot take (two seals since the last resolution, or a merge block), a missing or damagedlifecycle-state.v1specifically, or every 64th resolution (the forced reanchor) — replayed the whole lineage, reading every historical blob’s frame to re-derive its kind: 1 MiB of stored content read 1.1 MB, 16 MiB read 16.8 MB, 64 MiB read 67.1 MB. The cold baseline now derives from the nearest checkpoint this repository has itself sealed or verified under an adopted maintainer key (RFC 159’s anchor-trust function, the same oneseal/merge/sync sealalready use), and folds only the blocks after it (at most 63). Measured, one 1 MiB file rewritten and sealed per generation, a cold commit after each generation shown (candidate against 0.52.0, release build, time in ms / peak RSS in KiB): generation 1, 16/11,888 vs 18/11,696; 32, 81/12,104 vs 80/12,380; 63, 144/11,844 vs 144/11,888; 64, 146/11,888 vs 146/12,160; 65 (a new checkpoint), 14/11,876 vs 149/12,040; 70, 26/11,888 vs 157/11,984. Both binaries pay the same cost up to the next checkpoint; the candidate drops once a new one exists, 0.52.0 never does. With no checkpoint in reach at all (a repository that has only ever received history, before its first localsealorverify), the full replay still runs, unchanged. A restore, a fresh clone, or deleting the whole.prikk/cache/directory also removes the checkpoint record (.prikk/cache/replay-verified-blocks.v1— a different file from the incremental cache this fix targets), so a cold commit there is in exactly this no-checkpoint case: it costs what it did in 0.52.0 until the nextprikk verifyorsealrebuilds the record. Measured: 32 ms at 16 MiB stored on the commit right after deleting.prikk/cache/, 1 ms on the commit right after averifyrebuilds the record.verifyrecords the checkpoint whether or not it exits 0: the write (verified_blocks::record_verified_blocks, called whileverify_repository_with_optionsstill builds its own report) and the exit-code decision (verify_verdict’s own publication-trust check, read only afterward) are two separate steps — a failing trust check cannot prevent a write that already happened. Measured: a received ref signed by a key this repository had not adopted (bundle import, never followed bytrust maintainer add) made a release build’sprikk verifyexit 1 for “repository has publication-trust issues”; with the record file deleted first, it reappeared afterward regardless, 112 bytes, identical to a run before the deletion. The every-64th forced derivation is counted in the cache file itself, so this bound holds only while a cache file is present. The residual, extending RFC 159 ruling C1 fromsealtocommit: a state root (and a checkpoint’s own manifest) commits to a file’s blob id, never to its bytes, so neithersealnor a cold, anchoredcommitre-reads the content of a blob no live file names. Measured, below the checkpoint: a commit that edits the damaged file as text still refuses (it reads the old content to apply the edit); a commit that replaces it, appends to it (binary), deletes it, renames it (a declared move never reads old content either), or touches a different file entirely all succeed.verifystill reports the damage in every succeeding case. With no admitted checkpoint, every one of these still refuses, unchanged. -
Fixed in 0.49.0 (RFC 165, M4):
verifyreports garbage bytes in the ref log. Before 0.49.0, 100 zero bytes appended to the ref log container:verifyanddoctorexit 0 with no warning, asealappends behind them, andverifystayed 0 with the garbage in the middle of the file. The state was harmless (ref-log records are signed and chained), but nothing reported the bytes. The ref log is now a reported file, and--repair-tailscovers it. -
Fixed in 0.49.0 (RFC 167): resynchronisation over hostile content is linear, not quadratic.
verifyover a WAL torn tail packed with fake frame headers used to grow from 0.24 s at 256 KiB to 13.98 s at 2 MiB, extrapolating to hours at 64 MiB (a plain, unpacked tail stayed 0.01–0.02 s at every size, so the cost was specific to the hostile shape). Six readers shared the defect: the WAL, object containers, trust policy, the received index, the ref log container and the pointer index. Each now carries a work budget (bytes hashed, at most 8x the input) shared by every candidate the resynchronisation scan visits; a scan the budget cuts short is always reported as damage, never silently read as a tail. Measured at 64 MiB: 0.25–0.29 s, down from a projected 2.8–4.6 hours. Not a self-vouching header (a header-only checksum, which would remove the re-parse entirely): that is a format change, format-8 input, still planned for a future increment, not this one. -
A
commitholds all new file content in memory at once. 64 files of 4 MiB each peaks at about 284 MiB. Streaming and chunking (large objects, Stages B–D) are not yet built. -
The object index’s own lookup is a linear scan, and the whole index is held resident in memory for the duration of a write session. Not yet fixed.
-
bundle importpeaks at about 4.6 times the bundle’s size in memory (5.26 GiB for a 1.15 GiB bundle), from whole-file and whole-object buffering. Found during RFC 155’s design round, outside that RFC’s own scope; a candidate for a later release.prikk archive importdoes not share this defect: it processes one object at a time, so its own peak RSS is bounded by the single largest object the archive carries, not by the archive’s own total size — measured at about 5 times the largest object’s own size (an earlier version held a whole object type’s own batch at once, measured at 2.25 times the archive’s own size for 1 GiB concentrated in one type; fixed before 0.51.0 shipped). -
prikk extract’s own peak memory tracks the largest single file it actually reads, not the point’s own total content size (0.54.0): every file whose content has not changed since the admitted anchor streams straight from its stored blob id, one at a time, and only a text file edited since the anchor (at most 63 blocks of edits) is held resident. Measured against the real, shipped implementation (release build, fresh process per sample,/proc/self/status’s ownVmHWM): a 64-file, ~62.6 MiB mixed text/binary point sitting on a checkpoint (no file larger than about 1.6 MiB) peaks at about 4.5 MiB over the process floor; a since-checkpoint point whose largest single file is a 24 MiB binary (16 MiB more added across 32 further generations) peaks at about 50 MiB; a 30-generation, checkpoint-free lineage whose largest single file is about 1 MiB peaks at about 3 MiB. The real figure is closer to twice the largest single file’s own size, not once: decoding any Blob’s canonical payload (BlobPayload::decode_canonical) copies its content into a freshVecrather than reusing the envelope’s own already-read bytes, so the envelope and the decoded content are both resident for a moment. This is why the since-checkpoint point’s own 24 MiB file dominates its own measurement (about 2x its own size, not the point’s own 24.5 MiB total); a damaged blob anywhere at the point still refuses the whole extraction, the same all-or-nothing guarantee every other patch-replay reader gives. -
--plan-onlycosts the same as a real write, not less (0.54.0): it reads every file’s content once to measure it (plan_summary,crates/prikk-cli/src/extract.rs), the identical read a real write already pays, and only skips the write itself. Measured: on the since-checkpoint shape below (a 24 MiB file, a separategrowing.binreaching ~1.7 MiB across 32 further generations), a wholeextract --refrun peaks at about 55,250 KiB (ru_maxrss, two samples: 55,400 / 55,100 KiB) andextract --ref --plan-onlypeaks at about 54,994 KiB (55,052 / 54,936 KiB) — functionally the same cost, over a process floor (prikk log --limit 1) of about 12,376 KiB (12,368 / 12,384 KiB). This matters for planeter’s own size report: asking only for the size is not a cheap operation here, and the text must not imply otherwise. -
--claim’s own whole-command peak memory follows the identical “roughly twice the largest file read” rule as the ref/block-id path — there is no anomaly (0.54.0). An earlier measurement compared--claim’s own planning phase alone (prepare_extract_plan_for_claim, which derives state throughderive_next_state_for_candidate— the identical mechanismseal/merge/seal-from-acceptedalready share, notpatch_replay::extract’s own block-chain replay) against a page that otherwise describes the whole command’s own cost, which reads as one property when it was two: a first draft of that planning-only figure also had a fixture-construction mistake (32 distinct new files instead of one file repeatedly replaced), caught and corrected before it reached the architect — see the review record for the full account. The planning phase alone, corrected, costs little (release build, fresh process per sample,/proc/self/status’s ownVmHWM, process-floor baseline subtracted, ~7.0 MiB): on a parent shaped like the figure above (64 files, ~64 MiB, sitting on a checkpoint), ref/block-id peaks about 2.2 MiB over the floor during planning alone,--claimabout 2.9 MiB; on the since-checkpoint shape, ref/block-id peaks about 6.9 MiB over the floor during planning alone,--claimabout 7.5 MiB — both about 1.1–1.3x, not a scaling anomaly (a plausible, bounded reason:StoreBackedResolver::blob_kind,lifecycle_cache/store_resolvers.rs:80-94, decodes a Blob’s entire payload just to read one field, once perCreateFile/ReplaceBinarythe derivation folds forward, and the claim’s own one extraCreateFilepays this once more than the ref/block-id path’s own equivalent planning step does). The whole command, measured end to end (a realextract --claim <id> <dir>subprocess, on the identical since-checkpoint repository the ref/block-id figures above were measured against,ru_maxrss, two samples: 54,200 / 54,212 KiB): about 54,206 KiB, against the wholeextract --refrun’s own 55,250 KiB over the identical ~12,376 KiB floor — the same order of magnitude, not a multiple of it, because both commands are dominated by the same final step: streaming the repository’s own largest file (here, the untouched 24 MiBgenesis.bin) once, through the identicalBlobPayload::decode_canonicaldouble-buffer named above.--claim’s own small planning-phase overhead is real but negligible next to that one shared, dominant cost. Nothing here needs further investigation before 0.54.0. -
Fixed in 0.52.0: in 0.51.0, an archive of a repository that has accepted a sync artifact (
sync accept), or where a text file was edited, sealed, and later deleted, failed its ownarchive verifyand did not import — even though the source repository’s ownprikk verifypassed cleanly. The archive’s own closure check required every reference to resolve, more strictly than the store itself does for an accepted-but-unsealed claim’s own references or a sealed patch’s blob references. Both failures were fail-closed: nothing was written in either case, and the source repository itself stayed untouched. The archive’s own closure pass now shares the store’s own existence rules directly, so it checks exactly what, and exactly where, the store itself checks — a 0.51.0-written archive of either shape imports cleanly under 0.52.0, provided its own source repository verified clean. -
A repository holding any file over 256 MiB cannot be archived or bundled (the bound
archive import/bundle importalready enforce on the way back in,DEFAULT_BUNDLE_MAX_OBJECT_BYTES).archive exportrefuses up front for any over-bound object anywhere in the repository;bundle exportrefuses up front for any over-bound object the one exported ref reaches. Before 0.52.0,archive exportwrote such a file successfully, and only a laterarchive verify/archive importrefused it — fixed alongside the closure defect above. A copy of this repository’s own.prikk/directory is the real backup for a repository in this state. -
Content imported under a key this repository does not adopt (
bundle import,archive import) is stored, andprikk verifyreports its publication trust until that key is adopted. Replacing a received pointer with a later, sound import of the same ref moves the pointer, but does not remove the objects the earlier import wrote — there is no command that removes stored objects. A copy of this repository’s own.prikk/directory, taken before the import, is the way back. 0.54.0 (RFC 164 Rule E) changes what happens once the pointer has been replaced: a storedBlockorRefStatethat nothing in committed state reaches any more (the same reachability RFC 164 Rule E already computes for a dangling reference, applied here to the object itself) is reported as an unreached object, a non-blocking warning, and no longer failsverify’s publication-trust check. Whenverifyalso reports a damaged object, the walk cannot read past it: objects reached only through the damaged one are counted as unreached, and their publication trust is not checked, so the count can include objects that are in fact reached.verifystill fails on the damage itself (verify/reachability.rs,verify/objects.rs). While the received pointer is still present, everything it reaches stays reached, andverifykeeps failing on it exactly as before — this changes nothing for an import whose pointer has not yet been superseded. The bytes stay either way; no command removes them yet. 0.54.0 (received remove) adds a second way the pointer itself can go, on purpose:prikk received remove <remotes/…>removes the pointer directly, the same consequence as replacing it (everything it alone reached becomes an unreached-object warning on the nextverify), without waiting for a later, sound import to happen to supersede it. The objects themselves are still not removed by this or any command. -
A WAL whose damage exhausts the resynchronisation scan budget has no repair command (RFC 167).
prikk doctorreports the failed record and recommends preserving the repository and inspecting it; no command in 0.49.0 removes the damage, so the way out is a backup or a clone. (Reproduced with the hostile M5 repository at 256 KiB:doctorexits 1 with this recommendation.) -
Fixed in 0.53.0 (M6): a commit reads the queue three times, not four. Before 0.53.0, every commit read the active WAL in full, under one held lock, four times: the publication-cleanup check (is a prior
seal’s own drain of this queue still incomplete?), the authoring read (the tail/damage check, the active-queue limit, the commit-witness classification, and the baseline fold over already-queued patches, all off one pass), the pre-append check immediately before the durable write (the tail is clean, no record is damaged anywhere in the queue, the next sequence number, an idempotent retry), and a fourth, redundantreplay()that ran no check of its own – it existed only to fold the commit witness’s own running hash from bytes the second read had already decoded. That fourth read is gone: the witness now folds over the records the pre-append check already holds. The three reads that remain each still do real work, and the cost stays linear in the queue’s own length on purpose, so that a damaged queued commit is always noticed before more work is stacked on top of it. Measured at 100 / 800 / 3,200 queued commits: 207,555 / 1,610,968 / 6,422,700 bytes before, 157,046 / 1,209,605 / 4,818,404 after – about 25% fewer bytes at every depth. The external review’s own 6.2 ms at 800 / 4.0 ms at 100 (0.48.0) is unaffected: those figures were never doubled by the redundant read removed here, since the external review predates RFC 166’s own witness fold entirely. Source: external review 014,014-review.md:175-179. -
One damaged blob makes every file at that point unreadable (M7). The replay is whole-tree, so
prikk cat --pathfails for the intact files as well, and there is no salvage read that returns the sound files and names the damaged one. Deferred to 0.50.0 or later (ROADMAP.md, the 0.49.0 schedule). Source: external review 014,014-review.md:208-213. -
Three Windows residuals of the recovery log (RFC 168 §6, accepted). Each is a new-name or rename case that Windows has no primitive for, so none is closed in 0.49.0:
- (a) worktree files after a completed
branch switchorcheckoutmay not be durable after a power loss. The route: move the filesprikk worktree-statuslists as modified out of the way, thenprikk checkout --patch-materialize --ref <current branch>. Missing files are written by that checkout; a file the user deleted and the checkout writes back is removed by the user. - (b) a repair’s save in the same Windows boot as the log’s creation, in a repository created before 0.49.0. The log is created by the first write command, so this is rare.
- (c) the witness’s first creation, in a repository created before 0.49.0. A lost creation falls back to the older classification (RFC 166 rule 3), the one a session written by an older binary gets today.
- (a) worktree files after a completed
What scale to expect
The list above is about missing features. Scale is a separate question, and worth stating on its
own: what happens as a repository’s history gets deep, or its tracked file count gets large. Every
figure below was measured with a release build (the build you install) on Linux, on the
development state that shipped as 0.47.0 (a build that reported 0.46.0, plus the changes the CHANGELOG lists under 0.47.0), on
the RFC 139 corpus profile prikk-self (a real project’s shape; 155 tracked files at depth 256). Each figure names the depth range it rests on. Depth is the number
of sealed blocks.
-
Sealing cost grows roughly quadratically with history depth, and per-seal cost is worse than linear.
sealderives the next state root by walking the ancestor lineage, so each seal costs more the deeper the history is. Three independent builds, each grown to depth 1,024 (the deepest measured), median of three, mean over the 16 blocks ending at each depth:depth one sealcumulative time to build to it 32 0.09 s 4 s 64 0.18 s 11 s 128 0.30 s 30 s 256 0.64 s 98 s 512 1.4 s 6.4 min 1,024 5.0 s 34 min The per-seal exponent is 1.2 over depth 32–1,024 (1.15–1.24 across the three builds) and rises with depth: it is about 1 up to 256, and 1.6–1.8 between 512 and 1,024. The cumulative exponent is 2.0 over depth 128–1,024 (1.9 over 64–1,024). Quadratic is the release build’s shape too, but only from about depth 128 on: over depth 32–128, the only range a debug build measured, the cumulative exponent is 1.4 in release against 1.96 in debug (the cause was not investigated). A release seal is 15–33 times cheaper than a debug one at the same depth (0.30 s against 9.9 s at depth 128). Every 64th seal also writes a checkpoint and costs more than an ordinary one: about twice as much near depth 128 (the seal of block 129 took 0.61 s against 0.31–0.33 s for blocks 128 and 130), less at depth, because the lineage walk dominates both (1.6× at block 513, 1.4× at 961). The per-seal figures above are means over windows that end at every 64th block and so never include one of these; the cumulative column does include them. This is the figure that decides how long it takes to build, or import, a deep history, and it has been measured only to depth 1,024; the build to 2,048 was not attempted (the 2-hour rule stopped it, its build having been projected at 2.3 hours). Nothing here is projected past 1,024. 0.48.0 changes this.
seal,mergeandsync sealstart from the nearest checkpoint that an adopted maintainer signed and fold at most 63 blocks instead of walking the lineage. Release build, alternating with 0.47.0’s, three samples each (a shared machine, 1-minute load 1.0–2.8 at each step’s start), medians:0.47.0 0.48.0 one sealat depth 256 (an ordinary block)0.64 s 0.07 s one sealat depth 1,024 (an ordinary block)4.5 s 0.12 s the last ordinary block before a checkpoint, depth 1,024 4.3 s 0.46 s the sealthat writes a checkpoint, depth 1,0246.1 s 1.8 s sync seal --claimsof a 64-block catch-up, depth 1,024254–280 s 17.9–18.5 s cumulative time to build to depth 1,024 (three builds) 1,947 s 279 s It is not flat. A 0.48.0 build to depth 2,048 (three builds; 0.47.0 was not built that deep) averaged 1.7 s per ordinary seal over its last 16 blocks and 1,308 s cumulative: the fold and the checkpoint write grow with the size of the tree, not with depth. A checkpoint that no adopted maintainer signed, or that lies more than 63 blocks back, is not used, and the command does the full walk, as 0.47.0 does.
-
Sealing also needs memory that grows with the square of depth. One
seal’s peak resident memory, measured withgetrusageon the 0.47.0 release build: about 124 MiB at depth 256 and about 1.8 GiB at depth 1,024 (1,874,000 KiB sealing block 1,025, three samples).sealkeeps a copy of the tree’s state for every block of the lineage it walks. At depths past a few thousand blocks, memory rather than time is the first limit. In 0.48.0 it does not: onesealat depth 1,024 peaks at about 30 MiB (29–30 MiB over ordinary blocks, 53 MiB for the block that writes a checkpoint; three samples each), against 1.8 GiB, and a 64-blocksync sealcatch-up at that depth peaks at 56–57 MiB against 2.0 GiB. -
Checkout is close to linear in depth;
merge-evidenceis a little worse, and both are cheap at these depths. From depth 32 to 256:checkout --patch-plan36 → 152 ms andcheckout --patch-materialize125 → 591 ms (exponents 0.70 and 0.77; the tree itself grew as depth0.84, 29 → 155 files), andmerge-evidence34 → 455 ms when the two sides only add files (exponent 1.28; the baseline’s replay is all it does). When each side edits files that were edited before,merge-evidencecosts 69 → 473 ms (0.97). Peak memory at depth 256 is about 11 MB for all of them (polledVmHWM). Cost tracks history depth, not repository size, but at these depths depth is cheap: acommitthat has to replay the whole history (no cache) measured 69 ms at depth 33 and 494 ms at depth 256 (exponent 0.96). The depth1.45 figures this page used to give were measured before 0.43.0 (checkout was not yet anchored), at checkpoints that a divergence had shifted, in a debug build. A debug build measured today gives nearly the same shapes as release over the same depths (0.68 and 1.26), and costs 8–30 times more at each depth. -
A
committhat can use the cache costs about the same at any depth we measured. Between seals, the second and thirdcommit, and acommitafterworktree-status, take 39–44 ms at depth 256, against about 475–507 ms before 0.47.0 (they replayed the whole history each time). Acommitat a tip that edits text an earlier block edited, 487 ms → 52 ms; the samecommitediting that file, 936 ms → 53 ms. The cache’s independent full replay still happens every 64 uses. Depth range 32–256; three interleaved runs each. -
Incremental commit memory is flat up to a few thousand tracked files, then grows linearly at roughly 1.7 KiB per file beyond that — measured peak around 11.6 MiB at 100 files and 113 MiB at 64,000 (release, three samples at each of seven sizes).
-
Before 0.48.0, a command’s memory also followed the size of what the repository stored. Every object write read its whole object container into memory only to learn its length, and reading an object back read the whole container to decode one record. A
committhat added one small file to a repository with a 256 MiB blob container peaked at 266 MiB on 0.47.0; on 0.48.0 it peaks at 22 MiB, the same as with an 8 MiB container (22 MiB; release build, three samples each, container sizes 8, 64 and 256 MiB). A firstcommitof 4,000 files of 20 KB read 162 GB on 0.47.0 and reads 81 MB on 0.48.0 (16.2 s → 0.3 s), about the size of the files themselves: what a first commit reads now doubles when the file count doubles (1,000 files 20 MB, 2,000 files 40 MB, 4,000 files 81 MB). The writer used to re-read the whole object index after every object write, which was quadratic in the number of objects (a first commit of 8,000 files of 40 bytes read 9.4 GB; it now reads 1.6 MB).
A few things worth knowing about these numbers before relying on them:
- They describe shape and order of magnitude, not a guarantee for any particular repository.
They were measured against a small corpus of profiles — two profiles, chosen deliberately at
opposite ends of one axis but also differing 46x in breadth from each other, so results are
bracketed by two shapes rather than interpolated across every shape a real repository might
have. The timing figures above use one of them,
prikk-self. - The evidence is not equally deep. The sealing figures are three builds to depth 1,024; the
checkout and
merge-evidencefigures are five depths (32 to 256), three samples each, so their exponents are direction, not a fitted curve; the cache figures are five cells, depth 32–256; the commit-memory figures reach 64,000 files. - The machine was shared with other work (load average 1.7–13 during the sealing builds), which is why each sealing figure is the median of three and why absolute times can move by up to about two times between runs. Ratios within a table are the sturdier reading.
- All of the above was measured on Linux.
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 denyis not a configured gate.cargo auditis: it runs as a standing release gate locally (--no-fetch,rfcs/EXECUTION-ORDER.md§6 rule 9) and as a scheduled CI job every Monday against a freshly fetched advisory database (.github/workflows/security-audit.yml), so an advisory that arrives without any code change is still caught. - Releases are made by one maintainer under their own signing key, by the procedure in RFC 152 — see How prikk releases below. In v0 there is no second signer, no support window (only the latest release gets fixes) and no stability promise for the object format, the CLI’s JSON schemas or the library API. Each is absent on purpose and named here so nobody discovers it after depending on it.
release-signers.tomlis empty because no multi-signer policy exists yet, not because releases are unsigned: every release tag is signed by the maintainer key and verified before it is pushed. The file gains entries the day a second maintainer with a release key exists (§ What changes as the project grows).
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 7 and schema-2 Blocks with replay-derived clean-state Merkle roots. Format 7 is format 6 plus one rule: an object id may hold several container records, and the last one is authoritative — which is how one object carries several signers.
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.
Format 6 stays openable, and upgrades in place. A format-6 repository opens and works as before,
holding one record per id: a second signer’s copy of a stored object is refused, naming the upgrade.
prikk format upgrade moves it to format 7 in place: it takes the writer lock, refuses — changing
nothing — unless prikk verify would pass, and then overwrites only the one byte of the FORMAT marker, in place and flushed (RFC 168 §3.3; no rename).
Nothing stored is rewritten, because every format-6 repository is already a valid format-7 one. The
upgrade is explicit, never automatic, idempotent, and one-way: prikk 0.44.0 and earlier refuse a
format-7 repository at open, so upgrade only once every prikk that opens the repository is newer.
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), 6→7 (several records per object id). Prikk is early implementation software and has not committed to format stability. Bumps up to 6 each made every older repository unopenable, with no in-place migration; 6→7 is the first a repository crosses in place.
To carry work across one of the earlier transitions, use prikk bundle export on a version that still
opens the old repository and prikk bundle import into a new one. Never edit FORMAT by hand —
editing the marker does not change the on-disk shape it describes, and skips the verification
prikk format upgrade requires.
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 PBNDL003 (DC-44
increment 3, bundle-manifest-handoff-v1.md), which added a self-describing manifest section after
the author-key section — an addition PBNDL002 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. The manifest names the on-disk repository
format and the exporting tool’s own version, and states plainly that a bundle is one ref’s closure
only — other refs in the source repository, if any, are not included, and the bundle makes no claim
about them. Both prikk bundle export and prikk bundle verify print it.
PBNDL001 and PBNDL002 bundles are still accepted on import (PBNDL001 acceptance corrected
shortly after the PBNDL002 bump shipped — bundle-v1-import-regression-v1.md; PBNDL002
acceptance is this bump’s own §3 obligation, verified with real bytes, not a hand-built
approximation). 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, which in
turn decodes exactly like a PBNDL003 one whose manifest section is absent: the Patches a
PBNDL001 bundle carries read Unverifiable, the same outcome DC-53 already defines for any Patch
this repository never recorded AUTHOR key material for, and a PBNDL001/PBNDL002 bundle’s own
verify report simply states no manifest is present rather than printing absent fields as if they
had been checked.
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
MSRV rise policy. The floor above cannot go lower — it is the edition-2024 minimum — but it can
rise. It rises only when a dependency or language requirement forces it, never for convenience, and a
rise is a minor-version event: the release’s CHANGELOG.md carries a ### Changed — breaking once:
entry naming the specific dependency or language feature that forced it, and a line under ### Upgrading. A rise whose cause is not recorded
is indistinguishable from a rise for convenience, which this policy forbids.
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.
Version Numbers and Release Identity
One version covers the whole workspace: the workspace version and the seven internal crate pins
carry it together. In v0 a release with any runtime change is a minor bump (0.41.0 → 0.42.0); a
release that changes no runtime source (installer, metadata, docs) is a patch (0.27.1, 0.31.1).
Breaking changes happen before 1.0; each is stated in the CHANGELOG as breaking once, with the
migration in one line, and is why the crates say “API may change without notice before 1.0”.
A release is its signed, annotated tag. Tags are unprefixed versions such as 0.42.0, not
v0.42.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
untagged build is a development build even when prikk --version equals the latest release.
How prikk releases
A release is a theme delivered, not a date reached: the owner authorizes a cut and names its theme,
and main is the next release in progress — every commit on it has passed the fourteen local gates and
CI. The procedure (RFC 152 §3):
-
Readiness. The dev team checks every
--helpsynopsis and description against the binary, the CHANGELOG against the commits since the last tag, docs messages against the binary, the root-export diff, the package footprint and the memory ratio, and writes a smoke script covering every shipped feature that runs against anyprikkbinary. Findings are fixed as their own commits. The release commit is exactly three files: the version and its pins, the lockfile’s member versions, and the CHANGELOG heading dated the day it is prepared. -
Review. The architect reviews the sweep and the commit, runs the fourteen gates on that exact commit, and runs the smoke script against a release build of it.
-
Push, then CI green on every job, including the Windows and macOS suites — the only place platform code runs.
-
Tag. Annotated, GPG-signed with the repository’s configured key, verified with
git verify-tag --raw <tag> 2>&1 | grep VALIDSIG, then pushed. From 0.48.0 the tag’s message has as its subject the project name followed by the version (for 0.48.0: prikk 0.48.0), then a blank line, then the lineRelease notes: https://github.com/prikk-vcs/prikk/releases/tag/X.Y.Z.The Release workflow refuses to build unless the commit’s most recent CI run on
maincompleted successfully. The Release workflow builds four targets and publishes the assets. The Release page carries that version’s CHANGELOG section in full, so the notes are read there and not behind a link to a file that is reorganised over time. The signed tag covers the source at the tagged commit, and the section is part of that commit: verify the tag (git verify-tag --raw X.Y.Z 2>&1 | grep VALIDSIG, as in Verifying a release) and read the same section from the tagged tree (git show X.Y.Z:CHANGELOG.md) to compare it with the Release page. The tag does not cover the binaries (see Verifying a release). A tag is never moved or re-signed; a mistake ships as the next patch release. Tags before 0.48.0 keep their bare version message. -
Verify the artifact. Download the Linux asset, check its sha256 against the published file, read its build-info (commit and tag), run its
--version, compare the Release page’s notes with the tagged CHANGELOG section, and run the smoke script against it: what is checked is the bytes users get, not the tree they came from. -
Publish on the owner’s word, per release. The eight crates in dependency order from a detached worktree at the tag, each confirmed on the registry index; then
cargo install prikk --version Xand the smoke script once more against what crates.io serves. -
Tell the consumers. The CHANGELOG is the record. A project building on something a release changed hears about it directly, starting from the section’s
### Output changes.
Each release section leads with what a reader must know, each block only when it has something in it:
### Security (with the advisory id where one is published), ### Upgrading (what to do after
upgrading), and ### Output changes (what a program reading the CLI will see differently). Then come the
Added, Changed and Fixed entries (RFC 161).
What blocks a cut, without exception: any red gate; any commit on main the architect has not
reviewed; CI not green on the release tree; a --help text that is false on the shipped binary.
What protects a release, and what deliberately does not exist
Protects: signed tags on a key one known person holds; checksums and build-info published beside
every asset; the release-policy gates on every push in CI, and cargo audit against a fresh advisory
database on its own weekly schedule (not per push — a new advisory can arrive with nobody touching the
code, so it needs a schedule of its own, in security-audit.yml); the smoke script run against the
artifact, not the tree; publication by a person after the artifact is verified; and never unpublishing
— a bad version is superseded by a patch release, and yanked only on the owner’s word with a CHANGELOG
line saying why.
Does not exist, on purpose, in v0: a second signer; a support window; a stability promise for the
object format, the CLI’s JSON schemas or the library API. A checksum on a downloaded binary proves
integrity of transport. Whatever you obtain, verify its content with prikk verify.
What changes as the project grows
- A second maintainer with a release key. On the owner’s explicit word, in a commit naming the
person and the fingerprint,
release-signers.tomlgains its first entries; from then on a release tag must be signed by a key in that set, and the release-policy tool gains that one check. - A first production user. A support window is stated,
SECURITY.md’s advisory path is exercised once as a drill, and the memory and cost measurements release prep already takes become numbers a user can rely on. - 1.0. The stability promise arrives in three named layers, each on its own RFC: the object format
and exchange artifacts first (already versioned by schema), the CLI’s JSON schemas second (already
-v1everywhere), the library API last if ever. - Publication from CI. Possible at any time and deliberately not done while one person’s word is the control that matters most; revisited when a second maintainer exists.
Release-Policy Checks
Run cargo run --locked -p prikk-release-policy -- check from the repository root to execute the
release-evidence schema and sequence fixture tables. It asserts date-time formats, rejects unknown
schema assertions, fails when computed validity differs from a fixture’s expected outcome, and leaves
the worktree unchanged. It is one of the fourteen gates every candidate runs locally
(rfcs/EXECUTION-ORDER.md §6 rule 9), and CI’s policy job runs it with the other three
release-policy gates on every push.
Every review and release record states the commands actually observed, and what was unavailable or
inapplicable. Nothing — cargo audit, registry publication, a GitHub Release, Pages — is reported as
passed unless it was observed for that exact release.
Claim-to-Source Anchors
| Claim | Source anchor |
|---|---|
| Who releases, the procedure, what protects a release, and what grows with the project | RFC 152 |
| The earlier signer-transaction design and release controls, archived as not applicable to v0 | DC-35, DC-43 |
| 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 |
| Released change history | CHANGELOG |
Provenance
The compatibility, format and bundle sections implement RFC 114 and the DC-series format decisions. The release sections describe RFC 152: they replaced, on 2026-09-13, the DC-35 signer-governance, release-state, disputed-tag and evidence-snapshot sections written for a maintainer quorum the project does not have. Nothing here authorizes a signer or claims that any release passed a check it was not observed passing.
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 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/foundation/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 DurabilityContract methods on Windows
The trait has eleven methods (crates/prikk-store/src/foundation/fsutil/contract.rs:89-172); each has a row below.
| Method | Windows guarantee |
|---|---|
durable_append | Held. Content durability on an existing name is what Windows provides. |
durable_append_reporting_offset | Held, as durable_append: the same open, validated the same way. The offset is the length read from that handle (windows.rs:327-346). |
durable_overwrite | Held. An in-place write to an existing regular file, synced on the same handle (windows.rs:348-364); it renames nothing, so the atomic_replace row does not apply (RFC 168 §3.3). |
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). Its remaining callers are the four rebuildable caches and the worktree file writes. Repository state that cannot be rebuilt is written in place, not renamed (RFC 168 §3.3), so it does not depend on this row. |
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. The argument that a crash between this call and the entry becoming durable leaves the worktree marker dirty holds only for a crash before the marker clear. A power loss after a completed operation is residual (a) below, not covered by the marker. |
Windows residuals of RFC 168 (accepted by the owner), each by name.
- (a) Worktree files after a completed switch or checkout. On Windows, a power loss shortly after a completed
branch switchorcheckoutcan bring back old contents of rewritten files, bring back deleted files, or lose created files. Nothing in the repository is damaged. Worktree files keep their atomic replace (RFC 168 §3.4): writing a user’s file in place would change what hard links and running programs see. - (b) A repair’s first save in a repository created before 0.49.0. The recovery log is a new name there. On Windows, a power loss in the same boot as its creation can keep the repair’s truncate and lose the log’s first appearance, so the bytes that repair removed are not kept. Repositories created by 0.49.0 create the log at
init, and a later save appends to an existing name, which is durable. - (c) The commit witness’s first creation in a repository created before 0.49.0. A lost creation falls back to RFC 166 rule 3, the classification a session written by an older binary has always had.
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 has eleven methods today, all covered by the table above.
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
This table is derived from each command’s help text and module documentation. It is not traced through the call graph, and it is not an authoritative list until a test checks it.
| Command | Boundary |
|---|---|
key generate | Writes a new seed file only with --out; needs no repository |
key public / key status | Read-only (key status signs nothing) |
setup | Mutation (init, key files, trust) |
verify | Read-only |
log | Read-only |
status | Read-only |
diff | Read-only (writes nothing) |
cat | Read-only; writes only the file named by --output |
tree | Read-only |
show | Read-only |
doctor (no repair flags) | Read-only |
doctor --recovery-list | Read-only |
doctor --repair-wal-tail, --repair-index, --repair-pointer-index-tail, --repair-tails, --rebuild-pointer-index, --discard-damaged-commits, --restore-queue-target, --recovery-restore, --recovery-clear | Mutation, except with --plan-only, which writes nothing |
doctor --repair-main-ref | Recognized and always refused; writes nothing |
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 |
merge | Mutation (seals the merge) |
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 |
branch / branch list | Read-only |
branch create / branch close / branch switch | Mutation (switch writes the worktree) |
tag / tag list | Read-only |
tag create | Mutation |
mv | Mutation (moves the file and declares the rename) |
config get / config list | Read-only |
config set / config unset | Mutation (writes the repository’s config file) |
bundle preview / bundle verify | Read-only (writes nothing) |
bundle export | Read-only for the repository; writes only the file named by --output |
bundle import | Mutation |
trust maintainer add / remove | Mutation |
trust maintainer list / check | Read-only |
sync summary / sync have / sync build | Read-only for the repository; writes only the file named by --output |
sync compare / sync pending / sync tags | Read-only |
sync accept / sync seal / sync adopt-tag | Mutation |
unlock (no arguments) | Read-only; lists held locks |
unlock --lock <path> | Mutation (clears one lock) |
ref complete | Mutation, except with --plan-only (writes nothing) |
format upgrade | Mutation (rewrites the repository format in place) |
compact | Mutation, except with --plan-only (writes nothing) |
init | Mutation (creates .prikk/) |
commit | Mutation |
seal | Mutation |
The rows that were already here were 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. The rows added 2026-10-07 (the doctor repair rows, key, setup, diff,
cat, tree, show, merge, mv, config, bundle, branch switch, trust maintainer, sync,
unlock, ref complete, format upgrade, compact) are classified from each command’s help text in
crates/prikk-cli/src/commands.rs and its module doc, and were not traced through call graphs.
A name suggesting “plan” or “preview” is a hint, not proof.
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 full read-only command set, including worktree-status
(RFC 122), against a fixture repository authored on Linux, so this is a demonstrated property, not
merely a successful compile.
The macOS mutation test suite and Windows mutation test suite CI jobs (DC-81, DC-87 Stage 2) run
the suite that compiles there natively on macos-latest and windows-latest — not every test in the
workspace: a few are gated more narrowly than the platform, each for a named reason, so each platform’s own
count is smaller than stable’s. The gates that remain, by name:
- Linux only, by necessity: invalid-UTF-8 names. A directory or file name containing a byte that is not valid
UTF-8 cannot be created on macOS (APFS rejects it,
EILSEQ), and Windows names are UTF-16, so the byte sequence cannot be constructed there. Tests:wal/tests.rs(wal_for_layout_produces_byte_exact_paths_for_a_non_utf8_session_name),lock/tests.rs(active_lock_acquires_a_byte_exact_path_for_a_non_utf8_session_name), andcommit_boundary/worktree_patch/tests.rs(non_utf8_worktree_path_fails_closed), andunlock/tests.rsat line 284 (list_held_locks_reports_a_lock_under_a_non_utf8_session_name). - Corrected (0.50.0 step 2 Part C): the
fsutil/tests/conformance.rssuite is not Linux-only. DC-81 (macOS) and DC-97 (Windows) already ported it: of the four conformance assertions, three (create_exclusive_refuses_an_ already_occupied_path,durable_directory_entry_accepts_the_named_files_own_path, and the pre-existing guarantees G1/G2/G5/G8) run on all three platforms, and the macOS/Windows mutation CI jobs already run the full workspace suite (cargo test --workspace --locked --no-fail-fast), not a narrowed one — this has been true since those two rounds landed; the entry above was stale. G9 is the one genuine exception (set_permission_bits_masks_file_type_bits_out_of_a_recorded_mode, Linux and macOS only): it reads back POSIX mode bits (PermissionsExt::mode), which do not exist on Windows, so it cannot be reused unchanged there — Windows’ own no-op shape is covered separately (windows::tests::set_permission_bits_is_a_documented_noop), documented inconformance.rs’s own coverage-map comment. G6/G7 (append_and_truncate_reject_fifo_without_ blocking,read_rejects_fifo_content_despite_the_os_permitting_the_open) are Linux-and-macOS by necessity too (FIFOs are POSIX-specified, DC-81), already covered by the directory-sync bullet’s own reasoning below. - Not windows (macOS and Linux), by necessity: a directory-sync injection point. A directory fsync is a
failpoint only where a directory can be synced (Linux, macOS), and Windows has no such operation. The tests that
inject one are gated
not(windows)(or, for thecaller_testsmodule as a whole, the equivalentany(target_os = "linux", target_os = "macos")) and run on macOS: among them the lock, active-metadata and patch-checkout retry tests. An append never creates a name, so it syncs only the file (RFC 102 §3) — the WAL, trust and refs tests that used to inject this point on an append no longer have one there; each was retargeted to its own survivingRequiredFileSyncinjection point on the same call path instead, or confirmed one already existed. - Unix (macOS and Linux), by necessity: symlink creation.
a_symlinked_witness_path_refuses_the_same_way_every_other_session_file_doescreates a symlink, which macOS allows and Windows allows only with a privilege. - Resolved (0.50.0 step 2 Part C): the snapshot test that removes a file between stat and read
(
snapshot/tests.rs’sa_file_removed_mid_entry_is_refused_not_reported_unchanged, usingworktree:: before_stat_for_test). Named from source: both the seam and the test are a plain thread-local closure andstd::fs::remove_file— nothing unix-only, unlike the seam above it in the same file (between_plan_and_write_for_test, genuinely gated to matchpatch_checkout’s own Linux-only tests, DC-71), which this one had apparently been gated to match by copying rather than its own reason. Widened to run on all three platforms; no longer a gap. - Linux only, as written: individual tests gated
cfg(target_os = "linux")inside suites that run elsewhere.crates/prikk-cli/tests/verify_and_doctor/rfc158_incoming_bound.rsgates sixcontrol1_*tests (lines 96-227: bundle import, preview and verify; sync compare, build and accept; each on a sparse 1 GiB file). Incrates/prikk-cli/tests/rfc147_declaration_resolution.rsthe items at lines 514, 721, 746 and 762 are gated:a_rename_reports_a_mode_changeat 514, and the socket and FIFO fixture helpers at 721 and 746 (line 762 was not checked by name). Neither file records the reason for the gate at the gate itself. - Linux only, and
#[ignore]d: measurement units, run by hand.crates/prikk-cli/tests/append_length_io.rs(file-level#![cfg(target_os = "linux")], line 22; its three#[ignore]units are measurements) and the G2 table incrates/prikk-cli/tests/hostile_lengths.rs(hostile_lengths_g2_table, lines 371-383,#[ignore]). They run only with--ignored, so they are not part of the platform gates above.
Neither developer nor architect can run either platform locally as part of this project’s own environment, so 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
- Other platforms have no prebuilt binary. Prebuilt archives cover four targets: Linux
x86_64andaarch64, macOSaarch64(aarch64-apple-darwin), and Windowsx86_64(x86_64-pc-windows-msvc) (.github/workflows/release.yml:35-46). Anywhere else, build from source (cargo build/cargo install); see install. - DC-76’s negative controls are only partly demonstrated on Windows, for the eight guarantees
that remain (G5 retired in DC-98) — see “The
DurabilityContractmethods 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. - Re-checking out a file whose bytes already match, on such a platform: the file is recognized as
unchanged by its bytes alone, since there is no observable mode to compare them against, and
checkout --snapshot-materialize/--patch-materializecount it underunchanged files:exactly as they do on Linux. Until 0.43.0 every such file was counted underwritten files:instead, and the no-opset_permission_bitswas called again for it — a wrong count and pointless work, never a wrong byte on disk (DC-87). A file that disappears between its bytes being read and its mode being read is a different case and still refuses the checkout, on every platform.
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 blocksverify is linear: 27.04 ms at 160 blocks, per-doubling ratio 1.97 | Closed, and held by a gate — see below |
| 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 |
A text span’s identity includes its position among textually- and anchor-identical occurrences, recomputed against the buffer in front of it. Correctness therefore rests on an unstated, unchecked invariant: that each EditText is authored against the state its predecessors produced. Every authoring path in this codebase upholds it — no user-facing path reaches the failure — but a sequence violating it is refused as malformed evidence rather than reported as what it is | Latent; found 2026-09-03 by the patch-algebra property tests; owned by RFC 134 |
These are recorded rather than left implicit. The findings register they once pointed at was
retired deliberately — reviews carry findings while they are live, and documentation and RFCs
carry what outlives them — so this table, ROADMAP.md’s open-work index, and the RFCs themselves
are where they live now.
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).
The trade was real, it was measured, and it has been paid. That history is worth stating plainly:
The mechanism that bounds patch cost also created prikk’s own cost. History is sealed into a chain carrying state roots, and
verifymust re-derive those roots to check them. The first implementation re-derived each block’s state from genesis, so verifying the block at position i cost O(i²) and the whole chain O(N³) — about 34 seconds at 160 blocks.
Prikk did not inherit Darcs’s problem. It had a different one, and it lived in the verification path rather than the merge path — which mattered more, not less, because verification is this project’s central claim in a way that merge throughput is not. It was found by measurement rather than by reading the design, and it is now fixed.
verify derives each block’s state once, forward from its already-verified parent, memoizing as it
goes — never from genesis per block — and checks the result against that block’s recorded state
root. Combined with removing a repeated full-index decode on the same path, the cost is linear:
27.04 ms at 160 blocks, per-doubling ratio 1.97, against 167.85 ms and ×3.51 at the intermediate
stage.
The property is held by a gate rather than by a measurement: rfc111_index_decode_cost_gate.rs
fails if verify’s full-index-decode count ever grows with repository size again, and it was observed
failing before its fix. So the block-oriented design’s central trade held — the cost it created
was real, was found, and is now bounded and defended.
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 eight live durability
guarantees (G1–G4 and G6–G9 in
crates/prikk-store/src/foundation/fsutil/contract.rs; G5 was 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. - 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, the format
marker file, and the leaf files the default session and the write state need before their first write:
worktree.marker, worktree.provisional, current-branch (heads/main), active/default/queue.wal,
active/default/ref-name, active/default/declarations, active/default/witness, and recovery/log
(crates/prikk-store/src/foundation/layout.rs:280-305, and ensure_write_state at layout.rs:740-751).
The active WAL and active ref metadata are therefore empty when init returns. Ref, received-ref, and
trust storage are fixed, named container 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
current-branch
worktree.marker
worktree.provisional
recovery/
log
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
declarations
witness
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. Each is empty until first use, except FORMAT (its version) and
current-branch (heads/main). 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. The exceptions are the write-state files a repository
created before they existed lacks: recovery/log and the default witness, which the first write
creates exclusively (layout.rs:740-751), and the declarations file, which a write also self-heals
(layout.rs, the RFC 144 §4o.2 comment in init). recovery/ is never authority (next paragraph);
init creates its log, and a repository created before 0.49.0 gets the log and the witness at its first write.
recovery/log holds the bytes a repair removed, appended and flushed before the repair truncates. Each repair is one run, and each file it changes is one entry in that run. Every repair writes
here and nowhere else. Each entry records its source file, the offset it was cut at, the removed bytes, a hash of the file before that offset, and the
identity of the files that give those bytes their meaning. A repair names its run, for example recovery/log, run 3f9a0c1e8b2d4a57. A rewrite in place (the commit witness, ref-name) keeps the file’s previous bytes too, so a run can be undone exactly. It exists so that
a repair can be wrong about what it removed (a torn tail and a lone damaged record look the same) without anything being lost. It is never
authority: no classification reads it, and verify reports damage in it on its own line without changing its exit status. The log is never truncated
except by prikk doctor --recovery-clear, which removes the saved content for good. It is not quarantine/, which is retired.
The three commands:
prikk doctor --recovery-listprints the saved entries grouped by the repair run that wrote them: a line per run with its id and entry count, then each entry’s source and its label. ACutentry (trailing bytes a repair removed) prints the offset it was cut at and its length; aReplaceentry (the whole file’s own previous bytes, before a repair rewrote it in place —--keep-slot’s own saves, and the commit witness/ref-namerewrite) prints “replaced, N bytes saved” instead (0.51.0 step 1 Part C2, C4: “cut at 0” read as a truncation even when nothing was removed). It does not judge whether an entry can be restored.prikk doctor --recovery-restore <run id> [--plan-only]undoes the whole run, in reverse order: it prints its plan first (each step, and its conditions), then writes only when every condition holds. A step that already holds its result is skipped, so an interrupted restore finishes when it is run again. A later run that changed the same files is named, and the restore waits for it. It writes only when the source is exactly as long as the recorded offset, its bytes before that offset hash to the recorded hash, and the files that give the bytes their meaning are unchanged.prikk doctor --recovery-clear [--plan-only]lists what it will remove, then empties the log.
Repairs written before 0.49.0 are in recovery/ as *.bytes files. They are listed as older files, read or deleted by hand, and never restored.
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 7 in FORMAT; a repository created by 0.44.0 or earlier contains 6, still
opens, and moves to 7 in place with prikk format upgrade (see
Release Compatibility). 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; so is any number this build does not know.
The format marker is a gate: 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 deletes an object, and nothing is rewritten. Writing an id that already exists with identical bytes is a no-op. In format 7 an id may hold several records, and the last one is authoritative: another signer’s copy of a stored object — the same type, schema and payload under other signatures — is written as one superseding record carrying the union of both signature sets, and the index’s last entry for the id points at it. Every reader resolves an id to that last record, in every pass. The union is decided under the object-store lock against the record as stored at that moment, so two writers merging into one id keep both signature sets. A copy whose payload differs under the same id is corruption and is refused in every format; in format 6, any different copy is refused. 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. A reader resolves the live slot by reading the last complete record in the generation log, not
by assuming a.
An empty (or absent) generation log is trusted to mean “no compaction has ever run” only while slot
b itself holds no data (0.50.0 step 1 Part E4, 019 §5.7, corrected by handoff 165, 020 §3.1).
Slot b is never written except alongside the one generation record that names it live
(compact.rs, and, for the pointer index only, prikk doctor --rebuild-pointer-index’s own
rebuild), so an empty log over a non-empty slot b can only mean a compaction (or rebuild) genuinely
happened and its own record of that fact was lost afterward — not that one never happened. Rather
than refuse that state outright, every reader and writer deduces the live slot, testing content
in both directions: one slot is stale beside the other when either it equals, or is a prefix of,
a compaction of some earlier prefix of the other’s own history (a crash before the switch was
recorded, or a write cut short), or the other slot starts with a full compaction of it and simply
kept taking writes after. Whichever one direction holds names the live slot. Both directions holding
and the two slots folding to the same entries means either reading is correct. Neither direction
holding, or both holding while the folds disagree, means the state is genuinely ambiguous — proven,
not merely undecided: two equally honest command histories (add a key, compact, remove it; or
compact again, add the key back) can leave byte-identical slot files with opposite answers on
whether a key is trusted, so no rule reading only the slots can be right in both. This refuses with
its own distinct, typed error (PrikkError::AmbiguousGenerationLog, never confused with a damaged
record). 0.51.0 adds a way out the user can choose, named first: for the received index and the
trust policy container, prikk compact --received-index|--trust-policy --keep-slot a|b [--plan-only] prints each slot’s own entry count and folded state (for the trust policy, the keys
trusted in one slot and not the other, by name) and which slot content would deduce or that the
state is ambiguous, then — unless --plan-only — treats the chosen slot as live and compacts it
into the other, saving what it overwrites first, the identical save-before-write ordering an
ordinary deduced compaction already uses. A whole-.prikk/ backup (the trust policy text also says
to re-apply every trust change made since) stays the second way out; for the ref pointer index,
--keep-slot does not apply at all (its own rebuild never reads either slot as live, so there is
no choice to make) — the rebuild remains its one way out. The earlier, one-
directional version of this rule (Part E4, tested only whether b derives from a) is what review
020 found wrong: after an even number of compactions, where a is actually the newer slot, a
superseded entry left in b defeated that one-directional test and answered with the stale slot
anyway. Two still-narrower rules were tried and rejected earlier still: bare membership (Part E2)
lets a repeated value, such as a maintainer revoked and later re-trusted, fool the test into
resolving to the wrong slot; comparing only against the current content of one slot (Part E3) is
wrong once a crash lets that slot keep taking writes afterward. prikk verify and prikk doctor
warn whenever the state is merely deduced (not ambiguous), naming the container, the deduced slot,
and the exact command to end it. For the received index and the trust policy container, that
command is prikk compact --received-index/--trust-policy, and that compaction now also saves
the slot it is about to overwrite, and the generation log, as one recovery run first — both saves
land before either file is written, since a slot’s own meaning file (for these two containers) is
its generation log, read while it still holds its own before bytes — restorable byte-identically
until a later write touches either file. For the ref pointer index, compact --pointer-index
instead refuses outright in the deduced state, naming prikk doctor --rebuild-pointer-index --plan-only, then the rebuild: the rebuild already saves correctly (re-deriving from the ref log,
reading neither slot as live), so there is nothing a compaction’s own save would protect that the
rebuild does not already handle, and naming one way out rather than two avoids a second, narrower
save path entirely. A restore from the recovery log refuses outright instead of deducing, in either
the deduced or the ambiguous state: it is a deliberate writer, and a stale meaning file could
otherwise compare unchanged and pass a restore that should not proceed. For the ambiguous state too,
prikk doctor --rebuild-pointer-index is the pointer index’s own way out, for the identical reason:
it re-derives from the ref log without reading either slot as live, unaffected by the ambiguity.
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; 7 is the current format. Format 6 still opens and is written as format 6 until prikk format upgrade moves it to 7 in place (marker only; no downgrade). Formats 1-5 are rejected at open, with no bridge and no in-place migration. |
.prikk/config | Local settings, optional | Written only by prikk config set (unset removes the file); absent means every key takes its default. One key today, incoming.max-object-bytes. Never authority for history and never read from the worktree or from anything imported, so nothing received can set a value in it; bundle verify, which opens no repository, does not read it, and an older prikk build ignores it. |
containers/<type>/a.container | Content-addressed object authority | One checksum-framed record per object in format 6; in format 7 an id may hold several, and the last is authoritative. 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. |
recovery/log | Created by init (and at the first write to an older repository), never authority | The bytes every repair removed, one run per repair, appended durably before the truncation. Listed, restored and cleared by prikk doctor --recovery-list, --recovery-restore and --recovery-clear. Older recovery/*.bytes files from before 0.49.0 are listed separately. |
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 repository, under whichever format is current (see
the Authority Model table above), 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), prikk merge (DC-74), and single-ref prikk bundle export/verify/import (DC-44) have since shipped — see the sync,
merge, and backup and restore guides. Still
deferred: garbage collection, cache rebuild semantics, quarantine enforcement, stable
repository-format migration, multi-ref backup export, a rehearsed repository-format-migration
restore, 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 format 7 for new repositories; format 6 still opens and upgrades in place; 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 |
init creates the default active WAL (queue.wal) and active ref metadata (ref-name) empty; their contents are runtime active-session state. | 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 and slot b itself holds no data – an empty log over a non-empty slot b deduces the live slot by testing both directions (is either slot a compaction of some earlier point in the other’s own history, or did the other take real writes after a full compaction of it; handoff 165, correcting 0.50.0 step 1 Part E4’s one-directional test), warning (verify/doctor) rather than refusing when one direction decides it; when neither or both directions hold with disagreeing folds, the state is genuinely ambiguous (proven by command-built cases with byte-identical slots and opposite answers) and refuses with its own typed error, naming `prikk compact –received-index | –trust-policy –keep-slot a |
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.
The archive format, PREPO001
prikk archive export <file> writes a whole repository, verbatim, into one file (RFC 155).
prikk archive verify <file> reads one back, streamed, and checks structure, reference closure, and
every signature against the archive’s own carried key material. prikk archive import <file> [--adopt] reads the same file and lands it. This page documents the byte layout all three commands
use — the format itself, not the command surface (see commands for that).
With prikk 0.51.0, run archive verify on every export before relying on it (fixed in 0.52.0;
see the limitation for the two shapes and why).
Shape
PREPO001 (8 bytes, the whole file's own magic)
section 1
section 2
...
section N
manifest
end trailer (56 bytes, fixed)
Nothing is random-accessed to write it: every section streams forward, in fixed-size chunks, from
one on-disk source file to the output. A reader finds the manifest without scanning the whole file:
seek to length - 56, read the end trailer, then seek to the manifest’s own offset.
One section per carried file
| field | bytes | notes |
|---|---|---|
| magic | 8 | always PRPOSEC1 |
| kind | 2 | a fixed numeric code (below) — never a carried path |
body_len | 8 | known from a stat before any body byte is read |
| body | body_len | verbatim bytes of one source file, or a resolved family’s sound prefix |
| checksum | 32 | SHA-256 of magic ‖ kind ‖ body_len ‖ body, after the body — a trailer, not a header, so the encoder never needs the whole body in hand before writing the header |
An object-container section classifies its own frames exactly as the live store does (RFC 162
rule 2), held equal to that real decoder by a dedicated test rather than by a shared function call:
a complete frame — a full header and its full claimed body are both present — that fails its own
checksum is damage, named by archive verify and refused by archive import. An incomplete
frame — a crash tail in the source repository’s own live container, or bytes added since, carried
verbatim because object containers travel damage included — is named but never refused on: the
source repository’s own prikk verify only ever warns about this shape, so a backup of that
repository must still restore. The overrun case: a frame whose checksum fails is still
incomplete, not damage, when a later, genuinely sound frame starts inside the byte range its own
header claimed — that only happens when the first frame was torn (a crash mid-append) and a later,
real write finished past it, exactly as the live store’s own resync-and-continue reading already
handles for its own container file. The same bytes get the same outcome whether a crash or a hand
wrote them.
Section kinds
| code | kind | content |
|---|---|---|
| 0 | Patch container | verbatim, live slot’s bytes (object containers never compact — always slot a) |
| 1 | Block container | verbatim |
| 2 | RefState container | verbatim |
| 3 | Tag container | verbatim |
| 4 | Attestation container | verbatim |
| 5 | Blob container | verbatim |
| 6 | RecognitionClaim container | verbatim |
| 7 | Ref log | verbatim, slot a (never compacted) |
| 8 | Ref pointer index | resolved content only — no slot letter, no generation log |
| 9 | Received index | resolved content only |
| 10 | Trust policy | resolved content only — carried as an inert record, never adopted on import |
| 11 | Maintainer keys | verbatim — carried as an inert record, never adopted on import |
| 12 | Author keys | verbatim — recorded on import, conflicts refused and named |
Every repository this format carries from writes exactly 13 sections, in the order above
(ArchiveSectionKind’s own fixed list — crates/prikk-store/src/archive.rs).
Damage in an object container, the ref log, the maintainer record, or the author-key material
travels verbatim — the archive is a faithful copy, damage included, and archive verify names it
rather than refusing the export. A slotted family that cannot be resolved (an ambiguous lost
generation log, or genuine interior damage in the resolved content) refuses the whole export
before anything is written, naming the existing doctor/verify texts for that state.
The manifest
Written once, after every section, so every section’s own checksum is already known:
| field | bytes |
|---|---|
| magic | 8 (PRPOMAN1) |
repository_format | 4 |
tool_version_len | 2 |
tool_version | tool_version_len |
section_count | 8 |
| entries | 50 × section_count |
Each entry: kind(2) ‖ offset(8) ‖ length(8) ‖ checksum(32) — the section’s own header-to-trailer
span in the file, and its checksum, so a verifier can check every section’s structural soundness
without decoding the section headers itself.
The end trailer
Fixed 56 bytes, always the file’s last 56: magic(8, "PRPOEND1") ‖ manifest_offset(8) ‖ manifest_length(8) ‖ manifest_checksum(32).
manifest_checksum is SHA-256 of the manifest body (magic ‖ repository_format ‖ tool_version_len ‖ tool_version ‖ section_count ‖ entries) — the manifest cannot checksum itself
in its own body, so the checksum lives in the trailer instead, written last, after the manifest
body is already known in full (RFC 155 implementation Part V2: the manifest itself was not
integrity-checked before this, so a flipped byte inside it — repository_format, in particular —
passed archive verify as consistent).
archive verify
Reads the end trailer and the manifest first (seek, never scan), then checks:
- the manifest’s own checksum, before any of its fields (including
repository_format) are read as anything but raw bytes — a mismatch refuses the whole archive by name; - the manifest’s own
repository_formatis one this build actually knows how to read (today: 6 or 7) — an archive legitimately written by some other build, naming a format number this one predates, is refused by name rather than silently misread; - structure — every section’s own checksum matches the manifest’s recorded one, the fixed set of thirteen section kinds is present exactly once each (an unknown, duplicated, or missing kind refuses before any section body is read), and the manifest’s own declared layout reaches exactly the archive’s real byte length;
- reference closure, in two passes over the seven object sections: the first builds a set of
every carried object’s own id (32 bytes each, never the object’s bytes); the second re-streams and
checks every reference a
Block/Patch/Tag/Attestation/RefState/RecognitionClaimmakes against that set, naming any that is absent; - every signature, against the archive’s own carried author-key and maintainer material — small
key material, decoded once into memory, never the local repository’s. A signature by a key this
archive carries, that fails to verify, is damage, named, and refuses the whole archive — the one
case this check actually catches: a tamperer who left the signature’s own key in place but
altered the signed bytes. A signature by a key this archive does not carry — including every
signature at all, if the carried key section is empty or was swapped for something else — is
named “not checked” and leaves the archive internally consistent; it is not a weaker version of
the real check, because the real check happens only after import, against the receiving
repository’s own adopted set (R2). Stripping or swapping the carried key section therefore proves
nothing either way about an archive’s own trustworthiness, by design:
archive verifyanswers only “is this archive internally consistent with itself,” never “should you trust it.” The report always shows both counts together (“signatures checked: N, not checked: M”, in text and--format json), and names what a nonzeronot_checkedcount means, so a reader never has to guess whether 0 of 16 or 16 of 16 signatures were actually checked against carried material.
It reports internally consistent, never trusted: trust comes only from a receiver’s own
adopted set, checked after import. That note is printed — in text and in --format json — only
when the archive genuinely is internally consistent; a damaged or unresolvable archive gets its
damage lines and the error, never a note beside them that contradicts them. The carried maintainer
keys are listed, labeled “carried, not adopted.” It writes nothing, on any exit path — no scratch
file, no scratch directory.
The manifest’s own declared repository_format number (checked above, against what this build
knows how to read) is purely informative past that check: archive import always writes the
receiving repository’s own real, current format, never the carried number. A format-6 archive
built from format-7 bytes, or any other mismatch between the carried number and the carried
content’s own real shape, changes nothing about what import actually writes.
Carry-forward
A later PREPO version keeps this magic’s reader forever (RFC 114’s stability contract): the build
that ships the next bump must still read a PREPO001 file written by the last build before it,
proven by a fixture built from the old encoder — decode_bundle’s own precedent
(crates/prikk-store/src/bundle.rs), not hand-crafted bytes.
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 container locks are acquired by whichever operation is touching that container — the writer
(ref publication, trust add/remove, bundle import, and any object append) 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 queues commits on the default active WAL. Each commit appends one Patch and
its witness under the active lock, and a second commit before seal queues behind the first for the
same ref, up to the PRIKK_ACTIVE_PATCH_LIMIT bound. A commit for a different ref is refused while the
queue is non-empty (crates/prikk-store/src/commit_boundary/active.rs:118-131, :301-314).
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, the publication is finished by seal’s retry or by prikk ref complete <ref>
(RFC 165 R4). Both take the active lock and the same ref-specific lock, check the completing key against
the local maintainer trust policy, revalidate the retained WAL, RefState, Block, sequence, and old/new
ids, then append the exact deterministic RefUpdate. ref complete does not need the original signer. A
structurally incomplete final log frame may be truncated only by those paths after the complete prefix
verifies. A publishing command (seal, branch create, branch close, tag create) also refuses before it
appends when the ref log has an incomplete tail, unless the tail is its own ref’s interrupted publication
(ensure_may_publish, crates/prikk-store/src/refs.rs:337-343); commit does not check the ref log, so a
torn tail does not block a commit, but it blocks every publish except the pending retry of the ref that
leads, until it is repaired.
Doctor diagnoses interrupted publication but does not sign, append, promote, or reconstruct a
missing pointer. --repair-main-ref is a recognized input for this and performs no repair — it is
always refused, regardless of repository state.
Container Locking and Compaction
Four of the five container locks concern compaction. 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.
The fifth, the object-store lock, is not a compaction target and is described in its own section below; it is a leaf taken and released around a single object append.
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.
The Object Store Lock
Every object append — a Patch from commit, a Block from seal or merge, a Tag from
tag create, a RefState from any ref publication, the objects bundle import and sync accept
bring in — goes through one function, append_object_under_lock (object_store.rs), which holds the
ObjectStore container lock across the whole append. That span is the point: the container’s length
is read, the record is appended, and the index entry recording that offset is appended, all inside
one exclusive region.
Why the whole span and not just the write. The container is opened O_APPEND, so the kernel
already makes two concurrent appends land intact and in some order — the bytes were never the
problem. What raced was offset: derived from the container length before the append, and written
into the index after it. Two writers reading the same length both recorded the same offset, and one
index entry then pointed at the other’s record. The container stayed well-formed while the index
became wrong, which is why the symptom was index entry for <id> resolves to an envelope with computed id <other> and a failing verify, not a torn file.
This was reachable by anyone running two ordinary commands at once: commit held ActiveLock, while
merge, tag create and sync build held nothing at all, so nothing serialized them against each
other. Twelve concurrent prikk tag create in one repository reproduced it readily.
The lock is a leaf. Nothing is acquired while it is held, which is what makes it safe to take
inside a call that publish_ref already makes while holding a RefLock and the ref container locks.
The nesting is one-directional and no inversion is expressible, so the fixed order above gains one
line rather than a new pairwise rule:
The object-store lock is never held while acquiring any other lock.
It is fail-fast, like every other lock here, and that is a visible change. Two commands that both
append objects at the same moment no longer both proceed: one gets lock conflict and does nothing.
Before, they both usually succeeded — and sometimes left the index wrong instead. A refusal you can
see and retry replaces a corruption you cannot.
A failed acquisition leaves the lock file behind. If the lock file’s own creation fails partway —
a sync error, a full disk — the file can remain while the acquisition reports failure, and every
later object write meets lock conflict until an operator clears it with prikk unlock. This is the
same deliberate posture described below for every other lock: prikk does not decide on its own that a
lock is stale. It reaches the object-write path for the first time here, so it is worth knowing that
a transient I/O error during a commit can require prikk unlock before the next one.
Stale Locks and Manual Cleanup
If a process dies while holding any lock — active.lock, a ref lock, or one of the five 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 compaction-related container locks; - the object-store lock serializes two commands that both write objects against each other: the
second is refused with
lock conflictrather than queued. A command that writes many objects in one call (commit,bundle import,sync accept) holds it once across that whole write, not once per object — a concurrent writer meets the refusal for that whole span rather than only in the gaps between objects; - 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, multi-ref backup export, a
rehearsed repository-format-migration restore, and production-readiness claims. Single-ref
backup/restore tooling is no longer deferred — prikk bundle export/verify/import, see
Backup and Restore. 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’s --repair-main-ref input is recognized but always refused and performs no repair; exact interrupted publication completion is done by seal’s retry or by prikk ref complete 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 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
DurabilityContractmethods 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;
- a path component longer than 255 bytes;
- a total path longer than 1024 bytes;
.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.
Length Caps
Added by RFC 125 §3: before this, validate_repo_path had no length check of any kind, so an
over-long path entered signed history and only failed later — as a raw OS error, not a clean
refusal — at checkout on a repository that had already verified clean.
- 255 bytes per component — POSIX
NAME_MAX, the per-component ceiling shared by Linux, macOS, and Windows’ own NTFS component limit (255 UTF-16 code units, the same count as bytes here since repository paths are ASCII-only). A component longer than this cannot exist on any of the three platforms this project mutates on (platform support), regardless of where in the tree it sits or how deep the worktree root is. - 1024 bytes total — macOS’s
PATH_MAX(sys/syslimits.h), the strictest of the three platforms’ hard total-path limits: Linux’s is 4096, and Windows’ legacy default (MAX_PATH, 260) is a separate, narrower gap that depends on the worktree root’s own depth too and cannot be bounded from a repository-relative length alone. Chosen as the floor so a path this validator accepts is materializable on Linux and macOS unconditionally, regardless of worktree root depth. Not a guarantee against Windows’ legacyMAX_PATH— a sufficiently deep worktree root can still exceed it even for a path this validator accepts; that residual gap is a property of where a repository is checked out, not something a repository-relative length cap alone can close.
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 eight live durability
guarantees (G1–G4 and G6–G9 in
crates/prikk-store/src/foundation/fsutil/contract.rs; G5 was 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. - 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.
The current object model includes persistent Patch, Block, RefState, Blob, Tag, and RecognitionClaim
object directories. Tag objects are produced by prikk tag create and sync adopt-tag (RFC 117).
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.
Checkpoints and snapshots
A checkpoint is a Block that carries a snapshot. seal, merge and sync seal write one at a
ref’s first Block and at every 64th Block after it (RFC 136 §10.2); every other Block’s snapshot
reference is empty.
- What a snapshot is. A Blob of kind
Snapshotholding a v2 manifest: the Block’s own state, entry for entry — the exact leaves its state root hashes (path, NodeId, kind, mode, and the file’s content Blob id or the symlink target). A text file whose content only ever arrived throughEditTexthas no stored content Blob (edits carry spans, not whole files), so the checkpoint stores that content as an ordinary Text Blob first, and only then the manifest. A bundle carries the manifest and those content Blobs with the history. - What
verifydoes with it. Every snapshot is checked: the manifest must decode, recompute to its own Block’s state root, and name only content Blobs that are present. A failure is an integrity finding naming the Block.verifynever replays less because a snapshot exists. - A checkpoint changes cost, never output (RFC 136 §10.3a). Every command gives byte-identical output with or without checkpoints; a snapshot only lets a command start part-way along the chain.
- Who may start from one. Read-only reports (
checkout --patch-plan,--patch-delete-plan,bundle preview) may start at the nearest snapshot that passes validation. Commands that write the worktree (checkout --patch-materialize,--patch-materialize-delete,branch switch) start only at a snapshot whose Block this repository has itself confirmed by replay and an adopted maintainer signed, within 63 blocks of the tip; otherwise they replay from genesis.seal,mergeandsync sealderive the state they sign from the same kind of snapshot, under the same rule.rollback-preview,commit’s baseline andmerge-evidencealways replay the whole chain. - The replay-verified record and the provisional marker. Which Blocks count as confirmed is kept in
.prikk/cache/replay-verified-blocks.v1, a cache that is always safe to delete. A worktree written directly from an unconfirmed snapshot is marked provisional untilprikk verify. Both are described in snapshot materialization.
What a checkpoint costs. Measured on the RFC 139 corpus profile (profiles/prikk-self.toml), against
the same history sealed by a build from before checkpoints existed:
| history depth | checkpoints | repository bytes without | with | checkpoints add | growth over the repository without them |
|---|---|---|---|---|---|
| 64 | 1 | 628,486 | 629,944 | 1,458 | +0.2 % |
| 128 | 2 | 1,621,001 | 1,829,897 | 208,896 | +12.9 % |
| 256 | 4 | 3,069,495 | 4,425,512 | 1,356,017 | +44.2 % |
The last column is growth: what checkpoints add, as a share of the same repository sealed without them. Read as a share of the repository that has them, the same bytes are 11.4 % at depth 128 and 30.6 % at 256.
The manifest itself is small — 177 bytes at block 1, 12,268 bytes at block 193, where it lists 114 entries — and is under 2 % of what a checkpoint adds. The rest is the content Blobs a checkpoint stores for text files whose content had only ever arrived as edits. A repository whose files are mostly edited rather than created therefore pays close to one full copy of its text content per checkpoint, and one whose files are mostly created pays almost nothing, because those Blobs are already stored.
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 active session is a queue: each commit appends one Patch and its witness to the default active WAL
(RFC 166), and the queue holds up to the PRIKK_ACTIVE_PATCH_LIMIT bound described in the
FAQ. Active ref metadata records which branch ref owns a non-empty active WAL, and a
commit for a different ref is refused while the queue is non-empty (crates/prikk-store/src/commit_boundary/active.rs:118-131,
:301-314). 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’s repairs are opt-in, one flag each, and the full list is in the command reference.
They truncate damaged tails, rebuild the ref-pointer index from the ref log (--rebuild-pointer-index,
RFC 165 R5), and remove or declare lost a queued commit the queue no longer holds soundly, or give its
ref back (--discard-damaged-commits, --restore-queue-target, RFC 166 D5). The --recovery-* commands
undo or clear the saved repair log (RFC 168). Doctor holds no signer, so it does not sign or append
RefUpdates. An interrupted ref publication is completed by prikk ref complete <ref> (RFC 165 R4) or by
seal’s retry, and each checks the completing key against the local maintainer trust policy.
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 offers the repairs listed in the command reference. 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-policy containers only; never the ref log, the trust-key container, 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
Ten 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/ |
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 → Patchdoes not exist.parent_patch_ids(PatchPayloadtag 2) was removed outright at Patch schema 2 (0.24.0), not merely left unpopulated — there is no patch DAG, by construction, not by omission. Merge provenance is carried by block parentage instead. Schema 1 patches (written before the retirement) may still carry the tag on disk, always empty;accept_exchange_artifactrefuses it schema-blind — an incoming Patch carrying a non-emptyparent_patch_idsat any schema is 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, and verify_repository reads it. The
ReceivedRefs stage checks each received pointer’s RefState, target, required attestations, and previous
state (verify.rs:1484-1489, verify.rs:1916-1960), and the index file’s own tail and interior damage are
reported with the other appended files (verify.rs:1273). Every object a received pointer leads to (RefState,
Block, Patch, Blob, Attestation) is also an ordinary object-store entry, checked by the type-based object
scan like any other. See integrity and recovery diagnostics.
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 if it names anything but a supported format: 7, which new repositories get, or 6,
which a repository created by 0.44.0 or earlier has (RFC 114 rules formats 1-5 out of scope). The one
format-migration verb is prikk format upgrade, which moves format 6 to 7 in place after verification;
any other marker is refused by every command, including init itself against an already-initialized
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 has a set of opt-in repairs, each narrow, among them
a tail truncation (--repair-wal-tail) and a pointer-index rebuild from the ref log
(--rebuild-pointer-index). --repair-main-ref is a recognized input that performs no repair and is always
refused. The full list is in 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_idswas removed outright at Patch schema 2 (0.24.0), not left inert — an incoming Patch carrying a non-empty one (legal only at schema 1) is still 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. 0x0AwasProjectGenesis, removed by the repository-identity settlement. The code is permanently retired and must never be reassigned — see Trust Roots and Roles for why repositories carry no identity to anchor.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 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.
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, a distinct peer/sync trust framework, or stable migration policy yet, and no AUTHOR-identity revocation. Sync and tag adoption make no new trust decision of their own — every trust check either resolves against locally-held history or the receiver’s own already-adopted keys; see Trust Roots and Roles below for the tag-adoption specifics. 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 eight live durability
guarantees (G1–G4 and G6–G9 in
crates/prikk-store/src/foundation/fsutil/contract.rs; G5 was 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.
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. The same gate — the local operator’s own signer checked
against the local trust policy before any object or ref write — applies to every operation below
(prikk tag create/prikk branch create/prikk branch close since commit 053e442; prikk ref complete since RFC 165 R4 — there it gates the completing signer, separate from the check of the
original signer the interrupted publication’s own leading RefState already carries):
seal, merge, sync build, sync seal, sync adopt-tag, prikk tag create,
prikk branch create, prikk branch close, prikk ref complete.
This list is derived, not transcribed: it names every GatedOperation variant verify_signer_trusted
accepts (crates/prikk-store/src/trust.rs), and a test binds it bidirectionally against this page
(RFC 118 stage 3) so it cannot silently drift. That binding proves only that this page and the enum
agree on which operations gate — not that every operation which ought to gate does.
prikk tag create itself published maintainer-signed objects for months before 053e442 added this
exact check, and no enumeration of gated operations can catch an operation that is absent from it. That
is a standing, unenumerable risk this list cannot close, and it is recorded as an open item, not implied
away by the list’s own precision.
Tag adoption is the receiver’s own signed act (RFC 117 T4), never conjured from someone else’s
assertion. sync build/accept can move a Tag object into the received namespace, but arrival is
not adoption: trust does not expand on receipt. A received tag’s own MAINTAINER signature is
checked and its outcome (Sound against an adopted key, or Unverifiable against one this repository
has not adopted) is reported by sync tags — but that outcome never gates anything. An Unverifiable
tag is stored and reported exactly like a Sound one. Nothing advances until an operator explicitly
runs sync adopt-tag <name>.
sync adopt-tag creates a new, local Tag object, signed under the receiver’s own maintainer key —
not a copy of the sender’s. The sender’s tag and the receiver’s tag are different objects sharing the
same global identity: a locally re-signed tag carries a different author_key_id, and therefore a
different signature and a different object id, even though both name the identical patch set (a tag
names a patch set, not a block, which is why a received tag’s target has to be resolved against the
receiver’s own local blocks rather than pointed to directly — blocks diverge across repositories, patch
sets do not). Adoption requires the receiver’s own signer to be locally trusted — the same
trust-on-first-use maintainer policy described above, checked the same way seal checks it. Adoption
does not verify who the sender is. It does not check the received tag’s own signature outcome at
all: whether the sender’s tag was Sound or Unverifiable has no bearing on whether adoption succeeds,
because adoption asserts what the receiver now holds and signs under its own key, not anything about
who sent it.
Adoption refuses rather than guesses. It refuses if no received tag carries the requested name, if more than one received tag claims that name, if the named patch set is not held locally yet (not enough of this repository’s history has been synced), or if the patch set resolves to more than one local block. Each refusal is a person’s decision not to act on an unresolved question — not a cryptographic guarantee that the correct answer was found.
Repositories are anonymous. Identity lives in signer keys and in patch ids — never in a
repository. This is a settled property of the shipped design, not an open question or a deferred
non-goal: prikk has no peers, only artifacts. crates/prikk-cli/src/sync.rs
opens with “No network. No socket. No new dependency” — every sync subcommand reads and writes
files, and there is no session, no remote party, and nothing to authenticate as a repository in the
first place. Checkable, not merely asserted:
RecognitionClaimPayloadcarriesblock_id,patch_ids, andparent_block_ids— content ids only, never an originator field.SyncSummaryRefEntrycarriesref_name,digest, andpatch_count— again, no originator field.- Tags travel and are adopted under the receiver’s own key, above, not the sender’s.
- All trust is local (
trust maintainer add), gated throughGatedOperation.
An artifact asserts nothing binding. A claim never gates admission and never confers trust: the
receiver applies the claimed order and either it produces a valid state — which the receiver then
seals under their own key, verify_signer_trusted unchanged and still gating — or it does not. A
hostile or simply wrong claimed order cannot forge a state. So “what is a remote permitted to
assert?” has an answer: nothing that binds the receiver. The receiver is the sole authority over its
own store.
What this forecloses. There is no repository identifier to spoof, no peer to impersonate, and no origin field a receiver could be fooled by, because none of the three exists. That is a security property this design has, not a gap in it — the “global identity trust” and “remote trust” entries under Threat Boundaries’ non-goals list below describe capabilities not yet built, not this: there is no repository identity for a future increment to eventually add trust around, because the design never creates one to begin with.
Key Input and Local Trust Store
Current key input is intentionally minimal. The CLI reads AUTHOR key material from
a seed file (PRIKK_AUTHOR_SEED_FILE, else the key directory’s author.seed) and a key id
(PRIKK_AUTHOR_KEY_ID, else the key-id file beside the seed, else the legacy author), and MAINTAINER
key material the same way.
A key id is a name, not an identity. A new key’s default id is ed25519- and the first 16 hex
characters of its public key, so two installations do not collide by default. Nothing makes an id
unforgeable: anyone can sign under any id. What enforces identity is binding: a repository binds one key
id to one public key (trust-on-first-use for AUTHOR material, explicit adoption for MAINTAINER keys), and
refuses a different key under a bound id. Installations made before 0.45.0 share the legacy ids author
and maintainer, so they collide under that binding. The refusals say so, and name the route: new
history signed under a distinct id. A seed never travels through the
environment. The seed values are caller-provided 32-byte
Ed25519 secret seeds encoded as 64 hex characters. Prikk does not provide local secret storage,
but it does provide key generation and public-key derivation: prikk key generate draws a fresh
seed from the OS CSPRNG, prikk key public [--seed-file <path>] derives a public key from a seed
already held, and prikk setup composes both with init and trust maintainer add into one
command (RFC 135) — see First Run. 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.
prikk trust maintainer list and prikk trust maintainer check --key-id <ID> (RFC 138) read that
same set without changing it: list enumerates every currently adopted key and check answers
whether one specific id is adopted, both with --format json for a machine-readable answer. Both
answer “whose signatures does this repository accept on objects” — object trust, not ref
authority: adopting a key never lets it move a ref by itself. check exits 0 when the key is
trusted and 1 when it is not (amended 2026-10-14; the original ruling exited 0 either way,
which let check && … proceed for an untrusted key — a misreading that fails open on a trust
question). 1 also stays the exit for an unreadable policy, since both mean “do not proceed”; 2
is reserved for a missing or malformed --key-id. check && … now proceeds only for a trusted
key, and --format json tells “not trusted” (an answer object with "trusted": false) from
“cannot answer” (no answer object, an error on stderr).
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. An exact
interrupted publication is finished by seal’s retry or by prikk ref complete <ref> (RFC 165 R4); the
completing key must be an adopted maintainer key, and the original signer is not needed (see the gated
operations list above, which names prikk ref complete).
How seal derives the state it signs. It starts from the nearest checkpoint snapshot on the derivation line
and folds the blocks after it, comparing each folded block’s state root with the root that block signed. A
checkpoint is used only if this repository has replay-verified it (the record in .prikk/cache/), a maintainer
key it has adopted signed it and the signature verifies, its manifest validates, and it is at most 63 blocks back;
otherwise seal replays the whole lineage. The record is a rebuildable local file and decides only which
authenticated checkpoint to use, never whether one is authentic. What remains is a trusted maintainer who signed
a wrong root, together with write access to .prikk/cache/. Below the checkpoint seal still checks that every
block and patch is present, of the right type and schema, decodes, has the right shape, and that the lineage reaches
its first block, and it still checks a restored file against what was deleted; it no longer re-reads the content
of files no current file refers to, which prikk verify still checks.
A cold commit’s baseline, since 0.53.0 (F4), uses the identical checkpoint and the identical admission
rule. The same four conditions above decide which checkpoint a cold commit’s baseline may start from, and a
live file’s kind, seeded from that checkpoint’s own manifest, is therefore one an adopted maintainer signed,
not merely one a reader happened to check. The residual is the same: a trusted maintainer who signed a wrong
root, together with write access to .prikk/cache/. Below the checkpoint, a cold commit no longer re-reads the
content of a file it does not itself touch; prikk verify still does.
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, RefUpdate, and locally-published Tag envelopes against the repository-local
maintainer trust policy. A received, not-yet-adopted tag’s own signature is deliberately not part of
this check — see Trust Roots and Roles above. 0.54.0 (RFC 164 Rule
E): a stored Block or RefState that nothing in committed state reaches (no local ref, received
pointer, queued patch, or sealed block descending from them) has its publication trust check skipped
entirely, rather than checked and potentially failed — reported instead as a counted, non-blocking
unreached object warning. RefUpdate and locally-published Tag envelopes are unaffected; their
own publication trust is still checked regardless of reachability.
verify does not prove that a repository is globally trustworthy. It does check every reachable
Patch’s AUTHOR signatures 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.
Every signature of a role is checked, not only the first: an object may carry several signers
(format 7), and a second AUTHOR signature that does not verify fails verify just as a first would; so
does a MAINTAINER signature by an adopted key on a publication object. 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.
Several signers on one object. Signatures are not part of an object’s id, so two signers of the same
content produce the same id — in format 7 their copies merge into one stored object carrying both
signature sets. Nothing joins a stored object unchecked: bundle import and sync accept verify every
incoming signature first and refuse the whole import or exchange, writing nothing, if one fails; a
MAINTAINER signature is kept only when this repository has adopted its key, and is otherwise dropped and
reported; an AUTHOR signature needs recorded or carried key material, under unchanged trust-on-first-use
binding. show names every AUTHOR signer of a rename; author_key_id stays the first in canonical order
(key id bytes), which is ordering, not precedence — no signer on an object outranks another.
At most 4 counted signatures per object, at the door. Every signature verify checks costs a
verification, so bundle import and sync accept refuse — the whole operation, nothing written — any
object they would store, new or already held, carrying more than 4 counted signatures
(MAX_COUNTED_SIGNATURES_PER_OBJECT). Every signature counts except a MAINTAINER signature by a key this
repository has adopted that verifies; for a new object that includes non-adopted MAINTAINER, CI and AUDIT
signatures, which are still stored as carried so a later adoption can trust them. Counting stops at the
first signature past the limit, so for an object carrying many forged signatures the check runs at most five
verifications before it is refused; the refusal names the object, the count reached when counting stopped
(not the object’s total) and the limit. Local writers (commit, seal, sync seal, rollback drafts, tags)
never check the limit and are never refused. Whether a signature arrived through an import is not
recorded, so the count is read from the object as stored, under the lock every importer takes.
The residual. A signer that is neither adopted here nor the operator can be refused on an object whose set is full, and the operator’s own AUTHOR signatures count toward filling it. That needs a local AUTHOR signature on an object other signers also hold, which in practice only deterministic payloads such as rollback drafts reach.
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.
Not built yet, not refused — this page’s own Core Caveats already say so (“no key rotation,
hardware signing, remote trust, a distinct peer/sync trust framework, or stable migration policy
yet”): global identity trust, remote trust, hardware signing, key lifecycle management (with
one exception — rotating an AUTHOR key under its own key_id is refused, indistinguishably from
impersonation, per Core Caveats above; a new key_id is not restricted), stable repository-format
migration, and multi-maintainer thresholds (the trust store’s required policy already supports a
threshold; only required = 1 is deployed today).
Genuinely unclear from this project’s own record — no statement here or elsewhere on this page calls these either built-later or ruled out, so neither column would be honest: hosted forge semantics, production audit policy, and plugin execution.
What this page’s own repository-identity settlement (above) refuses, by contrast, is permanent — see Non-Goals for the confirmed refused set and what “refused” means as distinct from everything on this list.
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 |
A received tag’s MAINTAINER signature outcome is checked and reported but never gates; nothing advances until sync adopt-tag runs. | tag_travel.rs, RFC 117 T4 |
sync adopt-tag creates a new, locally-signed Tag object distinct from the sender’s, gated on the receiver’s own locally-trusted signer, not on the sender’s signature outcome. | tag_travel.rs, trust.rs, RFC 117 T4 |
| Tag adoption refuses on a name miss, a name collision among received tags, an unheld patch set, or an ambiguous patch-set resolution — it never picks. | tag_travel.rs, patch_set_digest.rs |
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 |
prikk trust maintainer list/check read the adopted-key set without changing it; check exits 0 for a trusted key and 1 for a key that is not (amended 2026-10-14; 1 also covers an unreadable policy). | main.rs, output/trust.rs, trust.rs (load_maintainer_trust_policy_or_empty) |
| Verify checks publication trust for Block, RefState, RefUpdate, and locally-published Tag envelopes; a received, unadopted tag is deliberately exempt. | 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 bundle export (introduced at PBNDL002, still carried unchanged at the current PBNDL003). 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, and
required-syncs the WAL file. Any required file 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. For the WAL, the tail is
everything after the last sound record, when no sound record follows it — whatever its shape (RFC 162
rule 3): a genuine interrupted-append prefix, zeros, garbage, or a last record whose header parses but
whose own checksum does not match. If a sound frame (magic, a valid header, a body that passes its
checksum) starts anywhere in the remainder, the frame at that offset is not a tail; it is interior
damage, reported as a failed record at its offset, and the sound records after it are still read. The
only question that decides tail from damage is whether a sound record follows the fault — not the
fault’s own shape, and not whether it merely looks parseable: a sound record anywhere behind it is always
damage, and nothing behind it is always a tail, including a last record whose own checksum fails (above).
A true tail is the only case doctor --repair-wal-tail truncates. (Before 0.48.0 the tail was the narrower, shape-defined case only —
a structurally incomplete frame, too few bytes for its own header or claimed body. A complete-but-invalid
last record — a checksum mismatch, an unsupported version, a malformed envelope — was refused as damage
even with nothing sound behind it, the same as interior damage still is. RFC 162 rule 3 widened the tail
from a question of shape to a question of position: not “does this parse as a legitimate partial frame”
but “is this the last thing in the file, with no sound record after it.” Widening loses nothing, because a
repair keeps every byte it removes, below — including, now, a record that was in fact a real write, torn
in a way this file cannot tell apart from a crash.)
Fixed in 0.49.0 (RFC 167, M5): checking “does a sound record follow the fault” used to cost time proportional to the square of the file’s size on a file packed with fake frame headers, each claiming a body reaching exactly to the end of the file — every one of them had to be fully read and checksummed before the scan could rule it out, and on this shape nearly every candidate claims nearly the whole remaining file. 0.49.0 gives the scan a work budget (bytes hashed, at most 8x the file’s own size, shared across every candidate it checks and every ordinary record check in the same read); once the budget runs out the scan stops and the file is damage, not a tail — ambiguity always resolves to damage, so a scan that cannot finish is treated exactly as a sound frame it did find would be, never as if it had found none. The same mechanism and bound apply to five other files with the identical scan: object containers, trust policy, the received index, the ref log container, and the pointer index.
Fixed in 0.49.0 (N6): a damaged last record used to read as a tail, and the repair could remove one the
user was told had succeeded. A record whose own bytes are all present but whose checksum fails (bit rot, a
partial write the storage layer itself reordered, and similar) is, by rule 3, indistinguishable from a
genuine crash-torn prefix once nothing sound follows it, so before 0.49.0 verify exited 0 and
doctor --repair-wal-tail truncated it either way, even when it had already been acknowledged. 0.49.0
writes a small local witness with each commit — the count, Patch id and frame hash of the last record it
acknowledges, checked against the WAL independently of the WAL’s own content — so the two cases are told
apart: a genuine, never-acknowledged crash tail is still removed exactly as before; an acknowledged record
that is now damaged or missing refuses instead (verify exits 1; commit/seal/rollback-draft refuse,
naming it), and prikk doctor --discard-damaged-commits [--plan-only] (RFC 166 D5) is its own way out — the
removed bytes are still saved to the recovery log (recovery/log), exactly as --repair-wal-tail saves them, before the
witness is rewritten to cover the resulting sound prefix.
A repair keeps every byte it removes. A record whose only fault is a damaged length, with nothing sound behind it, is
indistinguishable from an interrupted append, so --repair-wal-tail truncates it. Before it does, it appends exactly the bytes it will remove to
the recovery log (recovery/log, durably, under the same lock), and only then truncates; its output names the entry. prikk doctor --recovery-list
shows the entries by run, and prikk doctor --recovery-restore <run id> undoes the run, under the conditions in
Recovery log (the file is unchanged since the repair, its prefix is unchanged, and the files that give the bytes their meaning are unchanged). The
entry holds the raw WAL bytes, so a record that was removed by mistake can also be read from it: the removed region starts at the recorded offset of the
old WAL, with the same framing. If saving the file fails, nothing is truncated. The file is never authority: verify reads it only to print one
line about its damaged regions, and that line never changes its exit status (main.rs:964). Once the log is not
needed, prikk doctor --recovery-clear empties it in place (--plan-only previews this). It also means a repair can be wrong about what it removed without anything being lost, torn tail or damage.
What counts as a tail is not one rule for every framed file (RFC 162):
- The WAL and the pointer index end at their last sound record — the position rule above — because the pointer index leads the log by design and gets the same repair as the WAL.
- The object index is a pure cache (rule 1): a damaged tail or interior record there is never refused. A writer rebuilds it from the containers before appending; a reader falls back to scanning them.
- The object containers are classified by connectivity, not by position (rule 2, above): an unparseable frame is a harmless remnant unless something still committed — a sealed block’s state, a queued patch, a ref tip — still needs the object it might have held.
- The ref log keeps its own positive rule (RFC 164 Rule A): it truncates only a suffix that is a prefix of
the record it expected to write next (
refs/container.rs:917-947). A torn tail is a prefix of one well-formed frame, and nothing else. Two repairs reach it:ref complete(RFC 165 R4) for a tail belonging to one ref’s own pending publication, and--repair-tails(RFC 165 R5) for a tail that no ref’s pointer leads (doctor/repair_tails.rs:10-14). - The rest — the received, author-key and trust-key indexes, the trust-policy container, and the generation files — use RFC 164 Rule A’s position rule: the tail is everything after the last sound record, when no sound record follows, whatever its shape. A complete record whose checksum or envelope fails is damage, even when last (RFC 164 §9, §9.2), and is refused, not truncated.
The WAL repair, the pointer index’s own --repair-pointer-index-tail (mirroring the WAL exactly),
the object index’s own rebuild (its lost ids, when it cannot re-derive an entry), and
--repair-tails’s own coverage of every file it truncates all write a recovery file. The one
exception: a ref-log tail attributable to one ref’s own pending completion, truncated directly with
no recovery file, as part of prikk ref complete <ref>’s own write (RFC 165 R4 — seal’s own DC-38
retry is one instance of the same mechanism, not a separate path). A lead-free tail (RFC 165 R5’s
own M4) goes through --repair-tails instead, saving what it removes like every other file it covers.
The recovery log is written in place, so its saves do not depend on a rename. A save is an append to an existing name, flushed, like every other in-place write of repository state (RFC 168 §3.3). The one exception is the log’s first appearance in a repository created before 0.49.0: it is a new name, and on Windows a power loss in the same boot can lose it (RFC 168 §6, residual (b)): a power loss in that boot can keep the truncate and lose the entry. Nothing is ever truncated without the save call returning success first, so on a running system a repair still never drops bytes silently on any platform. “A repair keeps every byte it removes” holds as written on Linux and macOS.
What is written in place (RFC 168 §3.3). The current-branch pointer (truncated, then appended, under the switch marker), the
FORMAT marker (a one-byte overwrite), the commit witness (overwritten from its first byte, then its length set, never truncated
first), and ref-name (truncated, then appended). A torn write of any of them is refused or rebuilt, never read as cleared. The
four caches keep their replace, and worktree files keep theirs (RFC 168 §3.4, residual (a)).
One consequence to know: a crash-torn append of a blob whose content is itself a prikk container file leaves a partial frame with a sound frame in
its payload; by the rule that is damage, and doctor --repair-index indexes the embedded frame as an object. That object is content-addressed and
nothing references it, so it changes no state root and no signed output.
An interrupted append in an object container is not damage unless something still needs it. A container record is made durable before its index
entry is appended, so a frame that fails to parse (a genuine prefix, not a complete-but-invalid record) is what a crash between the two leaves.
The object index is never the witness of this (it is a pure cache; see above) — verify checks connectivity instead: every object a sealed
Block’s state, a queued Patch in any active session’s WAL, or a ref tip references must exist and read. An unparseable frame is reported as
possibly holding a missing object, and named as one, only while connectivity reports something of that container’s own type still missing;
otherwise it is a harmless remnant. verify and doctor report it as a warning (interrupted appends: N, PRIKK-DOCTOR-OBJECT-INTERRUPTED-APPEND)
naming the offset, and exit 0 unless connectivity itself fails — which fails doctor too, not only verify; every later object is still scanned
and read. A frame an index entry names, that the containers cannot actually produce, stays a failed item. The active WAL has no such case (an
append refuses past a tail, so an interior partial frame is always damage), and the ref log keeps a failed item for it today, because nothing at
the reader names a ref-log record.
RFC 164 round 2 Rule E (0.49.0) generalizes the same damage-vs-remnant question to a complete, decodable object that itself names a missing
reference — not an unparseable frame (the case above), but a fully sound Block whose own parent_block_ids, patch_ids, or snapshot_blob_ref
names an object that does not exist. Before this round that was always a hard failure, regardless of whether anything still needed the Block making
the claim. Now: verify/doctor compute, fresh on every run, every object id reachable from committed state (a ref, a received pointer, a queued
patch, or a sealed block reached from them — the same connectivity roots the paragraph above already uses, walked further: a ref’s RefState reaches
its own target Block or Tag and its prior RefState lineage; a Tag reaches its target Block; a Block reaches its own parents, patches, snapshot blob,
and merge lineage). A Block with a dangling reference is damage, exactly as before, only if the Block itself is reachable this way; otherwise it is an
unreferenced remnant — a warning naming the object and what it lacks (“re-run the import if you still have the bundle; otherwise it is harmless”),
and verify exits 0 over it, same as the frame-level case above. No command removes a remnant in 0.49.0. A remnant made reachable afterward (a new
branch created over it, say) is reclassified as damage on the very next run, since reachability is never cached. Scope, this round: only a Block’s
own three reference fields get this treatment — RefState’s and Tag’s reference fields are existence-checked only where a ref reaches them: a ref tip’s
target (a Block, or a Tag and then its Block) must exist when verify reads the ref (refs/verify/scan.rs:180,
via ensure_ref_target_valid, refs/verify/scan.rs:443), and a received tip’s previous RefState must exist
(verify.rs:1951). A RefState or Tag that no ref tip points at is not checked, independent of
Rule E, and extending that is separate, larger scope this round does not cover; RecognitionClaim’s own references stay untouched by design (never
trust-conferring, never existence-checked).
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. Since RFC 166 (0.49.0), the same issue is also raised when the metadata is present and
well-formed but disagrees with the session’s own commit witness (D6) — a session cannot have two
owners, so the two are checked against each other, not only the metadata’s own shape. Either shape
refuses with the same text, naming prikk doctor --restore-queue-target --ref <ref> [--not-current-branch] [--plan-only] (RFC 166 D5, as amended by §14) as the way out: the ref always
comes from the caller, never from the witness, and the previous metadata bytes are saved to recovery/log before the
file is rewritten in place (truncate, then append; RFC 168 A1). The WAL here is already non-empty, so a tear between
the truncate and the append can leave the file empty; the saved bytes are what undo it, by --recovery-restore <run id>.
Which branch the verb will accept (RFC 166 §14, 2026-10-05). D5’s original condition —
refuse unless the queue “validates against <ref>’s current tip by the same check seal makes” —
turned out not to exist: seal checks no such thing (a Patch names neither its own ref nor its own
base), so a queue built on one branch could be silently attached to, and then published onto, a
completely different one. §14 replaces it: with a readable commit record, <ref> must equal the
record’s own ref, unchanged; without one, <ref> must be the caller’s own current branch (the same
resolver commit uses for its default) unless --not-current-branch is given, and the same flag is
required when the current branch cannot be resolved at all. A restored owner is final for the verb —
a second restore refuses, the same as any healthy session.
Residual, accepted by the owner. A queue made with an explicit commit --ref X (or
rollback-draft on X) while the caller’s current branch was Y can still be restored to Y
without --not-current-branch, if the caller does not recognize their own commits in the plan (which
names each one’s own message and paths) and skips --plan-only. This reaches only a queue with no
readable commit record — a readable one already pins <ref> to itself — and is no worse than the
0.20.0-0.48.0 hand-edit this verb replaces.
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 suite that compiles
there natively on macos-latest/windows-latest; their per-platform test counts are refilled from the
candidate’s CI run and are not stated here) — with the caveat
that DC-76’s negative controls (eight remain; G5 retired in DC-98) are demonstrated on Windows for six of them:
G1, G2, G3, G4, G8, and G9 (as a documented no-op). G3 and G8 use the same failpoint injection mechanism as
Linux/macOS, wired into foundation/fsutil/anchored/windows.rs by DC-98 (failpoints.rs is compiled for Windows).
G6 and 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.
State that cannot be rebuilt is written in place and flushed, not renamed (RFC 168 §3.3; the list is in
What is written in place above). The write sites are overwrite_in_place_required callers: FORMAT
(format_upgrade.rs:83), the commit witness (commit_boundary/witness.rs:214), and a recovery restore
(recovery_log/restore.rs:336). Only the four caches (commit index, verified blocks, lifecycle cache, object
index) and worktree files still use the atomic replace (write_file_atomically, write_worktree_file_atomically):
a unique same-directory exclusive temp, complete file sync, atomic replace rename, and required parent sync
(foundation/fsutil/anchored/linux.rs:30). 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. If the final log frame is
structurally incomplete (a genuine tail, never a complete damaged record), the completion path below
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 — --repair-tails refuses outright on them (RFC 164 §9.2).
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 the completion path below revalidates the transition, appends nothing new, and removes active state.
prikk ref complete <ref> (RFC 165 R4)
The general completion verb — seal’s own DC-38 retry is now one instance of the same mechanism,
not a separate path. Every condition evaluated before any write, in order: (a) the leading RefState
verifies under the current trust policy, signed by an adopted maintainer key — any adopted key,
not only the one that started the publication; (b) it chains — its ref name, its previous state equal
to the log’s own tip, the next sequence; (c) its target exists, with the kind the ref requires; (d)
for a publication that consumed the active WAL (seal, sync seal), the retained WAL evidence still
matches; (e) no complete damage anywhere in the ref log or the pointer index. Any one failing refuses,
writing nothing.
K1: --plan-only prints the ref, the leading RefState id, its signer key id, its target and
kind, the log tip it chains to, the sequence, the completing key, and any partial tail it would
remove — the identical plan a real run prints before writing, both from the same one computation.
The write: one signed ref-log record, by the completing key, through the exact same write path
publish_locked’s own completion branch already uses — never a new code path for the append itself.
prikk doctor --rebuild-pointer-index (RFC 165 R5)
The way out when the pointer index — not the ref log — is what is damaged or untrustworthy: a complete damaged pointer-index record, or a stale pointer reading behind the log (the common cause of the latter: the damaged record is the ref’s own newest one, so the damage-tolerant reader falls back to an older, already-sound entry). Re-derives every ref’s own state from the ref log directly, structural and never trust-filtered — no signature is re-checked for a record already durable in the log, only a current lead re-enters trust (the same R4 rule above, reused, not re-derived).
It refuses, writing nothing, if the ref log itself has damage or a tail (repair that first — the
rebuild’s own source of truth must be trustworthy before anything is derived from it), if the pointer
index’s own container has a tail (0.50.0 step 1 A6 item 5: named --repair-tails the identical way the
generation log’s own tail already was, since 0db67cc4 — before this round the plan proceeded over it
and the torn slot was abandoned, unsaved, on the next write), if the generation log itself has a tail, or
if any lead anywhere in the repository is completable: completing it is the correct fix, and a
rebuild would otherwise drop an authorized transition, the one outcome this verb must never produce. A lead that
fails R4’s rule is dropped and named — unless it fails only by depth, more than one transition
ahead while every RefState in it still verifies and chains soundly back to the log: dropping that
would discard every one of those transitions, not only the first, so it is refused instead,
naming the ref and the depth, with the way back (restore the ref log from a copy, then run the
rebuild again). A merely stale pointer (behind the log) is restored, a distinct outcome from
either — nothing authorized is discarded restoring one.
K1: --plan-only prints, per ref, its state before and after, and every dropped lead or restored
ref — the identical plan a real run writes from.
The write: truncates the ref-pointer index’s other slot, writes the rebuilt records, then switches
the generation log to it — atomic, as compact already does for the same container; the old slot
survives until the next compaction. Only when every ref’s state would actually change (0.50.0 step
1 A6 item 5): when the pointer index already matches the log for every ref, a real run writes neither a
new slot nor a new generation record, and says so (“pointer index already matches the log; nothing
written”) — before this round it wrote a new generation unconditionally, even over an already-healthy
index.
The way back (0.50.0 step 1 Parts F/F2): before flipping away from the live slot, a real run
saves a full copy of it, the rebuilt slot’s own pre-rebuild bytes, and the generation log’s own
before/after bytes, to the recovery log under one run – prikk doctor --recovery-restore <id> undoes
the switch byte for byte, rewriting both slots and re-appending a generation record naming the old
slot live again. A restore refuses, and writes nothing, if anything since has changed what it would
overwrite — most notably an ordinary write landing in the rebuilt slot (a new branch, a publication):
its own saved hash, checked fresh at restore time, is what catches that, not a second file’s state.
A Write Never Buries a Crash State (RFC 163)
The rule, at six files: before an append, the writer confirms under its lock that the file ends at
its last sound record. If it does not, it refuses before appending to that file, naming the file, the
byte offset where the sound content ends, how many bytes follow, and the way out. For four of the six
(the trust-key and trust-policy containers together, the received index, and every generation log) this
is also a whole-operation guarantee: the command writes nothing at all when it refuses. For the other two
(the pointer index and the author-key container) it is narrower: only the guarded file itself is left
untouched — see each bullet below for what else the command may already have written, and why that is
harmless. Before this round, a torn
tail that verify already accepted as harmless (the pointer index, under RFC 162 rule 3 above) or said
nothing about at all (the other five, still under the pre-0.48.0 shape rule, N2) was invisible to the
next ordinary write at these files: the write appended behind it, blind, and turned an accepted crash
state into permanent damage — verify failing for good, and on some of these files a seal or commit
refused too.
RFC 164 Rule A (0.49.0) extends the same tail-by-position rule to all six of these files’ own read
and repair paths, not only the write-side refusal above. Before this: a torn prefix at these files
already read as a repairable-looking tail, but 100 or more zero or random bytes at the same position
read as damage under their own pre-0.49.0 shape rule – readers refused (“<container> has a damaged
entry”), and no repair verb existed for any of the six (or the three generation logs, listed together
with them below). Now: a tail is a record whose header or body is incomplete, or bytes that are not a
record header at all (no magic, or an unknown version), when nothing sound follows – a torn prefix,
zeros, or garbage all count, the same rule RFC 162 rule 3 already gave the WAL and the pointer index –
and readers tolerate it. verify/doctor report a tail (a warning, naming the file, the offset, the
byte count, and the repair) and interior damage (a sound record following bad bytes) separately, and
prikk doctor --repair-tails truncates every tail across all ten covered files (the seven RFC 164 Rule A files, the WAL, the pointer
index, and the ref log) in one run, saving what it removes first, refusing
before touching anything if any covered file has interior damage – see “Doctor Repair Boundary” below
for the full mechanism. The bullets immediately following describe the write-side refusal RFC 163
already gave each file, which RFC 164 does not change.
RFC 164 §9 (Addendum 1, 0.49.0): a complete record is never a tail, even when it is last. Rule A’s
first accepted text said “whatever its shape,” which let a complete record (its own header valid,
its whole claimed body present) whose checksum or envelope fails be truncated as if it were a crash’s
torn remnant. It is not one: a crash can only interrupt a write, so everything a crash leaves is
incomplete; a complete record was fully written, and a failing checksum on it is corruption after
the fact. For files whose last record carries a decision – the trust policy’s latest snapshot, a
generation log’s live slot, the pointer index’s newest pointer – silently removing it re-asserts the
previous decision: measured on a real build, a single flipped byte in the trust policy’s newest
snapshot, followed by the pre-§9 repair, silently re-trusted a key that snapshot had just revoked, with
verify exiting 0 throughout. §9 corrects this for the seven Rule-A files and the pointer index (the
WAL keeps RFC 162 rule 3 exactly as it was – see below): a complete record whose checksum or envelope
fails is damage, even when last. verify fails and names it; --repair-tails and
--repair-pointer-index-tail refuse and change nothing; readers fail closed on it exactly as they do
on any other interior damage, never silently resolving to an older, undamaged state. The cost, accepted
knowingly: a single corrupted byte in the last complete record of one of these files now stops the
commands that read that file (for the pointer index’s own generation log, that is nearly every
command), where before it was quietly repaired into a rollback. The way out is prikk doctor --rebuild-pointer-index
(RFC 165 R5; see Ref Pointer and Ref Log Recovery above), which re-derives the pointer index from the ref log and
refuses while the ref log itself is damaged or has a tail, or while any lead is completable. Restoring the file
from a copy is the other way out. The WAL is
deliberately not part of this correction: removing its own damaged last record loses a queued commit,
which is saved to recovery/log (under .prikk/) and disclosed (N6) – a loss of work in progress, not a rollback of
already-committed trust or ref state. The commit witness (N6, above) tells a genuine crash apart from later damage
there, so an acknowledged record is never removed as a tail.
RFC 164 §9.2 (Addendum 2, 0.49.0): the checksum decides whether a record is complete, not its header.
§9 decided completeness from the header’s own magic, version, and length fields – but any one of those
three can itself be the single corrupted byte a complete write left behind, no more a crash’s own
signature than a flipped body byte is. A flipped header field read as a tail under §9’s own rule, and
every reader rolled back to the state before the last record before any repair even ran – measured on
a release build: a single flipped magic, version, or length byte in the trust policy’s last snapshot, or
in the pointer index’s or its generation log’s, and trust maintainer check/branch list showed the
previous state immediately, with no --repair-tails involved at all. Corrected: at a tail candidate, the
checksum is recomputed with the format’s own real magic and version constants – never the stored,
possibly-corrupted bytes at that offset – over two candidates: the body the stored length claims, and
the body that runs to the end of the file (catching a corrupted length field itself). A match either way
means the record is complete, whatever its header says; only genuine corruption of the checksum or body
itself still resolves as a tail when nothing sound follows. One shared helper
(frame_resync::complete_by_checksum), called by all six decoders (trust keys, trust policy, author
keys, the received index, the three generation logs, the pointer index) and their write-side tail scans
alike, before any of those three header fields is trusted. Verified exhaustively, not sampled: a
store-level test per decoder flips every single byte offset of one complete record (decode only, cheap)
and asserts none of them decodes to a tail; on a release build, flipping every offset of a real last
record across the trust policy, the pointer index, and its generation log (263 offsets total) produced
zero rollbacks, before or after the repair, in every case. Still open: corruption spanning more than
one field of the last record at once (a zeroed sector, say) can still read as a tail – this rule
resolves a single field’s own corruption against an otherwise-intact record, not multiple fields
corrupted together; a per-record witness (format 8) is what would tell that shape apart reliably, and is
not yet built. The WAL is untouched by §9.2 too, for the same reason §9 leaves it alone.
RFC 164 Rule D (0.49.0): a publishing command’s own content object is now written only after the
pointer index’s own tail/damage check, not before. Before this round, the pointer-index bullet below
described a real gap: seal, tag create, merge, and sync seal each wrote their own new Patch and
Block objects first, and only reached the pointer index’s own compare-and-swap check – the same read
that now also checks the tail – immediately before the RefState object that publication itself writes.
A refusal there left the Patch and Block already durably written, unreferenced by anything until the
pointer index moved; harmless (nothing references them, verify exits 0), but unnecessary work repeated
on every retry until the tail is repaired. Each of these four commands now calls the same tail/damage
check once more, at the very top of the command, before that earlier content write – branch create
needed no change, since it writes no content object of its own before reaching the pointer index at all
(see below). The author-key container gets the same treatment for a new author’s first commit: the
check that used to run immediately before the WAL append now also runs at the top of
author_worktree_patch, before the commit’s blob, object-index, commit-index, and lifecycle-cache
writes. In every case, the retry after prikk doctor --repair-pointer-index-tail/--repair-tails still
reuses whatever was already durably written rather than writing it again – that half of each bullet
below is unchanged; what moved is only how much gets written before the file’s own tail is ever checked.
Refuses only when that write would actually append. An operation that turns out to be a no-op for one of these files — re-adding a maintainer key already adopted under the same public key, removing one that was never adopted, a commit by an author whose key material this repository already recorded — appends nothing, and is unaffected by a tail on the file it would otherwise have written to. The check still runs, and still reads what the operation already reads; it is the refusal that is conditioned on whether an append is actually about to happen, not the read.
The six files, and the way out:
- The pointer index.
seal,tag create(andsync adopt-tag),merge, andsync sealeach call the pointer index’s own tail/damage check at the very start of the command (RFC 164 Rule D, 0.49.0) — before writing their own Patch or Block objects — namingprikk doctor --repair-pointer-index-tailon an unclean tail; the repair already exists (RFC 162). The same check still runs again, redundantly and harmlessly, at the compare-and-swap step every publication performs immediately before its own RefState write.branch createwrites no content object of its own before that compare-and-swap step, so for it this one check was already both the first and the only content write in the command — it carries no separate early call. This is a claim about the pointer index file only, not about the whole command: a refusal over the tail itself now precedes every content-object write these commands make, so a retry afterprikk doctor --repair-pointer-index-tailstarts clean rather than reusing objects a refused attempt left behind. - Trust keys and trust policy, together.
trust maintainer add/removerefuse the same way, entirely before either container’s first write — a fix in this same round, after review: the first shape checked and appended to the trust-key container, then checked the trust-policy container, so a torn policy tail alone (the key container clean) left the key recorded but the policy refused, a half-applied write. Both containers’ tails are now decided before either is appended to, so the whole operation writes nothing at all when it refuses, whichever container’s tail caused it (RFC 163 §2 Addendum 2). The way out (0.49.0):prikk doctor --repair-tails, then retrytrust maintainer add/remove. - The received index.
bundle import(neversync accept, which does not touch the received namespace) refuses the same way, entirely before its first write — before even the bundle’s own objects are written, the same pre-write phase the author-key check already ran in (0.44.0, GHSA-px5q-233r-6hq5: a refused import must write nothing at all). Also refuses on a damaged entry, not only a torn tail (external review 016, N9, fixed the same round the guard’s tail-only walk was added): before the fix, the same walk resynced silently past 100 or more zero or random bytes and appended behind them, burying damageverifyhad already reported — the one guarded writer where that held, since the four sibling files below already refuse through a lookup that fails on any damaged entry. The way out for a tail (0.49.0):prikk doctor --repair-tails, then retry the import; genuine interior damage (a sound record follows the bad bytes) still gives no truncation advice, the same as the sibling files’ own damaged-entry case below. - The author-key container. A commit by an author key id not yet recorded now refuses on this check
at the very start of
author_worktree_patch(RFC 164 Rule D, 0.49.0), before the commit writes its own new blob content, an object-index entry, or updates the commit-index and lifecycle caches. The same check still runs again, redundantly and harmlessly, immediately before the WAL append that would queue the commit — exactly where it ran before this round, and where a commit by an already-recorded author key id still only ever reaches (see “Refuses only when that write would actually append” above). The way out (0.49.0):prikk doctor --repair-tails, then retry the commit. - The generation log, at each of the three compacting containers (the pointer index, the received
index, the trust policy container).
compactrefuses the same way, before its first write — before the retired slot is truncated, not only before the generation record itself — and only in--executemode: a--plan-onlypreview writes nothing and is unaffected by a tail on a log it will never write behind. The way out (0.49.0):prikk doctor --repair-tails, then retrycompact.
Where each check reads from. No new whole read was added where an existing one could carry the
answer: the pointer index’s guard rides the same replay ensure_current_matches’s own compare-and-swap
check already performs; the trust-key and trust-policy guards ride the same replay
add_trusted_maintainer/remove_trusted_maintainer already perform to compute the current key id list
and look up the key being added; the author-key guard rides the same replay
check_author_key_conflict already performs; the generation-log guard rides the same replay
resolve_live_slot already performs to pick the live slot. Only the received index’s guard is a new read, moved to
bundle import’s own pre-write phase: write_received_pointer reads nothing before appending today
(there is no CAS to enforce), so there was no existing read to build the check on. It grows with every
import that records a pointer, not only with the number of distinct remote refs (compact is what
reclaims stale entries) — measured on a release build at 1,000 and 10,000 entries, a lean tail-only walk
(no entry decoded, no allocation per record) against a full decode: 1,000 entries (159 KB), 182 µs
against 256 µs; 10,000 entries (1.6 MB), 1.65 ms against 2.01 ms. Both are still linear in the file’s own
size — checksumming every byte is unavoidable for telling a sound record from damage — so this is a
bounded saving, not a change of complexity class; at these sizes the absolute cost is small either way.
Not covered, on purpose. The object index keeps RFC 162 rule 1 (a writer rebuilds it before
appending, rather than refusing). The object containers keep rule 2’s connectivity classification, but
now also report an aggregate tail count per persisted object type (RFC 164 Addendum 1, N7:
verify/doctor print “trailing partial <type> container bytes” for each of the seven persisted
object types, 0 for a clean container) — reporting only, no repair; a frame that does not parse is
still classified as before (a warning if its own checksum never verified, connectivity-checked damage
otherwise), unchanged. The ref log keeps its own positive truncation rule (it truncates only a suffix
that is a prefix of the record it expected to write next) — the ref log was not in this round’s
scope. Its own interrupted publications (N3: a crash inside branch create/tag create leaving its ref-log
record torn, and a later seal of a different ref appending behind it) are settled by RFC 165: R3 makes every
publication refuse while another is incomplete (refs.rs:212, ensure_no_incomplete_publication), and R4’s
prikk ref complete <ref> finishes the interrupted one (main.rs:301; see Ref Pointer and Ref Log Recovery
above).
Doctor Repair Boundary
doctor --repair-wal-tail 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 --repair-index rebuilds the object index from the containers under the object-store lock — the
index is a pure cache, never touched by any other repair. doctor --repair-pointer-index-tail truncates
an incomplete trailing pointer-index record under the pointer-index lock, mirroring --repair-wal-tail
exactly. doctor --repair-tails (RFC 164 Rule C, 0.49.0) truncates every tail across all ten
covered files (the WAL, the pointer index, the seven RFC 164 Rule A files, and the ref log) in one run: ActiveLock
first (covering the WAL, trust keys, and author keys, none of which has its own dedicated container
lock), then the pointer-index, received-index, and trust-policy container locks together (each also
covering its own generation log) — reusing --repair-wal-tail’s and --repair-pointer-index-tail’s
own repair functions for the WAL and the pointer index, unchanged. It reads all ten files once, before
touching any of them: if any one has interior damage, it refuses immediately, naming every such file,
and truncates nothing anywhere. Otherwise each file with a tail is truncated under its own lock, saving
the removed bytes to recovery/log (under .prikk/) first; a file with no tail is reported clean. It is
mutually exclusive with every other repair flag in one invocation (args.rs:544-556).
None of the seven repair flags that change repository state ever removes a sound record: the tail repairs
truncate only what a true torn tail covers, --repair-index rebuilds only the cache, and
--discard-damaged-commits, --restore-queue-target and --rebuild-pointer-index act only on the rows their own
plans name; on genuine damage each refuses and changes nothing. This held exactly to the letter of “whatever its shape” until RFC 164 §9 (Addendum 1,
0.49.0): a complete record whose checksum failed used to satisfy “whatever its shape” too, and both
--repair-tails and --repair-pointer-index-tail would truncate it – removing a record that was, in
fact, sound in every way except its own checksum, and rolling back the decision it carried. §9 closed
that for a corrupted body byte; §9.2 (Addendum 2) closed the matching gap for a corrupted header
field – a flipped magic, version, or length byte rolled readers back before either repair verb even
ran, so completeness is now decided by the checksum (above), not by the header. Doctor diagnoses
ref-publication states but does not sign, promote, or reconstruct ref authority. Its only append is to the
recovery log (recovery/log), made before a repair removes or rewrites bytes (doctor/repair_tails.rs:300-301,
doctor/restore_queue_target.rs:29).
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.
Repairs are not gated on the whole-repository verification result, and a repository-wide error does not by itself
stop one. Each repair checks its own preconditions and writes nothing when they fail: --repair-tails refuses on
interior damage in any covered file, --rebuild-pointer-index refuses on ref-log damage or a tail or any
completable lead, and --discard-damaged-commits and --restore-queue-target refuse on their own plan conditions.
Those three act on the very errors verify reports for their own rows. Repairs also do 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, a fuzzing campaign for WAL/ref-log recovery (the
proptest decoder property tests in crates/prikk-store/src/test_gates/framed_decoder_fuzz.rs are a first fuzz
target, not that campaign), macOS and Windows filesystem validation, stale-lock policy, broad active-session
recovery, ref-log repair beyond ref complete and --repair-tails (a complete damaged ref-log record still needs
a copy, RFC 165), missing-object recovery, object quarantine or garbage collection, multi-ref backup export,
a rehearsed repository-format-migration restore, stable repository-format migration, and
production-readiness claims. Single-ref backup/restore tooling is no longer deferred —
prikk bundle export/verify/import, see Backup and Restore.
Claim-to-Source Anchors
| Claim | Source anchors |
|---|---|
| Commit persistence appends exact signed Patch envelopes to the active WAL and required-syncs the WAL file. | 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 a trailing prefix of the last frame, preserving every complete, sound record behind it; a damaged record with sound records behind it refuses rather than truncating past it (unchanged since RFC 162), and since RFC 166 it also refuses rather than remove a record the active session’s own commit witness already names as acknowledged. | wal.rs, doctor.rs, commit_boundary/classification.rs, PR-012, RFC 166 |
--discard-damaged-commits removes an acknowledged commit the active WAL no longer holds soundly, or declares it lost, saving the removed bytes first, all-or-nothing, the same way --repair-wal-tail does; --restore-queue-target --ref <ref> [--not-current-branch] gives an owned queue its owner back by rewriting the active ref metadata in place, after saving its previous bytes to recovery/log, with the ref always supplied by the caller, never read from the witness, and accepted only when a readable commit record agrees or (absent one) it is the caller’s own current branch or --not-current-branch is given (RFC 166 §14). Both are --plan-only-previewable and refuse, writing nothing, over every row they do not act on. | doctor/discard_damaged_commits.rs, doctor/restore_queue_target.rs, RFC 166 D5/§14 |
| 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, DC-36 |
Doctor’s --repair-main-ref input is recognized but always refused and performs no repair; prikk doctor itself signs nothing and appends to no repository container; its only append is the recovery log (recovery/log), before a repair removes or rewrites bytes. | doctor.rs, DC-38 |
Interrupted ref publication completion (prikk ref complete <ref>) requires an adopted maintainer signer — any adopted key, not only the one that started the publication — and, only for a WAL-consuming publication (seal, sync seal), matching retained WAL evidence. | ref_completion.rs, seal.rs, RFC 165 |
The ref-pointer index rebuild (prikk doctor --rebuild-pointer-index) re-derives it from the ref log alone, structural and never trust-filtered; it signs nothing. | pointer_rebuild.rs, RFC 165 |
| 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 were last
updated for the DC-37 and DC-38 implementations, reviewed as part of the combined 0.18.0 release.
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.
- Prose output fields, counters, severity labels, and issue-code names are current CLI vocabulary, not a
stable machine-readable schema.
prikk verify --format jsonemits the versionedverify-report-v1document (args.rs:147;output/verification.rs:124); doctor has no JSON output.
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;
- active WAL replay, including trailing partial WAL byte reporting;
- object-index and pointer-index trailing-partial and interior-damage reporting (the object index is a pure cache — a reader scans the containers in memory when it is damaged, rather than refusing — so this is never a failure by itself; see durability and crash recovery);
- connectivity: every object a sealed Block’s state, a queued Patch in any active session’s WAL, or a ref tip references must exist and be readable;
- received (
remotes/*) pointers: each pointer’s RefState, target, required attestations, and previous state (theReceivedRefsstage,verify.rs:1484-1489,verify.rs:1916-1960); - the active session’s commit witness (acknowledged queued commits) against its WAL (the
CommitWitnessstage); - 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;
- whether
.prikk/current-branchresolves (0.50.0 step 1 A4) — a warning, never a failure: the pointer is a default--refgiven explicitly always overrides, not an authority anything above checks.
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 and recognized ref-publication state issues are collected separately from hard structural verification errors. This lets the command return 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 reads the received-ref index. Ref pointers imported by prikk bundle import live in
refs/containers/received-index-{a,b}.container. The ReceivedRefs stage checks each pointer’s RefState,
target, required attestations, and previous state (verify.rs:1916-1960), and the index file’s own tail
and interior damage are reported with the other appended files (verify.rs:1273). What verify cannot
see is a pointer that is absent from the index: it checks the entries the index names, and no doctor verb
rebuilds this index.
Verify Output and Exit Behavior
The current CLI prints, among other lines: stage outcomes; object, block-state, ref-file, and ref item counts; checked objects, Blocks, rollback Blocks, sealed rollback Patches, refs, ref-log records, WAL records, persisted WAL Patches, and rollback draft WAL records; ref-publication issues; publication trust records and issues; sealed Blocks; object temp warnings; connectivity issues; interrupted appends; unreferenced remnants; trailing partial WAL, object-index, and pointer-index bytes; one line per appended file and per object container tail; the active WAL metadata state; the acknowledged-commit (commit witness) verdict; and commit-index, lifecycle-cache, and merge-baseline divergence counts. 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.
The command exits with failure, and --format json reports ok: false, when any of these conditions
holds (crates/prikk-cli/src/verify_verdict.rs, VERDICT_CONDITIONS):
stage-failure: a verification stage failed or did not complete;item-failure: an object, Block state, ref file or ref item, WAL record, or received ref failed; or a connectivity issue is present; or an appended file has interior damage. A connectivity issue is an object a sealed Block’s state, a queued Patch, or a ref tip references that cannot be read.prikk doctorfails on the same condition (PRIKK-DOCTOR-OBJECT-CONNECTIVITY), scoped to the active session whose queued Patch references it when that is the source;active-wal-metadata-integrity: a non-empty active WAL has missing or malformed active ref metadata;commit-witness-integrity: an acknowledged queued commit disagrees with the WAL in a way the WAL’s sound prefix cannot explain;commit-witness-substituted-earlier-record: the acknowledgment history disagrees with the WAL’s running hash at a record before the last acknowledged one;blocking-ref-publication: a one-transition pointer lead, matching active state retained after completed publication, or an unproved pointer/log divergence;publication-trust: publication-trust issues are present;commit-index-divergence,lifecycle-cache-divergence,merge-baseline-divergence, oractive-wal-ordering: the named check fails.
A structural verification error before a report can be produced also fails the command.
Trailing partial WAL bytes, trailing partial object-index bytes, object-index interior damage, trailing
partial pointer-index bytes, object container tails, interrupted appends, unreferenced remnants, object
temp files, and an unresolved current-branch pointer are printed as warnings. None of them fails
verify by itself; an appended file’s interior damage does. The object and pointer indexes are caches or
repairable tails, so their damage is never a failure — including when the torn entry is the one most
recently written, which a reader now rescans the containers for rather than reporting as a missing
object (0.50.0 step 1 A5) — but it is never silent either. current-branch is a default, never an
authority (RFC 151 §2.1): nothing above reads it, and an explicit --ref always works regardless of
whether it resolves. An absent pointer is not a warning (every repository initialized before RFC
151, and every one of this repository’s own fresh inits before the file was first written) — a
normal, unaffected state, reported as an informational line instead (0.50.0 step 1 Part D1):
current-branch: not set; commands without --ref use heads/main. 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 | Default session’s WAL has trailing bytes that are a prefix of one incomplete final record (a true torn tail); --repair-wal-tail truncates exactly these. |
PRIKK-DOCTOR-ACTIVE-SESSION-WAL-TRAILING-PARTIAL | warning | The same condition for a non-default active session; --repair-wal-tail truncates that session’s bytes. |
PRIKK-DOCTOR-VERIFY-WAL-RECORD-INCOMPLETE | error | A WAL record failed verification, including a partial frame with a sound record behind it (damage, not a tail); the message says how many sound records follow. No repair switch touches it. |
PRIKK-DOCTOR-OBJECT-CONNECTIVITY | error | An object a sealed Block’s state, a queued Patch, or a ref tip references cannot be read. Scoped to the active session whose queued Patch references it, when that is the source; no repair switch touches it. |
PRIKK-DOCTOR-OBJECT-INTERRUPTED-APPEND | warning | An object container has a frame that does not parse. If connectivity reports a missing object of this container’s type, the message names it as possibly held by that frame and says not to treat it as a harmless remnant; the connectivity finding is what needs repair. Otherwise no repair is required: an interrupted append that nothing still needs was never committed. |
PRIKK-DOCTOR-OBJECT-CONTAINER-TRAILING-PARTIAL | warning | An object container ends in an incomplete final frame. No automated repair; connectivity reports whether anything still needs what the frame may hold. |
PRIKK-DOCTOR-UNREFERENCED-REMNANT | warning | A stored object references a missing object, but nothing reachable from committed state needs it. The message reads object <owner> references missing <role> <id>. Harmless; no repair. |
PRIKK-DOCTOR-OBJECT-TEMP-DEBRIS | warning | A non-authoritative object publication temp remains. Doctor preserves it and does not remove it. |
PRIKK-DOCTOR-OBJECT-INDEX-TRAILING-PARTIAL | warning | The object index has trailing bytes that look like an incomplete final record. The object index is a pure cache (readers rescan the containers when it is damaged), so this is never a failure; --repair-index rebuilds it. |
PRIKK-DOCTOR-OBJECT-INDEX-INTERIOR-DAMAGE | warning | The object index has an interior record that failed to decode. Same non-failure reasoning as the trailing-partial row; --repair-index rebuilds it. |
PRIKK-DOCTOR-POINTER-INDEX-TRAILING-PARTIAL | warning | The pointer index has trailing bytes that look like an incomplete final record. --repair-pointer-index-tail truncates exactly these; unlike the object index, a damaged pointer-index entry (not merely a trailing partial) already fails the Refs stage outright rather than reaching this code. |
PRIKK-DOCTOR-APPENDED-FILE-TRAILING-PARTIAL | warning | One of the eight appended files (the ref log is one of them) has an incomplete final record. --repair-tails truncates exactly these bytes. |
PRIKK-DOCTOR-APPENDED-FILE-INTERIOR-DAMAGE | error | One of those appended files has a record that fails to decode. --repair-tails refuses it rather than guessing which bytes are safe to remove. |
PRIKK-DOCTOR-GENERATION-LOG-AMBIGUOUS | error | Handoff 165 Q1b: a compacting container’s generation log is lost, and its two slots fit two different, equally honest histories (the Q1 review’s own H1/H2 proof) – not damage, typed separately (PrikkError::AmbiguousGenerationLog). Printed first, before the *-INTERIOR-DAMAGE rows below, the current-branch warning, and any VERIFY-STAGE-INCOMPLETE error the same ambiguity causes. The recommendation names the rebuild for the ref pointer index, or a whole-.prikk/ backup for the others. |
PRIKK-DOCTOR-POINTER-INDEX-INTERIOR-DAMAGE | error | 0.50.0 P3c: the ref pointer index has a damaged entry, read directly rather than only through the Refs stage failure above. Printed first, before the current-branch warning and any VERIFY-STAGE-INCOMPLETE error the same damage causes (each of those names this code instead of “inspect the failing stage”). The recommendation names prikk doctor --rebuild-pointer-index --plan-only, then prikk doctor --rebuild-pointer-index. |
PRIKK-DOCTOR-RECEIVED-INDEX-INTERIOR-DAMAGE | error | 0.50.0 P3c: the received index has a damaged entry, read directly. No repair exists; the recommendation names a copy of the whole .prikk/ directory from a backup, never a single file. Printed first, the same as the pointer-index row above. |
PRIKK-DOCTOR-TRUST-POLICY-INTERIOR-DAMAGE | error | 0.50.0 P3c: the trust policy container has a damaged snapshot, read directly. No repair exists; the recommendation names a copy of the whole .prikk/ directory from a backup, then re-applying every trust change made since (an older copy could re-trust a revoked key). Printed first, the same as the pointer-index row above. |
PRIKK-DOCTOR-ACTIVE-REF-METADATA-MISSING | error | Default session’s WAL has records but its ref metadata is missing. |
PRIKK-DOCTOR-ACTIVE-REF-METADATA-MALFORMED | error | Default session’s WAL has records but its ref metadata is malformed. |
PRIKK-DOCTOR-ACTIVE-REF-METADATA-DEBRIS | warning | Default session’s WAL is empty but stale valid ref metadata remains. |
PRIKK-DOCTOR-ACTIVE-REF-METADATA-MALFORMED-DEBRIS | warning | Default session’s WAL is empty but malformed ref metadata remains. |
PRIKK-DOCTOR-ACTIVE-SESSION-REF-METADATA-MISSING / -MALFORMED | error | The same two conditions for a non-default active session; the message names the session. |
PRIKK-DOCTOR-ACTIVE-SESSION-REF-METADATA-DEBRIS / -MALFORMED-DEBRIS | warning | The same two debris conditions for a non-default active session. |
PRIKK-DOCTOR-ACTIVE-SESSION-REF-METADATA-UNREADABLE | error | A non-default active session’s ref metadata failed to read. |
PRIKK-DOCTOR-ACTIVE-SESSION-WAL-UNREADABLE | error | A non-default active session’s WAL failed to read. |
PRIKK-DOCTOR-COMMIT-WITNESS-STALE | info | The session’s own acknowledgment history is stale (an older binary’s seal drained the queue it covered). No action; the next commit or --repair-tails replaces it. |
PRIKK-DOCTOR-COMMIT-WITNESS-ACKNOWLEDGED-DAMAGE | error | A queued commit the session was told had succeeded is damaged. The default session’s message names --discard-damaged-commits (RFC 166 D5). |
PRIKK-DOCTOR-COMMIT-WITNESS-ACKNOWLEDGED-LOSS | error | An acknowledged queued commit is no longer present. The default session’s message names --discard-damaged-commits. |
PRIKK-DOCTOR-COMMIT-WITNESS-SUBSTITUTED-RECORD | error | The record at an acknowledged sequence does not match the commit that was acknowledged. Not a crash shape; no verb, and a copy is the way out. |
PRIKK-DOCTOR-COMMIT-WITNESS-UNKNOWN | error | The queue has an unexplained tail, and the acknowledgment history is unreadable, so the tail cannot be shown to be a crash leftover. The default session’s message names --discard-damaged-commits. |
PRIKK-DOCTOR-COMMIT-WITNESS-DAMAGED | warning | The acknowledgment history is unreadable, but the WAL it covers is wholly sound. The default session’s message names --repair-tails. |
PRIKK-DOCTOR-COMMIT-WITNESS-OWNERSHIP-MISSING | error | The active ref metadata names an owner the acknowledgment history does not recognize. The default session’s message names --restore-queue-target. |
PRIKK-DOCTOR-ACTIVE-SESSION-COMMIT-WITNESS-* (STALE, ACKNOWLEDGED-DAMAGE, ACKNOWLEDGED-LOSS, SUBSTITUTED-RECORD, UNKNOWN, DAMAGED, OWNERSHIP-MISSING) | as above | The same conditions for a non-default active session, with the session named in the message and the same severity. Their recommendations name no repair verb. |
PRIKK-DOCTOR-ACTIVE-SESSION-COMMIT-WITNESS-UNREADABLE | error | A non-default active session’s acknowledgment history failed to read. |
PRIKK-DOCTOR-MISSING-REQUIRED-DIRECTORY | error | A directory the repository layout requires is missing. |
PRIKK-DOCTOR-REQUIRED-DIRECTORY-WRONG-TYPE | error | A required directory name is occupied by something that is not a directory. |
PRIKK-DOCTOR-REQUIRED-DIRECTORY-UNREADABLE | error | A required directory could not be read. |
PRIKK-DOCTOR-VERIFY-STAGE-INCOMPLETE | error | A verification stage failed, or was not attempted because an earlier stage failed under --stop-on-first-error. 0.50.0 P3c: when one of the three *-INTERIOR-DAMAGE issues above is also present, the recommendation names its code instead of the plain “preserve the repository and inspect the failing stage” — decided by that issue’s own typed field, never by this stage’s own message text. Handoff 165 Q1b: GENERATION-LOG-AMBIGUOUS above gets the same treatment. |
PRIKK-DOCTOR-VERIFY-OBJECT-INCOMPLETE, -BLOCK-STATE-INCOMPLETE, -REF-FILE-INCOMPLETE, -REF-ITEM-INCOMPLETE | error | One item of that kind failed verification. |
PRIKK-DOCTOR-CURRENT-BRANCH | warning | .prikk/current-branch is malformed or names a branch that does not exist or is closed; every --ref default refuses until it is fixed, while an explicit --ref still works. |
PRIKK-DOCTOR-PROVISIONAL-WORKTREE | warning | The worktree was materialized from a snapshot and is not replay-verified. prikk verify clears it. |
PRIKK-DOCTOR-INTERRUPTED-MATERIALIZATION | warning | A checkout or branch switch stopped part-way, so commit refuses until it is re-run or the named file is moved aside. |
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, retained active cleanup, and unproved divergence are errors; candidate debris is a warning.
MissingForEmptyWal and ValidForNonEmptyWal are healthy metadata states and do not produce doctor
issues by themselves.
Doctor Repair Boundary
Doctor repairs are opt-in: a plain prikk doctor only diagnoses. The switches, all parsed in
crates/prikk-cli/src/args.rs:478-531, are:
--repair-wal-tail: truncates an incomplete final record from the default session’s WAL and, for each non-default active session, that session’s WAL (doctor.rs:1258).--repair-tails [--plan-only]: truncates the incomplete final bytes of the WAL, the pointer index, and the eight appended files, ten files in all (doctor/repair_tails.rs:76). It refuses interior damage in any of them, the identical way with or without--plan-only(0.50.0 step 1 A6 item 4) – the one repair that may cut across ten files in one run, and the last of the five recovery verbs to gain a preview.--repair-index: rebuilds the object index from the containers.--repair-pointer-index-tail: truncates an incomplete final pointer-index record.--rebuild-pointer-index [--plan-only]: re-derives the ref-pointer index from the ref log (RFC 165 R5,pointer_rebuild.rs). Before flipping away from the live slot, a real run saves a full copy of it, of the rebuilt slot’s own pre-rebuild bytes, and of the generation log’s own before/after bytes, to the recovery log under one run (0.50.0 step 1 Parts F/F2) –--recovery-restore <id>undoes the whole switch byte for byte: both slots and the generation record all go back to what they were, naming the old slot live again. A restore refuses, and writes nothing, if an ordinary write has landed in the rebuilt slot since (a new branch, a publication).--discard-damaged-commits [--plan-only]: removes an acknowledged queued commit that the commit witness names and the WAL no longer holds soundly (RFC 166 D5,doctor/discard_damaged_commits.rs).--restore-queue-target --ref <ref> [--not-current-branch] [--plan-only]: gives an active queue back its owner when its ref metadata is missing or does not match.--recovery-list,--recovery-restore <id>, and--recovery-clear: work with the recovery log.--repair-main-ref: a recognized input that performs no repair and is always refused (doctor.rs:1263-1269).
prikk ref complete <ref> [--plan-only] is a separate command, not a doctor switch. It completes an
interrupted publication that is a completable pointer lead (RFC 165 R4, ref_completion.rs).
Each repair that removes bytes saves them to the recovery log, .prikk/recovery/log (see
durability and crash recovery). --repair-index saves the ids it cannot
re-derive, when any, the same way.
--repair-pointer-index-tail
Truncates an incomplete trailing pointer-index record — a torn tail, positionally: the last record ends
past the file’s own length, and nothing sound follows it. Mirrors --repair-wal-tail exactly in shape
and contract, including saving the removed bytes first. Refuses, unchanged, on a genuinely damaged entry
(not merely a trailing partial). That entry is not recovered by truncation. prikk doctor --rebuild-pointer-index re-derives the pointer index from the ref log, and it refuses while a pointer
lead is still completable, so prikk ref complete <ref> runs first (pointer_rebuild.rs:148-192).
Holds the pointer-index lock for its whole run, matching --repair-index’s own discipline.
--repair-index
Rebuilds containers/index.container by scanning the object containers themselves. Use it when
verify reports
error: integrity error: index entry for <id> resolves to an envelope with computed id <other>
which means an index entry points at a different record’s bytes. The containers are self-describing — each record carries its own magic, framed length, and checksum — so everything needed to rebuild is still on disk, and a rebuild is a recovery rather than a reconstruction.
The repair reads and writes the index only; container bytes are never touched. It is idempotent: on a
healthy repository it reports object index: nothing to repair and writes nothing. The new index is
installed atomically — written to a temporary file, fsynced, renamed over the old one — so an
interruption leaves one whole index, never a mix.
The repair holds the object-store lock for its whole run — it is a writer to the index like any other, and it takes the same lock every other writer takes. Three consequences, all ordinary lock behaviour:
- a writer (
commit,seal,tag create, …) that arrives while a repair is running is refused withlock conflict, and retrying after the repair finishes succeeds; - a repair that arrives while a write is running is refused the same way;
- a stale
objects.lock, left behind by a failed acquisition, refuses the repair with the message namingprikk unlock— the one case where an operator must act before the repair can run at all.
Acquiring the lock only around the final install would not be enough: the repair scans the containers first, and a writer appending between the scan and the install would have its object discarded by an index rebuilt from the older state. The lock covers the scan and the install together.
This state was reachable before prikk 0.40 by running two object-writing commands concurrently; the object-store lock now prevents it (see concurrency and locking). This verb exists for repositories damaged before that lock existed.
Refusals differ by verb. --repair-wal-tail refuses while a repository-wide error exists, and skips an
active session that has its own error (doctor.rs:1258, 1275). --repair-tails refuses interior damage
in any file it covers. --rebuild-pointer-index refuses ref-log damage and any completable lead. --repair-index
checks neither; it rebuilds the index from the containers under the object-store lock. 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, or recover key material. Stale-lock cleanup
is prikk unlock. An interrupted publication is completed by prikk ref complete <ref> under RFC 165
R4’s rule (ref_completion.rs); doctor itself does not sign or append a ref state
(doctor.rs:1117).
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, doctor JSON output, stable
diagnostic schema, stable repository-format migration, multi-ref backup export, a rehearsed
repository-format-migration restore, and production readiness. (MAINTAINER key revocation is no
longer deferred — prikk trust maintainer remove. Single-ref backup/restore tooling is no longer
deferred either — prikk bundle export/verify/import, see
Backup and Restore.)
Claim-to-Source Anchors
| Claim | Source anchors |
|---|---|
| Repository verification reports counters for objects, WAL records, Blocks, refs, ref logs, rollback material, publication trust, ref-publication issues, trailing partial WAL bytes, and active WAL metadata state. | verify.rs, verification.rs |
| 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 the switches listed in the Doctor Repair Boundary; --repair-main-ref is recognized but performs no repair and is always 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 behavior through RFC 103, which retired format-1 support entirely (see
Doctor Repair Boundary above). 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 added strict
new-envelope admission and, before RFC 103, 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 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: a symlink create or delete, the one operation kind the proof does not replay, or (rarely) a pair classified independent whose replay does not agree. Before 0.45.0 it also stood for any rename. |
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: an operation does not replay alone against the baseline, and an earlier operation on its side either orders before it or acts on the same file. |
pair_replay_failed | An operation on one side does not replay against the baseline, and nothing earlier on its side explains why. No history commit can author is known to reach it: it signals a patch not authored against its side’s history (crafted or damaged input) or a proof-engine fault. |
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.
Conflict Witness Kinds
reason_code: pair_conflict names the outcome bucket, not the reason: all twelve conflict-witness
kinds patch_algebra distinguishes internally reported as the same generic pair_conflict code,
which told a reader nothing about why a specific pair conflicted (conflict-witness-presentation
handoff v1). MergeEvidenceDisplayItem now additionally carries the specific kind, path, and node
identity a cross-side (Conflict/OrderedDependency) item’s witness recorded:
| Field | Meaning |
|---|---|
witness_kind | The specific reason this pair conflicts or orders, as a stable kebab-case label. None for items with no underlying witness. |
witness_path | The repository-relative path the witness recorded as the reason for conflict, when it recorded one. Distinct from each side’s own operation.path/peer_operation.path: derived from whichever operand’s own path field or live baseline path resolves (conflict-witness-path-derivation handoff v1), so it is present even for node-identity conflicts where neither side’s own operation carries a path at all — as long as the shared node has a live entry in the baseline. None when the two operands resolve to two different paths (a single field cannot pick one) or when the node has no live baseline entry to derive from. |
witness_node_id | The shared node identity, typed (not a rendered string — no stable human-facing node-identity rendering exists in this codebase to freeze into this field). The only correlating signal for the conflicts that stay genuinely path-less: a node-identity mismatch against a node with no live entry in the baseline at all. |
Current witness_kind labels:
| Label | Meaning |
|---|---|
same-path-create | Both sides create a node at the identical path. |
rename-destination-conflict | Both sides rename the same node to two disjoint destinations. Distinct from same-path-create: one node, two paths, versus same-path-create’s two nodes, one path. |
node-id-reuse | A node identity is reused across unrelated creations. |
live-state-mismatch | A side’s precondition does not match the state the other side’s operation requires. |
kind-mismatch | The two sides disagree about the node’s kind (file, symlink, etc.). |
mode-mismatch | The two sides set different, incompatible permission modes. |
blob-mismatch | The two sides replace binary content with different, incompatible results. |
text-span-overlap | Both sides edit the identical text span. |
text-anchor-stale | A text edit’s anchor no longer matches the state it was planned against. |
delete-mutation-conflict | One side deletes a node the other side mutates. |
unsupported-operation | The operation kind is outside the currently supported algebra subset. |
malformed-operation | The operation itself failed to decode into usable facts. |
unknown-relation | The pair’s relation does not match any more specific classification. |
These labels are an external interface, the same footing reason_code and proof-phase strings
are already on (Privacy and Output Limits, below): renaming, removing, or reusing one is a breaking
change to any tool reading merge evidence.
Conflict Resolution Is Refused By Design
Automation may not author a conflict resolution. This is a settled architectural position (conflict-witness-presentation handoff v1), not an unscoped feature waiting to be built, and it follows directly from two decisions already made elsewhere in this project:
- DC-35: “Automation may verify evidence but cannot occupy either accountable approval identity.”
- DC-74 applied that at the patch layer already, and its own reasoning is why arbitration can’t
be added later as an ergonomics feature: in a context-dependent (Darcs-style) model, merging a
patch transforms it — its canonical bytes change, its
ObjectIdmoves, and the original AUTHOR signature no longer covers the result, so whoever performs the merge must re-sign content they did not write. That is DC-35’s “automation cannot occupy an accountable approval identity,” arriving at the patch layer. Prikk’s merge design avoids this entirely:prikk merge(DC-74) adopts the other side’s patches verbatim, unmodified, under their original AUTHOR signatures — nobody re-signs content they did not write.
An automatic conflict arbitrator would reintroduce exactly the problem DC-74 was built to avoid: resolving a conflict means producing new content — a patch — and a patch must be authored and signed by whoever is accountable for it. So conflict arbitration is refused by the architecture itself, not merely unscoped or deferred to a future increment.
Resolution already exists, and needs nothing further built: prikk merge refuses on any
conflict, and a person reconciles the two sides by authoring ordinary patches under their own key —
the same committing every other change in this repository already goes through.
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,
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.
Conflict resolution itself is not on this list — see Conflict Resolution Is Refused By Design, above. “Deferred” means eventually built; automatic conflict resolution is refused by the architecture and will not be built at all. A conflict-resolution UI remains deferred in the sense above, but only for the human-authored-patch workflow this page already describes, never for an automatic resolver.
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 |
Conflict-witness kinds (thirteen) are generated with their labels from one macro invocation, the same discipline VerificationStage uses. | types.rs, conflict-witness-presentation handoff v1 |
MergeEvidenceDisplayItem publishes witness_kind/witness_path/witness_node_id, but never the underlying left_op_seq/right_op_seq pair or raw text_span bytes. | display.rs, mapping.rs |
| Automatic conflict resolution is refused by design (DC-35 applied at the patch layer, per DC-74). | DC-35, DC-74, merge_execute.rs, conflict-witness-presentation handoff v1 |
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.
Non-Goals
This page collects, in one place, every decision this project has made to permanently refuse building something — as distinct from work that is merely unscheduled.
Refused means a decision exists, and the answer is no, not ever: automation is not permitted
to do it, the design deliberately does not create the thing that would be needed, or the project
has scoped its supported surface to exclude it. Deferred means the opposite: the thing may be
built, nobody has scheduled it, and it lives in ROADMAP.md
instead — the project’s planning authority for everything that is open, not refused.
This page does not restate its sources. Each entry below is a pointer to the page that carries the authoritative decision, plus the citation behind it. If you cannot find a decision behind a claim of “refused” anywhere in this project, it is not a non-goal — it is an opinion, and it does not belong here or anywhere else as though it were settled.
Automatic conflict resolution
Automation may not author a conflict resolution on a person’s behalf. A resolution is itself a signed patch, and signing on someone else’s behalf is exactly what this project’s signing model exists to prevent — not a feature gap that a future increment closes.
Decision: DC-35 (signing authority), applied at the patch layer by DC-74 (merge execution). Full statement: Conflict Resolution Is Refused By Design.
Repository identity
There is no repository identifier, no peer identity, and no origin field anywhere in a persisted object — not because nobody built one yet, but because the design never creates anything for a future increment to add trust around. Identity lives only in signer keys and patch ids.
Decision: the repository-identity settlement (RFC 115’s own investigation). The object code
0x0A, formerly ProjectGenesis, is permanently retired and must never be reassigned.
Full statement: Trust Roots and Roles.
The library crates are not a supported dependency surface
The seven library crates behind the prikk CLI (prikk-store, prikk-object, prikk-crypto,
prikk-hash, prikk-error, prikk-ffi, prikk-replay) are implementation detail, not a product
of their own — the prikk CLI is the one supported surface. This is a scope decision, stated
independently of the API-stability disclaimer each crate’s own README also carries for the period
before prikk reaches 1.0.
Decision: the published-crate-posture review (2026-08-26).
Full statement: each crate’s own README, e.g.
prikk-store.
What is not on this page
Networked transport was considered and left off. ROADMAP.md’s own Sync theme currently
reads this as settled and by design, but RFC 116 — the ruling ROADMAP.md cites — states it in
deferral terms of its own (“deferred,” “in this increment,” “if a protocol is later wanted”), not
as a permanent refusal. Restating ROADMAP.md’s stronger framing here, without the owner
confirming it narrows RFC 116’s own words rather than merely summarizing them, would be exactly
the kind of restatement this page exists to avoid. Not included until that is confirmed.
A handful of other candidates were checked and did not hold up as citable refusals: semantic
merge (ROADMAP.md cites DC-16, but DC-16’s own non-goals section scopes what that RFC builds,
the same way every RFC scopes itself — it does not rule that semantic merge will never be built);
DC-57’s merge-scope bounding (the same self-scoping pattern); the Windows durability gap with no
openat equivalent (a platform constraint, not a project decision); and symlink/rename authoring
(named as a gap in Data Model Relationships and
Lifecycle, but the one place that discusses its future,
ROADMAP.md’s editor-integration theme, calls it deferred, not refused). None of these carry a
decision stating the answer is permanently no.
One existing page names something it should not. Trust and Threat Model’s own “Current non-goals” list includes items its own earlier text calls “capabilities not yet built” — deferrals, not refusals, by that page’s own words two sections above the list. That page is not this page’s to rewrite; noted here so the inconsistency is visible rather than quietly carried forward.
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, all 14 of them, with one script:
scripts/gates.py
It prints one <name> <exit> line per gate as it finishes (and again as a summary), and exits 0
only when every one of the 14 does. scripts/gates.py --list prints every gate’s own name and
exits 0 without running any of them; any other argument is refused at exit 2, naming the argument,
rather than silently running the full suite. Each gate runs under an R1 (systemd-run --user)
scope when that is on PATH; otherwise the script says plainly that it ran without one, rather
than silently skipping it. It is a .py file, not a .sh one, on purpose: this project’s own
governed-procedure
scanner (tools/release-policy/src/command_scan/) has no model of shell control flow, and
scripts/gates.py’s orchestration (looping over the gates, wrapping each conditionally, aggregating
exit codes) cannot be expressed as a .sh file without being read as a wall of unclassified
commands under that scanner’s own, deliberately simple grammar.
The 14 gates, verbatim:
cargo fmt --all -- --check
cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
cargo test --workspace --locked
cargo +1.85.0 test --workspace --locked
cargo +1.85.0 check --workspace --all-targets --locked
git diff --check
cargo audit --no-fetch
RUSTDOCFLAGS="-D rustdoc::private_intra_doc_links" cargo doc --workspace --no-deps
cargo run --locked -p prikk-release-policy -- check
cargo run --locked -p prikk-release-policy -- boundary-check
cargo run --locked -p prikk-release-policy -- reference-check
cargo run --locked -p prikk-release-policy -- size-check
cargo clippy --workspace --all-targets --all-features --locked --target x86_64-pc-windows-gnu -- -D warnings
cargo clippy --workspace --all-targets --all-features --locked --target x86_64-apple-darwin -- -D warnings
The cross-target rows
The last two gates above (Windows and macOS clippy) run every time now, not only when a change
touches #[cfg(target_os)]-gated code: running them always costs little next to the other twelve,
and making them conditional already let a Windows failure through twice.
They exist because “touching cfg-gated code” is broader than adding a #[cfg(target_os)] line
yourself: it also covers adding un-gated code to a file that already contains that gating. That shape
adds no cfg line to your diff, so it is easy to miss — and missing it once left the project’s own
main branch red on the macOS and Windows Clippy jobs for three consecutive changes, which is the
reason these two no longer depend on anyone noticing.
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.