Skip to content
zit Tags and Properties
Login

Tags and Properties

Tags and Properties for Check-in d8e5e8117d

Context
2026-07-19
15:16
tag: add `zit tag add NAME CHECKIN`, minting a SINGLETON sym card

`tag` was read-only: `tag list` was the only subcommand, so a repository could not be given a name for a check-in at all (blocker B6). The verb told the truth about what existed, but the capability was simply absent.

WHAT CHANGED

`zit tag add NAME CHECKIN` mints one CONTROL artifact carrying a single `T +sym-NAME` card targeting the resolved check-in's 64-hex hash.

THE SIGN IS THE WHOLE CORRECTNESS ARGUMENT. The card is a SINGLETON (`+`), not a propagating (`*`) tag. `repostate.resolveName` (src/repostate.zig:891) picks the max-`event.mtime` check-in among ALL active `sym-` rows, so a propagating `*sym-v1` would be inherited by every descendant and then resolve to the NEWEST of them — the name would silently drift off the check-in the user tagged, which is the same class of wrong answer as B1. `applyTagOps` (src/repostate.zig:727) gates propagation on `op.type != .singleton`, so a `+` card pins the name with ZERO repostate changes. The contrast with a branch is deliberate and now documented at both sites: a *branch* sym propagates precisely because descendants must inherit it; a *tag* sym does not, precisely because they must not.

`amend`'s steps 5-6 — assemble the control manifest, run the runtime-safety round-trip self-check, `putArtifact` + `commit` under the single fsync — are extracted into `mintControlArtifact`, and `tag add` is its second consumer. Both verbs resolve a check-in, compose a T run, then mint; that last step is now written once. amend's existing battery guards the extraction unchanged. The helper's error set is `logstore.OpenError`, the widest of the three the body can produce, so each verb renders it under its own name.

`tag add` closes its read view BEFORE the read-write append (commit's durability pattern), so it owns its repository lifetime rather than sharing the listing path's `defer repo.deinit`. Resolution and the empty-NAME check both precede the mint: a run that cannot resolve its CHECKIN mints nothing, asserted by a test.

`selectInheritedSyms`'s comment said zit mints no singleton syms itself, so the §6.2 exclusion of them from a branch move's cancel set was reachable only via a synced `T +sym-X *`. That is no longer true — the comment is corrected. The behavior needed no change, and its unit test already covered a singleton sym.

RECORDED, NOT ASSUMED: branch creation shares no read-only gap. It is already served by `commit --branch`, which mints the propagating branch/sym pair directly on the check-in manifest (src/cli.zig `composeCommitTags`), so there is no `branch add` to write. Cancel/remove semantics stay deferred to 0.2 — closing "cannot create tags" is the blocker; a full tag lifecycle is not.

RED PROOF (per BLOCKER-FIX-PLAN.md section 2, no product code changed until RED was proven; markers `// REGRESSION B6:`)

End-to-end, src/e2e.zig — all five failed pre-fix at `tag add` with exit 2 ("unknown subcommand 'add'"):

cli REGRESSION B6: tag add creates a tag that tag list and tag list CHECKIN report cli REGRESSION B6: a created tag pins to its check-in and is not inherited by a descendant (tag before commit) cli REGRESSION B6: a created tag pins to its check-in and is not inherited by a descendant (commit before tag) cli REGRESSION B6: a created tag survives pack cli REGRESSION B6: a created tag survives a clone

The pinning pair runs both orderings — tag-then-commit exercises inheritance at commit time, commit-then-tag exercises the propagation walk over already crosslinked descendants. Identical behavior in both is what proves the mechanism rather than luck. pack and clone prove the round trip: a synced artifact that does not survive sync is a worse lie than a missing verb.

Added alongside them: `tag add`'s arity errors, and an unresolvable CHECKIN leaving no tag behind. Unit, src/cli.zig: the parser's add grammar (both positionals required, `--help`/bad option winning over arity, `--` handling), `composeTagAddCards` minting exactly one `+sym-` card at the target, its control round trip through `artifact.parse` proving the sign survives the wire, and leak-freedom under `checkAllAllocationFailures`.

TEST EVIDENCE NOW

In this worktree at the committed tree: `zig fmt --check .`, `zig build`, `zig build test`, and `zig build e2e` each exit 0 — full battery, no filtering.

CHANGED EXPECTATIONS (both encoded the missing capability, nothing else)

cli tag B-tag-7 (src/cli.zig) and cli tag B-tag-13 (src/e2e.zig) used the literal token `add` as their stand-in for "not a subcommand". `add` IS one now, so both use `cancel`, a word that still is not.
cli tag B-tag-11's verbatim `tag_help_expected` gains the `add` usage line and its paragraph, because `tag --help` must teach the new grammar.

No borrowed technique: the singleton-sign argument and the mint extraction are original to this change. No performance claim is made.

SIBLING MERGE

This checkout absorbed the sibling B3 commit (1aeb531cac) before landing. The only conflict was in src/e2e.zig and was positional, not semantic: both changes append a new regression block at the end of the file. Both blocks are kept, one copy each; the two candidate B3 hunks fossil presented were byte-identical, and the full battery above was re-run on the merged tree, not just on mine. check-in: d8e5e8117d user: dmestas tags: blockers

11:56
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. check-in: bdb49cc5ca user: dmestas tags: blockers