fix: pre-1.0 correctness — CI enforcement, shadcn renderer props, browser/agent parity - #81
Merged
Conversation
A1 of the pre-1.0 correctness milestone. `.github/workflows/ci.yml` hand-listed
eight pnpm filters and silently omitted three packages that hold product logic:
@dspack-studio/composer-core 89 tests (both planners, ledger, findings, flow schema)
@dspack-studio/wireframe-renderers 4 tests
@dspack-studio/contracts 2 tests
The root `pnpm test` script would have caught them (it did not, either: it was
scoped `--filter './packages/**'`, so it missed all three apps) and CI never
invoked it. Two independent lists, neither complete, neither checked.
FAIL-FIRST PROOF 1 — CI as it stands reports success while a deliberately
failing composer-core test never executes. A temporary probe was added at
packages/composer-core/src/ci-omission-proof.test.ts:
it("PROOF: composer-core tests do not run in CI", () => {
expect("composer-core never executed").toBe("this assertion must fail");
});
then the EXACT command sequence of the workflow's "Unit tests" step was run:
$ pnpm --filter @dspack-studio/a2ui-ingest test
Test Files 1 passed (1)
Tests 3 passed (3)
$ pnpm --filter @dspack-studio/agui-bridge test
Test Files 2 passed (2)
Tests 6 passed (6)
$ pnpm --filter @dspack-studio/replay test
Test Files 5 passed (5)
Tests 21 passed (21)
$ pnpm --filter @dspack-studio/scenarios test
Test Files 1 passed (1)
Tests 5 passed (5)
$ pnpm --filter @dspack-studio/shadcn-renderers test
Test Files 3 passed (3)
Tests 21 passed (21)
$ pnpm --filter agent test
Test Files 4 passed (4)
Tests 48 passed (48)
$ pnpm --filter web test
Test Files 1 passed (1)
Tests 6 passed (6)
$ pnpm --filter composer test
Test Files 7 passed (7)
Tests 68 passed (68)
EXIT CODE OF CI'S 'Unit tests' STEP: 0
$ grep -c composer-core <captured output>
0
Green, and the string "composer-core" does not appear once in the entire step
output. The failing test was never collected, let alone run.
FAIL-FIRST PROOF 2 — the new configuration fails on the same probe:
$ pnpm test
Scope: 12 of 13 workspace projects
packages/composer-core test: ❯ src/ci-omission-proof.test.ts (1 test | 1 failed) 6ms
packages/composer-core test: × PROOF: composer-core tests do not run in CI 5ms
packages/composer-core test: → expected 'composer-core never executed' to be 'this assertion must fail' // Object.is equality
packages/composer-core test: ⎯⎯⎯⎯⎯⎯⎯ Failed Tests 1 ⎯⎯⎯⎯⎯⎯⎯
packages/composer-core test: FAIL src/ci-omission-proof.test.ts > PROOF: composer-core tests do not run in CI
packages/composer-core test: AssertionError: expected 'composer-core never executed' to be 'this assertion must fail' // Object.is equality
packages/composer-core test: Expected: "this assertion must fail"
packages/composer-core test: Received: "composer-core never executed"
packages/composer-core test: ❯ src/ci-omission-proof.test.ts:9:42
packages/composer-core test: Test Files 1 failed | 5 passed (6)
packages/composer-core test: Tests 1 failed | 89 passed (90)
ERR_PNPM_RECURSIVE_FAIL A test failed in "@dspack-studio/composer-core"
EXIT CODE OF CI'S NEW 'Unit tests' STEP: 1
The probe is deleted in this commit; it exists only in these two outputs.
THE FIX is a workspace-level strategy, not another list:
root package.json "test": "pnpm build:contracts && pnpm -r --no-bail test"
ci.yml "Unit tests" step: `pnpm test`
so the command a contributor runs locally and the command CI enforces are the
same string by construction. A package added tomorrow with a `test` script is
covered without anyone editing a workflow.
Two details keep the workspace run self-sufficient rather than reintroducing a
list. `pnpm build:contracts` (0.6s) runs first because the shadcn-renderers and
contracts suites read packages/contracts/out/, which is gitignored and only
exists after the catalog build — CI already gates catalogs in an earlier step,
so this is idempotent, and it makes `pnpm test` work from a bare checkout.
`--no-bail` reports every failing package in one run instead of stopping at the
first; pnpm still exits non-zero (proof 2 above ran all 11 packages and still
exited 1). The composer's own `test` script already invokes demo-assets.mjs, so
its build-time reference bake needs nothing from the root.
CI unit coverage, before -> after:
8 packages, 24 test files, 178 tests
11 packages, 30 test files, 273 tests
packages/astryx-renderers stays outside the run: it declares no `test` script
because it has no tests. That is now the ONLY way a package can escape CI, and
it is visible in package.json rather than in a workflow file nobody reads.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A2 of the pre-1.0 correctness milestone, tests only. Both files are RED at this
commit by design; the fix is the next one.
Two new suites, aimed at one invariant: A REQUIRED CATALOG PROP MUST NOT BE
SILENTLY IGNORED BY ITS NATIVE RENDERER.
1. packages/shadcn-renderers/src/shadcn-v3-surface.test.tsx — real shipped
material, end to end. The shipped shadcn/ui v3 dspack document and profile
in packages/contracts, through the real `emitSurface`, through the real
registry, asserting the text a user is meant to read. No hand-written
fixture: if the contract's examples change, the suite renders whatever they
now say. (Adds @aestheticfunction/dspack-emit as a devDependency of
shadcn-renderers — the same published emitter every other twin uses.)
2. apps/composer/app/required-prop-consumption.test.ts — the structural guard,
behavior-level rather than a source grep. For every component in each
catalog, it builds an instance whose required props carry distinctive
SENTINEL values, renders it through the real registry, and asserts the
sentinels come out. A renderer that reads the right prop passes however it
is written; one that reads the wrong name fails however plausible its source
looks. It runs against BOTH governed catalogs and BOTH native registries,
because the bug being caught is a cross-catalog confusion. Sentinels are
derived from the SCHEMA, and an unhandled schema shape throws rather than
passing — the guard fails when it cannot reason.
The per-prop allowlist has exactly one entry, `action`, with its reason
written out: an A2UI Action becomes a callback, so no sentinel it carries
could appear in static markup. `child`/`children` are deliberately NOT
allowlisted — they carry ComponentIds and the marker `buildChild` renders
the id verbatim, so an unbuilt slot is a dropped sentinel like any other.
FAIL-FIRST OUTPUT 1 — the structural guard, against current code. It found
THREE required props ignored, not the one the milestone named:
$ npx vitest run app/required-prop-consumption.test.ts
❯ app/required-prop-consumption.test.ts (4 tests | 1 failed) 20ms
× required catalog props are observably consumed by their native renderer > shadcn: every required prop of every natively-drawn component reaches the output 13ms
→ expected [ …(3) ] to deeply equal []
✓ required catalog props are observably consumed by their native renderer > astryx: every required prop of every natively-drawn component reaches the output 6ms
✓ required catalog props are observably consumed by their native renderer > the allowlist stays a short list of justified structural props 0ms
✓ required catalog props are observably consumed by their native renderer > rejects a visual that ignores a required prop, rather than passing it silently 0ms
AssertionError: expected [ …(3) ] to deeply equal []
- []
+ [
+ "Button.child ignored (no sentinel in output: [\"sentinel-child\"])",
+ "AlertDialog.triggerLabel ignored (no sentinel in output: [\"SENTINEL~triggerLabel\"])",
+ "Table.rows ignored (no sentinel in output: [\"SENTINEL~rows.0.cells\",\"SENTINEL~rows.0.label\",\"SENTINEL~rows.0.value\",\"SENTINEL~rows.0.text\",\"SENTINEL~rows.1.cells\",\"SENTINEL~rows.1.label\",\"SENTINEL~rows.1.value\",\"SENTINEL~rows.1.text\"])",
+ ]
Astryx passes untouched, which is the point: these are three instances of ONE
mistake — a renderer reading the Astryx catalog's prop names while serving the
shadcn/ui v3 catalog.
prop the shadcn/ui v3 catalog declares the renderer reads result today
Table.rows (required) props.data empty <tbody>
Button.child (required, ComponentId) props.label wordless button
AlertDialog.triggerLabel (required) — nothing — no opener
AlertDialog.confirmLabel props.actionLabel blank confirm
FAIL-FIRST OUTPUT 2 — the shipped examples, against current code:
$ npx vitest run src/shadcn-v3-surface.test.tsx
❯ src/shadcn-v3-surface.test.tsx (4 tests | 3 failed) 20ms
× shipped shadcn/ui v3 examples render their real content > ex.support-ticket-queue: one body row per emitted row, with every cell 13ms
→ expected [] to have a length of 3 but got +0
× shipped shadcn/ui v3 examples render their real content > ex.orders-table-loading: the loading placeholder keeps its row structure 2ms
→ expected [] to have a length of 3 but got +0
× shipped shadcn/ui v3 examples render their real content > ex.delete-project-confirmation: the buttons say what they do 4ms
→ expected 'Danger zone Deleting a project remove…' to contain 'Cancel'
✓ shipped shadcn/ui v3 examples render their real content > draws the components under test natively, and names the one it does not 1ms
Test Files 1 failed (1)
Tests 3 failed | 1 passed (4)
and the third failure is worth reading in full, because it is the composer's
flagship destructive-action surface rendered exactly as a visitor sees it:
Expected: "Cancel"
Received: "Danger zone Deleting a project removes its environments, deploys, and logs for everyone in the workspace. Deleting Northwind Checkout is permanent 3 environments, 412 deploys, and 18 GB of build logs are destroyed immediately. There is no backup to restore from. Type the project name to confirm The name must match exactly, including capitalisation. Delete Northwind Checkout? The project, its three environments, and every deploy log are deleted immediately. Anyone with a link to a preview deploy will get a 404. Keep project"
Every governed word survives except the words ON THE BUTTONS. "Cancel" (the
Button's `child` Text) is gone, "Delete project and all data" (`confirmLabel`)
is gone, "Delete project" (`triggerLabel`) is gone. A delete-confirmation whose
confirm button is blank is the worst possible place for dropped content, and
every gate in the stack was green over it: A3 validates the INSTANCE against
the catalog, and an instance stays perfectly valid when the renderer drawing it
reads a prop the catalog never declared.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A2 of the pre-1.0 correctness milestone. Turns the previous commit's two
suites green by fixing the three renderers they measured, using the ACTIVE
shadcn contract as the source of truth (packages/contracts/out/
catalog.shadcn-v3.v1_0.json — read, not assumed).
One mistake, three instances of it. This registry serves BOTH governed
catalogs (see the header of registry-parity.test.ts), and the two do not agree
on names. Each renderer had been written against the Astryx/neutral catalog
and then pointed at the production shadcn/ui v3 catalog without anyone
re-reading it:
what shadcn/ui v3 declares read before read now
Table.rows (REQUIRED) props.data props.rows ?? props.data
Button.child (REQUIRED, id) props.label props.label ?? child slot
AlertDialog.triggerLabel (REQUIRED) — nothing — rendered as the trigger
AlertDialog.confirmLabel props.actionLabel confirmLabel ?? actionLabel
Table had no path to a row at all: shadcn/ui v3's Table declares `columns` and
`rows` and neither `data` nor `children`, so the nested-children branch could
never fire either. Every shipped shadcn table drew headers over an empty
<tbody>. Both row sources are now read, each commented with the catalog that
declares it; the nested-children branch, the density/dividers/striped
projections and the status -> Badge behavior are untouched.
Button now falls back to building its `child` ComponentId when the instance
carries no literal `label`. AlertDialog renders shadcn's actual anatomy —
Trigger + Content — with the trigger emitted only when the catalog declares
one, and takes the confirm label from `confirmLabel` before `actionLabel`.
ASTRYX IS UNCHANGED, MEASURED RATHER THAN ASSERTED. Every A2UI instance the
Astryx contract emits in this repo (the contracts build's surfaces plus the
recorded replay fixtures — 172 of them) was rendered through the shadcn
registry before and after this commit and diffed:
$ npx tsx neutral-render.tmp.mjs > after.txt # 172 neutral instances
$ git stash push -- packages/shadcn-renderers/src/components
$ npx tsx neutral-render.tmp.mjs > before.txt # 172 neutral instances
$ git stash pop && diff before.txt after.txt
IDENTICAL — neutral-catalog rendering through the shadcn registry is
byte-for-byte unchanged
That is why AlertDialog's trigger sits in a Fragment rather than a wrapper div
and why the panel's `id` is conditional: a catalog with no `triggerLabel` must
render the same element with the same attributes it did before this renderer
learned that triggers exist. packages/astryx-renderers is not touched by this
branch at all (`git diff main -- packages/astryx-renderers` is empty).
Green: packages/shadcn-renderers 25 tests, apps/composer 72, `pnpm test` 273
across 11 packages, `pnpm -r typecheck` clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A3/A4 of the pre-1.0 correctness milestone, tests only. All three files are RED
at this commit by design; the seam that turns them green is the next one.
THE CANONICAL BEHAVIOR, investigated rather than assumed. The same governed
project got a different validation truth depending on which door it came
through: apps/agent/src/project.ts emit() validated the emitted surface against
A2UI 0.9.1 AND 1.0, while apps/composer/app/validation.ts browserEmit()
validated 0.9.1 only. Reading the stack rather than picking a side:
@aestheticfunction/dspack-gen 0.5.0, dist/run/orchestrator.js:231
for (const version of options.a2uiVersions ?? ["0.9.1", "1.0"]) {
neither caller overrides it — apps/agent/src/project.ts:568,633 and
apps/composer/app/hosted-build.ts:233 both call runPipeline without
`a2uiVersions`, so BUILD already validates both versions on both doors.
apps/agent/src/project.ts emit(): ["0.9.1", "1.0"]
apps/composer/scripts/demo-assets.mjs bake(): ["0.9.1", "1.0"]
apps/composer/app/validation.ts browserEmit(): "0.9.1" <- outlier
Three of the four twins already said both, and the fourth is the one users see
least — the browser's instant-feedback emit. So the browser moves. No version
is deleted to make outputs agree.
FAIL-FIRST OUTPUT — the browser half, against current code:
$ npx vitest run app/validation.test.ts
❯ app/validation.test.ts (4 tests | 3 failed) 115ms
× browserEmit — the same emit seam the agent runs > validates BOTH canonical A2UI versions, and says which ones it ran 46ms
→ Cannot read properties of undefined (reading 'map')
× browserEmit — the same emit seam the agent runs > is projectEmit plus surface selection: same verdict, same finding set, same catalog 19ms
→ (0 , projectEmit) is not a function
✓ browserEmit — the same emit seam the agent runs > selects the contract's worked examples — the surfaces a browser-backed project has 1ms
× browserEmit — the same emit seam the agent runs > reports every packaged reference project identically under both versions 49ms
→ Cannot read properties of undefined (reading 'map')
TypeError: Cannot read properties of undefined (reading 'map')
❯ app/validation.test.ts:37:24
36| const result = browserEmit(contract, profileJson, contractSurfaces…
37| expect(result.runs.map((r) => r.version)).toEqual([...A2UI_VERSION…
| ^
A missing computation can only be observed as a missing output, so the failure
reads as an absent `runs`: the browser's result had no second run to report.
The assertion behind it is not a label check — 0.9.1 and 1.0 catalogs are
structurally distinguishable (0.9.1 requires `$defs.theme` and forbids
`$defs.surfaceProperties`; 1.0 is the exact inverse), and the test asserts both
shapes, so it can only pass if the second catalog was really compiled and
gated.
The same run against the agent half and the seam:
$ npx vitest run src/project.test.ts -t "shared emit seam"
FAIL src/project.test.ts > emit > is the shared emit seam plus file writing: same verdict, same findings, same A2UI versions
TypeError: (0 , projectEmit) is not a function
$ npx vitest run src/emit.test.ts
FAIL src/emit.test.ts [ src/emit.test.ts ]
Error: Cannot find module './emit' imported from '…/packages/composer-core/src/emit.test.ts'
THE EQUIVALENCE ARGUMENT the three files make together. Rather than compare two
apps that cannot import each other, each half is proven against the shared seam
where it lives:
agent route == projectEmit + file writing (apps/agent)
browserEmit == projectEmit + surface selection (apps/composer)
projectEmit == both A2UI versions, one verdict (packages/composer-core)
so the two doors are equal by construction, not by coincidence. The one
asymmetry is documented and asserted rather than hidden: a repository-backed
project also emits the surfaces in its `surfacesDir`, which a browser-backed
project does not have, so the agent test feeds the seam that same list
explicitly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…es 1.0 too
A3/A4 of the pre-1.0 correctness milestone. Turns the previous commit's three
suites green by extracting the smallest seam that stops the measured
divergence, and moving the browser onto the canonical two-version reading.
THE DIVERGENCE, and which side was wrong. apps/agent/src/project.ts emit()
validated the emitted surface against A2UI 0.9.1 AND 1.0; apps/composer/app/
validation.ts browserEmit() validated 0.9.1 only. dspack-gen's `runPipeline`
— the generator behind BUILD on both doors — defaults to
`a2uiVersions: ["0.9.1", "1.0"]` and neither caller overrides it, so
GENERATION already gated both versions everywhere. The agent's emit matched
generation; so does the composer's build-time reference bake. Validating FEWER
versions than the generator that produced the surface is the one reading that
cannot be right, so the browser moved. No version was deleted to make the two
outputs agree.
THE SEAM: packages/composer-core/src/emit.ts, `projectEmit(contract,
profileJson, surfaces)` — profile load, per-surface emission, per-version
catalog gates, coverage, fidelity, casualty classification, warnings. Pure: no
filesystem, no network. Plus `A2UI_VERSIONS`, pinned to dspack-gen's default
with the reason written down, so there is ONE place the studio follows it.
WHY THAT BOUNDARY. Everything the two copies said IDENTICALLY moved in;
everything genuinely different about each door stayed out, which is what makes
each side assertable as "the seam plus exactly one thing":
agent route == projectEmit + FILE WRITING (it owns the filesystem)
browserEmit == projectEmit + SURFACE SELECTION (it has no surfacesDir)
Both are asserted, from both sides. The other known twins between these two
files — id minting, the accept gate, the scripted adapter — are deliberately
NOT extracted: they have not been measured to diverge, and a refactor is not a
correctness fix. Recorded as follow-ups instead. apps/composer/app/state.tsx is
untouched.
IMPACT, MEASURED AND HONEST. No shipped reference example and no packaged
reference project surfaces a NEW finding under 1.0 that browser users did not
already see:
shadcn-v3-project a2ui@0.9.1 pass, 0 failing gates
a2ui@1.0 pass, 0 failing gates -> 1.0-only: none
astryx-project a2ui@0.9.1 pass, 0 failing gates
a2ui@1.0 pass, 0 failing gates -> 1.0-only: none
and validation.test.ts asserts exactly that for both packaged references, so
the claim stays true or the suite goes red. Users therefore see no new noise;
what they gain is that a 1.0 failure would now be SHOWN rather than silently
passed over. The Checks presentation needed no change: per-version findings are
distinguished by their `a2ui@<version>` target, and the agent has been feeding
that same view two versions' worth of findings since it added 1.0 — this makes
the browser's finding set match rather than introducing a new shape. The build
path's cross-version dedup (composer-core buildFailure, "identical catalog-gate
errors under both A2UI versions render ONCE") is likewise already in place.
The emit result now also carries `runs` — one entry per validated version, with
its compiled catalog and verdict — and the agent route reports `a2uiVersions` on
the wire. The divergence was invisible for years precisely because nothing in
either result said which versions had run.
Green: pnpm test 291 tests across 11 packages; pnpm -r typecheck clean;
playwright composer-smoke 13/13, composer-agent 44/44, studio e2e 106 passed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
dspack-studio-composer | 18c9285 | Aug 12 2026, 03:14 PM |
There was a problem hiding this comment.
Pull request overview
This PR is part of the pre-1.0 correctness/readiness milestone, tightening enforcement so CI reliably exercises product-logic packages, fixing shadcn/ui v3 renderer prop consumption so shipped examples render real user-facing text, and eliminating browser/agent drift by centralizing emit + A-gate validation in a single shared seam (composer-core).
Changes:
- Replace CI’s hand-maintained pnpm filter list with the repo-root
pnpm testinvariant (contracts build + recursive tests) and update contributor docs accordingly. - Fix three “silent drop” renderer defects in shadcn-renderers (Table
rows, Buttonchild, AlertDialogtriggerLabel/confirmLabel) and add regression suites rendering shipped examples + a structural required-prop consumption guard. - Introduce
composer-coreprojectEmitseam and refactor both browser validation and agent emit route to call it; add parity tests on both sides.
Reviewed changes
Copilot reviewed 18 out of 19 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Updates local testing guidance to match CI’s enforced pnpm test. |
| pnpm-lock.yaml | Locks new dependency additions (notably @aestheticfunction/dspack-emit). |
| packages/shadcn-renderers/src/shadcn-v3-surface.test.tsx | New regression suite rendering shipped shadcn/ui v3 examples through the real registry. |
| packages/shadcn-renderers/src/components/TableRender.tsx | Fixes shadcn-v3 Table.rows vs Astryx data row source mismatch. |
| packages/shadcn-renderers/src/components/ButtonRender.tsx | Fixes shadcn-v3 Button.child (ComponentId) vs Astryx label consumption. |
| packages/shadcn-renderers/src/components/AlertDialogRender.tsx | Fixes shadcn-v3 trigger/confirm label rendering and maps confirm label naming. |
| packages/shadcn-renderers/package.json | Adds @aestheticfunction/dspack-emit for new test usage. |
| packages/composer-core/src/index.ts | Exports new emit seam API/types from composer-core. |
| packages/composer-core/src/emit.ts | Adds shared projectEmit implementation and canonical A2UI version list. |
| packages/composer-core/src/emit.test.ts | Adds unit tests pinning multi-version validation behavior and casualty classification. |
| packages/composer-core/package.json | Adds @aestheticfunction/dspack-emit dependency and updates package description. |
| package.json | Updates root test script to build contracts then run all workspace tests (--no-bail). |
| CONTRIBUTING.md | Updates contributor test instructions to match CI’s invariant. |
| apps/composer/app/validation.ts | Refactors browser emit/validation path to use composer-core projectEmit. |
| apps/composer/app/validation.test.ts | Adds tests asserting browserEmit == projectEmit + surface selection; pins 0.9.1/1.0 parity. |
| apps/composer/app/required-prop-consumption.test.ts | Adds behavior-level guard ensuring required catalog props are observably consumed by native renderers. |
| apps/agent/src/project.ts | Refactors agent /project/emit to use composer-core projectEmit and writes per-version outputs. |
| apps/agent/src/project.test.ts | Adds tests asserting agent route == projectEmit + file writing; validates versions are reported on the wire. |
| .github/workflows/ci.yml | Switches unit-test step to pnpm test (root invariant) instead of a hand-curated filter list. |
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+57
to
+66
| {hasTrigger && ( | ||
| <button | ||
| type="button" | ||
| className="mb-3 inline-flex h-9 items-center justify-center rounded-md border border-input bg-background px-4 py-2 text-sm font-medium shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground" | ||
| aria-haspopup="dialog" | ||
| aria-controls={panelId} | ||
| > | ||
| {String(props.triggerLabel)} | ||
| </button> | ||
| )} |
Comment on lines
+164
to
+165
| const gateId = gate.name.startsWith("schema-compile") ? "A1" : gate.name === "catalog-shape" ? "A2" : "A3"; | ||
| findings.push(...catalogGateFindings(gateId as "A1", gate, `a2ui@${version}`)); |
ryandmonk
added a commit
that referenced
this pull request
Aug 12, 2026
The audit found 62 of 134 composer testids in no spec, all of them on the paths a NEW team touches first. The Build/Preview path was well covered; provider configuration, surface authoring, flow decomposition, and governance authoring were not covered at all. This closes those four gaps with behaviour-level tests — state changes, persisted results, honest error text, the thing actually rendering — never "the element exists". No product source is touched. No spec makes a model call. Provider configuration (e2e/composer-settings.spec.ts, agent config) Ollama and OpenAI-compatible endpoints configured through the real agent against e2e/serve-provider.mjs — a real HTTP server speaking both discovery protocols, added as a third webServer. It serves discovery only: a build against it would be a model call. Covers discovery (including the agent's embedding-model filter), model choice, persistence and re-open, manual model entry when a server does not enumerate, and the honest failure text from an unreachable endpoint (which configures nothing). The credential invariant is proved end to end rather than asserted cosmetically: the fixture's /keyed endpoint 401s unless the exact key arrives, so the model list only appears if the key travelled browser → agent → provider — and localStorage/sessionStorage are then asserted to contain no trace of it. The agent-ABSENT half lives where it is true by construction, in composer-prod-smoke.spec.ts: with the agent probe blocked, "Agent not running" is a setup step with real instructions, and both provider forms are inert rather than dead-looking. Flows (e2e/composer-flows.spec.ts, agent-free config) Plan editing before anything is built (rename, retitle, reorder with a real swap, add, trim) and that planning creates nothing; the drive creating the flow immediately with PENDING steps, the plan freezing into per-step rebuilds, Preview's outline state and flow-lint's matching warning; a rebuilt step binding on accept; the editor's cancel creating nothing; a walk completing on the surface's own emitted action; and a step over a surface the emitter refuses showing the emitter's own reason. Surface authoring (e2e/composer-surfaces.spec.ts, agent-free config) Author → live gates → live preview → save → listed under its HUMAN title → rendered in Preview → survives reload → reported by Checks against that surface. Plus both honest-failure directions: a `must` rule blocks the save and nothing is written (before or after a reload), and fixing the violation unblocks it; a `should` rule warns without blocking. Governance (e2e/composer-governance.spec.ts, agent-free config) Intent authoring gated on a real description, then governing Build and the surface editor; the rationale gate on rules; a saved rule visibly firing in the impact panel and in Checks, and its removal undoing exactly that; the typed-rule form projections; and the session-scope honesty a browser project states about governance edits. Browser/agent parity (e2e/composer-parity.spec.ts, agent config) The product-level twin of the emit-seam unit equivalence: one repository project is EXPORTED and imported back as a browser project, so both carry byte-equal vocabulary, and the same authored surface must get the same verdict — same refusal text, same clean state, same surface-scoped findings in Checks (compared non-vacuously). Scoped to the surface on purpose: a repository additionally emits its surfacesDir, a documented corpus asymmetry that would make whole-table equality a lie. Config hygiene: the exhibit config ignored composer specs by hand-typed name — the same shape as the CI filter list of #81, where adding a spec silently opted it into the wrong suite. It now ignores them by pattern. Counts: composer-smoke 14 → 28, composer-agent 44 → 49, composer-production 14 → 15, exhibit 110 → 110 (unchanged, verified). Uncovered testids 62 → 22, and the 22 are all on surfaces outside this milestone (Catalog, Mapper, Components, Repository, hub). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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.
Phase A of the pre-1.0 product-readiness milestone. Correctness and enforcement only — no contract, governance, intent, or A2UI change.
CI was not enforcing the packages that hold product logic
ci.ymlhand-listed pnpm filters and omittedcomposer-core(both planners, ledger, findings, flow schema),wireframe-renderers, andcontracts. Fail-first proof in the commit body: a deliberately failing composer-core test, run through CI's exact command sequence, exits 0 and the package never appears in the output. The fix replaces the filter list with the rootpnpm test(build:contracts && pnpm -r --no-bail test). Coverage 8 packages / 178 tests → 11 packages / 291 tests.Three silent-drop defects in the shadcn renderers, not one
The structural guard requested for the Table bug found the same class twice more. In the production shadcn/ui v3 catalog:
Table.rows(required)props.data(the Astryx name)rows ?? dataButton.child(required)props.labellabel ?? built childAlertDialog.triggerLabel(required)AlertDialog.confirmLabelprops.actionLabelconfirmLabel ?? actionLabelOn the flagship destructive-action example every governed word rendered except the words on the buttons — "Cancel", "Delete project", "Delete project and all data". Invisible because Preview defaults to the wireframe registry, which prints emitted props.
The guard (
required-prop-consumption.test.ts) is behavior-level, not a grep: for every component in each catalog it populates the required props with sentinels, renders through the real registry, and asserts each sentinel is observably consumed — with a one-entry allowlist (action, reason stated) and a throw on unhandled schema shapes. Regression coverage renders the real shipped examples through the real emitted catalog. Astryx verified unchanged: all 172 Astryx-emitted instances diffed byte-for-byte identical.Browser/agent validation parity
Canonical behavior confirmed from source, not assumed:
runPipelinedefaults to both A2UI versions and neither door overrides it — the browser's 0.9.1-only emit was the outlier. Nothing deleted. Both doors now run one shared seam (composer-core/emit.tsprojectEmit), each door being "the seam plus exactly one thing" (the agent adds file writing; the browser adds surface selection), asserted from both sides. Measured impact: no shipped reference surfaces a new 1.0 finding — pinned by test so the claim stays true or goes red.Full CI job run locally end-to-end: 291 unit tests, typecheck, studio Playwright 106/4-skipped, composer smoke 13/13, composer-agent 44/44.
🤖 Generated with Claude Code