From 4dd509ea491d455a09e9304e906f93bcc1e7a7b6 Mon Sep 17 00:00:00 2001 From: Cindy Zhang Date: Sat, 31 Oct 2026 11:59:24 -0700 Subject: [PATCH 1/8] spec(AST-110): an ejected component builds on its own and behaves like the package --- docs/architecture/cli-surface.md | 2 + docs/specs/AST-110-swizzle-output/spec.md | 192 ++++++++++++++++++++++ 2 files changed, 194 insertions(+) create mode 100644 docs/specs/AST-110-swizzle-output/spec.md diff --git a/docs/architecture/cli-surface.md b/docs/architecture/cli-surface.md index 0cbb8abaa7822..0942c34ade5da 100644 --- a/docs/architecture/cli-surface.md +++ b/docs/architecture/cli-surface.md @@ -665,6 +665,8 @@ non-interactive guarantee. verification. - `foundation/discovery/theme-discovery.mjs` and `api/theme` — typed integration theme descriptor discovery, package-aware selection, source copy, and build. +- `api/swizzle` — the component source copy; what a copy contains and promises + is `spec:AST-110`'s. - `foundation/agent-docs` — the shared expected-block renderer and managed-file writer used by init and upgrade, and the layout that decides which files hold the block (INV31). diff --git a/docs/specs/AST-110-swizzle-output/spec.md b/docs/specs/AST-110-swizzle-output/spec.md new file mode 100644 index 0000000000000..ade8b3c2ce440 --- /dev/null +++ b/docs/specs/AST-110-swizzle-output/spec.md @@ -0,0 +1,192 @@ +--- +schema_version: 4 +template_version: 2 +kind: system-spec +id: spec:AST-110 +authority: draft +archive_reason: null +superseded_by: null +approved_by: null +approved_at: null +phase: proposed +owners: [cixzhang, josephfarina] +affects_architecture: [architecture:cli-surface] +affects_families: [] +affects_contributing: [] +affects_consumer_docs: [cli] +--- + +# Swizzle output system spec + + + +```json +{ + "paths": [ + "packages/cli/api/swizzle/**", + "packages/cli/clients/cli/commands/swizzle*" + ] +} +``` + + + +## Intent + +A product team runs `astryx swizzle ` to own a component's source. The +copy builds in their app with no edits, answers to the same theme targets and +class names, and behaves like the package component it came from until they +change it. Core and the CLI keep that true for every component swizzle offers, +and the repository proves it before a change that could break it merges. + +## Non-goals + +- Which component a name copies and what `swizzle --list` reports: + `spec:AST-035` FR11–FR12 and `architecture:cli-surface` INV28. +- Where the command runs: `spec:AST-081` FR3. +- How a product compiles StyleX for its own code or a copy: `spec:AST-080`. +- Keeping a copy the product has edited in step with later Core releases. +- Making Core's internal helpers public API (`spec:AST-017` FR21, + `architecture:public-component-api` INV12). +- Equivalent internal implementations remain valid when they satisfy this + contract. +- Internal modules, files, function names, algorithms, data structures, storage + layouts, manifests, journals, locks, transaction protocols, and CI job/workflow + topology belong in architecture or implementation unless callers or + interoperating systems intentionally depend on that exact mechanism as a public + protocol. In that case, state who depends on it and why an equivalent + implementation would not satisfy the contract. + +## Requirements + +- **FR1 — An ejected component builds on its own.** In a project with the same + Core version and a StyleX compiler (`spec:AST-080`), the output of + `astryx swizzle ` type-checks and bundles with no edits. It imports from + its owner package only paths that package's `exports` lists, and never a + `./internal/*` path. +- **FR2 — The helpers it needs come with it.** Every module the component + reaches that is not reachable through a public export is copied into the + output, keeping its folder layout relative to the component, and copied files + import each other by relative path. A symbol the owner package exports publicly + is imported from that public path. Nested folders the component imports are + copied with it. The output holds only source the component runs: tests, + stories, spec records, and component docs are not copied. +- **FR3 — Shared runtime state stays single.** A module whose state coordinates + components with each other (the layer stack, the modal outlet, interaction + modality, the bottom-sheet stack) keeps that state in one store per document, + under a registered key that carries the store's protocol version. A copy and + the package component on the same protocol version use the same store, so + stacking, Escape, focus return, and modality behave as they do with the package + alone. A copy on another protocol version keeps a store of its own and never + reads a store of a different shape. A module whose state only caches work may + be duplicated by a copy. +- **FR4 — Private attributes stay contained.** Each private-attribute owner + module the component reaches (`architecture:public-component-api` INV23) is + copied whole and unedited. The copy sets private attributes only through those + owner modules; no copied file spells a private attribute name or selector by + hand. +- **FR5 — Theme targets and class names are unchanged.** The copy answers to the + same theme targets and emits the same documented class names, data attributes, + and custom properties as the package component of that version, so a theme, + `xstyle`, and product CSS reach it the same way. +- **FR6 — Behavior matches the package.** Until the product edits it, the copy's + keyboard handling, focus, roles, names, states, announcements, and events match + the package component of the same version. +- **FR7 — Every offered component is proven before merge.** `swizzle --list` + offers only component folders. Before a change to a component package's source + or to the swizzle copy merges, every offered component is ejected into a clean + project, type-checked, bundled with a StyleX compiler, and rendered once on the + server; a failure blocks the merge through a required check. Release checks run + the same proof for every component and also render each copy in a real + browser. The proof adds no required context and no wall-clock time to a pull + request's slowest required check. + +### Platform support + +- Supported feature/engine floor: the product uses the same Core version the + copy came from and a StyleX compiler that `spec:AST-080` supports; browser + support is Core's. +- Unsupported behavior: a project without a StyleX compiler fails at import, as + the command's output states. A copy kept across a Core upgrade is outside FR1 + and FR6. +- Browser evidence: a real-browser render of every copy at release time (FR7); + server rendering is the pull-request smoke evidence. + +## Current-state impact + +- `architecture:cli-surface` points to this record for what `swizzle` writes. +- The CLI's swizzle copy follows the component's module graph (FR1–FR2, FR4), + writes no test, story, spec, or doc file, and lists only component folders + (FR7). The CLI owner implements and owns these changes. +- Core keeps the layer stack and modal outlet in versioned document stores + (FR3), as interaction modality and the bottom-sheet stack already do. +- The FR7 proof joins the repository's existing required test lane + (`spec:AST-030` FR11), selected by the shared change-scope classification and + reported in the single pull-request report: standard runners, about three + job-minutes on a pull request that changes component source or the swizzle + copy (about two to three percent of pull-request runner time), no added + wall-clock time to green, and the full proof at release time. +- The `swizzle` command doc states FR1 and FR6 for product teams. + +### Known gaps + +- On `main`, a static check of the 154 components swizzle can copy (Core 106, + Lab 40, Charts 6, RichText 1, Vega 1) finds 90 whose imports do not resolve + after the copy, 110 whose copy includes test, spec, or doc files, and 3 that + import nested folders the copy leaves out (Markdown, Table, Lab Schedule). +- `swizzle --list` offers 6 folders that are not components: `__tests__`, + `gestures`, `i18n`, `internal`, `motion`, and `NavItem`. +- In the published 0.7.5, none of Item, Token, Dialog, BottomSheet, Button, and + Tooltip type-checks or bundles after swizzle; they reach 21 helpers Core does + not export. +- The layer stack and modal outlet keep module state, so a copied Dialog or + BottomSheet starts a second stack (FR3). +- No private-attribute owner module exists yet (FR4). + +## Verification + +| Contract | Verification | Representative states | Mutation or failure expectation | +| -------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | +| FR1 | Eject every offered component into a clean project; type-check and bundle | Every component in `swizzle --list`; Core, Lab, Charts, RichText, Vega | A type or resolve error, or an import of a path the owner's `exports` lacks or of `./internal/*`, fails | +| FR2 | Import graph of each output | Helpers in `utils`, `hooks`, `Layer`; nested folders; colocated tests and docs | A copied file imports a module that is neither in the output nor a public export, or a test, story, spec, or doc is copied | +| FR3 | Render a copied and a package layer component together; inspect stores; open, stack, and Escape | Dialog over package Popover; BottomSheet over package Dialog; modality | Two stores of one protocol version exist in a document, or Escape and focus return differ from the package alone | +| FR4 | The private-attribute lint over each output | Components that reach an owner module | A hand-written private attribute name or selector, or an edited owner module, in the output | +| FR5 | Render package and copy side by side; compare class names, data attributes, and theme targets | Default; each variant; a theme override | A theme target, documented class name, or data attribute present on one and not the other | +| FR6 | The component's own interaction and accessibility checks run against the copy | Keyboard, pointer, screen-reader tree | A check that passes on the package fails on the copy | +| FR7 | The required test lane's swizzle proof and the release-time proof | A change to component source; a change to the swizzle copy; an unrelated change | An offered component missing from the proof, a failure that does not block, or a non-component folder in `swizzle --list` | + +Known verification gap: none of these checks exist on `main`. + +## Decision log + +### DEC-1 — Swizzle copies what a component needs and imports what is public + +**Reference:** `spec:AST-110/DEC-1` +**Decider:** `cixzhang`, `2026-10-31` + +A copy that builds is the whole value of swizzle, and the helpers a component +reaches are internal by `spec:AST-017` FR21. The copy therefore carries those +helpers with it and imports only public paths, so no internal helper becomes +public API to make a copy build. + +Rejected: exporting the helpers copies need, because anything reachable from a +package path is public (`architecture:public-component-api` INV12). + +### DEC-2 — Shared runtime state lives in versioned document stores + +**Reference:** `spec:AST-110/DEC-2` +**Decider:** pending (`cixzhang`) + +A copy is a second bundle of the same modules. Core already keeps interaction +modality and the bottom-sheet stack in a per-document store under a registered, +versioned key so that duplicate bundles share one state; FR3 applies that rule +to every coordinating module, so copying a module never splits a stack and no +new public entry is needed. + +Rejected: a public runtime entry that copies import instead of copying, because +it makes the coordinators public API with a compatibility promise only copies +need. + +## Open questions + +- None. From a9f8aff64bc344feb0a0fd4b5205ecf33196fafd Mon Sep 17 00:00:00 2001 From: Cindy Zhang Date: Sat, 31 Oct 2026 12:11:54 -0700 Subject: [PATCH 2/8] spec(AST-110): theme overrides reach a copy; known gaps from the full 0.7.5 ledger --- docs/specs/AST-110-swizzle-output/spec.md | 27 ++++++++++++++--------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/docs/specs/AST-110-swizzle-output/spec.md b/docs/specs/AST-110-swizzle-output/spec.md index ade8b3c2ce440..65be0b525e175 100644 --- a/docs/specs/AST-110-swizzle-output/spec.md +++ b/docs/specs/AST-110-swizzle-output/spec.md @@ -88,7 +88,9 @@ and the repository proves it before a change that could break it merges. - **FR5 — Theme targets and class names are unchanged.** The copy answers to the same theme targets and emits the same documented class names, data attributes, and custom properties as the package component of that version, so a theme, - `xstyle`, and product CSS reach it the same way. + `xstyle`, and product CSS reach it the same way. The copy's styles sit in the + same cascade layer as the package component's, so a theme's component + overrides apply to the copy as they do to the package. - **FR6 — Behavior matches the package.** Until the product edits it, the copy's keyboard handling, focus, roles, names, states, announcements, and events match the package component of the same version. @@ -130,15 +132,18 @@ and the repository proves it before a change that could break it merges. ### Known gaps -- On `main`, a static check of the 154 components swizzle can copy (Core 106, - Lab 40, Charts 6, RichText 1, Vega 1) finds 90 whose imports do not resolve - after the copy, 110 whose copy includes test, spec, or doc files, and 3 that - import nested folders the copy leaves out (Markdown, Table, Lab Schedule). -- `swizzle --list` offers 6 folders that are not components: `__tests__`, - `gestures`, `i18n`, `internal`, `motion`, and `NavItem`. -- In the published 0.7.5, none of Item, Token, Dialog, BottomSheet, Button, and - Tooltip type-checks or bundles after swizzle; they reach 21 helpers Core does - not export. +- Built from the published 0.7.5, of the 112 names `swizzle --list` offers, 12 + build and render, 89 fail to build, 9 build but render wrong, and 2 build with + nothing to render. +- The copy sends `../X/file` imports to the public `X` entry, which lacks the + internal helpers (87 components), and deep StyleX modules are missing from + Core's exports (12). +- Copied styles land in the app's cascade layer above the theme's, so theme + component overrides stop applying (9 components; 38 once imports resolve). +- Copies include spec records and test files (104 components), leave out nested + folders (Markdown, Table), and `swizzle --list` offers 6 folders that are not + components: `__tests__`, `gestures`, `i18n`, `internal`, `motion`, and + `NavItem`. - The layer stack and modal outlet keep module state, so a copied Dialog or BottomSheet starts a second stack (FR3). - No private-attribute owner module exists yet (FR4). @@ -151,7 +156,7 @@ and the repository proves it before a change that could break it merges. | FR2 | Import graph of each output | Helpers in `utils`, `hooks`, `Layer`; nested folders; colocated tests and docs | A copied file imports a module that is neither in the output nor a public export, or a test, story, spec, or doc is copied | | FR3 | Render a copied and a package layer component together; inspect stores; open, stack, and Escape | Dialog over package Popover; BottomSheet over package Dialog; modality | Two stores of one protocol version exist in a document, or Escape and focus return differ from the package alone | | FR4 | The private-attribute lint over each output | Components that reach an owner module | A hand-written private attribute name or selector, or an edited owner module, in the output | -| FR5 | Render package and copy side by side; compare class names, data attributes, and theme targets | Default; each variant; a theme override | A theme target, documented class name, or data attribute present on one and not the other | +| FR5 | Render package and copy side by side under a theme with component overrides; compare | Default; each variant; a theme component override | A theme override, documented class name, data attribute, or theme target reaches one and not the other | | FR6 | The component's own interaction and accessibility checks run against the copy | Keyboard, pointer, screen-reader tree | A check that passes on the package fails on the copy | | FR7 | The required test lane's swizzle proof and the release-time proof | A change to component source; a change to the swizzle copy; an unrelated change | An offered component missing from the proof, a failure that does not block, or a non-component folder in `swizzle --list` | From 9b4e4b6cfe835ebfb858465da37e609cc5f12baf Mon Sep 17 00:00:00 2001 From: Cindy Zhang Date: Sat, 31 Oct 2026 12:47:59 -0700 Subject: [PATCH 3/8] spec(AST-110, AST-080): copy rules sit below the theme layer; helper collisions, layer contexts, inline FR4; record approval --- .../specs/AST-080-package-stylesheets/spec.md | 26 +++-- docs/specs/AST-110-swizzle-output/spec.md | 100 ++++++++++-------- 2 files changed, 68 insertions(+), 58 deletions(-) diff --git a/docs/specs/AST-080-package-stylesheets/spec.md b/docs/specs/AST-080-package-stylesheets/spec.md index 0f0e5d7c1a8e6..a07041e532f16 100644 --- a/docs/specs/AST-080-package-stylesheets/spec.md +++ b/docs/specs/AST-080-package-stylesheets/spec.md @@ -41,7 +41,8 @@ first. - Which StyleX variable groups a package stylesheet contains, owned by `spec:AST-066` FR3. - How a product compiles its own code, including any StyleX settings it uses for - its own styles or for components it ejects with `astryx swizzle`. + its own styles or for components it ejects with `astryx swizzle`; the layer a + swizzled copy's rules land in is FR7's. - How `@astryxdesign/build` plugs into each bundler, and how a source build chooses which modules to compile. - Minifying or compressing stylesheets. @@ -133,7 +134,10 @@ first. reproduces its prebuilt stylesheet's rules; compiling a subset produces the rules those modules use. A source build of Core always includes the Core definitions other packages' prebuilt stylesheets rely on under FR2, such as - variable groups and keyframes. Product rules stay outside `astryx-base`. A + variable groups and keyframes. Product rules stay outside `astryx-base`, except + a swizzled copy's component rules: they sit inside `astryx-base` like the + package component's, below `astryx-theme`, so a theme's component overrides + apply to the copy as they do to the package component (`spec:AST-110` FR5). A source build needs no data beyond the package's own source, and holds in development builds as in production builds. @@ -199,15 +203,15 @@ first. ## Verification -| Contract | Verification | Representative states | Mutation or failure expectation | -| -------- | ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| FR1 | package manifest and stylesheet build tests | every published package with styled modules; a module's rules in its package stylesheet | a styled published package has no `.css` export; a rule a module uses is missing | -| FR2 | real-browser computed styles with no StyleX compiler | a representative component from each package; default, hover, focus, disabled, dark, and right-to-left | removing the package stylesheet changes a computed style; a non-Core stylesheet repeats a group `astryx.css` declares | -| FR3 | stylesheet structure tests and real-browser cascade | each package stylesheet and `reset.css`; theme first and last; package stylesheets in either order; `reset.css` last; a theme override | an unlayered rule or another layer; a missing or reordered registration; the theme override loses in any load order | -| FR4 | packed-package and bundler fixtures | each package's stylesheet import in a production bundle | the subpath does not resolve from the packed package; the bundle drops the stylesheet | -| FR5–FR6 | consumer-doc checks | every setup snippet in the styling overview, getting started, and migration setup; each package README | a default setup omits `astryx.css` or a used package stylesheet; a setup loads a prebuilt stylesheet and compiles the same package's source; a package import is missing | -| FR7 | build-package compile tests and real-browser cascade | every Core module compiled through the build package; a subset of modules; a development build; Core source-built beside a prebuilt Lab stylesheet; a source-built component under a theme override | the every-module compile differs from the prebuilt rules or order; an Astryx rule sits in a nested or other layer; the theme override loses | -| FR8 | pull-request size report against base | a component whose styles change; a touch-only feature adding 0.9 KB and 1.1 KB gzip CSS to one component; a pull request with no styled change | a changed package stylesheet or component with no base-against-head gzip figure; a touch-only feature over 1 KB gzip CSS per component that is not flagged | +| Contract | Verification | Representative states | Mutation or failure expectation | +| -------- | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| FR1 | package manifest and stylesheet build tests | every published package with styled modules; a module's rules in its package stylesheet | a styled published package has no `.css` export; a rule a module uses is missing | +| FR2 | real-browser computed styles with no StyleX compiler | a representative component from each package; default, hover, focus, disabled, dark, and right-to-left | removing the package stylesheet changes a computed style; a non-Core stylesheet repeats a group `astryx.css` declares | +| FR3 | stylesheet structure tests and real-browser cascade | each package stylesheet and `reset.css`; theme first and last; package stylesheets in either order; `reset.css` last; a theme override | an unlayered rule or another layer; a missing or reordered registration; the theme override loses in any load order | +| FR4 | packed-package and bundler fixtures | each package's stylesheet import in a production bundle | the subpath does not resolve from the packed package; the bundle drops the stylesheet | +| FR5–FR6 | consumer-doc checks | every setup snippet in the styling overview, getting started, and migration setup; each package README | a default setup omits `astryx.css` or a used package stylesheet; a setup loads a prebuilt stylesheet and compiles the same package's source; a package import is missing | +| FR7 | build-package compile tests and real-browser cascade | every Core module compiled through the build package; a subset of modules; a development build; Core source-built beside a prebuilt Lab stylesheet; a source-built component under a theme override; a swizzled copy under a theme component override | the every-module compile differs from the prebuilt rules or order; an Astryx rule sits in a nested or other layer; the theme override loses; a swizzled copy's rules sit outside `astryx-base` or above `astryx-theme` | +| FR8 | pull-request size report against base | a component whose styles change; a touch-only feature adding 0.9 KB and 1.1 KB gzip CSS to one component; a pull request with no styled change | a changed package stylesheet or component with no base-against-head gzip figure; a touch-only feature over 1 KB gzip CSS per component that is not flagged | The real-browser cascade check uses a component whose theme changes its base colors, such as a theme that restyles the destructive Button, and asserts the diff --git a/docs/specs/AST-110-swizzle-output/spec.md b/docs/specs/AST-110-swizzle-output/spec.md index 65be0b525e175..cd51e4f6e406b 100644 --- a/docs/specs/AST-110-swizzle-output/spec.md +++ b/docs/specs/AST-110-swizzle-output/spec.md @@ -6,11 +6,11 @@ id: spec:AST-110 authority: draft archive_reason: null superseded_by: null -approved_by: null -approved_at: null -phase: proposed +approved_by: cixzhang +approved_at: 2026-10-31 +phase: accepted owners: [cixzhang, josephfarina] -affects_architecture: [architecture:cli-surface] +affects_architecture: [architecture:cli-surface, architecture:layer-runtime] affects_families: [] affects_contributing: [] affects_consumer_docs: [cli] @@ -70,7 +70,12 @@ and the repository proves it before a change that could break it merges. import each other by relative path. A symbol the owner package exports publicly is imported from that public path. Nested folders the component imports are copied with it. The output holds only source the component runs: tests, - stories, spec records, and component docs are not copied. + stories, spec records, and component docs are not copied. When a helper the + copy needs already exists in the output from an earlier swizzle, an identical + file is reused; a different file is never overwritten, even with + `--overwrite`, which replaces only the component's own files: the new helper + is written alongside under a version-suffixed name, and only the new copy's + imports point at it. - **FR3 — Shared runtime state stays single.** A module whose state coordinates components with each other (the layer stack, the modal outlet, interaction modality, the bottom-sheet stack) keeps that state in one store per document, @@ -78,25 +83,28 @@ and the repository proves it before a change that could break it merges. the package component on the same protocol version use the same store, so stacking, Escape, focus return, and modality behave as they do with the package alone. A copy on another protocol version keeps a store of its own and never - reads a store of a different shape. A module whose state only caches work may - be duplicated by a copy. -- **FR4 — Private attributes stay contained.** Each private-attribute owner - module the component reaches (`architecture:public-component-api` INV23) is - copied whole and unedited. The copy sets private attributes only through those - owner modules; no copied file spells a private attribute name or selector by - hand. + reads a store of a different shape. A context that connects a provider to its + consumers is coordinating state too: its context object lives under the same + kind of versioned key, so a copied `useToast` reaches the package's + `LayerProvider`. A module whose state only caches work may be duplicated by a + copy. +- **FR4 — Private attributes stay contained.** A module that owns a private + data attribute, holding its name and the styles that set it, and that the + component imports is copied whole and unedited, so callers, the copy + included, still never write the attribute by hand. - **FR5 — Theme targets and class names are unchanged.** The copy answers to the same theme targets and emits the same documented class names, data attributes, and custom properties as the package component of that version, so a theme, - `xstyle`, and product CSS reach it the same way. The copy's styles sit in the - same cascade layer as the package component's, so a theme's component - overrides apply to the copy as they do to the package. + `xstyle`, and product CSS reach it the same way. The copy's component rules + sit in `astryx-base` like the package component's, below `astryx-theme`, so a + theme's component overrides apply to the copy as they do to the package + (`spec:AST-080` FR7). - **FR6 — Behavior matches the package.** Until the product edits it, the copy's keyboard handling, focus, roles, names, states, announcements, and events match the package component of the same version. -- **FR7 — Every offered component is proven before merge.** `swizzle --list` - offers only component folders. Before a change to a component package's source - or to the swizzle copy merges, every offered component is ejected into a clean +- **FR7 — Every offered component is proven before merge.** Before a change to + a component package's source or to the swizzle copy merges, every component + `swizzle --list` offers (`spec:AST-035` FR12) is ejected into a clean project, type-checked, bundled with a StyleX compiler, and rendered once on the server; a failure blocks the merge through a required check. Release checks run the same proof for every component and also render each copy in a real @@ -120,45 +128,43 @@ and the repository proves it before a change that could break it merges. - The CLI's swizzle copy follows the component's module graph (FR1–FR2, FR4), writes no test, story, spec, or doc file, and lists only component folders (FR7). The CLI owner implements and owns these changes. -- Core keeps the layer stack and modal outlet in versioned document stores - (FR3), as interaction modality and the bottom-sheet stack already do. - The FR7 proof joins the repository's existing required test lane (`spec:AST-030` FR11), selected by the shared change-scope classification and - reported in the single pull-request report: standard runners, about three - job-minutes on a pull request that changes component source or the swizzle - copy (about two to three percent of pull-request runner time), no added - wall-clock time to green, and the full proof at release time. + reported in the single pull-request report, with the full proof at release + time. +- `architecture:layer-runtime`: the layer stack, the modal outlet, and the + layer contexts (`LayerProvider` and its consumers, such as `useToast`) keep + their state and context objects under versioned keys (FR3, DEC-2). +- `spec:AST-080` FR7 places a swizzled copy's component rules in `astryx-base` + (FR5). - The `swizzle` command doc states FR1 and FR6 for product teams. ### Known gaps -- Built from the published 0.7.5, of the 112 names `swizzle --list` offers, 12 - build and render, 89 fail to build, 9 build but render wrong, and 2 build with - nothing to render. - The copy sends `../X/file` imports to the public `X` entry, which lacks the - internal helpers (87 components), and deep StyleX modules are missing from - Core's exports (12). + internal helpers, and deep StyleX modules are missing from Core's exports + (FR1, FR2). - Copied styles land in the app's cascade layer above the theme's, so theme - component overrides stop applying (9 components; 38 once imports resolve). -- Copies include spec records and test files (104 components), leave out nested - folders (Markdown, Table), and `swizzle --list` offers 6 folders that are not - components: `__tests__`, `gestures`, `i18n`, `internal`, `motion`, and - `NavItem`. -- The layer stack and modal outlet keep module state, so a copied Dialog or - BottomSheet starts a second stack (FR3). -- No private-attribute owner module exists yet (FR4). + component overrides stop applying (FR5). +- Copies include spec records and test files and leave out nested folders + (FR2), and `swizzle --list` offers folders that are not components + (`spec:AST-035` FR12). +- The layer stack, the modal outlet, and the layer contexts keep module state, + so a copied Dialog, BottomSheet, or `useToast` starts its own stack or misses + the package's provider (FR3). +- No private data-attribute owner module exists yet (FR4). ## Verification -| Contract | Verification | Representative states | Mutation or failure expectation | -| -------- | ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | -| FR1 | Eject every offered component into a clean project; type-check and bundle | Every component in `swizzle --list`; Core, Lab, Charts, RichText, Vega | A type or resolve error, or an import of a path the owner's `exports` lacks or of `./internal/*`, fails | -| FR2 | Import graph of each output | Helpers in `utils`, `hooks`, `Layer`; nested folders; colocated tests and docs | A copied file imports a module that is neither in the output nor a public export, or a test, story, spec, or doc is copied | -| FR3 | Render a copied and a package layer component together; inspect stores; open, stack, and Escape | Dialog over package Popover; BottomSheet over package Dialog; modality | Two stores of one protocol version exist in a document, or Escape and focus return differ from the package alone | -| FR4 | The private-attribute lint over each output | Components that reach an owner module | A hand-written private attribute name or selector, or an edited owner module, in the output | -| FR5 | Render package and copy side by side under a theme with component overrides; compare | Default; each variant; a theme component override | A theme override, documented class name, data attribute, or theme target reaches one and not the other | -| FR6 | The component's own interaction and accessibility checks run against the copy | Keyboard, pointer, screen-reader tree | A check that passes on the package fails on the copy | -| FR7 | The required test lane's swizzle proof and the release-time proof | A change to component source; a change to the swizzle copy; an unrelated change | An offered component missing from the proof, a failure that does not block, or a non-component folder in `swizzle --list` | +| Contract | Verification | Representative states | Mutation or failure expectation | +| -------- | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| FR1 | Eject every offered component into a clean project; type-check and bundle | Every component in `swizzle --list`; Core, Lab, Charts, RichText, Vega | A type or resolve error, or an import of a path the owner's `exports` lacks or of `./internal/*`, fails | +| FR2 | Import graph of each output | Helpers in `utils`, `hooks`, `Layer`; nested folders; colocated tests and docs; a helper an earlier swizzle wrote, identical and different | A copied file imports a module that is neither in the output nor a public export, or a test, story, spec, or doc is copied, or an existing helper is overwritten | +| FR3 | Render a copied and a package layer component together; inspect stores; open, stack, and Escape | Dialog over package Popover; BottomSheet over package Dialog; modality; a copied `useToast` under the package `LayerProvider` | Two stores of one protocol version exist in a document, or Escape and focus return differ from the package alone, or a copied consumer misses the package provider | +| FR4 | Scan each output for private data-attribute names and selectors; compare copied owner modules with the package's | Components that reach an owner module | A hand-written private attribute name or selector, or an edited owner module, in the output | +| FR5 | Render package and copy side by side under a theme with component overrides; compare | Default; each variant; a theme component override | A theme override, documented class name, data attribute, or theme target reaches one and not the other | +| FR6 | The component's own interaction and accessibility checks run against the copy | Keyboard, pointer, screen-reader tree | A check that passes on the package fails on the copy | +| FR7 | The required test lane's swizzle proof and the release-time proof | A change to component source; a change to the swizzle copy; an unrelated change | An offered component missing from the proof, or a failure that does not block | Known verification gap: none of these checks exist on `main`. @@ -180,7 +186,7 @@ package path is public (`architecture:public-component-api` INV12). ### DEC-2 — Shared runtime state lives in versioned document stores **Reference:** `spec:AST-110/DEC-2` -**Decider:** pending (`cixzhang`) +**Decider:** `cixzhang`, `2026-10-31` A copy is a second bundle of the same modules. Core already keeps interaction modality and the bottom-sheet stack in a per-document store under a registered, From 8a3a72adbd9fe86befdff23d5884b95343869026 Mon Sep 17 00:00:00 2001 From: Cindy Zhang Date: Sat, 31 Oct 2026 13:00:08 -0700 Subject: [PATCH 4/8] spec(AST-110): npm dependencies, identity symbols, and the contained-instance rejection --- docs/specs/AST-110-swizzle-output/spec.md | 43 +++++++++++++++-------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/docs/specs/AST-110-swizzle-output/spec.md b/docs/specs/AST-110-swizzle-output/spec.md index cd51e4f6e406b..0c275c29fc4df 100644 --- a/docs/specs/AST-110-swizzle-output/spec.md +++ b/docs/specs/AST-110-swizzle-output/spec.md @@ -63,7 +63,9 @@ and the repository proves it before a change that could break it merges. Core version and a StyleX compiler (`spec:AST-080`), the output of `astryx swizzle ` type-checks and bundles with no edits. It imports from its owner package only paths that package's `exports` lists, and never a - `./internal/*` path. + `./internal/*` path. The npm packages the copied files import are the owner + package's own dependencies; swizzle reports each one with the version range the + owner package declares, and the copy builds once the product installs them. - **FR2 — The helpers it needs come with it.** Every module the component reaches that is not reachable through a public export is copied into the output, keeping its folder layout relative to the component, and copied files @@ -86,8 +88,10 @@ and the repository proves it before a change that could break it merges. reads a store of a different shape. A context that connects a provider to its consumers is coordinating state too: its context object lives under the same kind of versioned key, so a copied `useToast` reaches the package's - `LayerProvider`. A module whose state only caches work may be duplicated by a - copy. + `LayerProvider`. A symbol that identifies objects across modules, such as a + brand or a context key, is registered under the same kind of key, so a copy + and the package agree on it. A module whose state only caches work may be + duplicated by a copy. - **FR4 — Private attributes stay contained.** A module that owns a private data attribute, holding its name and the styles that set it, and that the component imports is copied whole and unedited, so callers, the copy @@ -152,19 +156,27 @@ and the repository proves it before a change that could break it merges. - The layer stack, the modal outlet, and the layer contexts keep module state, so a copied Dialog, BottomSheet, or `useToast` starts its own stack or misses the package's provider (FR3). +- Core's private type additions, such as `focusVisible`, do not travel with a + copy, and a unique symbol splits identity between a copied and a package + AppShell (FR1, FR3). +- Copies of components that import npm packages, such as d3 and vega-util, do + not build until the product installs them, and swizzle does not name them + (FR1). +- Some names `swizzle --list` offers differ from their folder's export names + (Chat, NavMenu, Indicator, Resizable) (`spec:AST-035` FR12). - No private data-attribute owner module exists yet (FR4). ## Verification -| Contract | Verification | Representative states | Mutation or failure expectation | -| -------- | ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| FR1 | Eject every offered component into a clean project; type-check and bundle | Every component in `swizzle --list`; Core, Lab, Charts, RichText, Vega | A type or resolve error, or an import of a path the owner's `exports` lacks or of `./internal/*`, fails | -| FR2 | Import graph of each output | Helpers in `utils`, `hooks`, `Layer`; nested folders; colocated tests and docs; a helper an earlier swizzle wrote, identical and different | A copied file imports a module that is neither in the output nor a public export, or a test, story, spec, or doc is copied, or an existing helper is overwritten | -| FR3 | Render a copied and a package layer component together; inspect stores; open, stack, and Escape | Dialog over package Popover; BottomSheet over package Dialog; modality; a copied `useToast` under the package `LayerProvider` | Two stores of one protocol version exist in a document, or Escape and focus return differ from the package alone, or a copied consumer misses the package provider | -| FR4 | Scan each output for private data-attribute names and selectors; compare copied owner modules with the package's | Components that reach an owner module | A hand-written private attribute name or selector, or an edited owner module, in the output | -| FR5 | Render package and copy side by side under a theme with component overrides; compare | Default; each variant; a theme component override | A theme override, documented class name, data attribute, or theme target reaches one and not the other | -| FR6 | The component's own interaction and accessibility checks run against the copy | Keyboard, pointer, screen-reader tree | A check that passes on the package fails on the copy | -| FR7 | The required test lane's swizzle proof and the release-time proof | A change to component source; a change to the swizzle copy; an unrelated change | An offered component missing from the proof, or a failure that does not block | +| Contract | Verification | Representative states | Mutation or failure expectation | +| -------- | ---------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| FR1 | Eject every offered component into a clean project; type-check and bundle | Every component in `swizzle --list`; Core, Lab, Charts, RichText, Vega; a component that imports an npm dependency | A type or resolve error, or an import of a path the owner's `exports` lacks or of `./internal/*`, fails, or a dependency the copy imports goes unreported | +| FR2 | Import graph of each output | Helpers in `utils`, `hooks`, `Layer`; nested folders; colocated tests and docs; a helper an earlier swizzle wrote, identical and different | A copied file imports a module that is neither in the output nor a public export, or a test, story, spec, or doc is copied, or an existing helper is overwritten | +| FR3 | Render a copied and a package layer component together; inspect stores; open, stack, and Escape | Dialog over package Popover; BottomSheet over package Dialog; modality; a copied `useToast` under the package `LayerProvider`; a symbol-branded object passed between copy and package | Two stores of one protocol version exist in a document, or Escape and focus return differ from the package alone, or a copied consumer misses the package provider, or a brand check fails across them | +| FR4 | Scan each output for private data-attribute names and selectors; compare copied owner modules with the package's | Components that reach an owner module | A hand-written private attribute name or selector, or an edited owner module, in the output | +| FR5 | Render package and copy side by side under a theme with component overrides; compare | Default; each variant; a theme component override | A theme override, documented class name, data attribute, or theme target reaches one and not the other | +| FR6 | The component's own interaction and accessibility checks run against the copy | Keyboard, pointer, screen-reader tree | A check that passes on the package fails on the copy | +| FR7 | The required test lane's swizzle proof and the release-time proof | A change to component source; a change to the swizzle copy; an unrelated change | An offered component missing from the proof, or a failure that does not block | Known verification gap: none of these checks exist on `main`. @@ -194,9 +206,10 @@ versioned key so that duplicate bundles share one state; FR3 applies that rule to every coordinating module, so copying a module never splits a stack and no new public entry is needed. -Rejected: a public runtime entry that copies import instead of copying, because -it makes the coordinators public API with a compatibility promise only copies -need. +Rejected: never copying stateful modules and importing a contained package +instance instead, because it needs a public runtime entry with a compatibility +promise only copies need, and leaves a copy on another version without the +state it needs. ## Open questions From 8294fdbe15470ce3bb0b5bad85e4993ad8d459e6 Mon Sep 17 00:00:00 2001 From: Cindy Zhang Date: Sat, 31 Oct 2026 13:38:25 -0700 Subject: [PATCH 5/8] spec(AST-110): name the base-layer mechanism; AST-080 and cli-surface changes wait for current; response and proof scope --- docs/architecture/cli-surface.md | 2 - .../specs/AST-080-package-stylesheets/spec.md | 26 ++++---- docs/specs/AST-110-swizzle-output/spec.md | 66 ++++++++++++------- 3 files changed, 54 insertions(+), 40 deletions(-) diff --git a/docs/architecture/cli-surface.md b/docs/architecture/cli-surface.md index 0942c34ade5da..0cbb8abaa7822 100644 --- a/docs/architecture/cli-surface.md +++ b/docs/architecture/cli-surface.md @@ -665,8 +665,6 @@ non-interactive guarantee. verification. - `foundation/discovery/theme-discovery.mjs` and `api/theme` — typed integration theme descriptor discovery, package-aware selection, source copy, and build. -- `api/swizzle` — the component source copy; what a copy contains and promises - is `spec:AST-110`'s. - `foundation/agent-docs` — the shared expected-block renderer and managed-file writer used by init and upgrade, and the layout that decides which files hold the block (INV31). diff --git a/docs/specs/AST-080-package-stylesheets/spec.md b/docs/specs/AST-080-package-stylesheets/spec.md index a07041e532f16..0f0e5d7c1a8e6 100644 --- a/docs/specs/AST-080-package-stylesheets/spec.md +++ b/docs/specs/AST-080-package-stylesheets/spec.md @@ -41,8 +41,7 @@ first. - Which StyleX variable groups a package stylesheet contains, owned by `spec:AST-066` FR3. - How a product compiles its own code, including any StyleX settings it uses for - its own styles or for components it ejects with `astryx swizzle`; the layer a - swizzled copy's rules land in is FR7's. + its own styles or for components it ejects with `astryx swizzle`. - How `@astryxdesign/build` plugs into each bundler, and how a source build chooses which modules to compile. - Minifying or compressing stylesheets. @@ -134,10 +133,7 @@ first. reproduces its prebuilt stylesheet's rules; compiling a subset produces the rules those modules use. A source build of Core always includes the Core definitions other packages' prebuilt stylesheets rely on under FR2, such as - variable groups and keyframes. Product rules stay outside `astryx-base`, except - a swizzled copy's component rules: they sit inside `astryx-base` like the - package component's, below `astryx-theme`, so a theme's component overrides - apply to the copy as they do to the package component (`spec:AST-110` FR5). A + variable groups and keyframes. Product rules stay outside `astryx-base`. A source build needs no data beyond the package's own source, and holds in development builds as in production builds. @@ -203,15 +199,15 @@ first. ## Verification -| Contract | Verification | Representative states | Mutation or failure expectation | -| -------- | ---------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| FR1 | package manifest and stylesheet build tests | every published package with styled modules; a module's rules in its package stylesheet | a styled published package has no `.css` export; a rule a module uses is missing | -| FR2 | real-browser computed styles with no StyleX compiler | a representative component from each package; default, hover, focus, disabled, dark, and right-to-left | removing the package stylesheet changes a computed style; a non-Core stylesheet repeats a group `astryx.css` declares | -| FR3 | stylesheet structure tests and real-browser cascade | each package stylesheet and `reset.css`; theme first and last; package stylesheets in either order; `reset.css` last; a theme override | an unlayered rule or another layer; a missing or reordered registration; the theme override loses in any load order | -| FR4 | packed-package and bundler fixtures | each package's stylesheet import in a production bundle | the subpath does not resolve from the packed package; the bundle drops the stylesheet | -| FR5–FR6 | consumer-doc checks | every setup snippet in the styling overview, getting started, and migration setup; each package README | a default setup omits `astryx.css` or a used package stylesheet; a setup loads a prebuilt stylesheet and compiles the same package's source; a package import is missing | -| FR7 | build-package compile tests and real-browser cascade | every Core module compiled through the build package; a subset of modules; a development build; Core source-built beside a prebuilt Lab stylesheet; a source-built component under a theme override; a swizzled copy under a theme component override | the every-module compile differs from the prebuilt rules or order; an Astryx rule sits in a nested or other layer; the theme override loses; a swizzled copy's rules sit outside `astryx-base` or above `astryx-theme` | -| FR8 | pull-request size report against base | a component whose styles change; a touch-only feature adding 0.9 KB and 1.1 KB gzip CSS to one component; a pull request with no styled change | a changed package stylesheet or component with no base-against-head gzip figure; a touch-only feature over 1 KB gzip CSS per component that is not flagged | +| Contract | Verification | Representative states | Mutation or failure expectation | +| -------- | ---------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| FR1 | package manifest and stylesheet build tests | every published package with styled modules; a module's rules in its package stylesheet | a styled published package has no `.css` export; a rule a module uses is missing | +| FR2 | real-browser computed styles with no StyleX compiler | a representative component from each package; default, hover, focus, disabled, dark, and right-to-left | removing the package stylesheet changes a computed style; a non-Core stylesheet repeats a group `astryx.css` declares | +| FR3 | stylesheet structure tests and real-browser cascade | each package stylesheet and `reset.css`; theme first and last; package stylesheets in either order; `reset.css` last; a theme override | an unlayered rule or another layer; a missing or reordered registration; the theme override loses in any load order | +| FR4 | packed-package and bundler fixtures | each package's stylesheet import in a production bundle | the subpath does not resolve from the packed package; the bundle drops the stylesheet | +| FR5–FR6 | consumer-doc checks | every setup snippet in the styling overview, getting started, and migration setup; each package README | a default setup omits `astryx.css` or a used package stylesheet; a setup loads a prebuilt stylesheet and compiles the same package's source; a package import is missing | +| FR7 | build-package compile tests and real-browser cascade | every Core module compiled through the build package; a subset of modules; a development build; Core source-built beside a prebuilt Lab stylesheet; a source-built component under a theme override | the every-module compile differs from the prebuilt rules or order; an Astryx rule sits in a nested or other layer; the theme override loses | +| FR8 | pull-request size report against base | a component whose styles change; a touch-only feature adding 0.9 KB and 1.1 KB gzip CSS to one component; a pull request with no styled change | a changed package stylesheet or component with no base-against-head gzip figure; a touch-only feature over 1 KB gzip CSS per component that is not flagged | The real-browser cascade check uses a component whose theme changes its base colors, such as a theme that restyles the destructive Button, and asserts the diff --git a/docs/specs/AST-110-swizzle-output/spec.md b/docs/specs/AST-110-swizzle-output/spec.md index 0c275c29fc4df..192206ac51591 100644 --- a/docs/specs/AST-110-swizzle-output/spec.md +++ b/docs/specs/AST-110-swizzle-output/spec.md @@ -13,7 +13,7 @@ owners: [cixzhang, josephfarina] affects_architecture: [architecture:cli-surface, architecture:layer-runtime] affects_families: [] affects_contributing: [] -affects_consumer_docs: [cli] +affects_consumer_docs: [cli, styling-overview] --- # Swizzle output system spec @@ -44,7 +44,7 @@ and the repository proves it before a change that could break it merges. - Which component a name copies and what `swizzle --list` reports: `spec:AST-035` FR11–FR12 and `architecture:cli-surface` INV28. - Where the command runs: `spec:AST-081` FR3. -- How a product compiles StyleX for its own code or a copy: `spec:AST-080`. +- How a product compiles StyleX for its own code: `spec:AST-080`. - Keeping a copy the product has edited in step with later Core releases. - Making Core's internal helpers public API (`spec:AST-017` FR21, `architecture:public-component-api` INV12). @@ -92,28 +92,37 @@ and the repository proves it before a change that could break it merges. brand or a context key, is registered under the same kind of key, so a copy and the package agree on it. A module whose state only caches work may be duplicated by a copy. -- **FR4 — Private attributes stay contained.** A module that owns a private - data attribute, holding its name and the styles that set it, and that the - component imports is copied whole and unedited, so callers, the copy - included, still never write the attribute by hand. +- **FR4 — Private attributes stay contained.** Each private data attribute's + owner module (`architecture:public-component-api` INV23) that the component + imports is copied whole and unedited, so callers, the copy included, still + never write the attribute by hand. - **FR5 — Theme targets and class names are unchanged.** The copy answers to the same theme targets and emits the same documented class names, data attributes, and custom properties as the package component of that version, so a theme, - `xstyle`, and product CSS reach it the same way. The copy's component rules - sit in `astryx-base` like the package component's, below `astryx-theme`, so a - theme's component overrides apply to the copy as they do to the package - (`spec:AST-080` FR7). + `xstyle`, and product CSS reach it the same way. Swizzle marks its output + directory as Astryx source with an `astryx-swizzle.json` file that names the + owner package and its version. `@astryxdesign/build` compiles every module + under a marked directory in its library pass, with the library class prefix, + into `astryx-base` below `astryx-theme`, so a theme's component overrides + apply to the copy as they do to the package component. A product that + compiles StyleX without the build package adds the output directory to its + library pass, as the styling overview states. The marker is a protocol + between the swizzle copy and the build package; a path rule alone cannot + tell a copy from the product's own code. - **FR6 — Behavior matches the package.** Until the product edits it, the copy's keyboard handling, focus, roles, names, states, announcements, and events match the package component of the same version. - **FR7 — Every offered component is proven before merge.** Before a change to a component package's source or to the swizzle copy merges, every component - `swizzle --list` offers (`spec:AST-035` FR12) is ejected into a clean + swizzle can copy is ejected into a clean project, type-checked, bundled with a StyleX compiler, and rendered once on the server; a failure blocks the merge through a required check. Release checks run the same proof for every component and also render each copy in a real browser. The proof adds no required context and no wall-clock time to a pull request's slowest required check. + Every component swizzle can copy means Core's components as `swizzle --list` + offers them (`spec:AST-035` FR12) and every component of each integration + package in the repository, copied with `--package`. ### Platform support @@ -128,7 +137,6 @@ and the repository proves it before a change that could break it merges. ## Current-state impact -- `architecture:cli-surface` points to this record for what `swizzle` writes. - The CLI's swizzle copy follows the component's module graph (FR1–FR2, FR4), writes no test, story, spec, or doc file, and lists only component folders (FR7). The CLI owner implements and owns these changes. @@ -139,8 +147,16 @@ and the repository proves it before a change that could break it merges. - `architecture:layer-runtime`: the layer stack, the modal outlet, and the layer contexts (`LayerProvider` and its consumers, such as `useToast`) keep their state and context objects under versioned keys (FR3, DEC-2). -- `spec:AST-080` FR7 places a swizzled copy's component rules in `astryx-base` - (FR5). +- The `swizzle.copy` result lists each helper with whether it was written, + reused, or written under a version-suffixed name (FR2), and each npm + dependency the copy imports with the owner package's declared range (FR1). + An existing component file still fails with `ERR_FILE_EXISTS` unless + `--overwrite` is passed; an existing helper never fails the command and is + never replaced, with or without `--overwrite` (FR2). +- `@astryxdesign/build`'s Vite, PostCSS, Babel, and Next integrations compile + modules under an `astryx-swizzle.json` directory in the library pass (FR5). +- The styling overview states where a copy's rules land and the one setting a + product without the build package needs (FR5). - The `swizzle` command doc states FR1 and FR6 for product teams. ### Known gaps @@ -165,18 +181,22 @@ and the repository proves it before a change that could break it merges. - Some names `swizzle --list` offers differ from their folder's export names (Chat, NavMenu, Indicator, Resizable) (`spec:AST-035` FR12). - No private data-attribute owner module exists yet (FR4). +- `spec:AST-080` FR7 keeps every product rule, a copy's included, outside + `astryx-base`; its exception for a swizzled copy's rules (FR5) and the + `architecture:cli-surface` pointer to this record land when this record + becomes current. ## Verification -| Contract | Verification | Representative states | Mutation or failure expectation | -| -------- | ---------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| FR1 | Eject every offered component into a clean project; type-check and bundle | Every component in `swizzle --list`; Core, Lab, Charts, RichText, Vega; a component that imports an npm dependency | A type or resolve error, or an import of a path the owner's `exports` lacks or of `./internal/*`, fails, or a dependency the copy imports goes unreported | -| FR2 | Import graph of each output | Helpers in `utils`, `hooks`, `Layer`; nested folders; colocated tests and docs; a helper an earlier swizzle wrote, identical and different | A copied file imports a module that is neither in the output nor a public export, or a test, story, spec, or doc is copied, or an existing helper is overwritten | -| FR3 | Render a copied and a package layer component together; inspect stores; open, stack, and Escape | Dialog over package Popover; BottomSheet over package Dialog; modality; a copied `useToast` under the package `LayerProvider`; a symbol-branded object passed between copy and package | Two stores of one protocol version exist in a document, or Escape and focus return differ from the package alone, or a copied consumer misses the package provider, or a brand check fails across them | -| FR4 | Scan each output for private data-attribute names and selectors; compare copied owner modules with the package's | Components that reach an owner module | A hand-written private attribute name or selector, or an edited owner module, in the output | -| FR5 | Render package and copy side by side under a theme with component overrides; compare | Default; each variant; a theme component override | A theme override, documented class name, data attribute, or theme target reaches one and not the other | -| FR6 | The component's own interaction and accessibility checks run against the copy | Keyboard, pointer, screen-reader tree | A check that passes on the package fails on the copy | -| FR7 | The required test lane's swizzle proof and the release-time proof | A change to component source; a change to the swizzle copy; an unrelated change | An offered component missing from the proof, or a failure that does not block | +| Contract | Verification | Representative states | Mutation or failure expectation | +| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| FR1 | Eject every offered component into a clean project; type-check and bundle | Every component in `swizzle --list`; Core, Lab, Charts, RichText, Vega; a component that imports an npm dependency | A type or resolve error, or an import of a path the owner's `exports` lacks or of `./internal/*`, fails, or a dependency the copy imports goes unreported | +| FR2 | Import graph of each output | Helpers in `utils`, `hooks`, `Layer`; nested folders; colocated tests and docs; a helper an earlier swizzle wrote, identical and different | A copied file imports a module that is neither in the output nor a public export, or a test, story, spec, or doc is copied, or an existing helper is overwritten | +| FR3 | Render a copied and a package layer component together; inspect stores; open, stack, and Escape | Dialog over package Popover; BottomSheet over package Dialog; modality; a copied `useToast` under the package `LayerProvider`; a symbol-branded object passed between copy and package | Two stores of one protocol version exist in a document, or Escape and focus return differ from the package alone, or a copied consumer misses the package provider, or a brand check fails across them | +| FR4 | Scan each output for private data-attribute names and selectors; compare copied owner modules with the package's | Components that reach an owner module | A hand-written private attribute name or selector, or an edited owner module, in the output | +| FR5 | Render package and copy side by side under a theme with component overrides; compare | Default; each variant; a theme component override; a copy built by the build package and by a plain StyleX setup | A theme override, documented class name, data attribute, or theme target reaches one and not the other | +| FR6 | The component's own interaction and accessibility checks run against the copy | Keyboard, pointer, screen-reader tree | A check that passes on the package fails on the copy | +| FR7 | The required test lane's swizzle proof and the release-time proof; both cover Core's list and every integration package's components through `--package` | A change to component source; a change to the swizzle copy; an unrelated change; Core, Lab, Charts, RichText, Vega | An offered component missing from the proof, a failure that does not block, a new required context, or the proof pushing its job past the pull-request time budget | Known verification gap: none of these checks exist on `main`. From 285f61ca70d93c776303c3824066678dd5aace55 Mon Sep 17 00:00:00 2001 From: Cindy Zhang Date: Sat, 31 Oct 2026 14:04:21 -0700 Subject: [PATCH 6/8] spec(AST-110): cascade placement is the build package's; stock StyleX is a known gap --- docs/specs/AST-110-swizzle-output/spec.md | 57 ++++++++++++----------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/docs/specs/AST-110-swizzle-output/spec.md b/docs/specs/AST-110-swizzle-output/spec.md index 192206ac51591..73d2d93b7f606 100644 --- a/docs/specs/AST-110-swizzle-output/spec.md +++ b/docs/specs/AST-110-swizzle-output/spec.md @@ -44,7 +44,8 @@ and the repository proves it before a change that could break it merges. - Which component a name copies and what `swizzle --list` reports: `spec:AST-035` FR11–FR12 and `architecture:cli-surface` INV28. - Where the command runs: `spec:AST-081` FR3. -- How a product compiles StyleX for its own code: `spec:AST-080`. +- How a product configures StyleX for its own code: the build package's + options and the product's StyleX configuration own that. - Keeping a copy the product has edited in step with later Core releases. - Making Core's internal helpers public API (`spec:AST-017` FR21, `architecture:public-component-api` INV12). @@ -60,7 +61,7 @@ and the repository proves it before a change that could break it merges. ## Requirements - **FR1 — An ejected component builds on its own.** In a project with the same - Core version and a StyleX compiler (`spec:AST-080`), the output of + Core version and a StyleX compiler, the output of `astryx swizzle ` type-checks and bundles with no edits. It imports from its owner package only paths that package's `exports` lists, and never a `./internal/*` path. The npm packages the copied files import are the owner @@ -104,9 +105,8 @@ and the repository proves it before a change that could break it merges. owner package and its version. `@astryxdesign/build` compiles every module under a marked directory in its library pass, with the library class prefix, into `astryx-base` below `astryx-theme`, so a theme's component overrides - apply to the copy as they do to the package component. A product that - compiles StyleX without the build package adds the output directory to its - library pass, as the styling overview states. The marker is a protocol + apply to the copy as they do to the package component. This placement holds + for products that build with `@astryxdesign/build`. The marker is a protocol between the swizzle copy and the build package; a path rule alone cannot tell a copy from the product's own code. - **FR6 — Behavior matches the package.** Until the product edits it, the copy's @@ -127,19 +127,20 @@ and the repository proves it before a change that could break it merges. ### Platform support - Supported feature/engine floor: the product uses the same Core version the - copy came from and a StyleX compiler that `spec:AST-080` supports; browser - support is Core's. + copy came from and a StyleX compiler; FR5's cascade placement needs + `@astryxdesign/build`. Browser support is Core's. - Unsupported behavior: a project without a StyleX compiler fails at import, as - the command's output states. A copy kept across a Core upgrade is outside FR1 - and FR6. + the command's output states. On the stock StyleX plugins a copy builds and + behaves as FR1 and FR6 state, but its rules land in the product's layer, above + `astryx-theme`. A copy kept across a Core upgrade is outside FR1 and FR6. - Browser evidence: a real-browser render of every copy at release time (FR7); server rendering is the pull-request smoke evidence. ## Current-state impact - The CLI's swizzle copy follows the component's module graph (FR1–FR2, FR4), - writes no test, story, spec, or doc file, and lists only component folders - (FR7). The CLI owner implements and owns these changes. + and writes no test, story, spec, or doc file. The CLI owner implements and + owns these changes. - The FR7 proof joins the repository's existing required test lane (`spec:AST-030` FR11), selected by the shared change-scope classification and reported in the single pull-request report, with the full proof at release @@ -155,8 +156,9 @@ and the repository proves it before a change that could break it merges. never replaced, with or without `--overwrite` (FR2). - `@astryxdesign/build`'s Vite, PostCSS, Babel, and Next integrations compile modules under an `astryx-swizzle.json` directory in the library pass (FR5). -- The styling overview states where a copy's rules land and the one setting a - product without the build package needs (FR5). +- The styling overview states that a copy's rules land in `astryx-base` with + `@astryxdesign/build`, and above `astryx-theme` with the stock StyleX plugins + (FR5). - The `swizzle` command doc states FR1 and FR6 for product teams. ### Known gaps @@ -181,22 +183,25 @@ and the repository proves it before a change that could break it merges. - Some names `swizzle --list` offers differ from their folder's export names (Chat, NavMenu, Indicator, Resizable) (`spec:AST-035` FR12). - No private data-attribute owner module exists yet (FR4). -- `spec:AST-080` FR7 keeps every product rule, a copy's included, outside - `astryx-base`; its exception for a swizzled copy's rules (FR5) and the - `architecture:cli-surface` pointer to this record land when this record - becomes current. +- Products on the stock StyleX plugins have no library-pass setting, so a + copy's rules land above `astryx-theme` there and theme component overrides do + not reach it (FR5). +- `spec:AST-080` FR5 presents the stock StyleX path as the default for ejected + components, and its FR7 keeps every product rule, a copy's included, outside + `astryx-base`. Their swizzle cases (FR5) and the `architecture:cli-surface` + pointer to this record land when this record becomes current. ## Verification -| Contract | Verification | Representative states | Mutation or failure expectation | -| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| FR1 | Eject every offered component into a clean project; type-check and bundle | Every component in `swizzle --list`; Core, Lab, Charts, RichText, Vega; a component that imports an npm dependency | A type or resolve error, or an import of a path the owner's `exports` lacks or of `./internal/*`, fails, or a dependency the copy imports goes unreported | -| FR2 | Import graph of each output | Helpers in `utils`, `hooks`, `Layer`; nested folders; colocated tests and docs; a helper an earlier swizzle wrote, identical and different | A copied file imports a module that is neither in the output nor a public export, or a test, story, spec, or doc is copied, or an existing helper is overwritten | -| FR3 | Render a copied and a package layer component together; inspect stores; open, stack, and Escape | Dialog over package Popover; BottomSheet over package Dialog; modality; a copied `useToast` under the package `LayerProvider`; a symbol-branded object passed between copy and package | Two stores of one protocol version exist in a document, or Escape and focus return differ from the package alone, or a copied consumer misses the package provider, or a brand check fails across them | -| FR4 | Scan each output for private data-attribute names and selectors; compare copied owner modules with the package's | Components that reach an owner module | A hand-written private attribute name or selector, or an edited owner module, in the output | -| FR5 | Render package and copy side by side under a theme with component overrides; compare | Default; each variant; a theme component override; a copy built by the build package and by a plain StyleX setup | A theme override, documented class name, data attribute, or theme target reaches one and not the other | -| FR6 | The component's own interaction and accessibility checks run against the copy | Keyboard, pointer, screen-reader tree | A check that passes on the package fails on the copy | -| FR7 | The required test lane's swizzle proof and the release-time proof; both cover Core's list and every integration package's components through `--package` | A change to component source; a change to the swizzle copy; an unrelated change; Core, Lab, Charts, RichText, Vega | An offered component missing from the proof, a failure that does not block, a new required context, or the proof pushing its job past the pull-request time budget | +| Contract | Verification | Representative states | Mutation or failure expectation | +| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| FR1 | Eject every offered component into a clean project; type-check and bundle | Every component in `swizzle --list`; Core, Lab, Charts, RichText, Vega; a component that imports an npm dependency | A type or resolve error, or an import of a path the owner's `exports` lacks or of `./internal/*`, fails, or a dependency the copy imports goes unreported | +| FR2 | Import graph of each output | Helpers in `utils`, `hooks`, `Layer`; nested folders; colocated tests and docs; a helper an earlier swizzle wrote, identical and different | A copied file imports a module that is neither in the output nor a public export, or a test, story, spec, or doc is copied, or an existing helper is overwritten | +| FR3 | Render a copied and a package layer component together; inspect stores; open, stack, and Escape | Dialog over package Popover; BottomSheet over package Dialog; modality; a copied `useToast` under the package `LayerProvider`; a symbol-branded object passed between copy and package | Two stores of one protocol version exist in a document, or Escape and focus return differ from the package alone, or a copied consumer misses the package provider, or a brand check fails across them | +| FR4 | Scan each output for private data-attribute names and selectors; compare copied owner modules with the package's | Components that reach an owner module | A hand-written private attribute name or selector, or an edited owner module, in the output | +| FR5 | Render package and copy side by side under a theme with component overrides; compare | Default; each variant; a theme component override; a copy built by `@astryxdesign/build` | A theme override, documented class name, data attribute, or theme target reaches one and not the other | +| FR6 | The component's own interaction and accessibility checks run against the copy | Keyboard, pointer, screen-reader tree | A check that passes on the package fails on the copy | +| FR7 | The required test lane's swizzle proof and the release-time proof; both cover Core's list and every integration package's components through `--package` | A change to component source; a change to the swizzle copy; an unrelated change; Core, Lab, Charts, RichText, Vega | An offered component missing from the proof, a failure that does not block, a new required context, the proof adding wall-clock time to the slowest required check, or the proof pushing its job past the pull-request time budget | Known verification gap: none of these checks exist on `main`. From 70b633bdb645a66b2dd41c818e22e4bcb0b9960e Mon Sep 17 00:00:00 2001 From: Cindy Zhang Date: Sat, 31 Oct 2026 14:29:31 -0700 Subject: [PATCH 7/8] spec(AST-110): copies join the one announcement channel per document --- docs/specs/AST-110-swizzle-output/spec.md | 42 +++++++++++++---------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/docs/specs/AST-110-swizzle-output/spec.md b/docs/specs/AST-110-swizzle-output/spec.md index 73d2d93b7f606..8b5d9edce2ab0 100644 --- a/docs/specs/AST-110-swizzle-output/spec.md +++ b/docs/specs/AST-110-swizzle-output/spec.md @@ -81,10 +81,11 @@ and the repository proves it before a change that could break it merges. imports point at it. - **FR3 — Shared runtime state stays single.** A module whose state coordinates components with each other (the layer stack, the modal outlet, interaction - modality, the bottom-sheet stack) keeps that state in one store per document, - under a registered key that carries the store's protocol version. A copy and - the package component on the same protocol version use the same store, so - stacking, Escape, focus return, and modality behave as they do with the package + modality, the bottom-sheet stack, the announcement channel of `spec:AST-086` + FR9) keeps that state in one store per document, under a registered key that + carries the store's protocol version. A copy and the package component on the + same protocol version use the same store, so stacking, Escape, focus return, + modality, and announcements behave as they do with the package alone. A copy on another protocol version keeps a store of its own and never reads a store of a different shape. A context that connects a provider to its consumers is coordinating state too: its context object lives under the same @@ -148,6 +149,9 @@ and the repository proves it before a change that could break it merges. - `architecture:layer-runtime`: the layer stack, the modal outlet, and the layer contexts (`LayerProvider` and its consumers, such as `useToast`) keep their state and context objects under versioned keys (FR3, DEC-2). +- `useAnnounce` keeps the announcement channel of `spec:AST-086` FR9 under a + versioned key, so a copied Dialog joins the package's announcer instead of + mounting a second pair of live regions (FR3, DEC-2). - The `swizzle.copy` result lists each helper with whether it was written, reused, or written under a version-suffixed name (FR2), and each npm dependency the copy imports with the owner package's declared range (FR1). @@ -171,9 +175,10 @@ and the repository proves it before a change that could break it merges. - Copies include spec records and test files and leave out nested folders (FR2), and `swizzle --list` offers folders that are not components (`spec:AST-035` FR12). -- The layer stack, the modal outlet, and the layer contexts keep module state, - so a copied Dialog, BottomSheet, or `useToast` starts its own stack or misses - the package's provider (FR3). +- The layer stack, the modal outlet, the layer contexts, and `useAnnounce` + keep module state, so a copied Dialog, BottomSheet, or `useToast` starts its + own stack, misses the package's provider, or mounts a second pair of live + regions, breaking `spec:AST-086` FR9's one channel per document (FR3). - Core's private type additions, such as `focusVisible`, do not travel with a copy, and a unique symbol splits identity between a copied and a package AppShell (FR1, FR3). @@ -188,20 +193,21 @@ and the repository proves it before a change that could break it merges. not reach it (FR5). - `spec:AST-080` FR5 presents the stock StyleX path as the default for ejected components, and its FR7 keeps every product rule, a copy's included, outside - `astryx-base`. Their swizzle cases (FR5) and the `architecture:cli-surface` - pointer to this record land when this record becomes current. + `astryx-base`. The pull request that makes this record current amends both + with their swizzle case (FR5) and adds the `architecture:cli-surface` pointer + to this record. ## Verification -| Contract | Verification | Representative states | Mutation or failure expectation | -| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| FR1 | Eject every offered component into a clean project; type-check and bundle | Every component in `swizzle --list`; Core, Lab, Charts, RichText, Vega; a component that imports an npm dependency | A type or resolve error, or an import of a path the owner's `exports` lacks or of `./internal/*`, fails, or a dependency the copy imports goes unreported | -| FR2 | Import graph of each output | Helpers in `utils`, `hooks`, `Layer`; nested folders; colocated tests and docs; a helper an earlier swizzle wrote, identical and different | A copied file imports a module that is neither in the output nor a public export, or a test, story, spec, or doc is copied, or an existing helper is overwritten | -| FR3 | Render a copied and a package layer component together; inspect stores; open, stack, and Escape | Dialog over package Popover; BottomSheet over package Dialog; modality; a copied `useToast` under the package `LayerProvider`; a symbol-branded object passed between copy and package | Two stores of one protocol version exist in a document, or Escape and focus return differ from the package alone, or a copied consumer misses the package provider, or a brand check fails across them | -| FR4 | Scan each output for private data-attribute names and selectors; compare copied owner modules with the package's | Components that reach an owner module | A hand-written private attribute name or selector, or an edited owner module, in the output | -| FR5 | Render package and copy side by side under a theme with component overrides; compare | Default; each variant; a theme component override; a copy built by `@astryxdesign/build` | A theme override, documented class name, data attribute, or theme target reaches one and not the other | -| FR6 | The component's own interaction and accessibility checks run against the copy | Keyboard, pointer, screen-reader tree | A check that passes on the package fails on the copy | -| FR7 | The required test lane's swizzle proof and the release-time proof; both cover Core's list and every integration package's components through `--package` | A change to component source; a change to the swizzle copy; an unrelated change; Core, Lab, Charts, RichText, Vega | An offered component missing from the proof, a failure that does not block, a new required context, the proof adding wall-clock time to the slowest required check, or the proof pushing its job past the pull-request time budget | +| Contract | Verification | Representative states | Mutation or failure expectation | +| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| FR1 | Eject every offered component into a clean project; type-check and bundle | Every component in `swizzle --list`; Core, Lab, Charts, RichText, Vega; a component that imports an npm dependency | A type or resolve error, or an import of a path the owner's `exports` lacks or of `./internal/*`, fails, or a dependency the copy imports goes unreported | +| FR2 | Import graph of each output | Helpers in `utils`, `hooks`, `Layer`; nested folders; colocated tests and docs; a helper an earlier swizzle wrote, identical and different | A copied file imports a module that is neither in the output nor a public export, or a test, story, spec, or doc is copied, or an existing helper is overwritten | +| FR3 | Render a copied and a package layer component together; inspect stores; open, stack, and Escape | Dialog over package Popover; BottomSheet over package Dialog; modality; a copied `useToast` under the package `LayerProvider`; a symbol-branded object passed between copy and package; a copied Dialog announcing beside a package component | Two stores of one protocol version exist in a document, or Escape and focus return differ from the package alone, or a copied consumer misses the package provider, or a brand check fails across them, or a copy adds a second live region instead of joining the package announcer (`spec:AST-086` FR9) | +| FR4 | Scan each output for private data-attribute names and selectors; compare copied owner modules with the package's | Components that reach an owner module | A hand-written private attribute name or selector, or an edited owner module, in the output | +| FR5 | Render package and copy side by side under a theme with component overrides; compare | Default; each variant; a theme component override; a copy built by `@astryxdesign/build` | A theme override, documented class name, data attribute, or theme target reaches one and not the other | +| FR6 | The component's own interaction and accessibility checks run against the copy | Keyboard, pointer, screen-reader tree | A check that passes on the package fails on the copy | +| FR7 | The required test lane's swizzle proof and the release-time proof; both cover Core's list and every integration package's components through `--package` | A change to component source; a change to the swizzle copy; an unrelated change; Core, Lab, Charts, RichText, Vega | An offered component missing from the proof, a failure that does not block, a new required context, the proof adding wall-clock time to the slowest required check, or the proof pushing its job past the pull-request time budget | Known verification gap: none of these checks exist on `main`. From c984b8eb72582a57ec096b9a8f636aaf881df543 Mon Sep 17 00:00:00 2001 From: Cindy Zhang Date: Sat, 31 Oct 2026 14:50:12 -0700 Subject: [PATCH 8/8] spec(AST-110): shared state holds with package components inside a copy --- docs/specs/AST-110-swizzle-output/spec.md | 31 +++++++++++++++-------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/docs/specs/AST-110-swizzle-output/spec.md b/docs/specs/AST-110-swizzle-output/spec.md index 8b5d9edce2ab0..86b5cf077c3f0 100644 --- a/docs/specs/AST-110-swizzle-output/spec.md +++ b/docs/specs/AST-110-swizzle-output/spec.md @@ -92,8 +92,13 @@ and the repository proves it before a change that could break it merges. kind of versioned key, so a copied `useToast` reaches the package's `LayerProvider`. A symbol that identifies objects across modules, such as a brand or a context key, is registered under the same kind of key, so a copy - and the package agree on it. A module whose state only caches work may be - duplicated by a copy. + and the package agree on it. The rule holds in both directions: a copy inside + a package overlay, and package components inside a copied Dialog or other + overlay. A package component inside an open copied modal delivers its + announcements inside that modal (`spec:AST-086` FR14), and package group and + layout context stops at the copy's layer boundary as it does at a package one + (`spec:AST-038` FR7). A module whose state only caches work may be duplicated + by a copy. - **FR4 — Private attributes stay contained.** Each private data attribute's owner module (`architecture:public-component-api` INV23) that the component imports is copied whole and unedited, so callers, the copy included, still @@ -179,6 +184,10 @@ and the repository proves it before a change that could break it merges. keep module state, so a copied Dialog, BottomSheet, or `useToast` starts its own stack, misses the package's provider, or mounts a second pair of live regions, breaking `spec:AST-086` FR9's one channel per document (FR3). +- In the reverse direction, package components inside a copied Dialog or + overlay do not see its layer boundary or modal: a package picker announces + outside the copied modal (`spec:AST-086` FR14), and package group context + crosses into it (`spec:AST-038` FR7) (FR3). - Core's private type additions, such as `focusVisible`, do not travel with a copy, and a unique symbol splits identity between a copied and a package AppShell (FR1, FR3). @@ -199,15 +208,15 @@ and the repository proves it before a change that could break it merges. ## Verification -| Contract | Verification | Representative states | Mutation or failure expectation | -| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| FR1 | Eject every offered component into a clean project; type-check and bundle | Every component in `swizzle --list`; Core, Lab, Charts, RichText, Vega; a component that imports an npm dependency | A type or resolve error, or an import of a path the owner's `exports` lacks or of `./internal/*`, fails, or a dependency the copy imports goes unreported | -| FR2 | Import graph of each output | Helpers in `utils`, `hooks`, `Layer`; nested folders; colocated tests and docs; a helper an earlier swizzle wrote, identical and different | A copied file imports a module that is neither in the output nor a public export, or a test, story, spec, or doc is copied, or an existing helper is overwritten | -| FR3 | Render a copied and a package layer component together; inspect stores; open, stack, and Escape | Dialog over package Popover; BottomSheet over package Dialog; modality; a copied `useToast` under the package `LayerProvider`; a symbol-branded object passed between copy and package; a copied Dialog announcing beside a package component | Two stores of one protocol version exist in a document, or Escape and focus return differ from the package alone, or a copied consumer misses the package provider, or a brand check fails across them, or a copy adds a second live region instead of joining the package announcer (`spec:AST-086` FR9) | -| FR4 | Scan each output for private data-attribute names and selectors; compare copied owner modules with the package's | Components that reach an owner module | A hand-written private attribute name or selector, or an edited owner module, in the output | -| FR5 | Render package and copy side by side under a theme with component overrides; compare | Default; each variant; a theme component override; a copy built by `@astryxdesign/build` | A theme override, documented class name, data attribute, or theme target reaches one and not the other | -| FR6 | The component's own interaction and accessibility checks run against the copy | Keyboard, pointer, screen-reader tree | A check that passes on the package fails on the copy | -| FR7 | The required test lane's swizzle proof and the release-time proof; both cover Core's list and every integration package's components through `--package` | A change to component source; a change to the swizzle copy; an unrelated change; Core, Lab, Charts, RichText, Vega | An offered component missing from the proof, a failure that does not block, a new required context, the proof adding wall-clock time to the slowest required check, or the proof pushing its job past the pull-request time budget | +| Contract | Verification | Representative states | Mutation or failure expectation | +| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| FR1 | Eject every offered component into a clean project; type-check and bundle | Every component in `swizzle --list`; Core, Lab, Charts, RichText, Vega; a component that imports an npm dependency | A type or resolve error, or an import of a path the owner's `exports` lacks or of `./internal/*`, fails, or a dependency the copy imports goes unreported | +| FR2 | Import graph of each output | Helpers in `utils`, `hooks`, `Layer`; nested folders; colocated tests and docs; a helper an earlier swizzle wrote, identical and different | A copied file imports a module that is neither in the output nor a public export, or a test, story, spec, or doc is copied, or an existing helper is overwritten | +| FR3 | Render a copied and a package layer component together; inspect stores; open, stack, and Escape | Dialog over package Popover; BottomSheet over package Dialog; modality; a copied `useToast` under the package `LayerProvider`; a symbol-branded object passed between copy and package; a copied Dialog announcing beside a package component; a package picker inside a copied Dialog announcing a result count; a package grouped control inside a copied overlay | In either direction (copy under package, package under copy): two stores of one protocol version exist in a document, Escape and focus return differ from the package alone, a copied consumer misses the package provider, a brand check fails, a second live region appears instead of one announcer (`spec:AST-086` FR9), an announcement escapes the open modal (`spec:AST-086` FR14), or group context crosses the layer boundary (`spec:AST-038` FR7) | +| FR4 | Scan each output for private data-attribute names and selectors; compare copied owner modules with the package's | Components that reach an owner module | A hand-written private attribute name or selector, or an edited owner module, in the output | +| FR5 | Render package and copy side by side under a theme with component overrides; compare | Default; each variant; a theme component override; a copy built by `@astryxdesign/build` | A theme override, documented class name, data attribute, or theme target reaches one and not the other | +| FR6 | The component's own interaction and accessibility checks run against the copy | Keyboard, pointer, screen-reader tree | A check that passes on the package fails on the copy | +| FR7 | The required test lane's swizzle proof and the release-time proof; both cover Core's list and every integration package's components through `--package` | A change to component source; a change to the swizzle copy; an unrelated change; Core, Lab, Charts, RichText, Vega | An offered component missing from the proof, a failure that does not block, a new required context, the proof adding wall-clock time to the slowest required check, or the proof pushing its job past the pull-request time budget | Known verification gap: none of these checks exist on `main`.