Skip to content

Reclaim a handed-over key on a curated row uuid instead of reviving it - #365

Draft
MaxGhenis wants to merge 6 commits into
codex/thesis-ledger-factsfrom
fix/remint-consumed-key-reclaims
Draft

MaxGhenis wants to merge 6 commits into
codex/thesis-ledger-factsfrom
fix/remint-consumed-key-reclaims

Conversation

@MaxGhenis

Copy link
Copy Markdown
Contributor

Fixes finding L2 from the round-2 independent review of #360 (pre-existing, outside #360's diff).

Stacked on #360. This branch starts at #360's head (f9e0f893), because the enrichment differential it extends lands in #360. Until #360 merges, the diff below includes #360's commits. Once #360 merges, I'll merge the base tip in so only this change remains.

Bug

resolve_uuid's first branch handles a catalog row whose uuid differs from its key's registry binding (the curator-remint path). It planned revive + supersede even when the key had retired after handing its lineage over through a succeeds event. The grammar refuses to revive a consumed key ("revives a consumed lineage — handed-over predecessors are terminal"), so staging rejected the plan before any write. The failure was safe, but the curator got a staging error instead of a valid plan.

The reviewer's repro:

  • registry: mint B(US/null)/U1, retire B, E(US/null, economy) succeeds B
  • catalog: a geography-free docket row with a hand-edited, unregistered U2
  • docket: the entry gains country US, which re-keys the row onto B

Before this PR the builder planned revive B + supersede B→U2, and staging rejected both events.

Fix

  • When the key is retired and handed over, the builder now plans a reclaimed event on the row's uuid. That uuid is new to the registry on this path, which the existing ownership check enforces. This matches the branch below it, which already reclaims a handed-over key on a fresh uuid4. Both branches now share _plan_reclaim.
  • A reclaim changes no live binding, so it needs no --allow-remint, the same as the existing fresh-uuid reclaim. --check still fails until the reclaim is written ("identities missing from the registry").
  • If the row's uuid is already known to the registry, the builder refuses at plan time with a message naming the cause ("handed its lineage over … a reclaimed identity is a NEW series and mints fresh"). Before, the message blamed a supersede. The message also names two remedies: give the row an unregistered uuid, or delete the row so the builder reclaims on a fresh one. A test runs the second remedy.
  • The UuidRegistry docstring now lists the reclaim event (it said "five event kinds" and left reclaim out). The backlog doc has a dated entry.

Invariants (stated and tested)

For every registry history the grammar admits, and for every existing docket-only row and entry, at both call sites:

  1. Soundness. Every plan build_catalog returns stages under the full grammar, with main's ceremony note on gated events, and the staged registry agrees with the catalog: live bindings and rows are in bijection.
  2. Completeness. A retire + succeeds hop is planned exactly when UuidRegistry admits that pair.
  3. Refusals. A refusal is a plan-time SystemExit carrying a named diagnostic, and never happens when the grammar admits the hop.
  4. No hijack. Without a hop, the row takes its key's binding or a uuid new to the registry, never a uuid another key holds.
  5. Handed-over keys are terminal. A retired, consumed key is never revived or superseded. It comes back only through a reclaim on a uuid new to the registry, and its old uuid stays with its successor.

Tests

  • test_a_curated_row_uuid_reclaims_a_handed_over_key: the reviewer's exact repro at build_catalog level. The plan is exactly reclaimed B/U2 plus E's gated retire, with no revive and no supersede. It stages, B is live on U2 and no longer consumed, U1 stays with E, and agreement holds.
  • test_a_handed_over_key_reclaims_the_row_uuid_through_main: the same move end to end. --check fails first, main then writes exactly the reclaim without --allow-remint, --check passes, and a rerun is byte-idempotent.
  • test_a_registered_row_uuid_never_reclaims_a_handed_over_key: the row carries B's own pre-remint uuid. The builder refuses with and without --allow-remint and leaves both artifacts byte-untouched. The named remedy (delete the row) then reclaims on a fresh uuid, and --check passes.
  • test_docket_enrichment_is_planned_exactly_when_the_grammar_admits_it (Hypothesis, 400 examples): the docket call-site arm of Let an enriched docket placeholder be enriched again #360's differential, with the same four assertions. It reuses the state-aware _generated_registry. It draws entries with no country, US or GB; rows at any placeholder geography carrying any uuid, including unregistered ones; and rows of another concept that reach the entry only through a curated alias (one draw in five). It also covers reviewer note N2.
    • Steering and measured reach (--hypothesis-show-statistics, seeds 1–3): the handed-over-entry class with an unregistered row uuid (L2) appears in 1.5–4.3% of cases (about 7–23 per run). The same class with a registered uuid appears in 9–10%, and admitted hops in 4.5–6.5% (about 22–35 per run).

Evidence

