Skip to content

feat: catalog composer vertical slice — connect, enrich, map, emit, preview, validate, generate - #27

Merged
ryandmonk merged 6 commits into
mainfrom
feat/composer-vertical-slice
Aug 3, 2026
Merged

feat: catalog composer vertical slice — connect, enrich, map, emit, preview, validate, generate#27
ryandmonk merged 6 commits into
mainfrom
feat/composer-vertical-slice

Conversation

@ryandmonk

@ryandmonk ryandmonk commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

What

The Phase 1 vertical slice of the catalog composer: Studio evolves from exhibiting the canonical pipeline to authoring project-specific A2UI catalogs for user projects — as UI over the three seams that already existed (dspack-export FrameworkAdapter discovery, dspack-emit Profile mapping, a2ui-ingest Registry rendering). No new emitter, no new IR, no pipeline logic in Studio. The exhibit (apps/web) is untouched.

  • packages/composer-core (pure): project manifest (zod, strict), x-bootstrap ledger reading on WebCrypto (sectionHash pinned byte-for-byte against real dspack-export output fixtures), normalized findings, adapter manifests as data (react-generic's missing rendering ref IS the wireframe fallback and the Vue door).
  • packages/wireframe-renderers: wireframeRegistryFor(catalog) — every catalog name renders as an honest labeled wireframe derived from the catalog itself; zero user code executes.
  • apps/agent /project/*: connect · discover (dspack-export CLI, refusals verbatim) · emit (loadProfile → per-surface emitSurface → A1–A3 both A2UI versions → out/) · validate (dspack-validate + gen/core lintSurface) · save (shape-gated; refuses to drop the ledger) · run (AG-UI SSE generation under the project contract + profile). New import-isolation rule: dspack-export only in apps/agent.
  • apps/composer: six views (Project/Inventory/Component/Mapper/Preview/Validate) in the AF chrome; hosted mode ships the pre-emitted demo project and states what needs the agent — nothing simulated.
  • apps/composer/demo-project: "Acme UI" — a real non-canonical 5-component React library, dspack-export-bootstrapped, human-enriched, with a JSON profile exercising a 5→4 lossy valueMap, compound subFlatten, string/boolean propMaps, and a declared casualty.

Release train: COMPLETE — this PR consumes published registry packages

  • dspack-emit#24 merged (156235d) → @aestheticfunction/dspack-emit@0.4.0 on npm (gitHead = the merge commit; integrity sha512-KNX8DU…).
  • dspack-gen#48 merged (f3a1dcb) → tag v0.1.3 → OIDC → @aestheticfunction/dspack-gen@0.1.3 on npm (gitHead = the merge commit; emit range ^0.3.1 || ^0.4.0).
  • Swap commit 6b51af4: the temporary local override is gone; every specifier resolves from the registry (emit ^0.4.0 in agent/composer, 0.4.0 exact in contracts, gen ^0.1.3 everywhere); lockfile regenerated from scratch — exactly one dspack-emit (0.4.0) and one dspack-gen (0.1.3) in the tree, zero 0.3.x resolutions, no file/tarball/worktree references.
  • c6d39bb: the one string-level consumer of the old casualty refusal message — the break-it e2e — now asserts the 0.4.0 message (the authored class + reason, i.e. the verbatim reason the test's name always asked for). The failing CI run on the swap commit is the fail-first evidence.

CI: fully green (unit suites, typechecks, static export, and the 110-test Playwright e2e suite): run 30847630695.

Registry-backed verification

Published-package checks (clean external installs): emit 0.4.0 — root exports incl. loadProfile/scaffoldProfile/profileSchema, shipped profile.v1.schema.json, fail-closed pathed refusals, installed dspack-emit bin emits the acme contract with gates PASS and refuses an invalid profile at exit 2, and the emitted acme catalog is byte-identical to the 0.3.2-generated fixture (zero canonical output drift). gen 0.1.3 — root + ./core + ./adapter-types resolve, PipelineEvent in published types.

Fresh clone of this branch (depth-1, cold pnpm store, no sibling repos or worktrees): single-version lockfile proof; canonical build:catalogs A-gates green + check:sync byte-identical; 9 suites / 84 tests, 0 type errors; both static exports build; through the agent inside the clone: connect (ledger states correct), emit (ok: true, six components), validate (ok: true), mapper persistence (save → profile changed on disk; invalid profile refused with a pathed finding), scripted /project/run delivers the surface; the built composer serves with the honest "agent: not running" state and the preview renders.

Interactive evidence (this branch, earlier session)

Connect → ledger ownership + awaitingAuthorship checklist → Emit: A1/A2/A3 PASS both versions with the fidelity/coverage table → Preview: wireframe AND native shadcn (full native coverage of all six names), casualty surface refused → Mapper: live valueMap edit persisted + re-emitted → Validate: one normalized finding shape → /project/run: live ollama:gemma4:e4b passes first-attempt under the project-scoped vocabulary (0/72 on the full shadcn vocabulary in the recorded evals; scoped schema 16,679 B vs 70,940 B).

Scope boundaries, risks, rollback

  • apps/web source is untouched; the only e2e change is the casualty-message assertion above. Shared-surface changes are additive (.claude/launch.json composer entry).
  • Not in this slice (deliberate): governance form builder, re-discovery merge, in-browser emit for demo fidelity, AI assistance, export bundle, Vue/Vuetify implementations, navigation restructuring, deployment.
  • Security posture held: no credentials transit the browser; user components never execute in the composer; agent paths confined to the project directory; saves cannot drop the provenance ledger.
  • Rollback: revert the merge — no deployment changes, no data migrations; the canonical lane (contracts byte-sync, fixtures, receipts) is untouched.

Docs: docs/COMPOSER.md (run instructions, seam ownership, evidence, plan deltas).

🤖 Generated with Claude Code

ryandmonk and others added 6 commits August 3, 2026 14:46
composer-core (pure, no protocol deps, mirrors replay's discipline): the
project.json manifest schema (zod, strict), x-bootstrap ledger reading on
WebCrypto (sectionHash pinned byte-for-byte against real dspack-export 0.3.0
output fixtures: pristine sections verify tool-owned, enriched components
verify human-owned, authored governance human-authored), the normalized
ComposerFinding shape every gate reports in, and the ComposerAdapter
manifests binding the three existing seams by REFERENCE (registry ids, not
imports — import isolation intact; react-generic has no rendering ref, which
IS the wireframe fallback and the Vue door).

wireframe-renderers: wireframeRegistryFor(catalog) derives a Registry from
the catalog itself — every name renders as a labeled outline (scalar props
as chips, ComponentId/ChildList via buildChild, Action as inert buttons),
wrapped in withProvenance so X-ray tracing works. Zero user code executes.
Tested against a real emitted catalog from a non-canonical contract (acme):
zero unimplemented,  classification, static-markup render.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
POST /project/{connect,discover,emit,validate,save,run} — thin orchestration
over published packages; the agent parses nothing itself. connect returns
manifest + ledger states + surface inventory; discover shells the
dspack-export CLI (its refusal table speaks verbatim); emit loads the JSON
profile (loadProfile), emits every surface, runs A1-A3 for both A2UI
versions, writes out/, and reports coverage/fidelity/warnings as normalized
findings; validate shells dspack-validate and runs gen/core lintSurface;
save is shape-gated and refuses to drop the x-bootstrap ledger; run streams
AG-UI SSE generation under the PROJECT contract + profile (the scoped
vocabulary from the grammar-ceiling evidence).

New import-isolation rule, mirroring @a2ui/*-only-in-a2ui-ingest:
@aestheticfunction/dspack-export is imported/spawned only in apps/agent.

Ships apps/composer/demo-project — the Acme UI fixture: a real
non-canonical 5-component React library, dspack-export-bootstrapped,
human-enriched (props discovery cannot extract, compound composition,
owner-authored governance), with a JSON profile exercising valueMap
projections, subFlatten, and a declared casualty.

TEMPORARY (paired-PR protocol): pnpm override points
@aestheticfunction/dspack-emit at the local feat/profile-as-data build;
replaced by ^0.4.0 from npm once dspack-emit#24 releases.

7 new route tests run the real gates against a temp copy of the demo
project (27/27 agent tests green).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
apps/composer, a second static-export app (the exhibit is untouched): six
views over the project documents. Project (connect via local agent, ledger
ownership checklist, awaitingAuthorship as the todo list, discovery with
verbatim refusals); Inventory (derived lifecycle chips — described / needs
props / mapped / casualty / unclassified); Component (the enrichment step:
prose AND prop authoring — the spike proved discovery is variant-centric,
so adding props is required capability); Mapper (propMap/valueMap grid over
the JSON profile, casualty declaration with reason-first flow, fidelity
rail from the last emit); Preview (A2uiCanvas under wireframe or native
shadcn registries, canvas-scoped light/dark, coverage note, per-surface
warnings, refused surfaces stated); Validate (one normalized finding table
across document/S/A/coverage/fidelity gates).

Honest-magic posture mirrors the exhibit: hosted mode ships the pre-emitted
demo project (build-time scripts/demo-assets.mjs over the same published
APIs the agent runs); S1-S3 validate IN the browser (dspack-gen/core);
everything needing files or the emitter states 'requires the local agent'
plainly, never simulates.

Chrome is the shared AF brand layer (globals.css transcription + next/font
set, same as apps/web); the canvas is scoped (data-design-system /
data-mode / data-project-canvas) so project styling never leaks into
chrome.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
docs/COMPOSER.md records what the slice is, how to run it, the seam
ownership table, the spike + verification evidence, the temporary
pnpm-override state (paired with dspack-emit#24 / dspack-gen#48), and the
plan deltas implementation taught us. launch.json gains the composer dev
entry; composer test passes with no tests (contracts precedent);
wireframe-renderers gains @types/react-dom.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…drop the local override

The paired-PR protocol completes: the temporary pnpm override to the local
feat/profile-as-data build is deleted; every specifier now resolves from
the registry (emit ^0.4.0 in agent/composer, 0.4.0 exact in contracts per
its pin convention; gen ^0.1.3 everywhere for the widened emit range).
Lockfile regenerated from scratch: exactly one dspack-emit (0.4.0) and one
dspack-gen (0.1.3) in the tree, zero 0.3.x resolutions, no file/tarball/
worktree references.

Registry verification performed before this commit: 0.4.0 gitHead equals
the #24 merge commit; root exports + shipped profile schema + loadProfile
fail-closed + installed CLI (gates PASS; invalid profile refused at exit 2
with pathed issues) all from a clean external install; and the emitted
acme catalog is BYTE-IDENTICAL to the 0.3.2-generated fixture — no
canonical output drift.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
dspack-emit 0.4.0 deliberately upgraded the declared-casualty refusal from
a bare unknown-component error to the profile's authored class + reason —
the verbatim reason this test's own name always asked for. The stale
expectation was the one string-level consumer of the old message; the
failing CI run on the swap commit (actions/runs/30847210112) is the
fail-first evidence. Verified locally against the static export + agent
with the registry packages: 1 passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ryandmonk
ryandmonk marked this pull request as ready for review August 3, 2026 19:56
Copilot AI review requested due to automatic review settings August 3, 2026 19:56

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

Introduces the Phase 1 “catalog composer” vertical slice: a new authoring UI (apps/composer) backed by thin local-agent orchestration (apps/agent), plus new pure/core packages (packages/composer-core, packages/wireframe-renderers) to connect/discover/enrich/map/emit/preview/validate project-specific A2UI catalogs while keeping pipeline logic in published dspack packages.

Changes:

  • Add @dspack-studio/composer-core (manifest schema + ledger reading + normalized findings + adapter manifests) and @dspack-studio/wireframe-renderers (catalog-derived universal wireframe registry + tests/fixtures).
  • Add apps/composer Next app (client-only) with demo project + build-time demo asset emission script.
  • Add /project/* routes to the agent for connect/discover/emit/validate/save/run; bump registry dependencies (dspack-emit@0.4.0, dspack-gen@0.1.3, dspack-export@0.3.0, dspack-spec@0.4.1) and update one e2e expectation.

Reviewed changes

Copilot reviewed 59 out of 60 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
pnpm-lock.yaml Updates lockfile for new apps/packages and published dspack versions.
packages/wireframe-renderers/tsconfig.json Adds TS config for new wireframe renderer package.
packages/wireframe-renderers/src/wireframe.test.tsx Adds unit tests for wireframe registry coverage/classification/rendering.
packages/wireframe-renderers/src/registry.tsx Implements wireframeRegistryFor catalog-derived registry.
packages/wireframe-renderers/src/index.ts Exports public API for wireframe renderers package.
packages/wireframe-renderers/src/classify-props.ts Implements prop classification derived from catalog JSON.
packages/wireframe-renderers/package.json Adds new workspace package metadata/scripts/deps.
packages/wireframe-renderers/fixtures/acme.catalog.v0_9_1.json Adds committed emitted catalog fixture used by tests.
packages/contracts/package.json Bumps contracts to @aestheticfunction/dspack-emit@0.4.0.
packages/composer-core/tsconfig.json Adds TS config for new composer-core package.
packages/composer-core/src/project.ts Implements strict project manifest schema + parser.
packages/composer-core/src/ledger.ts Implements x-bootstrap ledger reading + section hashing + preservation guard.
packages/composer-core/src/index.ts Exposes composer-core public API.
packages/composer-core/src/findings.ts Defines normalized finding shape + helpers.
packages/composer-core/src/composer-core.test.ts Adds unit tests for manifest, ledger pinning, findings, adapters.
packages/composer-core/src/adapters.ts Adds adapter manifest data model and initial adapter set.
packages/composer-core/package.json Adds new workspace package metadata/scripts/deps.
packages/composer-core/fixtures/acme-ui.enriched.dspack.json Adds enriched contract fixture for ledger tests.
packages/composer-core/fixtures/acme-ui.dspack.json Adds pristine contract fixture for ledger tests.
e2e/break.spec.ts Updates expected refusal message for dspack-emit 0.4.0.
docs/COMPOSER.md Adds composer runbook and architecture notes.
apps/web/package.json Bumps @aestheticfunction/dspack-gen to ^0.1.3.
apps/composer/tsconfig.json Adds Next/TS config for new composer app.
apps/composer/scripts/demo-assets.mjs Build-time demo emission script generating hosted demo artifacts.
apps/composer/package.json Adds composer app package metadata/scripts/deps.
apps/composer/next.config.mjs Configures static export + transpiled workspace packages.
apps/composer/demo-project/tsconfig.json Adds TS config for the shipped demo component library.
apps/composer/demo-project/surfaces/uses-casualty.dsurface.json Adds demo surface exercising a declared casualty.
apps/composer/demo-project/styles/globals.css Adds demo project CSS token definitions.
apps/composer/demo-project/project.json Adds demo project manifest for the composer.
apps/composer/demo-project/lib/utils.ts Adds demo helper (cn) used by demo components.
apps/composer/demo-project/dspack-export.config.json Adds demo discovery config for dspack-export.
apps/composer/demo-project/components/ui/tag-pill.tsx Adds demo component.
apps/composer/demo-project/components/ui/note-field.tsx Adds demo component.
apps/composer/demo-project/components/ui/mini-stepper.tsx Adds demo component (used as casualty).
apps/composer/demo-project/components/ui/info-card.tsx Adds demo components/subcomponents.
apps/composer/demo-project/components/ui/action-button.tsx Adds demo component.
apps/composer/demo-project/acme.profile.json Adds demo dspack-emit profile exercising mapping features + casualty.
apps/composer/demo-project/acme-ui.dspack.json Adds demo contract used for composer/agent flows.
apps/composer/demo-project/.gitignore Ignores derived out/ in demo project.
apps/composer/app/views/validate-view.tsx Adds Validate view (combined normalized findings table).
apps/composer/app/views/project-view.tsx Adds Project/Connect + ledger ownership UI.
apps/composer/app/views/preview-view.tsx Adds Preview view rendering emitted catalog through registries.
apps/composer/app/views/mapper-view.tsx Adds mapping editor UI for profile component plans + casualties.
apps/composer/app/views/inventory-view.tsx Adds component inventory UI with derived lifecycle chips.
apps/composer/app/views/component-view.tsx Adds enrichment UI for prose + props editing.
apps/composer/app/state.tsx Implements composer context state + agent/demo mode behaviors.
apps/composer/app/page.tsx Client-only entrypoint (disables SSR for composer shell).
apps/composer/app/layout.tsx Adds layout + shadcn styles + AF chrome styles.
apps/composer/app/globals.css Adds AF chrome styles and scoped integration tweaks.
apps/composer/app/fonts.ts Adds next/font self-hosted typography setup.
apps/composer/app/demo-data.ts Imports shipped demo assets into the app.
apps/composer/app/composer.tsx Implements top-level shell + nav across six views.
apps/composer/app/agent-client.ts Adds typed HTTP client for local agent /project/* routes.
apps/composer/.gitignore Ignores Next output and generated demo emit artifacts.
apps/agent/src/server.ts Wires agent server to handle composer /project/* routes.
apps/agent/src/project.ts Implements /project/* route handlers (connect/discover/emit/validate/save/run).
apps/agent/src/project.test.ts Adds route-level tests against a temp-copied demo project.
apps/agent/package.json Adds new agent deps for project orchestration and bumps dspack-gen.
.claude/launch.json Adds composer dev launch entry and formatting changes.
Files not reviewed (1)
  • pnpm-lock.yaml: Generated file
Suppressed comments (2)

apps/composer/app/state.tsx:172

  • When the agent save request fails (!result.ok), this returns an empty findings array. The Mapper/Component views treat that as "no issues" and can show success UI even though the save did not persist.
      const result = await agentSave(projectPath, "profile", doc);
      if (!result.ok) {
        setNotice(`Save failed: ${result.error}`);
        return [];
      }

apps/agent/src/project.ts:325

  • Contract saves write a temp file under outDir for validation but never remove it, which will accumulate .contract-check-* files over time. Also, using only process.pid risks collisions across concurrent saves in the same process.
  const tmp = join(ctx.outDir, `.contract-check-${process.pid}.json`);
  mkdirSync(ctx.outDir, { recursive: true });
  writeFileSync(tmp, JSON.stringify(document, null, 2));
  const harness = packageFile("@aestheticfunction/dspack-spec", "scripts/validate.mjs");
  try {

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

Comment on lines +52 to +58
const runs = ["0.9.1", "1.0"].map((version) => {
const out = transformFromJson(contract, { a2uiVersion: version, surface: { messages: allMessages }, profile });
for (const gate of out.validation.gates) {
if (!gate.pass) findings.push({ gate: "A1", code: gate.name, severity: "error", target: `a2ui@${version}`, message: gate.name });
}
return { version, out };
});
Comment on lines +157 to +160
void save((d) => {
d.components = d.components.filter((p: any) => p.dspackId !== selected);
d.casualtyComponents.push({ dspackId: selected, attempted: plan?.a2ui ?? "(none)", class: "cannot-represent", reason });
})
Comment on lines +143 to +147
const result = await agentSave(projectPath, "contract", doc);
if (!result.ok) {
setNotice(`Save failed: ${result.error}`);
return [];
}
Comment thread docs/COMPOSER.md
Comment on lines +73 to +76
- **pnpm override**: `@aestheticfunction/dspack-emit` points at the local
`feat/profile-as-data` build (dspack-emit PR #24). Replace with `^0.4.0`
from npm at release; CI on this branch is red until then by construction
(the established paired-PR protocol).
const casualty = payload.findings.find((f: any) => f.gate === "A3" && f.code === "emit-surface");
expect(casualty.target).toBe("uses-casualty");
expect(casualty.message).toContain("declared casualty");
expect(casualty.message).toContain("dropdown-menu casualty".split(" ")[0] === "dropdown-menu" ? "steps" : "steps");
Comment thread apps/agent/src/project.ts
* isolation rule, mirroring @a2ui/* and @ag-ui/* confinement).
*/
import { execFile } from "node:child_process";
import { mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync, existsSync, statSync } from "node:fs";
@ryandmonk
ryandmonk merged commit 0ec010b into main Aug 3, 2026
2 checks passed
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