feat(composer): ledger-v2 entry-level ownership — decisions, not defaults - #31
Merged
Conversation
…ults Pairs with dspack-export#14 (ledger v2). The composer now reads and acts on per-component ownership: - composer-core: componentEntryStatuses (WebCrypto per-entry hashes, pinned byte-for-byte against a real dspack-export 0.5.0 golden); v2-aware section state (the omitted whole-section signal derives from entries); explicit actions restoreComponent / addTombstone / removeTombstone; applyFreshFact limited to scalar leaves and pure additions; preservesLedger now also refuses wholesale destruction of deletion memory (per-entry map, non-empty tombstone list, version tag) - composer app: rediscovery report rendered as decisions — added / refreshed / readopted / preserved-yours / removed-with-source / kept- missing / suppressed / conflicts all named; deletions awaiting decision carry Restore and Never-rediscover buttons; freshDelta facts are review info with per-fact explicit Accept; Ownership panel lists entry states with orphan/tombstone actions; inventory shows per-entry ownership chips (v1 documents keep section-level display — no invented states) - agent: rediscover passes the 0.5.0 report through unchanged; tests cover v1->v2 migration on the real demo project and the full skip-and-ask loop (hand-delete -> asked, not restored -> tombstone -> suppressed) through the HTTP surface - paired-PR protocol: pnpm link override to the dspack-export feat/ledger-v2 worktree; agent range ^0.5.0. CI stays red until dspack-export 0.5.0 is on the registry and the override is dropped (same protocol as the emit 0.4.0 train) Fail-first: the updated agent assertions (report.components.added, report.migration, ledger.entryLevel) and the composer-core v2 suite all fail against published dspack-export 0.4.0 / the previous composer-core by construction. Full workspace green under the override: composer-core 21/21, agent 29/29, all other packages unchanged and passing; composer app builds. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The ratified conflict outcomes, surfaced as decisions: - "Keep nested": tombstone + retired memory through the ordinary ledger-preserving save; the conflict row moves to suppressed and stops reporting on subsequent rediscoveries - "Restore top-level": passes the one-shot restoreTopLevel intent through the agent to dspack-export regenerateSections — the entry returns from fresh extraction tool-owned, the nested authored representation stays untouched, and the report line says both now exist; refusals are the tool's words verbatim - deciding nothing remains a valid outcome: memory and report persist Agent e2e covers the full loop on the real demo project — which turned out to carry its own #13-shaped conflicts (info-card sub-vocabulary discovered top-level in source), live confirmation the protection matters. 30/30 agent, all workspace suites green, composer builds. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- applyFreshFact refuses accepting values onto a non-list authored value (previously it silently replaced it — the one path where acceptance could destroy authored data); authored-order preservation pinned - decision actions (resolveDeletion, resolveConflict, clearTombstone, acceptFreshFact) are serialized behind a ref lock: two rapid clicks can no longer compute from the same stale contract and drop the first decision on save - agent rediscover now passes its output through the same harness gate as /project/save before writing (one-validator principle held on the new v2 write path); malformed restoreTopLevel pinned as a 400 - consumes export-side migration semantics: the demo project's first rediscovery now ASKS about spark-line (deletedAwaitingDecision + seeded memory) instead of silently adding, and the test restores it through the explicit intent; entryHashRetired dropped from the client type and report UI (deletion memory is never auto-retired upstream) - addTombstone dedup + byte-boundedness and clearTombstone exact-one removal pinned; composer-core v2 fixture refreshed from the regenerated 0.5.0 golden (generatedBy provenance) composer-core 23/23, agent 31/31, all suites green, composer builds. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…erride The ledger-v2 paired-PR protocol completes: the link override to the feat/ledger-v2 worktree is deleted and the lockfile regenerated from scratch against the real registry. Exactly one dspack-export version (0.5.0, agent-only) resolves; no link:/file:/worktree references remain. Registry verification preceding this swap: gitHead matches merge commit 431f427; 95 packed files byte-size-identical to a local pack of that commit; a 30-check behavioral battery from a cold-cache registry install (migration both branches incl. v1 hand-deletion skip-and-ask, orphan memory persistence, tombstones + decideRegeneration residency guard, all three restoredConflict outcomes + intent refusals, freshDelta direction + non-persistence, malformed pathed refusals); strict node16 type surface; bin invocation; real-repo trial (next-template) regenerated by the registry bin byte-identical modulo timestamp. All workspace suites green on the registry dependency. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Implements Studio-side support for @aestheticfunction/dspack-export@0.5.0 ledger v2, adding entry-level ownership semantics for components, explicit “decision” actions (restore / tombstone / conflict intents / freshDelta acceptance), and updated agent + UI surfaces to render and execute those decisions.
Changes:
- Bumps
@aestheticfunction/dspack-exportconsumption to^0.5.0and wires the new rediscovery report + intent plumbing (restoreTopLevel). - Adds composer-core ledger v2 primitives (per-entry statuses, v2 decision helpers, freshDelta acceptance) and pins behavior to a real 0.5.0 golden fixture with expanded tests.
- Updates composer UI/state to render rediscovery as decisions and provide explicit action buttons for deletions, conflicts, tombstones, and fresh facts.
Reviewed changes
Copilot reviewed 11 out of 12 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | Locks registry dependency bump to @aestheticfunction/dspack-export@0.5.0. |
| apps/agent/package.json | Updates agent dependency to dspack-export ^0.5.0. |
| packages/composer-core/src/ledger.ts | Adds ledger v2 entry-level ownership model, v2 decision helpers, and freshFact acceptance logic. |
| packages/composer-core/src/index.ts | Re-exports new ledger v2 APIs and types. |
| packages/composer-core/src/composer-core.test.ts | Adds pinned tests for v2 hashes, entry states, decision helpers, and freshFact acceptance behavior. |
| packages/composer-core/fixtures/shadcn-demo.v2.dspack.json | Adds a real dspack-export 0.5.0 golden fixture for byte-pinned hashing/ownership tests. |
| apps/composer/app/views/project-view.tsx | Renders ledger v2 entry-level ownership and a decision-driven rediscovery report UI with action buttons. |
| apps/composer/app/views/inventory-view.tsx | Surfaces per-entry ownership chips in inventory (v2-only). |
| apps/composer/app/state.tsx | Adds state + handlers for explicit v2 decisions (deletions, conflicts, tombstones, freshFact acceptance) with a decision lock. |
| apps/composer/app/agent-client.ts | Extends rediscover client to accept restoreTopLevel and updates report typing. |
| apps/agent/src/project.ts | Adds restoreTopLevel request validation and gates rediscovery writes through the harness validator. |
| apps/agent/src/project.test.ts | Expands rediscovery tests to cover migration, skip-and-ask behavior, tombstones, restoredConflict, restoreTopLevel intent, and malformed intent 400s. |
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
Suppressed comments (3)
apps/composer/app/state.tsx:339
- In resolveConflict’s keep-nested path, the code updates rediscovery state and shows a success notice even if saveContract is refused (returns findings) or if a prior "Save failed: ..." notice was just set by saveContract. That can make the report look resolved even though the tombstone decision was not persisted.
if (decision === "keep-nested") {
const result = addTombstone(contract, id);
if (!result.ok) {
setNotice(`Cannot keep '${id}' nested: ${result.reason}`);
return;
apps/composer/app/state.tsx:390
- clearTombstone always shows a success notice after saveContract, even if the save was refused (findings returned) or if saveContract set a "Save failed: ..." notice. That can mislead users into thinking the tombstone was removed when it wasn’t persisted.
const result = removeTombstone(contract, id);
if (!result.ok) {
setNotice(`Cannot remove tombstone '${id}': ${result.reason}`);
return;
}
apps/composer/app/state.tsx:409
- acceptFreshFact updates rediscovery state (removing the fact from the report) and shows a success notice unconditionally after saveContract. If the save is refused (findings returned) or fails (Save failed: ...), the UI will claim the fact was accepted even though it wasn’t persisted.
const result = applyFreshFact(contract, componentId, fact);
if (!result.ok) {
setNotice(`Cannot accept ${fact.path} on '${componentId}': ${result.reason}`);
return;
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+140
to
+142
| const allToolOwned = | ||
| componentEntries.length > 0 && componentEntries.every((e) => e.state === "tool-owned" && !e.alsoTombstoned); | ||
| sections.push({ section, state: allToolOwned ? "tool-owned" : "human-owned" }); |
Comment on lines
+117
to
+122
| for (const id of Object.keys(recorded)) { | ||
| if (!(id in entries)) statuses.push({ id, state: "orphaned" }); | ||
| } | ||
| for (const id of tombstones) { | ||
| if (!(id in entries)) statuses.push({ id, state: "tombstoned" }); | ||
| } |
Comment on lines
+289
to
+311
| const result = decision === "restore" ? restoreComponent(contract, id) : addTombstone(contract, id); | ||
| if (!result.ok) { | ||
| setNotice(`Cannot ${decision} '${id}': ${result.reason}`); | ||
| return; | ||
| } | ||
| await saveContract(result.document); | ||
| setRediscovery((r) => | ||
| r | ||
| ? { | ||
| ...r, | ||
| components: { | ||
| ...r.components, | ||
| deletedAwaitingDecision: r.components.deletedAwaitingDecision.filter((d) => d !== id), | ||
| ...(decision === "tombstone" ? { suppressed: [...r.components.suppressed, id] } : {}), | ||
| }, | ||
| } | ||
| : r, | ||
| ); | ||
| setNotice( | ||
| decision === "restore" | ||
| ? `'${id}' will be restored from source on the next rediscovery (deletion memory cleared).` | ||
| : `'${id}' tombstoned: rediscovery will never re-add it. Remove the tombstone from the Ownership panel to undo.`, | ||
| ); |
This was referenced Aug 4, 2026
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.
Pairs with dspack-export#14 (merged,
431f427, released as@aestheticfunction/dspack-export@0.5.0). Implements the Studio half of the ratified per-component-ledger decision, including the ratifiedrestoredConflictresolutions, and closes the composer-visible half of the granularity finding disclosed in #29 / dspack-export#13. This PR now consumes the published registry package — the paired-PR override is gone and CI is green.What the composer does
Reads ownership per entry (
composer-core): tool-owned / human-owned (stale hash) / human-owned-unattributed (post-migration) / orphaned (deletion memory) / tombstoned — WebCrypto per-entry hashes pinned byte-for-byte against a committed real 0.5.0 golden. v2 documents derive the components section state from entries (the v1 whole-section signal is deliberately omitted on v2-active docs); v1 documents keep the existing section-level presentation with no invented entry states.Renders every regeneration class as a decision: added, refreshed, readopted, preserved (yours), removed-with-source, kept-missing-in-source, suppressed, tombstoned-but-present, restored top-level (both exist), deletions awaiting decision, restructure conflicts.
Explicit actions, phrased as intent — none runs automatically, all serialized against double-clicks (ref lock), refusals surfaced verbatim:
suppressedthereafter) / Restore top-level (one-shotrestoreTopLevelintent through the agent — restored tool-owned from fresh, nested authored representation untouched, both exist) / leave unresolved (nothing changes).Migration UX: the first rediscovery of a v1 project migrates the ledger; fresh-only ids ask (
deletedAwaitingDecisionwith seeded memory) instead of silently adding — restoring is one explicit decision.Agent:
/project/rediscoveracceptsrestoreTopLevel(400 on malformed shape, tool refusals verbatim as 409) and gates its write through the same harness validator as/project/save(one-validator principle held on the v2 write path).Registry-backed evidence (post-swap)
gitHead= merge commit431f427; 95 packed files byte-size-identical to a local pack of that commit; 30-check behavioral battery from a cold-cache registry install (migration both branches incl. v1 hand-deletion skip-and-ask + byte stability outsidex-bootstrap, orphan memory persistence across transient absences, tombstones + thedecideRegenerationresidency guard, all three conflict outcomes + mixed-intent refusals with every id named, freshDelta direction + non-persistence, malformed pathed refusals incl.components: null); strict node16 compile of the published type surface; real bin invocation; real-repo trial (shadcn-ui/next-template) regenerated by the registry bin byte-identical to the pre-merge worktree run modulo timestamp — zero drift between local simulation and registry behavior.4c94777): override deleted,^0.5.0, lockfile regenerated from scratch against the registry;pnpm whyproves exactly one dspack-export version (0.5.0, agent-only); zerolink:/file:/worktree references anywhere.--frozen-lockfile): all 9 unit suites green (102 tests, incl. agent 31/31 covering the FM-11: take-it-home — client-side validator, MCP config, local-agent path #13 restructure + hand-deletion cases, skip-and-ask, tombstone add/remove, keep-nested-equivalent tombstone flow, restore-top-level, unresolved persistence, migration ask-not-add, and the malformed-intent 400); typecheck clean (a first-pass transient reflects the known pre-existing contracts build-order coupling that CI's own step order avoids); composer + web static builds green; exhibit Playwright 106 passed / 4 pre-existing skips; composer production smoke 12/12 (production still Phase 2 — untouched).Known, disclosed gap: the new decision UI is pinned at the composer-core/agent layers (every transition and refusal), not at the rendered-DOM layer — the repo has no agent-mode Playwright harness (demo mode is v1 and never renders the v2 UI). Follow-up candidate, alongside axe coverage for the new buttons.
Out of scope, per ratification: per-prop ownership, nested merge controls, stored-base three-way merge, spec changes, Phase 3 AI, casualty-classification UI (#30), deployment (not part of this PR).
🤖 Generated with Claude Code