Skip to content

feat(composer): ledger-v2 entry-level ownership — decisions, not defaults - #31

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

feat(composer): ledger-v2 entry-level ownership — decisions, not defaults#31
ryandmonk merged 4 commits into
mainfrom
feat/ledger-v2-composer

Conversation

@ryandmonk

@ryandmonk ryandmonk commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

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 ratified restoredConflict resolutions, 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:

  • Deletions: Restore (clears exactly the orphaned record; the entry returns from source) / Never rediscover (tombstone). Not deciding is honored — memory persists and keeps reporting; a transient extraction absence never retires it.
  • Conflicts (ratified): Keep nested (tombstone + retired memory → suppressed thereafter) / Restore top-level (one-shot restoreTopLevel intent through the agent — restored tool-owned from fresh, nested authored representation untouched, both exist) / leave unresolved (nothing changes).
  • freshDelta facts are review-only with per-fact explicit Accept, limited to scalar leaves and pure additions; accepting values appends only newly discovered values and refuses non-list authored values (never a replacement).

Migration UX: the first rediscovery of a v1 project migrates the ledger; fresh-only ids ask (deletedAwaitingDecision with seeded memory) instead of silently adding — restoring is one explicit decision.

Agent: /project/rediscover accepts restoreTopLevel (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)

  • Registry package verified before the swap: gitHead = merge commit 431f427; 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 outside x-bootstrap, orphan memory persistence across transient absences, tombstones + the decideRegeneration residency 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.
  • Swap (this branch, 4c94777): override deleted, ^0.5.0, lockfile regenerated from scratch against the registry; pnpm why proves exactly one dspack-export version (0.5.0, agent-only); zero link:/file:/worktree references anywhere.
  • Cold fresh clone (fresh git clone, cold pnpm store, --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).
  • CI green on this PR after the swap.
  • Adversarial pre-release sweep (8 verifiers + completeness critic, executed counterexamples) — all findings fixed and pinned on both repos.

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

ryandmonk and others added 4 commits August 4, 2026 08:28
…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>
@ryandmonk
ryandmonk marked this pull request as ready for review August 4, 2026 13:45
Copilot AI lite review requested due to automatic review settings August 4, 2026 13:45

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 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-export consumption to ^0.5.0 and 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.`,
);
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