feat: ledger v2 — per-component ownership, deletion memory, tombstones - #14
Merged
Conversation
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>
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>
There was a problem hiding this comment.
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",componentsper-entry hashes,doNotRediscover) and v2-aware fail-closed rules (includingdecideRegenerationrefusing tombstones/orphans). - Rework
regenerateSectionsto do entry-level reconciliation forcomponents, including orphan “skip-and-ask”, tombstone suppression, and explicitrestoreTopLevelintents. - 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.
| 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the ratified per-component ledger decision (Model A, 2026-08-04) plus the ratified
restoredConflictresolution semantics. Resolves both granularity limitations recorded in #13, entirely insidemetadata["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).deletedAwaitingDecisionand 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 assuppressedButPresent). Never auto-created.restoredConflictresolutions (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 →suppressedthereafter), 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, reportedrestoredTopLevel {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.generated.componentsis written only when every entry is tool-owned and no tombstone/orphan exists, so v2-active documents refuse under shipped 0.4.0'sdecideRegeneration(byte-tested against the published npm package via thedspack-export-shippedalias).decideRegenerationadditionally 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'sregenerateSectionspredates tombstones (pure-addition would re-add suppressed ids) — which is exactly the ratified version floor: ledger-v2 documents require ≥ 0.5.0.freshDeltaon 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).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 reportdeletedAwaitingDecision— nothing is silently added across the v1→v2 boundary. Malformed / partially migrated / contradictory ledgers (includingcomponents: null) refuse with pathed findings. Still no force flag anywhere.added,refreshed,unchanged,readopted,preservedEnriched,removedWithSource,keptMissingInFresh,deletedAwaitingDecision,suppressed,suppressedButPresent,restoredConflict,restoredTopLevel.Evidence
restoreTopLevelfrom the tarball, strict node16 compile of the published type surface, real bin invocation, version. Real-repo trial (scripts/trial.shon shadcn-ui/next-template): config init + generate + schema validation clean, output carriesledger: '2'andgeneratedBy @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