Check Result
uv run --locked pytest tests/test_build_series_catalog.py -q 239 passed
uv run --locked python scripts/build_series_catalog.py --check catalog current: 221 series
uv run --locked ruff check . All checks passed
Counterfactual: the new tests against the unfixed builder (#360 head), seeds 1–5 The 3 deterministic tests fail. The docket differential fails on 5/5 seeds and shrinks to staging rejecting "revives a consumed lineage" at the docket site.
Reviewer's bounded-exhaustive harness (both call sites), unfixed builder, depth 4 3,416 states, 40,992 builds, 578 divergences, all STAGING-REJECTED hop=False revive=True at the docket site. This reproduces the review.
Same harness on this fix, depth 4 3,416 states, 40,992 builds, 0 divergences
Same harness on this fix, depth 5 17,094 states (188 with a live consumed key), 205,128 builds, 0 divergences
Same harness on this fix, depth 6 58,728 states (2,381 with a live consumed key), 704,736 builds, 0 divergences

The committed ledger never reaches the changed branch: --check output is unchanged, and so is the catalog.

axiom: n/a: catalog builder and registry tooling, with no policy encoding.

🤖 Generated with Claude Code

MaxGhenis and others added 6 commits October 16, 2026 18:58
_plan_enrichment required uuid_owner[row_uuid] == the row's own key.
uuid_owner records FIRST ownership and never moves, so a placeholder
that was itself the successor of an earlier enrichment (geography-free
-> geography-only) never owns its UUID, and its observation fell to the
mint path and was refused as UUID reuse. This blocked
usaspending.dod.prime_award_obligations (a2649fd9) from taking its
FY2026 observation (Thesis run 38962977031).

Drop that conjunct. The live-binding conditions that remain
(enrichment-shaped, the row's key live, and bound to row_uuid now) are
the contract and still refuse stale, superseded, retired, handed-over
and reclaimed rows. The mint-path refusal now says "first bound to",
since uuid_owner is not the current binding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Regressions:
- the production state (registry lines 196/249/250 for a2649fd9): the
  geography-only placeholder takes its first print as exactly the
  retire + succeeds pair Thesis's append guard admits;
- the chain through main (docket entry learns a country, then the first
  print lands): no ceremony, one UUID, --check after each hop;
- six stale rows after a chain (superseded, retired, handed over,
  reclaimed, chain root, another series' UUID) still refused at plan time
  by the live-binding conditions alone, artifacts byte-untouched.

Properties (Hypothesis):
- every builder-reachable chain, any country, vintage and entity, keeps
  one UUID, writes one retire + succeeds pair per hop, passes --check
  and is byte-idempotent;
- in any grammar-admitted history, a live binding its key did not first
  own was established by succeeds (why the owner conjunct only ever
  refused chains);
- differential: the builder plans an enrichment exactly when
  UuidRegistry accepts the pair, everything it plans stages and agrees
  with its catalog, and refusals are plan-time diagnostics.

Counterfactually verified: with the owner conjunct restored, both
regressions, the chain property and the differential property fail
(the differential on 5/5 seeds, shrinking to the dod shape); the
stale-row tests pass either way.

The mint-path diagnostic now says "already knows (first bound to …)",
so test_mint_never_reuses_a_bound_uuid matches that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Independent review of #360 found a state the live binding misses. A
placeholder B hands its lineage to C, then (docket loses and regains its
country) takes a fresh UUID back from a coarser placeholder A. B is live
and bound to that UUID but still consumed, and the grammar never lets a
predecessor hand over twice. Without the dropped owner conjunct the
builder planned retire(B) + C succeeds B, which staging then rejected.
_plan_enrichment now also requires prior_own_key not in
registry.consumed, so it refuses at plan time again (as base did).

The review's LOW finding, pre-existing: a docket entry matched to a
placeholder through a curated alias keyed the docket row under the alias,
so the planned succeeds crossed concepts and died in staging. Enrichment
now also requires the prior's concept to equal the canonical one.
Observed buckets always take the prior's concept, so only that docket
path changes, from a staging failure to a plan-time refusal.

Tests:
- the review's lifecycle through main (seven events, B live and
  consumed), refused with and without --allow-remint, artifacts
  byte-untouched;
- the cross-concept docket alias, refused at plan time;
- the registry generator gains a "return" move (mint a coarser
  placeholder and hop it onto a retired key that already handed over),
  and retired hop targets and consumed rows are weighted up, so the
  differential property reaches live-but-consumed keys.

Counterfactuals: without the consumed check, the lifecycle regression
and the differential property fail on 5/5 seeds; without the concept
check, the alias regression fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Round-2 review of #360 (LOW L1, L3; NIT N1):

- L1 (new in this PR): a docket entry matching a placeholder through a
  curated alias was refused with the UUID-reuse message, whose remedy
  (curated alias + --allow-remint) cannot help.
- L3 (pre-existing): a placeholder that already handed over once was
  refused with "superseding UUIDs must be new", blaming a remint nobody
  made.

When the placeholder holds the row's UUID live but either hand-over rule
fails, _plan_enrichment now raises a diagnostic naming the cause: lineage
never crosses concepts (name the docket entry by the placeholder's own
concept), or a placeholder hands over at most once (delete its catalog
row and rerun with --allow-remint). Both callers already refused every
such row, since the UUID is registered, so only the message changes.

Tests exercise each suggested curation: renaming the docket entry
enriches cleanly, and deleting the consumed placeholder's row then
--allow-remint revives the observed identity's own UUID and retires the
placeholder, with --check passing. _event_kind now reads planned events
too (absent markers there are None, not missing).

N1: the backlog bullet and docstring said the live binding alone is the
contract; they now state the hand-over rules too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in dd929cb and 86da023 (append lane) and the #363 fold, so the branch is current with its strict-protected base.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When a catalog row carried a uuid other than its key's binding,
resolve_uuid planned revive + supersede even when the key had retired
after handing its lineage over through a succeeds event. The grammar
refuses to revive a consumed key, so staging rejected the plan before
any write. A docket entry gaining a country reached it by re-keying a
geography-free placeholder row onto the handed-over country key
(finding L2 of the chronicle#360 round-2 review).

The builder now plans a reclaim on the row's uuid, which is new to the
registry on that path, as the handed-over branch already did with a
fresh uuid. A row whose uuid the registry already knows is refused at
plan time with a message naming the cause. The enrichment differential
gains a docket call-site arm (alias rows, no-country/US/GB entries,
unregistered row uuids), steered so the handed-over class is reached on
every seed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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