Skip to content

fix: mint record ids across git refs and add a spec-id uniqueness lint#137

Open
REPPL wants to merge 3 commits into
fix/iss-117-impact-write-pathsfrom
fix/iss-115-120-id-allocator-class
Open

fix: mint record ids across git refs and add a spec-id uniqueness lint#137
REPPL wants to merge 3 commits into
fix/iss-117-impact-write-pathsfrom
fix/iss-115-120-id-allocator-class

Conversation

@REPPL

@REPPL REPPL commented Jul 25, 2026

Copy link
Copy Markdown
Owner

One allocator fix for the record-id minting class (ledger issues iss-115, major + iss-120, minor): sequential ids (iss-N/itd-N/spc-N) were minted from the local working tree only, so two branches cut from the same base silently minted the same next id — the 2026-07-21 episode cost four renumber commits when two programmes both took spc-10, spc-11, iss-110 and iss-111.

Chained PR: base is fix/iss-117-impact-write-paths (#136), not main — this branch ledger-resolves its issues with the --impact verb that #136 introduces. Merge #136 first; GitHub retargets this PR automatically.

What changed

  • New canonical primitive internal/core/recordid (MaxAcrossRefs): every family allocator now mints max+1 over the union of the working-tree scan and the highest id in filenames on every local + remote-tracking branch tip (git for-each-refgit ls-tree per tip). A parallel branch can no longer re-mint an id its sibling has committed. One shared home, wired into all three allocators (capture/alloc.go, intent/create.go, spec/store.go) — no fourth copy.
  • Detection completes the class: new spec-id uniqueness record-lint rule through the existing validateIDUnique primitive (mirrors the issue/intent rules iss-74 added), flagging every file in a colliding set.
  • Degrade policy: outside a git repository there is nothing to collide against, so tree-only minting is complete and quiet; a repository whose git is unreadable is the real silent-collision risk and degrades loudly to stderr on all three CLI mint paths.
  • Accepted residual, documented at the primitive: two branches minting before either commits can still collide — the armed detectors catch it on the PR's merge preview in CI. Rejected alternatives (recorded in .abcd/work/DECISIONS.md): id-range leases, mint-at-merge, non-sequential ids — all trade away human-readable sequential ids without a maintainer mandate.
  • ADR ids: no allocator exists (hand-numbered); outside this item's directive, noted only.

Verification

  • Hermetic-git failing reproduction watched first: two branches from one base, branch A mints and commits, branch B minted the same id (fail) → mints past it (pass). Spec-id lint rule likewise watched to fail (both colliding files flagged) before passing.
  • Ref scan is bounded: O(branch tips) not O(history), 4096-ref and 8 MiB output caps; measured ≈60 ms for a full three-family scan across all refs of this repo (11 ref objects).
  • Gate run independently by the orchestrator: make preflight exit 0 (build, gofmt, vet, tests, race) and record-lint exit 0 — re-run after every commit.
  • iss-115 and iss-120 resolved open/ → resolved/ on this branch with impact: fix stamped.
  • Independent reviews before this PR opened: correctness PROMOTE (zero findings; sibling-completeness sweep confirmed no mint path bypasses the primitive, and no reachable silent under-count in the capped ref scan). Security initially HOLD on one confirmed finding — the itd/spc mints lacked the integer-ceiling guard the iss path already had, so a hostile ref-carried itd-<MaxInt> filename could wrap the id negative — fixed in the third commit (guard mirrored into both paths, watched-fail tests proving loud-refuse plus zero-write), then re-reviewed PROMOTE with no bypass found.

REPPL added 3 commits July 25, 2026 15:06
Sequential record ids (iss-N, itd-N, spc-N) were minted from the local
working tree only, so two branches cut from the same base silently minted
the same next id (iss-115, iss-120): invisible on each branch, surfacing
only at merge where it breaks the bidirectional intent-spec link.

Consolidate the git-ref side of the maximum onto one canonical primitive,
recordid.MaxAcrossRefs, and fold it into all three allocators (capture's
reservePath, intent's nextIntentID, spec's NextID) so minting is max+1 over
the union of the working tree and every local/remote-tracking branch tip.
Once one branch commits an id, the other mints past it. A repository git
cannot read degrades loudly to working-tree-only minting on stderr (never a
silent fallback); a non-repository directory has no refs to collide with and
mints quietly. The residual window (two branches minting before either
commits) is accepted and caught by the armed record-lint uniqueness rules on
the merged PR union.

Complete the class for detection: add the spec_id_unique record-lint rule
through the shared validateIDUnique primitive (mirroring issue_id_unique and
the intent-id guard), flagging every file that claims a duplicate spc-N.

Assisted-by: Claude:claude-opus-4-8
Both close on the refs-union allocator + spec_id_unique lint landed in the
preceding commit. Resolved with --impact fix (a bug-class correctness fix).

Assisted-by: Claude:claude-opus-4-8
The iss allocator already refuses near math.MaxInt, but the itd and spc mints
computed max+1 with no guard. A hand-crafted MaxInt filename on any scanned
source — a local file or a fetched remote-tracking ref carrying
itd-<MaxInt>-x.md / spc-<MaxInt>-x.md — parses to math.MaxInt with no error,
so max+1 wrapped to math.MinInt and minted a negative id (itd--… / spc--…).
That malformed record was persisted by WriteFileAtomic BEFORE Validate ran,
and the family's mint was DoS'd while the hostile source was present.

Mirror the capture allocator's ceiling guard in both nextIntentID and spec
NextID: refuse loudly ("counter near the integer ceiling; refusing to
allocate") before the arithmetic, so nothing is written.

Also restore .abcd/record-lint.json to its original compact formatting (a
prior commit reflowed it pretty via a one-off serializer, inflating a 1-line
semantic change); the spec_id_unique rule addition is preserved.

Assisted-by: Claude:claude-opus-4-8
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant