Skip to content

fix: pre-1.0 correctness — CI enforcement, shadcn renderer props, browser/agent parity - #81

Merged
ryandmonk merged 5 commits into
mainfrom
fix/pre1-correctness
Aug 12, 2026
Merged

fix: pre-1.0 correctness — CI enforcement, shadcn renderer props, browser/agent parity#81
ryandmonk merged 5 commits into
mainfrom
fix/pre1-correctness

Conversation

@ryandmonk

Copy link
Copy Markdown
Contributor

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.yml hand-listed pnpm filters and omitted composer-core (both planners, ledger, findings, flow schema), wireframe-renderers, and contracts. 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 root pnpm 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:

Catalog declares Renderer read Now
Table.rows (required) props.data (the Astryx name) rows ?? data
Button.child (required) props.label label ?? built child
AlertDialog.triggerLabel (required) nothing rendered
AlertDialog.confirmLabel props.actionLabel confirmLabel ?? actionLabel

On 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: runPipeline defaults 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.ts projectEmit), 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

ryandmonk and others added 5 commits August 12, 2026 10:39
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>
Copilot AI lite review requested due to automatic review settings August 12, 2026 15:11
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 12, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
dspack-studio-composer 18c9285 Aug 12 2026, 03:14 PM

@ryandmonk
ryandmonk merged commit 1a901fd into main Aug 12, 2026
2 of 3 checks passed
@ryandmonk
ryandmonk deleted the fix/pre1-correctness branch August 12, 2026 15:15

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

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 test invariant (contracts build + recursive tests) and update contributor docs accordingly.
  • Fix three “silent drop” renderer defects in shadcn-renderers (Table rows, Button child, AlertDialog triggerLabel/confirmLabel) and add regression suites rendering shipped examples + a structural required-prop consumption guard.
  • Introduce composer-core projectEmit seam 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>
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