Skip to content

feat: ledger v2 — per-component ownership, deletion memory, tombstones - #14

Merged
ryandmonk merged 3 commits into
mainfrom
feat/ledger-v2
Aug 4, 2026
Merged

feat: ledger v2 — per-component ownership, deletion memory, tombstones#14
ryandmonk merged 3 commits into
mainfrom
feat/ledger-v2

Conversation

@ryandmonk

@ryandmonk ryandmonk commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Implements the ratified per-component ledger decision (Model A, 2026-08-04) plus the ratified restoredConflict resolution semantics. Resolves both granularity limitations recorded in #13, entirely inside metadata["x-bootstrap"] — exporter-owned, non-semantic metadata; no dspack spec change.

What changes

Ledger v2 (ledger: '2'):

  • ledger.components — per-entry content hashes (id → sha256). Tool-owned entries refresh deterministically; an enriched entry is preserved verbatim (stale recorded hash or absence from the map = the human-owned signal).
  • Deletion memory (skip-and-ask) — an orphaned hash means rediscovery never silently restores the component: it reports deletedAwaitingDecision and carries the hash forward until a human resolves it. Memory is never auto-retired: a transient extraction absence cannot convert a recorded deletion into a future silent re-add.
  • doNotRediscover — plain ids rediscovery must never re-add (suppressed; present-anyway reconciles as suppressedButPresent). Never auto-created.
  • Explicit restoredConflict resolutions (ratified): the exporter can prove a restructure happened but not that it is intended — so the orphaned hash is never auto-retired on conflict detection. The owner picks: keep nested (tombstone + retire → suppressed thereafter), restore top-level (regenerateSections(existing, fresh, { restoreTopLevel: [id] }) — restored from fresh tool-owned, nested authored representation preserved deep-equal, orphan hash replaced by the restored entry's own, reported restoredTopLevel {id, parent}), or leave unresolved (no change; keeps reporting). Unhonorable intents (present / tombstoned / missing-from-fresh / malformed) refuse the whole run with every id named; mixed lists are never partially honored.
  • Fail-closed against pre-v2 toolinggenerated.components is written only when every entry is tool-owned and no tombstone/orphan exists, so v2-active documents refuse under shipped 0.4.0's decideRegeneration (byte-tested against the published npm package via the dspack-export-shipped alias). decideRegeneration additionally refuses any v2 ledger carrying tombstones or deletion memory — even when the components section itself was hand-deleted. Honest limit, stated plainly: shipped 0.4.0's regenerateSections predates tombstones (pure-addition would re-add suppressed ids) — which is exactly the ratified version floor: ledger-v2 documents require ≥ 0.5.0.
  • freshDelta on preserved enriched entries — fresh-side facts only, report-only, never persisted, never a merge. Source removals inside enriched entries are explicitly not claimed (the A2 trigger; no telemetry).
  • Migration — automatic, byte-stable outside x-bootstrap (pinned both branches with differing fresh metadata). Tool-owned v1 sections attribute every entry (changed source refreshes — pinned); human-owned sections migrate unattributed with byte-identical re-adoption. Migration asks instead of guessing: a v1 human-owned section cannot distinguish "hand-deleted" from "new since the snapshot", so fresh-only ids get seeded deletion memory and report deletedAwaitingDecision — nothing is silently added across the v1→v2 boundary. Malformed / partially migrated / contradictory ledgers (including components: null) refuse with pathed findings. Still no force flag anywhere.
  • Report classifications for composer UIs: added, refreshed, unchanged, readopted, preservedEnriched, removedWithSource, keptMissingInFresh, deletedAwaitingDecision, suppressed, suppressedButPresent, restoredConflict, restoredTopLevel.

Evidence

  • Fail-first: the 15-test ledger-v2 battery ran against pre-change code (15/15 failed) before implementation; the three restore-intent tests failed against the pre-resolution code likewise.
  • Adversarially verified: an 8-verifier sweep + completeness critic attacked the release checklist with executed counterexamples; every finding is fixed and pinned (decideRegeneration residency guard, migration skip-and-ask, no auto-retirement, null-map refusal, intent precision, migration precision, GENERATOR_VERSION 0.3.0→0.5.0, vacuous bin check).
  • 139/140 tests green (skip = DS_MCP-gated roundtrip — run separately against ds-mcp: 1/1 green). Pack-and-install boundary: APIs incl. restoreTopLevel from the tarball, strict node16 compile of the published type surface, real bin invocation, version. Real-repo trial (scripts/trial.sh on shadcn-ui/next-template): config init + generate + schema validation clean, output carries ledger: '2' and generatedBy @0.5.0. Goldens regenerated deterministically (SOURCE_DATE_EPOCH=1781049600).

Release

0.5.0 — a new persisted metadata contract and the ratified version floor. Note: OIDC trusted publishing failed with PUT-404 at both 0.3.0 and 0.4.0 (npm-side registration; the workflow mechanism is identical to dspack-spec's working one). One tag-triggered attempt, then manual OTP fallback.

🤖 Generated with Claude Code

Resolves the two granularity limitations recorded in #13 by moving
components-section ownership from whole-section to per-entry, inside the
existing metadata["x-bootstrap"] ledger (exporter metadata; no spec change).

- ledger: '2' + per-entry hashes (ledger.components, id -> sha256)
- orphaned hash = deletion memory: rediscovery skips and asks
  (deletedAwaitingDecision), never silently restores
- doNotRediscover tombstones (plain ids, authored data)
- fail-closed write rule: generated.components omitted whenever any entry
  is human-owned or any tombstone/orphan exists, so shipped 0.4.0 paths
  refuse v2-active documents (byte-tested against the published package
  via the dspack-export-shipped devDep)
- freshDelta on preserved enriched entries: fresh-side facts only,
  report-only, never persisted, never a merge
- automatic migration, byte-stable outside x-bootstrap; malformed /
  partially-migrated ledgers refuse with pathed findings; no force flag
- per-entry report classifications for composer UIs (added, refreshed,
  unchanged, readopted, preservedEnriched, removedWithSource,
  keptMissingInFresh, deletedAwaitingDecision, suppressed,
  suppressedButPresent, restoredConflict, entryHashRetired)

Fail-first: the full src/tests/ledger-v2.test.ts battery (15 tests,
including the #13 restructure case and the shipped-0.4.0 fail-closed
pair) was written and run against the pre-change code first — 15/15
failed — before the implementation landed. Golden fixtures regenerated
deterministically (SOURCE_DATE_EPOCH=1781049600) and now carry v2
ledgers; pack-and-install boundary test extended to exercise orphan
skip-and-ask and tombstone suppression from the tarball. 126/127 tests
green (1 pre-existing DS_MCP-gated skip).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ryandmonk and others added 2 commits August 4, 2026 08:48
A conflict's orphaned hash is never auto-retired — the exporter can prove
the restructure happened but not that it is intended as permanent, so
retirement stays an owner decision. The three ratified outcomes:

- keep nested: tombstone + retired hash (a document edit the composer
  saves); subsequent runs report suppressed and the conflict stops —
  tombstone classification already precedes the conflict check, now
  pinned by test
- restore top-level: new regenerateSections(existing, fresh,
  { restoreTopLevel: [id] }) one-shot intent — restored from fresh as
  tool-owned, nested authored representation preserved, orphaned hash
  replaced by the restored entry's own, reported as restoredTopLevel
  {id, parent} (both representations now exist); also serves plain
  orphans as an immediate restore. Unhonorable intents (present,
  tombstoned, missing from fresh) refuse the whole run with every id
  named; nothing partially honored; never persisted
- leave unresolved: no change, memory preserved, conflict keeps
  reporting (pinned)

Fail-first: the three restore-intent tests failed against the previous
code (no options parameter, no restoredTopLevel class); outcomes 1 and 3
are behavior pins. 131/132 green. Pack smoke extended: restore intent
from the tarball plus a strict node16 tsc compile of the published type
surface — which caught RegenerateOptions missing from the library entry
(now exported).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An 8-verifier adversarial sweep + completeness critic over the release
checklist produced executed counterexamples; each is now closed and
pinned:

- decideRegeneration refuses v2 standing decisions (tombstones, deletion
  memory) even when the components section was hand-deleted — the
  whole-file path previously allowed an overwrite that erased them
- v1 human-owned migration never silently restores an absence: the
  migration run seeds deletion memory for every fresh-only id and asks
  (deletedAwaitingDecision) — absence of a record is not evidence of
  newness until v2 has been writing records; a hand-deletion made under
  0.4.0 is no longer un-deleted by the upgrade
- deletion memory is never auto-retired: entryHashRetired removed; an
  orphan whose id is transiently absent from fresh keeps its record and
  keeps asking (previously two runs converted a recorded deletion into a
  silent re-add)
- ledger.components = null refuses with a pathed finding instead of
  crashing; pathed findings pinned for bad tombstones + unknown versions
- intent-refusal precision pinned: every unhonorable id named, mixed
  valid+invalid lists never partially honored
- migration precision pinned: tool-owned migration attributes every
  entry and refreshes changed source; byte-stability outside x-bootstrap
  (not merely outside metadata) pinned for both branches with differing
  fresh metadata; restore pinned with fresh-differs-from-orphan hash
  replacement and deep-equal nested preservation
- GENERATOR_VERSION 0.3.0 -> 0.5.0 (checklist item; provenance in every
  golden regenerated deterministically)
- pack test's bin check was vacuous (|| true); it now runs the real bin
  against the committed golden with a real exit code
- CHANGELOG/README wording corrected: shipped 0.4.0's decideRegeneration
  refuses v2-active documents; its regenerateSections predates
  tombstones — that limitation is the version floor's justification,
  stated plainly instead of overclaimed

139/140 green (skip = DS_MCP-gated roundtrip, run separately against
ds-mcp: 1/1 green); pack smoke green including strict node16 type
surface and the real bin invocation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ryandmonk
ryandmonk marked this pull request as ready for review August 4, 2026 13:26
Copilot AI lite review requested due to automatic review settings August 4, 2026 13:26
@ryandmonk
ryandmonk merged commit 431f427 into main Aug 4, 2026
1 check passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements “ledger v2” for metadata["x-bootstrap"], moving components ownership from section-level to per-entry hashes with explicit deletion memory/tombstones and restore-intent handling, while keeping non-components regeneration semantics unchanged.

Changes:

  • Add ledger v2 shape (ledger: "2", components per-entry hashes, doNotRediscover) and v2-aware fail-closed rules (including decideRegeneration refusing tombstones/orphans).
  • Rework regenerateSections to do entry-level reconciliation for components, including orphan “skip-and-ask”, tombstone suppression, and explicit restoreTopLevel intents.
  • Add extensive v2 test coverage + update fixtures/docs/changelog and bump release version to 0.5.0.

Reviewed changes

Copilot reviewed 14 out of 15 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/regenerate.ts Entry-level regeneration engine for components, v1→v2 migration, restore intents, and shape validation.
src/emit/bootstrap.ts Defines LEDGER_VERSION, extends ledger schema, writes v2 ledger data, and adds v2-aware decideRegeneration refusals.
src/index.ts Exports LEDGER_VERSION and new regeneration-related types/options.
src/generate.ts Bumps embedded generator version to 0.5.0.
src/tests/regenerate.test.ts Updates existing regeneration expectations to v2 per-entry reporting/signals.
src/tests/ledger-v2.test.ts Adds comprehensive v2 behavior + interop/fail-closed test suite (including restore-intent semantics).
scripts/pack-test.sh Extends pack/install boundary test to cover v2 behaviors and type-surface compilation.
README.md Documents ledger v2 behavior and the version floor/interop story.
CHANGELOG.md Adds 0.5.0 release notes covering ledger v2 semantics and migration behavior.
package.json Bumps package version to 0.5.0 and adds dspack-export-shipped devDep alias for 0.4.0 verification.
package-lock.json Updates lockfile dependencies for new devDep alias (but currently has inconsistent top-level version fields).
fixtures/shadcn-demo/shadcn-demo.dspack.json Regenerates fixture with v2 ledger and updated generatedBy version.
fixtures/shadcn-v4-demo/shadcn-v4-demo.dspack.json Regenerates fixture with v2 ledger and updated generatedBy version.
fixtures/vuetify-demo/vuetify-demo.dspack.json Regenerates fixture with v2 ledger and updated generatedBy version.
fixtures/dtcg-tokens-demo/dtcg-tokens-demo.dspack.json Regenerates fixture with v2 ledger and updated generatedBy version.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread scripts/pack-test.sh
export type Surface = [BootstrapLedger, RegenerateReport, ComponentReport, FreshFact];
export default useAll;
TYPES
"$OLDPWD/node_modules/.bin/tsc" --noEmit --strict --target es2022 --module node16 --moduleResolution node16 smoke-types.mts
@ryandmonk
ryandmonk deleted the feat/ledger-v2 branch August 4, 2026 13:46
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.

2 participants