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.