Skip to content
zit Timeline
Login

Timeline

Parents and children of check-in [8797f53546]

2026-08-10
17:25
control files: registry, provenance opt-in, one scratch prefix, advisory dir

Tasks 1-5 of the control-file consolidation plan. The checkout root goes from twelve zit-owned names to six live ones; the authoritative-tier container (task 6) and legacy removal (task 7) are deliberately not in this change.

Registry: control_paths now carries every reserved root name with its tier -- authoritative, advisory, scratch, legacy -- and is the single authority the case table, the extras exclusions and the ignore-glob coverage pin all derive from or test against. A name added without its glob line or case coverage is a test failure, proven by mutation. The pin caught two live gaps on arrival: a crashed update's transaction directory had no ignore-glob line, and a crashed import's staging directory was never reserved at all.

import-git --provenance: the per-object map and the source ref manifest are now instrumentation, published only on request. The default destination is the repository and its checkout record -- fossil's own shape. Both files are still written and verified in staging every run; without the flag they leave with the raw generation, and the staging manifest check enforces whichever set was chosen.

Scratch: one prefix. .zit-tmp-txn- (update transactions), .zit-tmp-import- (import staging), .zit-tmp-pack (pack scratch; name only, the truncate-reuse and rewrite-exclusion lock protocol are untouched). One registry prefix row and one glob line cover the tier including names that do not exist yet.

Advisory: .zit-cache is a directory -- repo (derived-state cache) and vstat (materialization plan), with the tree-checkpoint's member reserved. A legacy cache FILE is served through a read fallback and migrated by the first write via rename through a scratch name: the derivation survives with no bytes copied and no re-derive. Verified on a 66k-file pkgsrc checkout: identical SHA-256 across migration of a 35.8MB cache, fallback reads at parity, no-op update 7.69s vs 7.74s.

Legacy spellings of every renamed or relocated name stay reserved and ignore-globbed until the removal release. check-in: f1ac625093 user: dmestas tags: sub-manifest

13:41
update: cost tracks the diff, not the width

Three fixes, each found by measuring the previous one's residue.

markUnchanged: a path byte-identical between baseline and target -- same digest, same mode, with requireClean having already proven the on-disk file matches -- is skipped by every mutation phase: staging both directions, the undo journal, deletion and rewrite. An update used to delete and rewrite the entire tree regardless of how little changed.

.zit-vstat: the stat sidecar written at materialization is now the BASELINE PLAN -- version-bound, CRC'd, carrying mode and plan order -- so requireClean rebuilds the baseline with zero tree flattens when it describes the current check-in, and proves most files clean by inode/size/mtime instead of reconstructing their content out of the log. Advisory throughout: any mismatch, staleness or corruption falls back to the flatten and the content compare, which stay authoritative.

ManagedSet.fromSortedPaths: building the 66k-entry managed set through insert was quadratic -- an O(n) sorted-position memmove plus the LIVE sortedUnique assert per call -- and measured at 23.9 SECONDS of a pkgsrc update, larger than every real phase combined. The plan's own order is already sorted and unique, so the set is now built in one pass, order enforced per adjacent pair, invariant asserted once.

Measured, same store and targets as #52's gate: sqlite (2.2k files): 117.8s flat -> 0.6s adjacent / 1.2s distant pkgsrc (66k files): 42.2s flat -> 6.8s adjacent / 8.9s distant pkgsrc user CPU: ~25s -> 1.3s Residue is stat I/O (two ~66k-file sweeps), not computation.

The lockstep flatten prune #52 proposed was never built: the flatten measured 178ms at 66k files once the real costs fell.

Tests: dirty AND delete of a skipped-diff file both refuse with the tree untouched; sidecar intact/absent/rebound-to-wrong-check-in all produce identical trees; an unchanged file provably keeps its inode; an updated tree equals a from-scratch materialization. Closes #52. check-in: 8797f53546 user: dmestas tags: sub-manifest

01:23
repocache: extend across appends; import warms at completion

The derived-state cache was all-or-nothing: any log append was a total miss, and the next read re-derived the whole store. On a real 1M-artifact pkgsrc store that was 352s per cold read, re-paid after every commit.

Repository.open's miss path now distinguishes growth from rewrite. On growth it replays the cached record body (parse-free), folds only the tail frames through the same crosslink fold a cold derive uses, and rewrites the cache: every append healed by the next read at O(tail). Measured on pkgsrc: 352.31s cold, 2.40s after a commit, output byte-identical to a forced full re-derive.

Sound because extension refuses whenever the fold could be wrong rather than stale: validatePrefix proves the old tail frame still verifies at its offset (a rewritten log cannot extend), and a shun fingerprint in the cache header forces the full derive on any shun-table change, since the body was distilled under the old mask. Cache format v2; a v1 cache misses once and rebuilds.

import-git now warms the cache at completion, after the atomic rename, against the published store -- clone has done this since warmCacheFromLog existed, so an import-built store no longer defers the whole derive onto its first reader.

Tests: extension equals cold derive with a fork and control tag in the tail; a forged-cache canary proving the fold starts from the stored body; shun change takes the full derive with the mask applied.

Also backs out 1a4b74f2d5 (parallel staging): measured ~3% on a corpus where staging is ~5s of 149s -- threading complexity the number never justified. Closes #51. check-in: 84d3fe8f32 user: dmestas tags: sub-manifest