Artifact e5699688b0f43b7480ed5d89f11585c2f61114bbe3ec9dae5061741f37b37e7a:
- File
src/repostate_test.zig
-
2026-07-19 11:56:16
— part of check-in
[bdb49cc5ca]
on branch blockers
— repostate: break same-mtime resolution ties by DAG ancestry, not hash order
Three commits made inside the same second made `zit` return the WRONG file content for `tip` and `trunk`, and made the binary contradict itself: `zit info tip` reported "leaf: no" while `zit leaves` named the same check-in a leaf. `zit cat f.txt tip` returned the ROOT's content and `zit cat f.txt trunk` returned the MIDDLE's, on a three-commit chain whose newest content was `v3`. A version-control system that silently hands back an old revision under a name that means "newest" is disqualifying, so this lands before anything else.
WHAT WAS WRONG — three independent tie sites, not one
The originally-recorded diagnosis blamed a single lexicographic artifact-hash tiebreak in `mergeBaseOf`. That was wrong for the reported symptom: the RED-proof gate built the equal-mtime chain in BOTH hash orderings and observed IDENTICAL failures, which rules out hash order as the mechanism. Fixing only that line would have left `cat tip` and `cat trunk` still returning wrong answers. The real picture:
- `tipForm` — `mtime > best_mtime`, a strict `>` with no tiebreak at all, so the first entry in `events` INSERTION order won. Symptom: `tip` -> ROOT. - `resolveName` — ties settled by `tagxref` hash-map ITERATION order. Symptom: `trunk` -> MID. - `mergeBaseOf` — lexicographic artifact hash. A genuine defect, proven RED at unit level, but not the cause of the tip/trunk symptom and unreachable from the e2e merge path.
WHAT CHANGED
Equal-`event.mtime` ties are now settled by DAG ancestry: a descendant beats its ancestor. Genuinely concurrent nodes — same mtime, no ancestry relation — keep a deterministic least-artifact-hash fallback, documented at each site; hash order between UNRELATED leaves was never the bug. The shared helper (`MtimeMaxPick`) is used by `tipForm`, `resolveName`, `openLeafOf` and `dateResolve`, so every symbolic-name surface settles ties one way.
Timeline ordering gets a second pass that re-orders each equal-mtime RUN descendants-first, deliberately overriding the hash tiebreak. It is a per-run repeated-maximal-element extraction, NOT a sort predicate: "descends-from, else hash" is not a strict weak order, and handing it to `std.mem.sort` would be undefined behavior. Runs are bounded by the check-ins sharing one millisecond.
Settling a tie by ancestry allocates, so `resolveName`/`resolve`/`openLeafOf` gained `OutOfMemory`. Every caller switches it as an ABORT, never as a mismatch or "already correct" verdict — reporting an allocation failure as the exit-0 up-to-date message is exactly the lie those switches exist to prevent. `repository_test`'s closed-error-set assertion moved 2 -> 3 for that reason: it encoded the pre-fix set, not an inconvenient expectation.
Two ride-alongs on the same resolution surface:
- SF1 — `zit update trunk` used to fail as "invalid check-in ID": update accepted ONLY canonical 40/64-hex. Its argument is a check-in name like every other verb's, so it now runs the shared 8.3 procedure. A canonical ID still short-circuits, leaving update's cold path byte-for-byte as it was. - SF5 — a `tip` that names a non-leaf is now an in-binary assertion, not just a test. It is legitimate ONLY under a non-monotone clock (a child stamped strictly earlier than its parent); a child that TIES its parent means the ancestry tiebreak failed to fire, which is the defect itself. This tripwire would have caught the original bug.
RED PROOF (recorded by the RED-proof gate, 2026-07-19; per BLOCKER-FIX-PLAN.md section 2, no product code changed until RED was proven)
Unit, `src/repostate_test.zig`, deterministic equal-mtime chains built directly in BOTH hash orderings — no wall-clock dependence:
repostate REGRESSION B1: tip resolves to the descendant on an equal-mtime chain (head has the greatest hash) repostate REGRESSION B1: tip resolves to the descendant on an equal-mtime chain (head has the least hash) repostate REGRESSION B1: resolveName picks the descendant on an equal-mtime chain (head has the greatest hash) repostate REGRESSION B1: resolveName picks the descendant on an equal-mtime chain (head has the least hash) repostate REGRESSION B1: tip and resolveName agree with the leaf set in both hash orderings repostate REGRESSION B1: mergeBaseOf picks the deepest common ancestor when every candidate shares an mtime [failed pre-fix: expected 2, found 1]
End-to-end, `src/e2e.zig`, same-second commits with NO sleep between them — same-second IS the point:
cli REGRESSION B1: same-second commits resolve tip and trunk to the newest check-in cli REGRESSION B1 (SF1): update accepts a symbolic name and lands on the newest check-in cli REGRESSION B1: open materializes the newest same-second check-in into a fresh clone cli REGRESSION B1: a same-second two-branch merge picks the correct base and is never up to date
All of the above failed against pre-fix trunk; the two hash-ordering pairs failed IDENTICALLY, which is the evidence that retired the hash-order theory.
TEST EVIDENCE NOW
In this worktree at the committed tree: `zig fmt --check .`, `zig build`, and `zig build test` each exit 0 — full battery, no filtering. The only pre-existing expectation changed is the `repository_test` error-set count noted above, and it changed because it encoded the old set.
Markers `// REGRESSION B1:` are stable so state probes can grep trunk for them.
No borrowed technique: the ancestry tiebreak and the per-run topological micro-ordering are original to this change. (user: dmestas, size: 75991) [annotate] [blame] [check-ins using]
-
2026-07-19 14:32:52
— part of check-in
[bc7b914316]
on branch trunk
— Merge blockers: resolve same-mtime ties by DAG ancestry (B1/B2)
Three commits inside one second made 'zit cat f.txt tip' return the root's content and 'trunk' the middle's, while 'zit leaves' named the newest — the binary contradicting itself, exit 0, no warning.
The diagnosis that opened the campaign was wrong and is worth recording: it blamed a single lexicographic-hash tiebreak in mergeBaseOf. Building the equal-mtime chain in BOTH hash orderings produced identical failures, which ruled hash order out as the mechanism. There were three independent tie sites — tipForm settled on events insertion order, resolveName on tagxref iteration order, mergeBaseOf on artifact hash. A fix confined to the originally-blamed line would have left tip and trunk still wrong.
Equal event.mtime is now settled by ancestry: a descendant beats its ancestor. Genuinely concurrent nodes keep a deterministic least-hash fallback — hash order between unrelated leaves was never the defect. Timeline gets a second pass re-ordering equal-mtime runs descendants-first.
Ride-alongs on the same surface: 'update' accepts symbolic names (SF1), and a 'tip' naming a non-leaf is now an in-binary assertion (SF5) — the tripwire that would have caught this originally.
Merged tree re-verified on trunk: fmt, build, test, e2e, dst 6x1000 all green. Remote CI is unavailable (self-hosted runner offline); the local battery is the gate per operator direction. (user: dmestas, size: 75991) [annotate] [blame] [check-ins using]
-
2026-07-19 11:56:16
— part of check-in
[bdb49cc5ca]
on branch blockers
— repostate: break same-mtime resolution ties by DAG ancestry, not hash order