feat: catalog composer vertical slice — connect, enrich, map, emit, preview, validate, generate - #27
Merged
Merged
Conversation
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>
There was a problem hiding this comment.
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/composerNext 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
outDirfor validation but never remove it, which will accumulate.contract-check-*files over time. Also, using onlyprocess.pidrisks 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 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"); |
| * 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"; |
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.
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
FrameworkAdapterdiscovery, dspack-emitProfilemapping, a2ui-ingestRegistryrendering). 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 (sectionHashpinned 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
156235d) →@aestheticfunction/dspack-emit@0.4.0on npm (gitHead = the merge commit; integritysha512-KNX8DU…).f3a1dcb) → tagv0.1.3→ OIDC →@aestheticfunction/dspack-gen@0.1.3on npm (gitHead = the merge commit; emit range^0.3.1 || ^0.4.0).6b51af4: the temporary local override is gone; every specifier resolves from the registry (emit^0.4.0in agent/composer,0.4.0exact in contracts, gen^0.1.3everywhere); 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, shippedprofile.v1.schema.json, fail-closed pathed refusals, installeddspack-emitbin 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-typesresolve,PipelineEventin published types.Fresh clone of this branch (depth-1, cold pnpm store, no sibling repos or worktrees): single-version lockfile proof; canonical
build:catalogsA-gates green +check:syncbyte-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/rundelivers 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: liveollama:gemma4:e4bpasses 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
.claude/launch.jsoncomposer entry).Docs:
docs/COMPOSER.md(run instructions, seam ownership, evidence, plan deltas).🤖 Generated with Claude Code