The transformation foundation: internal Identity/Route/Collect model, profile v2, fidelity, functions — 0.5.0 - #29
Merged
Merged
Conversation
The emitter is about to be rewritten onto a smaller internal representation
(Identity / Route / Collect), with the v1 surface-plan directives desugaring
into it. That refactor is only legitimate if it is invisible, and nothing in
the suite could currently prove it: out/ carries committed catalogs, but no
test compares them, so every description in a catalog could change silently.
So pin the artifacts themselves. 19 digests across the reachable matrix:
- shadcn x {TypeScript profile, JSON profile} x a2ui {0.9.1, 1.0}, with the
two spellings asserted equal — profile-as-data is only credible if
authoring a profile as a file changes nothing;
- astryx and shadcn scaffolded profiles x both A2UI versions;
- the hand-authored surface, every worked example in the contract, and seven
hand-built surfaces that reach the directives the examples miss
(textChildProp, actionProp, textProp, subFlatten transparent+asText,
subTable, subText+subButtonText, the Column wrap, id deduplication).
Warnings are hashed alongside messages: a refactor that quietly stopped
warning about a flattened compound would otherwise pass.
Verified the gate is not vacuous by injecting a one-word change into a single
warning message — two digests moved and the suite failed.
…ing into it
Ten surface-plan directives accumulated one per collision. Measured, that
vocabulary does not generalize: only four are used by both shipped profiles,
the second emit target reuses none, and the scaffolder can reach two. They are
point-fixes for one collision — shadcn's compound composition meeting A2UI's
fixed catalog vocabulary — not a vocabulary for projecting design systems onto
protocols.
Underneath they answer three questions: does this node emit, and as what
(Identity); where does each piece of its subtree land (Routing); how does a
repeated structure become an array (Repetition). This adds that as types, and
desugars all ten directives into it. The v1 spellings keep working and keep
emitting byte-identically; past desugar.ts the engine sees only primitives.
Three refinements came from implementation contact, each a discovery rather
than an escape hatch:
- a route's source is an ordered Selector[], not one selector. The audited
label lift ("the sub's label, else the first direct text under it") is a
fallback chain, and ordered selectors express it without a special case;
- a selector names a SET of subs, because a table row accepts a header-cell
wherever it accepts a cell. One selector naming both is honest; two collects
racing for one destination is not;
- first-write-wins becomes a uniform write discipline instead of `??=`
scattered per directive. Only textProp/textChildProp overwrote, and that
asymmetry is now an explicit field rather than an implicit code path.
subTable — the most table-shaped directive, 10 required keys used once in the
whole corpus — becomes two nested collects. Its fixed three-level depth is
nesting, its literal `{ cells }` record key is an item field name, and its
`drops` map is ordinary drops with authored reasons.
Sub-coverage becomes derivable. `subCoverage` is authored prose the engine
never reads, enforced only by a test in another repo; deriveSubCoverage()
reproduces it from the model. Building it surfaced a case worth keeping:
alert-dialog's content/header/footer are named by no directive at all — they
are walked through by the subtree consumer on the way to a named sub. A
derivation counting only named subs would have called them missing while the
emitter handled them correctly.
Totality is asserted directly, because the failure mode of a desugaring layer
is not mistranslation but silent omission: a directive that falls through
still type-checks, still loads, and simply stops taking effect.
emitNode read v1 directives directly. Now it reads Identity / Route / Collect, and desugar.ts is the only compatibility boundary. All 19 pinned artifacts are byte-identical across both A2UI versions; no golden was re-baselined. Two orderings had to become DECLARED data rather than emergent behaviour, because both are observable in hashed artifacts and both were previously decided by the shape of a method: WriteOrder — when each route and collect writes, and therefore object key order. A route-driven engine resolves in declaration order, which is not v1's method order: it emitted `caption,columns,rows` where declaration order gives `columns,rows,caption`. Diagnostics — an explicit (tree path, band, phase, rule, seq) sort key, with phases derived from where v1 actually pushed each warning rather than invented. The band is what makes a parent's post-child diagnostics sort after its whole subtree; a flat (node, phase) sort would hoist the wrap warning above the children it wraps. The engine collects diagnostics in any order and sorts once. One finding worth recording. v1 resolves all consuming directives in a SINGLE document-order walk, so tree position — not directive order — decides which sibling claims a destination: an alert-dialog writes triggerLabel before title because the trigger precedes the content. Resolving each consuming route independently reproduced the same values in a different key order. That looked at first like a v1 implementation accident to be preserved grudgingly. It is not. Walking a compound once and distributing what you find is what consuming a compound MEANS, and the alternative — letting the order two directives happen to be authored in decide which of two siblings wins — is the accident. So consuming routes are resolved as one group, by position. Also fixes a warning the first cut of the engine dropped entirely: the subContent composition-flattened casualty had no home once applySubContent was gone. The byte gate caught it. Structural boundary test: the engine source may not name any of the ten legacy directives, nor reach into `surfacePlan` at all. Verified non-vacuous by reintroducing a `plan.surfacePlan?.subTable` read — two assertions failed. 166 tests green (was 140); build, check:sync, test:pack and test:json-render all pass.
…ble profile `subCoverage` is authored prose the engine never reads, enforced only by a test in another repo. The internal model can derive the same facts — but a gate that looks correct and quietly breaks valid legacy behaviour is worse than the prose it replaces, so measure before refusing anything. Measured across all four reachable contract/profile pairs: pinned shadcn v2.3.0 20 subs under 3 compounds 0 unresolved astryx v0.1.4 0 subs (props-based) 0 unresolved acme v0.1.0 4 subs under 1 compound 0 unresolved production shadcn v3.0.0 29 subs under 6 compounds 1 unresolved Zero false positives on every profile that ships. The single finding is `radio-group -> radio-group-item` in the production profile — which is exactly the measured T2 failure (a sibling family A2UI models as data on the parent, not as children) that motivated the collect primitive. The derivation found the known gap on its own, without being told it existed. So making unresolved coverage fatal at load would be safe for every v1 profile today, and would correctly refuse the production profile until T2 lands. That decision stays separate and unmade here; this is the evidence for it. Reports only, exits 0. Astryx is included precisely to show a zero: it declares no sub-components at all, so the entire sub-* directive family is dead by construction for props-based contracts.
…sion dispatcher
v2 is the first profile language that authors the internal model directly:
Identity / Route / Collect in a closed string syntax, instead of the ten v1
directives named after the collisions that produced them.
routes: { from: ["sub(alert-dialog-title).text"], to: "prop:title" }
collects: { of: ["table-body"], into: "prop:rows", shape: "records",
field: "cells", row: ["table-row"], cells: ["table-cell"] }
subs: { "card-header": "transparent", "card-title": { "asText": "h3" } }
The dispatcher is explicit: loadProfile reads profileVersion, validates "1"
against profile.v1.schema.json exactly as before (frozen), validates "2"
against the new profile.v2.schema.json and then PARSES every plan's surface
block — a v2 profile that loads is a profile the engine can emit, with grammar
errors surfacing at load as pathed findings rather than mid-emission. Loaded
v2 profiles are stamped language:"v2"; unknown versions refuse naming the
supported set; no document is ever silently interpreted under the wrong
version. One language per document: each schema refuses the other's syntax.
The grammar is deliberately closed — no expression language, no JSON paths,
no predicates, no computed destination names, no cross-instance routing, and
the unimplemented capability spellings (plan-level transparent, onto, joinOn,
a second children route) are refused, not reinterpreted; each arrives with
its capability class T1-T5 as a grammar addition.
Two rules the parser derives that v1 made authors imply:
- Write order is a function of what a route reads and writes, never authored
and never declaration position. A same-phase double-claim on one
destination refuses; distinct-phase layering is legal because the outcome
is fully determined (v1's Table relies on exactly this: a passthrough prop
wins over the collect harvest because Verbatim runs first).
- consumesSubtree is derived from the presence of consuming routes/collects,
and children-routing plus subtree consumption is refused as contradictory.
Also threads Collect.flatten as declared data, replacing the origin-string
sniffing (`origin.endsWith(".header")`) that could never have worked for
authored v2 collects, and drops the dead dropReason helper.
The keystone proof: the ENTIRE shadcn profile re-spelled in v2 loads through
the dispatcher and emits byte-identically to the v1 profile — same catalog
digests as the pinned byte gate (both A2UI versions), same messages and
warnings for all seven directive surfaces and both worked examples. v2 is a
new spelling, not a new emitter.
198 tests green (was 166): +29 profile-v2 (byte-identity, dispatcher,
refusal matrix), +1 schema-mirror gate for the v2 pair, +2 browser-boundary
picking up the new modules. All 19 pinned digests untouched.
…rcement parse-v2 checks what a plan can be wrong about on its own; this adds what it can only be wrong about AGAINST A CONTRACT, at both entries (transform, emitSurface), before anything emits — v2 profiles only, because v1 behaviour is frozen: - every sub(x) a route, collect, or disposition names must be a declared sub-component of the plan's contract component; - every self.props.X must be declared — by the contract as an ordinary prop, or by the plan's own structural block as a SAME-NAMED passthrough. The gate initially demanded contract declaration outright and correctly refused the v2 Table re-spelling, which is how the v1 semantic surfaced: passthrough props (caption/columns/rows) are A2UI structural slots authored directly on surface nodes, declared by the plan, deliberately absent from the contract. Reading an undeclared prop into a RENAMED destination declares nothing anywhere, and still refuses; - derived sub-component coverage must be complete: every declared sub routed, collected, absorbed, transparent, re-identified, or dropped with a reason — anything else refuses per sub, naming the decision to make. v1 gets the report path instead: computeCoverage now derives sub-coverage rows from each mapped compound's transformation model (the authored subCoverage prose stays what it always was — documentation the engine never reads). A resolved sub is a mapped row carrying its classification; an unresolved sub is an unclassified row plus a warning plus a cannot-represent fidelity entry — the exact triple the existing --strict-coverage exit already keys on, so the flag now covers subs with no new plumbing. The measured gates behind making this fatal (scripts/sub-coverage-report.mjs, the 2aac076 record): pinned shadcn v2.3.0, astryx and acme all derive zero unresolved subs — re-proven here as a regression pin — so enforcement refuses no shipped artifact, only future profiles with genuine gaps. Which is the point. 206 tests green (+7): fully-resolved passes; unresolved sub refuses naming the sub; undeclared sub refuses; renamed-undeclared-prop refuses while the passthrough shape passes; emitSurface runs the same gate; v1 with a stripped subFlatten still transforms with five card.* unclassified rows; shipped profiles derive zero unresolved. All 19 pinned digests untouched.
…ver it The surface path could refuse and it could warn, but it could not ACCOUNT. All thirteen fidelity sites lived in mapping.ts — catalog-level, what a profile can carry in principle. What one emission actually did to one surface was scattered across advisory warnings with no classes and no strict mode, so a lossy projection could ride to production as a note. Now every transformation reports itself: source (the surface path plus selector), destination, originating profile rule (route/collect origin, propMap key, or the synthesis), kind (projected, moved, lifted, flattened, synthesized, wrapped, dropped, deduplicated), and fidelity class reusing the existing taxonomy. The ledger rides EmitSurfaceResult.fidelity — additive alongside the byte-frozen messages and warnings, ordered by the same declared (path, band, phase, rule) keys as the diagnostics, so its sequence is contract rather than accident. Classifications worth naming: the audited label lift is maps-cleanly (content preserved, position changed — relocation is not loss); a valueMap HIT is maps-cleanly while a fall-through to the declared default is synthesis-defaults (the distinction the old code could not see because it collapsed both into one ?? expression); compound consumption records each harvest as a clean move AND one lossy flatten for the structure that did not survive; id deduplication is maps-cleanly. --strict-surface makes configured classes FATAL (exit 5): bare means lossy,cannot-represent; --strict-surface=lossy,synthesis-defaults tightens it; an unknown class is a usage error. Without the flag the ledger still prints — loss is never invisible, the flag only decides whether it is also fatal. Also restores out/ artifacts a test run had regenerated: the committed out/catalog.v0_9_1.json is STALE relative to the shipped profile (its Table.rows description still names a status column the profile no longer emits) — a pre-existing housekeeping gap deliberately NOT fixed here, flagged for its own commit. Fidelity tests write to a scratch dir, not tracked out/. 215 tests green (+9). Byte gates untouched: same messages, same warnings, same 19 digests — the ledger is a new channel, not a change to the old ones.
The emitted catalog always carried A2UI's check vocabulary — Checkable,
CheckRule, FunctionCall — but our inlined FunctionCall had dropped the
upstream anyFunction constraint (inline-defs.ts records the drop), and no
catalog ever emitted a functions section. Net effect, measured: an instance
could `call` any name at all and gate A3 passed it silently. Declared checks
resolved to nothing, invisibly.
A correction to this milestone's own premise, from measurement: the dspack
CONTRACT declares no `checks` anywhere — its `constraints` are prose usage
guidance ({context, rule, severity}), correctly recorded as knowledge-layer
casualties. The hole was never contract-side; it was the unconstrained
FunctionCall plus the inexpressible functions section, both emit-side. Fixed
where it actually is.
A v2 profile now declares its function vocabulary as data:
"functions": {
"matchesRegexp": { "description": "...", "returns": "boolean",
"args": { "type": "object", "properties": { ... },
"required": ["pattern"] } } }
Emission: the catalog carries the functions section (both A2UI versions —
including through the v1.0 literal rebuild, which first dropped it);
FunctionCall.call is constrained to the declared names; and the whole call
object must match one declared branch, so a call with missing required args
refuses too (the branch requires `args` exactly when the declared schema
carries required params — the first cut emitted the arg schemas as decoration
that nothing referenced, and the test caught both that and the 1.0 drop).
Fail-closed both ways: an undeclared function call fails A3; declaring is the
only way to permit. Definitions are declarative schemas — implementations
live in renderers, exactly as A2UI intends; nothing executable can enter the
profile or the catalog. v1 documents cannot declare functions (the field is
v2 language; the v1 schema refuses it), and profiles that declare none emit
byte-identically to before — all 19 pinned digests and the v2 re-spelling
byte-equivalence stand untouched.
222 tests green (+7).
…undecided subs The old scaffold had two measured failures, and they were opposites. For props-based contracts it emitted no surface plan at all — catalog-valid, surface-unusable, a hard throw on the first child, for exactly the idiom the docs call typical. For compounds it invented the judgment it should have surfaced: every sub auto-classified transparent-or-asText from acceptsChildren, which is precisely the guess that is wrong for a repeated item like radio-group-item (the measured T2 gap). Too little in one place, too much in the other. The rewrite derives instead of guessing, and emits the v2 primitive language: - children route + ChildList slot where the contract OBSERVABLY supports children: the component appears with child nodes in a worked example (the example corpus is the contract's own definition of expressible — that is measurement, not judgment), or declares sub-components. Provenance lands in the synthNote, naming the example. - text route + slot where a worked example shows the component carrying text. - sub-components are NOT decided. Each is an explicit unresolved decision: in notes, under x-scaffold.unresolved, and absent from the surface block — which the v2 contract gate refuses to transform, per sub, until the author decides. The refusal IS the deliverable: a work checklist with pathed findings, not a pretense that judgment was exercised. The authoring loop closes and is tested: scaffold shadcn -> loads -> transform refuses naming card/table/alert-dialog families -> resolve ONE compound's subs -> its findings disappear while the others still refuse. Two scaffold digests moved INTENTIONALLY and two became refusal assertions, each explained at the pin: astryx-scaffold digests now describe a scaffold that emits (children slots the old one omitted); shadcn-scaffold's pinned behaviour is now the refusal itself, because the old digests pinned invented sub-classifications. The 15 v1-profile pins are untouched. 223 tests green (+1 authoring-loop). Build, pack, json-render all pass.
…the v2 surface
A 17-agent review/verify pass (three lenses, every finding independently
refuted-or-confirmed by execution) confirmed 13 findings collapsing to ten
distinct defects — all in the new v2/ledger surface, none in the byte-frozen
v1 paths. All ten fixed, each with a regression test named for its finding:
GATE SIDE DOORS. validateProfileAgainstContract iterated only
profile.components and skipped surface-less plans, so a mapped compound in
`synthesized` escaped every check (coverage even fabricated "transparent
grouping" rows for its subs), and omitting the surface block transformed
fail-open while `surface: {}` refused — the gate flipped on the presence of
an empty key. Now both collections validate and a missing block is exactly
as unresolved as an empty one.
BLESSED-BUT-UNEMITTABLE. {drop: reason} on a non-collecting plan passed the
gate then crashed emission with a false unknown-component message; drops are
now warn-and-discard in the child rewrite, same semantics as the collect
path, authored reason on record. sub(x).firstText as a route's FIRST
selector parsed, derived Consume, and then resolved nothing — consuming the
subtree while writing nothing; it now refuses at load as what it is, a
fallback selector. A children route sharing its destination with a value
route had the later phase clobber the earlier one through emitChildren's
direct assignment, orphaning the synthesized Text in the hashed artifact;
the children destination is now exclusive in both directions at load. The
one-sub-one-disposition check read scalar cells at the wrong nesting level
(dead disjunct) — cells+disposition contradictions now refuse.
SILENT SUPPRESSION. Any route reading self.props.X disabled the propMap
projection of X even when writing a differently-named destination —
routedVerbatim now requires the same-named passthrough shape, so a renamed
read and the projection both land.
LEDGER HONESTY. write() reported nothing, so first-wins-skipped harvests
ledgered as clean moves — it now returns whether the write landed, and a
discarded harvest ledgers dropped/lossy. Unrouted node text was the one
unledgered loss channel (v1 discards it silently; warnings stay byte-frozen)
— it now gets a dropped/lossy entry. asText that destroys nested component
structure recorded only synthesis-defaults — it now records flattened/lossy
naming the destroyed components, mirroring the cell path.
FUNCTIONS. Declared arg schemas were emitted unvalidated: a dangling $ref
slid past gate A1 (ajv defers ref resolution) and threw raw out of gate A3 —
pass-or-crash, never a finding. Function args must now be self-contained;
any $ref refuses at load with its path.
One refuted finding, recorded for honesty: the committed out/ reports are
stale (true) but nothing consumes them and the npm package ships dist+src
only — flagged as housekeeping, not fixed here.
234 tests green (+11). All 19 byte pins, the v2 keystone byte-equivalence,
build, pack and json-render gates unchanged.
…the lost /tmp measurement The prior production measurement (2aac076: 29 subs under 6 compounds, 1 unresolved radio-group-item) was generated from a hand-built 15-plan profile that lived only in /tmp and did not survive a reboot. Per the release requirement, that number is no longer a current gate; this replaces it with evidence that cannot be lost and cannot quietly drift. eval/ carries three durable pieces, none of which ship in the npm package (files: dist,src only) and none of which is Studio's profile: - shadcn-v3.dspack.json — the production contract at dspack 48643ff, a PINNED copy (sha256 ea87346f…, verified on every rebuild) so the fixture measures one corpus, stably; - build-eval-profile.mjs — the mechanical derivation: scaffoldProfile(v3) plus the six shipped plans transplanted in the v2 re-spelling that profile-v2.test.ts byte-proves equivalent (now extracted to src/transform/shadcn-v2-respelling.ts so test and builder share one source). Zero casualties declared — the representation analysis showed some old casualty reasons were wrong (dialog is representable, T4), and classifying families as casualties here would undercount the very unresolved surface the fixture exists to measure; - shadcn-v3.eval.profile.json — the committed fixture, with an x-eval label stating purpose, provenance, and that downstream Studio still consumes the pinned v2.3.0 contract with the shipped v1 profile. The honest result SUPERSEDES the old number rather than reproducing it: 106 sub-components under 19 mapped compounds 20 resolved — exactly the transplanted families (card 5, table 7, alert-dialog 8; same class distribution as the pinned v2.3.0 measurement, since v3 kept those families intact) 86 unresolved — the lost profile's nine extra compound plans carried judgment that is gone; every decision they had made, and every one they had not, now shows as open Reported, not shaped: the 86 are deliberate, labelled as such in the fixture, and are the measure of the T1–T5 work ahead. The fatal v2 coverage gate refusing this fixture against the v3 contract — 86 pathed findings — is pinned as a test: the gate doing on the production corpus exactly what it was built to do. The reproducibility gate is also a test: rebuilding from committed inputs must equal the committed fixture byte-for-byte; the coverage report script now reads eval/ instead of the dead /tmp paths. 239 tests green (+4 eval, +1 browser-boundary picking up the respelling module).
Not a breaking v1 change — every v1 profile loads and emits byte-identically, proven by the pinned byte gate. 0.5.0 marks a capability boundary: - profile language v2 (Identity / Route / Collect authored directly), with an explicit profileVersion dispatcher and one-language-per-document schemas; - the internal transformation architecture: the engine consumes only the primitive model; v1 desugars at the only compatibility boundary; - load-time validation of selectors, destinations, write conflicts, and derived sub-component coverage (fatal for v2; report + --strict-coverage for v1); - new public fidelity output (EmitSurfaceResult.fidelity) and --strict-surface; - declared catalog functions, fail-closed on every call; - scaffoldProfile emitting honest v2 drafts with explicit unresolved decisions; - the reproducible production-v3 coverage evaluation fixture under eval/. README now presents v2 as the authoring language and v1 as compatibility syntax, matching docs/PROFILES.md.
…ng-traversal rule, scaffold boundary
There was a problem hiding this comment.
Pull request overview
Establishes the “transformation foundation” by introducing an internal surface transformation model (Identity/Route/Collect), adding profile v2 as a new authoring language with explicit version dispatch, and layering in contract-facing validation plus an emission fidelity ledger (including a --strict-surface CLI gate).
Changes:
- Add profile v2 (schema + runtime parsing) and explicit
profileVersiondispatch inloadProfile, including v2-only features like declared catalog functions. - Refactor the surface emitter to consume only the internal model (v1 directives desugar into it), and add ordered diagnostics + a per-emission fidelity ledger.
- Add contract-facing validation for v2 profiles (fail-closed) and derived sub-component coverage reporting (fatal for v2, reporting for v1), with extensive regression/compatibility tests.
Reviewed changes
Copilot reviewed 35 out of 36 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/validate-v2.test.ts | New tests covering v2 contract validation behavior and ensuring v1 remains report-only. |
| src/types.ts | Adds SurfaceFidelityEntry to represent per-surface transformation ledger entries. |
| src/transform/validate-v2.ts | Introduces v2-only contract-facing validation gate (ProfileContractError). |
| src/transform/shadcn-v2-respelling.ts | Provides v2 respelling for shipped shadcn surfaces as mapping evidence. |
| src/transform/scaffold.ts | Updates scaffoldProfile to emit v2 drafts based on observed examples and leave subs unresolved. |
| src/transform/profiles.ts | Extends Profile with v2 concepts (surface, functions, language). |
| src/transform/profile.v2.schema.json | Adds the v2 JSON schema (primitive language, functions, surface grammar). |
| src/transform/profile-schema-v2.ts | Runtime mirror of the v2 JSON schema for browser-friendly loading. |
| src/transform/profile-load.ts | Implements explicit profile version dispatch, v2 surface parsing at load, and function-args $ref refusal. |
| src/transform/parse-v2.ts | Parses v2 surface syntax into the internal model with static validation. |
| src/transform/model.ts | Introduces the internal Identity/Route/Collect types and derived coverage utilities. |
| src/transform/mapping.ts | Derives sub-component coverage from the internal model and emits coverage+warnings for gaps. |
| src/transform/index.ts | Runs v2 contract validation before mapping/emission. |
| src/transform/emit.ts | Emits declared catalog functions and restores FunctionCall constraints based on declared vocabulary. |
| src/transform/desugar.ts | Translates v1 directives into the internal model; adds surfaceModelOf dispatcher + caching. |
| src/targets/a2ui/surface.ts | Refactors surface emission to read only the internal model; adds fidelity ledger integration. |
| src/targets/a2ui/diagnostics.ts | Adds deterministic ordering for warnings/fidelity via explicit sort keys. |
| src/surface-fidelity.test.ts | New tests validating fidelity completeness, honesty, determinism, and --strict-surface. |
| src/schema-mirrors.test.ts | Extends schema mirror tests to cover v2 schema parity. |
| src/review-findings.test.ts | Regression tests for previously identified v2/ledger issues. |
| src/profile-v2.test.ts | Proves v2 respelling is byte-identical to v1 for messages/warnings and validates dispatcher/refusals. |
| src/profile-load.test.ts | Updates scaffold tests to v2 and adds “resolve one compound and it emits” authoring loop test. |
| src/functions.test.ts | Adds tests proving declared functions reach the catalog and constrain FunctionCall validation. |
| src/eval-fixture.test.ts | Adds reproducible evaluation fixture tests for production v3 contract measurement. |
| src/engine-boundary.test.ts | Ensures the surface engine never mentions v1 directives; adds ordering/meaning tests. |
| src/desugar.test.ts | Ensures v1 desugaring is total and preserves directive semantics. |
| src/cli.ts | Adds --strict-surface support and fidelity gating/printing. |
| src/byte-neutral.test.ts | New byte-neutrality gates/pins across catalogs and surfaces (including scaffold intentional moves). |
| src/snapshots/byte-neutral.test.ts.snap | Snapshot pins for byte-neutral surface digests. |
| scripts/sub-coverage-report.mjs | Adds measurement script for derived sub-component coverage (report-only). |
| README.md | Updates docs for v1/v2 profile-as-data, schemas, and authoring guidance. |
| package.json | Bumps version to 0.5.0 and normalizes description punctuation. |
| eval/build-eval-profile.mjs | Adds reproducible eval-profile builder with pinned contract hashing. |
| docs/PROFILES.md | Documents profile v2 language, ledger/strict-surface, and ordering doctrine. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+116
to
+120
| issues.push({ | ||
| path: at, | ||
| message: `self.props.${prop} reads a prop '${plan.dspackId}' does not declare in this contract, and it is not a same-named structural passthrough`, | ||
| }); | ||
| } |
Comment on lines
+194
to
+202
| if (childrenBecause) { | ||
| structural.children = { | ||
| schema: CHILD_LIST, | ||
| description: "Child component IDs, in order.", | ||
| synthNote: `Scaffolded: ${childrenBecause}.`, | ||
| }; | ||
| required.push("children"); | ||
| routes.push({ from: ["children"], to: "slots:children" }); | ||
| } |
… documented guarantee
The release blocker. Gate 1's doc claimed "compilation fails on any
unresolved $ref, proving the catalog is a valid, fully self-contained
schema" — false, because ajv defers ref resolution past compile(). A
structural slot schema carrying {$ref: "#/$defs/DoesNotExist"} produced
either all-gates-PASS (validateCatalog with no surface — a public export) or
a raw MissingRefError thrown out of gate 3's un-guarded getSchema on the
main transform path. Pass-or-crash, never a finding. The functions channel
had just been closed at load; structural schemas shared the class.
Fail-first, captured before the fix: three tests escaped as uncaught
"can't resolve reference #/$defs/DoesNotExist" errors — including with the
DEFAULT surface — and the hand-built broken catalog returned pass=true from
validateCatalog.
The fix is one coherent boundary, not a per-channel patch: gate 1 now walks
the finished catalog and resolves every internal $ref itself (JSON Pointer
resolution incl. ~0/~1 escapes and array indices), failing with the ref AND
the JSON path of the offending $ref keyword. That covers structural slot
schemas, function args if refs ever reappear, and any future authored-schema
channel, at the one place the complete catalog exists — version-exact, so a
ref to a def only one A2UI version carries is judged against the catalog
actually being emitted. Gate 3 skips-as-failed when gate 1 found dangling
refs, and is belt-and-braces try/caught so no raw ajv error can ever escape
validateCatalog again.
Deliberately NOT restricted: legitimate internal refs (the shipped profiles'
ComponentId / Action / DynamicString / ChildList / CheckRule structural refs,
and nested pointers like #/components/<Name>) validate exactly as before —
byte gates prove zero output change. Load-time stays unrestricted for
structural schemas because their valid targets are emit-time- and
version-dependent; the catalog gate is the honest place.
Also surfaced by the new packed-install smoke, and fixed: profileSchemaV2
was not exported from the package index — a real public-surface gap for a
release advertising v2. Now exported alongside ProfileContractError,
ProfileParseError, SurfaceV2 and SurfaceFidelityEntry.
Pack-test now proves the whole 0.5.0 surface SURVIVES PUBLICATION, from an
external install of the tarball: v2 keystone byte-identity, unknown-version
refusal, malformed selector/destination refusals, functions $ref refusal,
structural dangling-$ref as a pathed gate finding, the fidelity ledger,
--strict-surface (exit 5 on lossy; emitted bytes identical with and without
the flag), and the browser-safe loader chain in dist.
244 tests green (+5). All byte pins and the keystone untouched.
ryandmonk
force-pushed
the
feat/internal-model
branch
from
August 7, 2026 00:01
1d7154f to
1cbaf8f
Compare
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.
The foundation milestone for the ratified three-primitive transformation model, ahead of the T1–T5 representation capabilities. Fifteen commits; 239 tests (was 166 at baseline); every gate green locally, from a fresh clone, and against downstream Studio via a packed install.
What this establishes
emitNodereads Identity / Route / Collect; the ten v1 directives desugar intransform/desugar.ts, the only compatibility boundary — structurally enforced byengine-boundary.test.ts(proven non-vacuous by reintroducing a directive read: two assertions fail).profileVersiondispatcher: distinct reviewable schemas, one language per document, unknown versions refuse with pathed findings, loaded v2 profiles stampedlanguage: "v2".transparent,onto,joinOn, second children route) are refused, not reinterpreted.--strict-coveragefor v1. Measured before enforcement: pinned shadcn / astryx / acme all zero-unresolved.EmitSurfaceResult.fidelity) +--strict-surface(exit 5, configurable classes, never alters emitted bytes).$refrefuses at load, nothing executable can be expressed.scaffoldProfileemits honest v2 drafts: mechanical structure from evidence, every sub-component an explicit unresolved decision thattransform()refuses until the author makes it.eval/): the pinned v3 contract (sha256-verified copy at dspack48643ff) plus a mechanically derived measurement profile. Honest result, superseding the lost /tmp record: 106 subs under 19 mapped compounds — 20 resolved (card 5, table 7, alert-dialog 8), 86 unresolved. The 86 are deliberate open decisions and the measure of the T1–T5 work; the fixture is labelled NOT-Studio's-profile and ships outside the npm package.Compatibility
git diff 15216d4..HEADon the snapshot file is empty).7c1b72dfd83dde14/2dde439bbf643834), same messages and warnings across all directive surfaces and worked examples.Hardening
An adversarial 17-agent review/verify pass over the increment confirmed 13 findings (10 distinct defects, all in the new v2/ledger surface, none in v1 paths) — gate side doors, blessed-but-unemittable spellings, ledger honesty gaps, unvalidated function args. All ten fixed with named regression tests in
review-findings.test.ts.Doctrine now documented (docs/PROFILES.md)
Ordering is explicit because object-key order and warning order are observable in deterministic artifacts — goldens are never re-baselined for incidental sequence changes. Compound consumption resolves in document order as one traversal: tree position, not declaration order, decides.
Boundaries held
No T1–T5 capability is implemented or reachable (refusal-tested). Neither contract pin moves. Studio is not migrated to v3. Version is proposed as 0.5.0 (capability boundary under 0.x; zero breaking v1 behavior); not published.
🤖 Generated with Claude Code