50 descendants of b4a58862528ed58f
|
2026-07-31
| ||
| 02:28 |
fast-export: derive the per-commit file-command ceiling
Limits.max_file_commands_per_commit was a standalone number, and the caller pinned it to the width of our own benchmark corpus. That is a fixture size promoted to policy: no headroom, no way to raise it without a rebuild, and it refused legitimate histories. `git fast-export` lists an entire tree for a root commit and for any commit whose first parent was not exported, so the ceiling is reached by ordinary repositories whose initial commit lands a whole codebase at once. The bound itself is necessary. admitData is the parser's only byte admission and it sees the byte count in a `data` header alone; every file command additionally allocates a list slot and a duplicated path into the per- commit arena with nothing charging for them, so the command loop is otherwise unbounded against a subprocess pipe. It is now computed: fileCommandCeiling(limits) = max_total_data divided by min_retained_bytes_per_file_command, where that constant is @sizeOf(FileCommand) plus one -- a command's list slot plus the shortest path it can carry, the empty path being rejected. Nothing smaller is representable, which is what makes it safe to divide a byte budget by. The ceiling restates the arena authorization the caller already granted, in commands rather than bytes, and moves when that authorization moves. Refusal retains the observed count, the ceiling, and the budget that produced it, so the diagnostic names all three rather than only its own error. The remaining limits gain distinct names -- Data, Event, FileCommand, Line, Merge and Path -- in place of one LimitExceeded that could not say which bound was hit. Exact-count data may carry an optional trailing LF, so only the first following empty line is ambiguous with the commit terminator; readDataExact and readCommitData split that decision instead of guessing. check-in: 85d510d361 user: dmestas tags: sub-manifest | |
| 02:15 |
repostate: resolve a stored sym- selector directly
Names beginning with `sym-` are ambiguous in this repository. A user tag written `sym-x` is stored under the canonical spelling `sym-sym-x`, so resolving `sym-x` correctly looks for `sym-sym-x`. An imported repository, though, can carry a tag whose stored canonical spelling already begins with `sym-`, and nothing could address it: the ordinary lookup rewrote the name and missed the artifact that was actually there. Resolution now attempts the ordinary user-tag path first and consults the stored spelling only when that fails. The precedence is deliberate. A user tag continues to win over an imported canonical name, so no existing repository changes meaning, and the fallback is reachable only where the ordinary answer does not exist at all. check-in: fc9bcb81bd user: dmestas tags: sub-manifest | |
| 01:58 |
logstore: all-or-nothing artifact batch append
Callers publishing several artifacts together had only putArtifact, one at a time, so a failure partway left the store holding a prefix of the set with nothing naming what was missing. Each caller that cared wrote its own compensation, or quietly did not. putArtifactBatch takes the whole set and appends it under a single transaction: either every item is durable or the store is byte-identical to its prior state. ArtifactBatchItem carries the hash, the bytes, and an optional delta base, so a batch may mix full and delta-encoded members without the caller resolving storage order first. Nothing calls it yet. The importer, which needs a check-in's blobs to arrive as a unit or not at all, is the first caller and lands later on this branch. check-in: 7cf522fa5b user: dmestas tags: sub-manifest | |
| 01:41 |
control_paths: one worktree-path admission policy
Two layers decided independently what a reserved path is. update carried its own validateWorktreePath — an absolute/backslash/dot-dot check plus a hand-rolled case-insensitive comparison against the .zit family — while extras carried isControlName, which tested only the first /-delimited component of a path. They agreed by coincidence rather than construction, and the extras form admitted `.zit-managed/entries` because it asked about the component instead of the root. control_paths now owns both questions. validateWorktreePath and isReservedRoot are the single definitions; update delegates to the first and extras to the second. isControlName takes the full root-relative key that add and move already hand it, so a path rooted in a control entry is refused at any depth rather than only at depth zero. firstComponent goes with its last caller. The update test that pinned the old list is now a case table naming what is reserved and what merely resembles it, including the case-insensitive spellings the previous ad-hoc comparison happened to get right. check-in: 581db6e5ba user: dmestas tags: sub-manifest | |
| 01:26 |
update: materialize symlink worktree entries
A check-in's F card can name a symlink (§4.7.6-m5), but update refused one outright with error.SymlinkUnsupported, so any history carrying such a card could not be checked out, reverted, or updated to. The refusal was cheap to write and wrong: the reader already represents the entry, and only the worktree side lacked a branch for it. materializeEntry and writePayload now create the link from the card's target bytes instead of writing a regular file containing them, and isCurrentState compares against .sym_link and the recorded target rather than the size and mode of a file that is not there. The managed-set walks accept .sym_link beside .file, so a materialized link reads as a tracked entry instead of an intruder the admission check rejects. `zit revert` on a symlink card previously exited 1 naming the refusal. It now restores the entry, and the test reads the link back with readLink, which distinguishes native materialization from a regular file that merely holds the target's bytes. check-in: bb96853a0f user: dmestas tags: sub-manifest | |
| 01:24 |
build: forward --test-filter to the e2e test root
The e2e root is one test binary holding every scenario, and its runner took no filter, so narrowing to a single scenario meant editing build.zig or paying for the whole root. `zig build e2e -- --test-filter NAME` now compiles the filter into the test artifact, accepting both the two-argument and the `--test-filter=NAME` spellings and passing nothing through when the arguments do not name a filter. check-in: cd70f19286 user: dmestas tags: sub-manifest | |
|
2026-07-30
| ||
| 21:47 |
release_assert: a guard against assert used as input validation
std.debug.assert is right for an internal invariant and wrong for a rule about data the function did not produce, and it is wrong in two different ways. Under the ReleaseSafe default it survives and PANICS: on the write path that is the trade we want, a crash over a durable bad frame, but on a peer-reachable path it is a remote denial of service. Under an explicitly chosen --release=fast it compiles to nothing and the input flows through unvalidated. One fix, a checked error, closes both. Flags an assert only when all of: the fn is pub, returns an error union so a rejection was already available, the assert names one of its own parameters, and no earlier return in the body already rejects on that parameter. That last rule is what makes it signal -- delta.applyCopy returns MalformedDelta and then asserts to restate it, which is the CORRECT shape and must not be flagged. The baseline is keyed on file, function and COUNT, never on line numbers: line numbers churn on every edit above them, while a count still catches a second bad assert added to a function that already has one. It fails in both directions, so a fixed finding must be removed from the list and the list can only shrink. Existing findings are recorded, not fixed. Triage belongs in issue 19, where the ReleaseSafe default already reduces the write-path subset to crash-instead-of-corrupt and leaves the sync-side ones as the live exposure. check-in: 949e2b5542 user: dmestas tags: trunk | |
| 21:20 |
build: --release means ReleaseSafe, and pin it
src/ carries 1086 std.debug.assert calls. They are how a detected inconsistency becomes a crash instead of a durable write -- TigerStyle's 'downgrade catastrophic correctness bugs into liveness bugs' -- and that trade exists only while the assertion does. ReleaseFast and ReleaseSmall compile every one to nothing, so a binary built that way converts each guarded corruption into an unguarded one, in a store where a bad frame is permanent and a crash is not. Measured, not assumed. Read-side verbs are indistinguishable between the two modes: status, log and stat all sit inside noise on a 5 MB store. zit pack is not: 599ms fast against 15.2s safe, about 25x, because the delta encoder's best-of-N search is a tight per-byte loop where a bounds check is a large share of the work. That argues for setRuntimeSafety(false) on that measured loop -- local, reviewable, over code the fuzz and DST lanes already cover -- rather than stripping assertions from every other path in the binary. Not taken here. Note the option surface changes: stating a preference replaces the -Doptimize enum with a -Drelease bool, so the deliberate escape hatch is the top-level --release=fast. -Doptimize=ReleaseFast no longer parses. ci_test pins the preference, and says that if the project ever decides to ship unchecked, the pin should be deleted in that same commit so the decision is visible in history rather than arriving as drift. check-in: 4379187a10 user: dmestas tags: trunk | |
| 20:21 |
logstore: bound the priority window structurally, not with an assert
gatherCandidates capped the entries ADDED from the caller's priority slice, not the elements read: a duplicate hits ridListed and continues without advancing n, so the fill check never fires and the walk runs the caller's whole array with a linear dedup scan at each step. The only thing holding that back was an assert that the slice fits the window -- and std.debug.assert compiles to nothing in ReleaseFast and ReleaseSmall, so there was no guard in exactly the build where an unbounded scan would matter. Slicing the prefix holds in every mode, and turns an over-length slice from a precondition violation into defined behavior: accepted, tail ignored. Both callers today are inside the window either way. Two tests, both mutation-checked against the old shape. What is NOT pinned is the duplicate-heavy case: dedup leaves the surplus slots to the chronological walk, so the candidate set is identical either way and only the count of elements read differs. That is a cost property, not a behavioral one, and no test through this API can catch it -- the header comment says so rather than letting the pair read as covering it. check-in: bfec65b1a1 user: dmestas tags: trunk | |
| 18:47 |
merge shun-implementation: shunning and space reclamation
A repository-local shun table (sync SS3.3), the zit shun verb over it, and the four suppression directions the drafts require: never requested, never announced or served, an arriving copy discarded, and the bytes reclaimed on demand through the existing atomic rewrite. Built as eight slices, each through design review, TDD, spec audit, three review lenses, mutation proof and a full gate. Reading the eight as one change afterwards found what none of them could alone: a peer-reachable assert that crashed the client on a malformed DELTASRC, and a pre-rename guard that re-stat'ed a stale handle so two concurrent rewrites could un-shun each other. Two defects predating the feature are fixed on the way. FileStorage.sizeImpl answers from a cached watermark, so pack's tail-revalidate compared a key against the number it was derived from and could never abort -- the guard had never worked. And a malformed file card could trap the client through PendingDeltaQueue. Format version stays 1: an older binary meets an unknown frame kind and rejects cleanly, which is what it should do with a tombstone it cannot read. No interchange surface changed -- smoke covers ten fossil 2.28 scenarios including a fossil clone of a defer-shunned store and a real fossil push of a shunned artifact. Known and recorded, not blocking: a running zit serve keeps serving the old inode until restarted after a --force reclaim swaps generations. check-in: e57bcceadf user: dmestas tags: trunk | |
| 18:32 |
shun: whole-branch verification fixes
Read as one change rather than eight slices, which surfaced defects no per-slice review could see. A peer could crash us. ShunSet.contains asserted its key domain while its own vtable contract promised an out-of-domain name answers false and never traps -- and a 'file HASH zz SIZE' card reached it. It is a checked rejection now, and receiveDelta refuses a DELTASRC that is not a complete artifact name before one can mint a phantom, which also closes a pre-existing PendingDeltaQueue trap on the same card. Two rewrites could un-shun each other. rewriteAndSwap's pre-rename guard re-stat'ed the caller's handle, which after a competing rename points at the old inode -- so the guard passed and the loser silently reverted the winner. It reopens .zit by name under the temp lock now, as shunAppendRows already did. The client snapshot lacked the masked-basis guard the serve path has, so a kept delta rooted on a shunned base could be handed out in delta form. shun_help denied two slices that landed after it was written: it still told operators a shunned artifact is only suppressed in one direction and can be re-announced back into the store. Also: storestat's shun rows and bytes are wired into zit stat rather than left unused, so the printed breakdown still sums to the file size; the clone-send half is relabelled an erratum candidate against SS8.1/SS8.2 instead of claiming SS9.5, which is scoped to non-clone; and three stale doc bullets now describe the code that exists. closed check-in: d9578c6c6e user: dmestas tags: shun-implementation | |
| 17:20 |
shun S7: the shun+reclaim crash sweep
A DST sweep that shuns artifacts (forcing a delta base, a manifest and a mid-chain artifact across seeds), reclaims through rewriteAndSwap, and cuts the run at sampled write/sync/rename boundaries. The oracle classifies the recovered store as the PRE image or the POST image and fails on anything else. It earns its place: deleting packLog's pre-rename fsync is caught by this sweep and by nothing else in the suite. The concurrency-guard boundary is a second, DST-shaped pin on S3's fresh-stat fix rather than first coverage, and the module header says so. Two oracle clauses are graded honestly as tripwires, not coverage. Surviving content is re-hashed by packLog before its own commit, so a defect that would corrupt it aborts the rewrite instead and the sweep just sees the PRE image; no mutant can make that clause fire. The PRE half of the third clause is subsumed by the byte-equality check. Both are kept cheap and labelled. rewriteAndSwap and RewriteError are now pub: the property under test is a property of that sequence and reaching it needs the seam. faulty_dir gains fileSetLength and fileClose wrappers, deliberately ungated so no sibling sweep's cut numbering shifts. The default dst gate grows about 11s (~9%) for 12 sampled cuts per seed. check-in: cb22503707 user: dmestas tags: shun-implementation | |
| 14:53 |
ignore the crash-leftover working files: dst workspace and pack temp
.zit-dst-checkout-ws/ is the process directory the crash-recovery sweeps make their throwaway workspaces under. It is created at run start and removed by a defer at run end, so it is normally invisible -- but a killed or crashed sweep leaves the whole tree, .zit stores and all. .zit-pack-tmp is the fixed-name temp pack and shun --force write the new generation into before the atomic rename; a run that dies between create and rename leaves one beside .zit. Both are dotfiles, so a bare 'fossil extras' skips them and the tree reads clean while they sit there -- the same blind spot the .agent-config glob closed. Pinned by packaging_test. check-in: 9b95e3b9e5 user: dmestas tags: shun-implementation | |
| 13:15 |
shun S6: receive-side admission, asymmetric as the spec has it
SS7.7 L1434-1452, as one combined predicate: a target is shunned for admission when its complete name is in the shun table OR shun-sha1 applies to its SHA1 name. TargetAdmission.admits carries the whole rule, including the two card rules -- a clone file card BYPASSES the check and is stored; everything else consults it. That asymmetry is deliberate and is the applied text. zit's own erratum argued the discard should apply in clone too and four reviews rejected it against fossil's xfer.c. A test pins the bypass so a later reader does not 'fix' it back into the shape that was refused. For cfile the SS7.5 private discard runs first and the raw artifact-card count still increments -- both satisfied by placing the check below code that already existed, not by adding any. Admission is target-only. A shunned DELTASRC is not independently rejected: a held basis still serves, and an unavailable one falls to ordinary dangling-delta handling whose phantom SS3.3 already excludes from gimme. The planned e2e did not pin anything and was rewritten. A real zit push passes with the whole check removed, because transfer is receiver-driven and SS3.3 already stops this server begging for a shunned name. The shipped test POSTs an UNSOLICITED push+file container at a live serve -- the only shape that reaches this code -- and it goes red without it. Smoke S10 is real interop coverage but cannot pin this slice for the same reason, and its header says so rather than letting a green be misread. check-in: 620ed6a9c3 user: dmestas tags: shun-implementation | |
| 10:34 |
shun S4: stop advertising what the shun table names
LogIndexStore keeps a shunned holding in its enumeration and MASKS it rather than removing it, so a shun row appended between two rounds of one clone cannot renumber the survivors under the pager's cursor. held() answers false, the name leaves by_name, and SS9.5's 'igot for every eligible holding after the SS3.3 filters' is where that mask lives. The plan overstated what was missing. S1's log-level mask already nulled content and wire, so a clone silently skipped a shunned artifact before this slice. What was actually broken is narrower and worse: because by_name still resolved the name, emitCloneReply's basis proof fired in a safety build and the SERVE PROCESS PANICKED -- a fossil clone got 'server did not reply', not an absent artifact. Reproduced against fossil 2.28 and pinned as smoke S9. That also means the planned e2e ('a clone does not receive it') was never mutation-sensitive: it passes with this slice fully removed. The observable that pins the slice is the ABSENCE of an igot card naming a shunned artifact in a non-clone round's inventory walk, which now has its own test and a second round in the raw-HTTP e2e case. Smoke uses a defer-shunned fixture, not the planned packed one: pack rebuilds the delta chains and so removes the exact hazard the check exists to catch. logstore.zig carries a comment-only change: LogStore.shunned's doc claimed two questions were open that S4 and S5 have since closed. check-in: 230c2bbebe user: dmestas tags: shun-implementation | |
| 03:50 |
shun S3: the zit shun verb
Refuses by default and names what would be destroyed; --force proceeds; never prompts. Reclaims by default -- one rewrite for N hashes, which is why the usage takes HASH... --defer records the tombstone without rewriting, and is also gated by --force: the row is irreversible and S1's mask hides the content immediately, so it is not the safe half of the verb. The check-in this checkout is on cannot be shunned at all, with no override. runShun does NOT reuse runPack's plumbing verbatim as planned -- it diverges on the filter (the source's deferred rows UNION the newly named hashes), on writing tombstones into the new generation, and on its renderer. The shared body is extracted as rewriteAndSwap instead. Fixes a dead concurrency guard in that shared body, which predates this slice. FileStorage.sizeImpl returns a cached watermark rather than stat'ing, so validateTail was comparing a key against the number it was derived from and could never abort. pack carried the same dead guard. rewriteAndSwap now stats fresh at the check point; without that, this slice's claim that an aborted --force leaves no tombstone was unprovable. Consequence lines carry no counts or filenames: 'N descendant check-ins' and a blob's path both need a reverse F-card walk over every manifest, which is too much for the cheap refusal path. Deferred, not faked. No sub-manifest row: that type and its draft are not on this branch. shunConsequence is a total switch, so a ninth type will not compile without its diagnostic. check-in: be67814135 user: dmestas tags: shun-implementation | |
| 02:45 | feat(submanifest): add parser and sparse editor check-in: 8ca8c86174 user: dmestas tags: sub-manifest | |
| 00:39 |
shun S2: pack reclaims what the shun table names
packLog takes a borrowed Filter; a dropped rid is not copied, PackStats.dropped counts it, and packVerifyShunned refuses to swap in a store still holding a digest the filter was told to remove. keep_all is the identity, so every other caller behaves as before. The check runs BEFORE reconstruction and before the SS7.7 verify. A shunned artifact may be unlawful or simply broken, and verifying it would abort the very operation meant to remove it. Two tests pin the ordering: one frame whose content no longer hashes to its name, one that will not decode at all. The second closes a hole the first left in deriveCandidateHints. putArtifactPacked's floor_span became optional. A delta whose base the filter dropped has no keepable frame, and 'I have no floor' is a different claim from 'my floor spans N bytes'; an in-band sentinel could not tell that from a miscomputed span. pack_help no longer overclaims: damaged CONTENT does not block the reclaim, a damaged CHECKSUM still does, because the open-time pre-flight covers the whole store before any reclaim runs. Merges S5's e2e block alongside this slice's. Fossil's suggested resolution would have dropped this one. check-in: 341009e699 user: dmestas tags: shun-implementation | |
| 00:25 |
shun S5: never gimme a name in the shun table
sync SS3.3 L527-530 excludes shun-table names from the ordinary phantom scan. The predicate already existed and its call site hardcoded false; S5 supplies the durable state through a borrowed ShunSet whose .none identity leaves every existing construction site unchanged. The literal reading is kept: an igot for a shunned hash still CREATES the phantom (SS3.3 L511-514) -- it is simply never requested, so the phantom is inert and permanent. Three consequences the reviews forced out: A shunned phantom no longer counts as unresolved. It was told about and refused, not asked for and withheld; counting it made zit pull fail on every future pull, since the peer re-announces each round. The server consults the set too. SS3.3 states the filter on the phantom scan without qualifying a side, and a server under push-receive is a receiver, so an inbound igot could otherwise walk a shunned artifact straight back in. parseIgotCard now rejects a name that is not 40 or 64 hex, as parseGimmeCard already did (artifact-format SS3.2 L349-350). Widening the shun predicate to answer false for an out-of-domain name would have been the fail-open fix. check-in: 1c41f96e86 user: dmestas tags: shun-implementation | |
|
2026-07-29
| ||
| 21:21 |
shun S1: a shunned artifact reads as absent
content, contentCached and wire return null for a name in the shun table even while its frame is on disk, so reclaiming the bytes later never changes what a verb prints. The guard sits in the one shared read body behind a ReadMask the call site names, so 'the masked set is exactly these three' is compiler-checked rather than asserted in prose. It deliberately does NOT sit in contentAtOffset. sync SS7.7 L1445-1450 lets a held-but-shunned DELTASRC serve as a delta basis, and zit satisfies that for free because delta_of is a byte offset -- base resolution never re-enters the rid-keyed doors. A test pins it so a future tidy-up downward fails loudly. pack reads through an unmasked maintenance door: in this slice a shunned frame still copies forward, since dropping it is the S2 filter. pickBase probes through the masked door, so a shunned frame never roots a new delta. check-in: 3e5895dfb7 user: dmestas tags: shun-implementation | |
| 17:53 | merge: update submanifest work from trunk check-in: a9c5efeef4 user: dmestas tags: sub-manifest | |
| 17:49 | bench: add submanifest performance qualification check-in: 1bbd53a5e5 user: dmestas tags: sub-manifest | |
| 16:54 |
shun S0: the durable shun table in the log
FrameKind.shun carries a tombstone keyed by the shunned artifact's whole digest (sync SS3.3 L518-525: persistent, repository-local, keyed by complete artifact name). Never interned, so lookup() still means 'the content frame for this hash'; scan rebuilds the set. putShun is idempotent and returns BadKey for any width but 20 or 32 -- checked, not asserted, so a ReleaseFast build cannot append a durable un-openable frame. pack carries the table forward. Without that the repo's own compaction verb silently drops every tombstone, which would make this slice's durability claim false the first time anyone packs. The pack FILTER is still S2. storestat and the benchmark restate FrameKind's universe; both now fold from the enum so a future kind fails to compile rather than vanish from the byte accounting. check-in: ce40b2526e user: dmestas tags: shun-implementation | |
| 16:39 |
rfcs: land the errata-applied sync protocol on trunk
Trunk carried the pre-errata draft. The corrected text -- the SS3.3 shun-table definition and the SS7.7 receive-side shun admission rule -- was only ever committed on the sub-manifest branch, so any branch cut from trunk was building against superseded normative text. This is the file the errata records name as applied (sha256 068d7672...). check-in: d186a44ea3 user: dmestas tags: trunk | |
| 16:31 |
ignore the editor-tooling classes: .agent-config/* and CLAUDE.local.md
.agent-config/ collects per-session tooling traces at the repo root. Being dotfiles they never appeared in a bare 'fossil extras', so they were held out of commits by fossil's default alone rather than by any rule -- one 'addremove --dotfiles' would land them permanently. CLAUDE.local.md is checkout-local operator config. Both are globbed now and pinned by packaging_test. check-in: a27cf4b4a9 user: dmestas tags: trunk | |
| 14:19 |
merge logstore-index: checksum the store through a vendored crc32
std.hash.Crc32 is byte-at-a-time over one table (~350 MB/s here) and the open-time scan CRCs every payload byte, so it was ~93% of a cold open. Slicing-by-8 in its place: a 416 MB store opens in 292ms against 1.22s, warm reads run 3.3x, the write path 1.5x. Bit-identical by construction, so no format changes and no repository is affected. check-in: 602c2459f2 user: dmestas tags: trunk | |
| 13:21 |
crc32: export for the benchmark and the fuzz lane
Both consumers live in their own build roots and cannot reach crc32.zig by path, so the export is forced. Adds the differential fuzz lane against std — the only coverage that can see a self-consistent but numerically shifted implementation, which would orphan every existing .zit file. The implementation brief prohibited this export, citing repocache as the precedent for unexported leaf modules. That precedent does not apply: repocache has no out-of-module consumer. storestat and repoimage, which do, are both exported. Overridden deliberately. closed check-in: c57c16c5cd user: dmestas tags: logstore-index | |
| 13:19 |
repocache, bisect, undo: checksum through the vendored crc32
The other three on-disk formats defined in terms of this checksum: the cache image self-checksum, the bisect session trailer, the undo journal trailer. Same swap, same values. check-in: 55d6e7c7bb user: dmestas tags: logstore-index | |
| 13:18 |
logstore: checksum through the vendored crc32
The four frame checksums plus the file header. The open-time scan CRCs every payload byte, so this is ~93% of a cold open: 416 MB goes 1.22s to 292ms. No format change — the values are identical by construction. check-in: dd71dd3006 user: dmestas tags: logstore-index | |
| 13:16 |
crc32: vendored slicing-by-8 CRC-32/ISO-HDLC
Bit-identical to std.hash.Crc32, which every on-disk checksum in the store is already defined in terms of. std's is byte-at-a-time over one table (~350 MB/s here); this folds eight bytes per step. Nothing uses it yet. check-in: aef8f539b5 user: dmestas tags: logstore-index | |
|
2026-07-28
| ||
| 22:58 | rfcs: port applied errata; pin the §6.3 lowest-k tie-break and the one-name no-candidate refusal check-in: 307d7287fa user: dmestas tags: sub-manifest | |
| 18:29 | submanifests: opt-in auto-retune dispatch and scalar /config propagation check-in: 4b882d36b8 user: dmestas tags: sub-manifest | |
| 16:43 | submanifests: advise of drift at commit time, with an interactive retune offer check-in: 7b4e6df064 user: dmestas tags: sub-manifest | |
| 15:56 | submanifests: add auto-retune setting and the §6.4 drift-check core check-in: 9c20a3746d user: dmestas tags: sub-manifest | |
| 11:49 | submanifests: enable selects k by analysis instead of demanding one check-in: 081c3a0b90 user: dmestas tags: sub-manifest | |
| 11:42 | submanifests: pin that the k objective counts only leaf-naming S cards check-in: b728cc6af5 user: dmestas tags: sub-manifest | |
| 07:02 |
Sub-manifest: settings, the emission gate, and the analysis pass
Emission is off by default and turning it on is an explicit act that must name the partition parameter: there is no fallback value, enabling without one is refused, and a pinned value outside the supported range emits flat manifests and says so rather than substituting a parameter of its own. Turning it off returns the next commit to flat. The analysis pass reports what the parameter would cost over a name set; it pins nothing. A server whose setting is off now sends the avoid-submanifests pragma, which the receiving side already latched. It leads the pragma run in both the ordinary and the clone reply, so six tests that read a reply's first card were updated to account for it; each still asserts the same content it did before, behind the new card. Fossil 2.28 tolerates the pragma and the interoperability suite is unchanged. The parameter is repository-local. The specification requires it to propagate to clones, since sharing by content address holds only while every writer partitions identically; that shortfall is recorded rather than implied away. check-in: 92a40960d1 user: dmestas tags: sub-manifest | |
| 04:54 |
Sub-manifest: sync and repository state for trees
An absent S reference now phantomizes like any other missing reference, its dependent is deferred and retried when the artifact arrives, and the sweep excludes sub-manifests so it still terminates. Phantoms survive a restart through a rebuild-on-open invariant resting on a durable seen-submanifest setting rather than on records the log never wrote. The transfer gate decides by artifact shape on all three send paths, so a leaf is refused wherever a tree manifest is, and the memo returns its own copy rather than a view into pages the store may free. The gate remembers what it withheld from a peer, which is what lets an exchange with a peer that refuses sub-manifests terminate at all. That record is now cleared only when the peer's announced version actually changes: a peer that re-announces the same version in every reply was clearing it every round, so the same artifacts were offered and refused forever. One requirement is not implemented: a server whose setting is off must send the avoid-submanifests pragma, and emitting it changes the card sequence every clone-reply test walks. That is a protocol-shape change worth its own review rather than a rider here; the pragma is parsed and latched on receipt already. check-in: 8446adcd98 user: dmestas tags: sub-manifest | |
| 03:31 |
Sub-manifest: complete the conformance corpus
Sub-manifests have no reference implementation to check against, so the corpus is the oracle and had to be built rather than assumed. Adds the eleven vectors Appendix A.5 requires that were missing, among them the three-level tree whose absence is why the step-5 cursor contradiction survived into an implementation: no published artifact carried an interior node, so nothing exercised that path. Every authored vector is generated by the implementation and then rechecked along a path that shares no code with it, and each derived name is compared against the constant the test pins, so no hash in the corpus is hand-written. Two fuzz targets cover S-card classification and the tree walk. One A.5 bullet is not satisfiable as written: the re-tune vector asks for two parameters over the A.1 name list that yield trees sharing no sub-manifest, but that list's single qualifying boundary falls in the same place for the first three values and is suppressed above them. No vector was invented for it; an erratum is drafted and a test pins the finding so it stays visible. check-in: 72bba34ada user: dmestas tags: sub-manifest | |
| 02:47 |
Sub-manifest draft: carry the step-5 cursor correction
Regenerated from the RFC suite after the erratum submanifests-00-s5.1-step5-cursor-advance was applied. Step 5 of the tree walk now advances the referencing frame's cursor before pushing the interior frame; as published it omitted that, so returning from an interior descent re-read the same S card and failed the strictly-greater-than-LAST check, leaving every tree with an interior node unwalkable. The walk already implements the corrected reading. Also picks up an MD5 citation and reflowing in unrelated paragraphs. check-in: f2973c41c3 user: dmestas tags: sub-manifest | |
| 01:49 |
Sub-manifest: record why the final-group merge cap is unpinned
The merge fires when the two groups together hold at most MAX, and no test separates that from a strict comparison. Reaching the distinguishing case needs a natural boundary at exactly MAX minus the tail, which hash-driven boundaries do not produce in generated name sets; a search over two parameter values and nine hundred starts found no instance. Pinning it would mean choosing filenames by their leading zero bits rather than generating them. The note records the reasoning at the site so the gap is visible to the next reader. check-in: 9897845809 user: dmestas tags: sub-manifest | |
| 01:19 |
Sub-manifest: accept in the S-card writer what the parser accepts
The S hash is validated case-insensitively and kept raw, so a parsed card carries whatever spelling it was given. The writer asserted the narrower lower-case spelling, so re-emitting a card the parser had just accepted tripped the assertion. It now asserts the parser's own predicate. Reachable only once something emits, which nothing does yet; found by round-tripping an upper-case hash through create. check-in: 6e64c8976c user: dmestas tags: sub-manifest | |
| 00:45 |
Sub-manifest: canonical tree construction, inert behind the emission gate
Implements draft-fossil-submanifests-00 section 6: zbits over the decoded filename, the group scan with its MIN/MAX member bounds, and the final-group merge. One scan serves height 0 and every height above it, so the merge applies uniformly rather than through two code paths that can drift; appendix A.4 is the vector that fails when they do. Construction emits filenames through a minimal encoder scoped to its own output only. Existing emission keeps the escaping it has always used, so no check-in already written changes its bytes. R cards are emitted for tree manifests alone. Nothing emits yet: the construction is wired behind a disabled flag, leaves stored before the nodes that reference them. The gate that turns it on is a later change. Tests pin k = 3, the value the appendix vectors use, and cover byte-identity against A.2, A.3 and A.4, determinism across repeated builds, and a constructed tree walking back to the entries it partitioned. check-in: 9d7992613f user: dmestas tags: sub-manifest | |
|
2026-07-27
| ||
| 21:40 |
Sub-manifest: walk a tree manifest behind the enumeration facade
Implements draft-fossil-submanifests-00 sections 5.1 through 5.6: a stack walk over S runs with the section's depth bound, cycle check, strict ascent and first-filename identity, resolving sub-manifests through the existing baseline resolver. A tree that fails any check fails whole, so a partial enumeration is never presented as a file set. The walk sits in a pre-pass at the facade, leaving enumerate, lookup and their call sites untouched as section 9.3 requires. R-card verification runs over the walked enumeration. Step 5's cursor advance follows the erratum submanifests-00-s5.1-step5-cursor-advance: as published, step 5 omitted it, and no tree carrying an interior node could be walked. check-in: d1f955aaf3 user: dmestas tags: sub-manifest | |
| 19:29 |
Sub-manifest: card alphabet, artifact type, and the specification's vectors
Adds S as the twenty-second card letter and SUBMANIFEST as the ninth artifact type, per draft-fossil-submanifests-00 sections 3 and 4. Type determination reassigns after the card scan rather than during it, so no type is committed while cards are still being read (section 9.1). Reading and classification only: no tree walk, no emission. The draft's seven appendix vectors ship alongside, each named by the SHA3-256 the draft states and checked by a test that recomputes it. check-in: 9b8bad477a user: dmestas tags: sub-manifest | |
|
2026-07-25
| ||
| 14:51 |
Release zit 0.1.3
A serving release over 0.1.2. Where 0.1.2 made a large repository cloneable, 0.1.3 makes serving one fast and lean. A server against a large repository comes up immediately: the listener opens before the repository is read, so a client queues in the accept backlog while the server primes rather than being refused during a startup a large store made long. A push no longer stalls inside its own reply deadline; the snapshot rebuild it triggers now runs between connections. Serving reads the repository through an on-demand log-offset index rather than a resident copy, so peak memory to serve a 67,499-artifact repository fell from about 157 MB to about 32 MB, with clones byte-identical to before. Also: the end-to-end suite is safe to run concurrently with itself (fresh loopback ports), and the unclassifiable-artifact tolerance rule is now covered by deterministic simulation and an end-to-end scenario. check-in: a469147254 user: dmestas tags: trunk, version-0.1.3 | |
| 13:39 |
update: cover the unclassifiable-artifact rule with a DST sweep and an e2e
The rule that lets state assembly skip a stored artifact it cannot classify — when the artifact's bytes verify against the name it is stored under — shipped with the fix but was pinned only by two unit tests over hand-built logs, and by a manual clone of the real fossil repository. Neither the simulator nor the end-to-end suite exercised it, so a regression could pass every automated gate. Deterministic simulation grows an artifact-level generator (src/sim/ assembly_sweep.zig): it emits artifacts whose card sets sit in and around the required-card table — some verifying against their stored name, some stored under a wrong one — and asserts the split directly, that assembly completes iff every unclassifiable artifact verifies, derives no state from one, and stays fatal on a single artifact whose bytes do not match its name. It runs every seed. assembleState is made public for it to drive; its body is unchanged. The e2e suite gains a scenario that clones a served store carrying an anonymous wiki artifact (cards D L W Z, no U — the shape that first surfaced this) and asserts update tolerates it, with the read verbs and assembly agreeing on the same store. Reverting the verify-then-skip arm to an unconditional failure reddens the sweep, the e2e scenario, and both unit tests. check-in: c2851f933e user: dmestas tags: trunk | |
| 13:37 |
serve: read the served store through a log-offset index, not a resident copy
Priming a serve snapshot copied every artifact's container out of the log into an in-memory store and held them there for the server's lifetime. The log had already been scanned and every frame CRC-checked to build the rid table, so the copy was a second full read whose only product was resident bytes: on a 254k-artifact repository it was two thirds of the prime time and a few hundred megabytes of RSS, all to re-hold what the log already holds. The snapshot is now a log-backed store: an index of hash -> log location, and container bytes read from the log on demand as a round emits them, memoised only for the round and released after it. Persisted order is preserved so the clone pager still cursors correctly, the binary-digest to hex-wire-name conversion stays at its one site, and a read fault after the scan surfaces as a server storage error so a clone is never silently truncated. The old whole-store copy is retained only where it is actually needed: the pull client's holdings and the equivalence test's oracle. Measured, 67499-artifact repository, ReleaseSafe: a clone off the indexed serve is byte-identical to a clone off the copy-based serve (both materialise all 983 files of trunk identically), while serve peak RSS falls from 157 MB to 32 MB. The one remaining O(store) cost, the startup scan itself, is a separate change. check-in: e17845e40d user: dmestas tags: trunk | |
| 01:17 | Create new branch named "slice-serve-b" closed check-in: e88fa2b9ed user: dmestas tags: slice-serve-b | |
| 00:21 |
serve: open the listener before priming; move the post-push rebuild off the request path
zit serve built its whole in-memory snapshot of the repository before it opened the listener socket. The build is O(store), so past a few thousand artifacts it outlasted the client's readiness probe and the client reported the server never came up — the store could be served, just never reached. Bring-up now binds first: listen, announce, then prime, then accept. A client that connects during the prime queues in the kernel accept backlog instead of being refused, so the readiness window starts at the bind rather than after the scans. The listener open moves ahead of BOTH O(store) reads on the path — the config read and the prime. The connection deadline is armed only on accept, so priming never falls inside it. On a 254k-artifact store a connect now completes in ~2 ms against a ~5 s prime. A push made the snapshot stale, and the old code rebuilt it inside the pushing round's own reply deadline — an O(store) rebuild charged to the request that caused it. The rebuild moves to a maintain hook the accept loop runs between connections, with no connection open to charge it to; the append-only stat gate makes the stale snapshot self-correcting until then. The reorder is inherently a large-corpus latency property: reverting it leaves every unit test green, because the failure only shows when prime latency exceeds the readiness window. It is pinned in the milestone tier instead, where smoke already lives: zig build bench -- serve synthesizes a store, spawns the real server, and asserts time-to-first-connect is a small fraction of the prime (fixed order ~0.1; reverted order jumps past 1.0). The de-race and the accept-loop hook are pinned by ordinary unit tests. check-in: de218fa6f6 user: dmestas tags: trunk | |
|
2026-07-24
| ||
| 16:45 |
e2e: give the push and disconnect scenarios their own loopback ports
The 'cli push' and disconnect-robustness scenarios bound a fixed loopback port, so two e2e binaries running at once — the ordinary case under a parallel build or a busy CI runner — collided on it and one push exited non-zero, failing an unrelated-looking assertion. The failure was invisible because the assertion reported only the child's exit code and discarded its stderr. Each scenario now draws a fresh port from the kernel (bind :0, read back the assigned port, release) and waits for the child to accept before driving it, the same approach freshLoopbackPort takes for the Fossil smoke tests. The push assertion now surfaces the child's stderr on failure, so the next divergence is diagnosable rather than merely counted. Verified on Linux under the exact condition that reproduced the flake: two 'zig build e2e' runs in parallel, ten rounds, x86_64 and aarch64 — 20 of 20 green on each, where the fixed-port version failed intermittently. check-in: b4a5886252 user: dmestas tags: trunk | |