Skip to content

feat(rollback): derive global-alias ownership from ledger (RAN-491)#13

Merged
random1st merged 3 commits into
mainfrom
voroninr/ran-491-ownership-derivation
Jun 12, 2026
Merged

feat(rollback): derive global-alias ownership from ledger (RAN-491)#13
random1st merged 3 commits into
mainfrom
voroninr/ran-491-ownership-derivation

Conversation

@random1st

Copy link
Copy Markdown
Contributor

RAN-491: owning_package corruption no longer load-bearing

owning_package on global aliases decides what rollback tombstones, but it sits outside compute_state_fingerprint — corrupting it was invisible to drift-check. Widening the fingerprint would invalidate every existing baseline in every repo (mass false drift), so this PR implements the recorded decision decision.s5d.ownership-derivation instead:

Rollback derives the owner of each global alias from the append-only ledger + stored packages at the point of use. The stored field degrades to a verified cache.

Semantics (test-pinned)

  • Derivation confirms or vetoes tombstoning, never expands it — a wrong derivation can keep data alive but can never destroy an alias the stored field does not claim.
  • Mismatch (stored claims spec, ledger says other) → skip + loud warning, checked before the referenced-globals guard so corruption on shared globals is reported, not shadowed.
  • Ledger says spec but stored names another → suspected tamper, reported, no destructive action.
  • No ledger trace → stored-field fallback with warning (legacy repos keep working).

First-import derivation is correct by construction: AliasTable::resolve is first-creator-wins, apply_renames preserves the owner, no transfer API exists.

Also

  • rollback_spec extracted into the library (next to execute_import); CLI run_rollback and MCP tool_s5d_rollback are thin callers — the copy-paste duplication is gone.
  • owning_package_corruption_is_invisible_to_drift_check flips from "known limitation" to "by design".
  • Fingerprint untouched → zero false drift on existing baselines, no migration.

Evidence

  • Full S5D dogfood: decision (3 hypotheses, 5 adversarial probes, review-gate pass) + feature spec imported, drift-check synced.
  • 3 new tests (veto, suspected-tamper, legacy-fallback) + 3 existing rollback tests green; 203 passed total, fmt/clippy clean.

🤖 Generated with Claude Code

…CP duplication

RAN-491: owning_package decides what rollback tombstones but sits outside
compute_state_fingerprint, so corrupting it was invisible to drift-check
while remaining load-bearing. Chosen fix (decision.s5d.ownership-derivation,
TRIZ separation-on-condition): instead of widening the fingerprint -- which
would invalidate every existing baseline in every repo -- rollback now
derives the owner of each global alias from the append-only ledger plus
stored packages at the point of use. The stored field degrades to a
verified cache.

Semantics pinned by tests:
- derivation confirms or vetoes tombstoning, NEVER expands it: a wrong
  derivation can keep data alive but can never destroy an alias the stored
  field does not claim
- mismatch (stored claims spec, ledger says other) = skip + loud warning,
  checked BEFORE the referenced-globals guard so corruption on shared
  globals is reported rather than silently shadowed by kept_referenced
- ledger says spec but stored names another = suspected tamper, reported,
  no destructive action (dodge attempts surface instead of silently winning)
- no ledger trace = stored-field fallback with warning, so legacy repos
  (pre-ledger aliases, manual edits) keep working

First-import derivation is correct by construction: AliasTable::resolve is
first-creator-wins, apply_renames preserves the owner, and no ownership
transfer API exists.

rollback_spec is extracted into the library next to execute_import; CLI
run_rollback and MCP tool_s5d_rollback are thin callers now -- the previous
copy-paste duplication is gone, so the cross-check exists exactly once.

owning_package_corruption_is_invisible_to_drift_check flips from "known
limitation" to "by design": the field is no longer load-bearing, so
drift-check staying synced is intended; protection lives in rollback.
Fingerprint formula untouched -- zero false drift on existing baselines,
no migration day.
Tribunal round 1 (Codex, REJECT) found two real holes in the first
derivation:

1. It applied CURRENT package files to HISTORICAL ledger entries,
   ignoring LedgerEntry.spec_sha256 -- a backdated package edit could
   fabricate a false owner and veto a legitimate rollback.
2. It ignored rollback epochs: AliasTable::resolve is first-creator-wins
   within the ACTIVE epoch (rollback tombstones, next import re-creates
   with a new owner), so "first ever importer" false-flagged the
   legitimate second-epoch owner on an untampered repo.

Replay now: an import/reconcile entry contributes claims only when the
current file sha matches the entry; a mismatching or missing file poisons
every key unowned at that point (Unverifiable), cleared only by that
package's own rollback entry; a rollback entry releases the package's
ownerships.

Unverifiable deliberately does NOT route to the no-trace fallback: the
fallback tombstones, and a two-file tamper (alias field + any package
file) could otherwise downgrade the veto into destruction. Inconclusive
evidence keeps data and reports loudly; only zero-trace legacy aliases
fall back to stored-field trust.

Both counterexamples are pinned as tests:
- rollback_after_reimport_does_not_false_flag_new_owner
- rollback_skips_tombstoning_when_ownership_unverifiable

Tribunal verdict + resolution recorded in
.s5d/evidence/feat.s5d.ownership-derivation/tribunal-round1.md.
…g globals

Tribunal round 2 (Codex, REJECT) caught two lifecycle holes in the
round-1 poison model:

1. Normal evolution (import -> edit -> re-approve -> re-import) left the
   FIRST ledger entry with a stale sha, which poisoned the package's own
   keys and degraded its own rollback to an "unverifiable" skip. The
   read-rule now treats self-poison as harmless by construction:
   whichever of a package's imports actually created the key, the owner
   is that same package -- only a FOREIGN package in the poison set
   breaks certainty.

2. referenced_globals walked every package file regardless of record
   status, so a rolled-back spec left on disk pinned its globals alive
   forever. Pre-existing behavior, but inconsistent with the epoch
   semantics the derivation now implements: a Deprecated record's claim
   ended with its rollback. Such specs no longer count as referencing.

New DerivedOwnership::UnverifiedCandidate distinguishes "only ledger
candidate, unverified": consistent with the stored field -> fallback
tombstone with warning; conflicting -> non-destructive skip. Pure
multi-candidate ambiguity stays Unverifiable.

Both lifecycle variants pinned:
- rollback_after_edit_and_reimport_tombstones_own_globals
- rollback_epoch_transition_works_with_old_spec_file_left_on_disk
@random1st
random1st merged commit c713001 into main Jun 12, 2026
3 checks passed
@random1st
random1st deleted the voroninr/ran-491-ownership-derivation branch June 12, 2026 08:12
random1st added a commit that referenced this pull request Jul 1, 2026
New tests:
- approve_rejects_spec_modified_since_preview (#6)
- import_rejects_spec_modified_since_approval (#6)
- rollback_after_import_tombstones_aliases_and_removes_index (#10)
- rollback_without_successful_import_fails (#10)
- decide_rejects_winner_without_spec_ref (#15)
- reconcile_restores_synced_status_after_drift (#9)

Known bug (test ignored):
- rollback_of_first_spec_does_not_break_second_spec_sharing_global_artifact
  (#13) — CONFIRMED: rollback tombstones global aliases by owning_package,
  breaking other specs that share the same global artifact.

84 tests pass, 1 ignored (known bug).
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