Skip to content
Merged
12 changes: 11 additions & 1 deletion e2e/next-app/next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,14 @@ import { withAnimus } from '@animus-ui/next-plugin';
export default withAnimus({
verbose: true,
system: './src/ds.ts',
})({});
})({
// Required for TS-only syntax in external DS package SOURCE files: the kit
// entry exports an `as const` variant map, and the animus webpack loader
// passes componentless files through unchanged (next-webpack-integration ›
// "Files with no extractable components SHALL pass through unchanged") —
// without SWC transpilation of the workspace package, webpack's ES parser
// rejects the raw TS. The animus loader still runs `enforce: 'pre'`, so it
// sees original source before SWC either way. Turbopack (next16-app) needs
// no equivalent.
transpilePackages: ['@animus-ui/test-ds'],
});
11 changes: 6 additions & 5 deletions e2e/next-app/scripts/assert-build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,10 @@ async function main(): Promise<void> {

assertNoPlaceholders(css);

// Guardrail G2 (modern-css-surface): condition at-rules must nest inside a
// named @layer block. Non-vacuous here — the imported test-ds Card emits raw
// @container / @media / @supports rules into this build's CSS.
// arch-css-structural-gates › "Condition at-rules gated inside layer blocks":
// condition at-rules must nest inside a named @layer block. Non-vacuous
// here — the imported test-ds Card emits raw @container / @media /
// @supports rules into this build's CSS.
//
// Exempt: the theme's variable-level system fallback blocks (openspec:
// system-color-scheme), which live in the UNLAYERED variables part beside
Expand All @@ -137,9 +138,9 @@ async function main(): Promise<void> {
minReferences: 2,
});

// ── System color scheme (openspec: system-color-scheme, D2/D6) ──────────
// ── System color scheme (openspec: system-color-scheme) ─────────────────
//
// Guardrail G2: every root-targeting rule inside a prefers-color-scheme
// Every root-targeting rule inside a prefers-color-scheme
// block carries the `:root:not([data-color-mode])` guard, and both guarded
// blocks actually exist with custom properties (non-vacuous).
assertSystemSchemeGuard(css, { expectSchemes: ['light', 'dark'] });
Expand Down
2 changes: 1 addition & 1 deletion e2e/next-app/src/ds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export const tokens = createTheme()
900: '#78350f',
},
})
// System participation (openspec: system-color-scheme, D2). App-LOCAL theme —
// System participation (openspec: system-color-scheme). App-LOCAL theme —
// nothing else in the workspace consumes it, so opting in here cannot move a
// shared parity baseline (the parity harness builds
// `packages/extract/tests/test-system.ts`).
Expand Down
101 changes: 76 additions & 25 deletions e2e/vite-app/scripts/assert-build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ import {
assertConditionsInsideLayers,
assertHeadInjectionContract,
assertKeyframesExtracted,
assertKeyframesUniqueBodies,
assertLayerOrder,
assertNoDevDiagnostics,
assertNoEmotionImports,
assertNoLiteralAmpersand,
assertNoPlaceholders,
assertSelectorEmitted,
assertSystemFallbackParity,
assertSystemSchemeGuard,
assertVariantDeclarationParity,
findCssFiles,
findJsFiles,
layerBlock,
Expand Down Expand Up @@ -200,11 +204,11 @@ async function main(): Promise<void> {
);
});

// Guardrail G2 (modern-css-surface): every @container / @supports /
// non-breakpoint @media condition at-rule must nest inside a named @layer
// block. Runs NON-VACUOUSLY here — the test-ds Card (raw @container/@media/
// @supports) and the app Card (registered `_motionReduce` alias) both emit
// condition rules into this dist.
// arch-css-structural-gates › "Condition at-rules gated inside layer blocks":
// every @container / @supports / non-breakpoint @media condition at-rule must
// nest inside a named @layer block. Runs NON-VACUOUSLY here — the test-ds
// Card (raw @container/@media/@supports) and the app Card (registered
// `_motionReduce` alias) both emit condition rules into this dist.
//
// The one exemption is the theme's variable-level system fallback blocks
// (openspec: system-color-scheme): they belong to the UNLAYERED variables
Expand All @@ -217,9 +221,9 @@ async function main(): Promise<void> {
exemptSpans: systemSchemeVariableSpans(css),
});

// Container-unit emission pin (inc 11, spec "Container-relative units on
// scale-typed properties"): the test-ds Card authors `gap: '2cqi'` on a
// strict space-scale prop inside a nested @container block — the unit
// Container-unit emission pin (container-query-support › "Container-relative
// units on scale-typed properties"): the test-ds Card authors `gap: '2cqi'`
// on a strict space-scale prop inside a nested @container block — the unit
// string must ship verbatim (the resolver emits it; minifiers may reformat
// the prelude but not the declaration value).
if (!css.includes('gap:2cqi') && !css.includes('gap: 2cqi')) {
Expand All @@ -229,14 +233,14 @@ async function main(): Promise<void> {
);
}

// Merged-config extraction witness (openspec: first-class-extension, NS-1;
// rust-system-loader › "Merged configuration is the extraction authority"):
// App.tsx uses `top={12}` and `zIndex={10}` on Box, and the `positioning`
// group that registers both props comes ONLY from `.extend(testDs)` —
// src/ds.ts deliberately does not re-register it. These declarations can
// reach the dist CSS only through the MERGED configuration, and `top:12px`
// Merged-config extraction witness (rust-system-loader › "Merged
// configuration is the extraction authority"): App.tsx uses `top={12}` and
// `zIndex={10}` on Box, and the `positioning` group that registers both
// props comes ONLY from `.extend(testDs)` — src/ds.ts deliberately does not
// re-register it. These declarations can reach the dist CSS only through
// the MERGED configuration, and `top:12px`
// additionally pins the kit's `size` transform surviving the registry
// snapshot merge (no serialized round-trip, design D7).
// snapshot merge (no serialized round-trip).
for (const probe of [
['top:12px', 'top: 12px'],
['z-index:10', 'z-index: 10'],
Expand All @@ -249,7 +253,7 @@ async function main(): Promise<void> {
}
}

// Built-in condition composite witness (inc 06): the app Card authors
// Built-in condition composite witness: the app Card authors
// `_osDark` WITHOUT registering it — it must resolve through the DEFAULT
// built-in set across the full registry → manifest → plugin → engine wire.
if (
Expand All @@ -262,15 +266,16 @@ async function main(): Promise<void> {
);
}

// ── System color scheme (openspec: system-color-scheme, D2/D6) ──────────
// ── System color scheme (openspec: system-color-scheme) ─────────────────
//
// This lane is the VITE delivery witness: the theme opts in via
// `systemPreference` + `browserColorScheme` (src/ds.ts) and the plugin
// injects the bootstrap via the `appearanceBootstrap` option (vite.config.ts).
//
// Guardrail G2. Non-vacuous in BOTH directions here: `expectSchemes` demands
// the two guarded theme blocks exist and assign custom properties, while the
// app Card's unregistered `_osDark` condition puts an UNGUARDED
// The layer gate above runs non-vacuously in BOTH directions here:
// `expectSchemes` demands the two guarded theme blocks exist and assign
// custom properties, while the app Card's unregistered `_osDark` condition
// puts an UNGUARDED
// `@media (prefers-color-scheme: dark) { .animus-Card-… { … } }` block in the
// same sheet — which must not trip the gate. Only ROOT-targeting rules owe
// the guard.
Expand Down Expand Up @@ -307,15 +312,61 @@ async function main(): Promise<void> {
});

// Keyframes extracted through the rollup (Vite) adapter — fixture declares
// `animations = keyframes({ fadeIn, pulse })` in src/ds.ts; the assertion
// proves both blocks land in @layer anm-global, both animation-name refs
// resolve to a matching block, and neither got px-mangled by unit-fallback.
// `animations = keyframes({ fadeIn, pulse })` in src/ds.ts, and KitPulse
// consumes `kitMotion.pulse` from the test-ds package ENTRY; the assertion
// proves all three blocks land in @layer anm-global, all three
// animation-name refs resolve to a matching block, and none got px-mangled
// by unit-fallback.
assertKeyframesExtracted(css, {
insideLayer: 'anm-global',
minBlocks: 2,
minReferences: 2,
minBlocks: 3,
minReferences: 3,
});

// Exactly one @keyframes block per unique frame body: the kit collection
// must emit ONCE — not once via the external-entry scan and again via the
// consumer reference — and no app body may collide.
assertKeyframesUniqueBodies(css);

// Binding-backed vs inline parity (semantic-const-resolution): KitSized
// consumes the kit's `as const` variant map through a named import;
// InlineSized authors the identical map inline. Base + every option class
// must carry byte-equal declaration lists — a mismatch here is STOP
// evidence, never paper over it.
assertVariantDeclarationParity(css, {
components: ['KitSized', 'InlineSized'],
optionSuffixes: ['size-sm', 'size-md', 'size-lg'],
});

// Ancestor/repeated/alias subject witnesses (nested-selector-resolution):
// the composed class must sit at the SUBJECT position with the ancestor
// prefix preserved. Two `[data-active]`
// producers exist (app ActiveItem + kit GroupItem); the alias patterns pin
// the kit's registered `_groupHover` / `_dark` ancestor aliases arriving
// through the `.extend(testDs)` registry merge. Patterns tolerate the
// minifier stripping attribute-value quotes.
assertSelectorEmitted(css, {
pattern: /\[data-active="?true"?\]\s*\.animus-[\w-]+/,
label: 'raw ancestor subject ([data-active="true"] &)',
minMatches: 2,
});
assertSelectorEmitted(css, {
pattern:
/\.animus-ActiveItem-[0-9a-f]+\s*\+\s*\.animus-ActiveItem-[0-9a-f]+/,
label: 'repeated subject (& + &)',
});
assertSelectorEmitted(css, {
pattern: /\.group:hover\s*\.animus-[\w-]+/,
label: 'registered ancestor alias (_groupHover: .group:hover &)',
});
assertSelectorEmitted(css, {
pattern: /\[data-color-mode="?dark"?\]\s*\.animus-[\w-]+/,
label: 'registered ancestor alias (_dark: [data-color-mode="dark"] &)',
});

// Zero literal `&` in produced CSS.
assertNoLiteralAmpersand(css);

const jsFiles = await findJsFiles(DIST);
const jsSources: string[] = [];
for (const jsFile of jsFiles) {
Expand Down
46 changes: 43 additions & 3 deletions e2e/vite-app/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { Card as TestDsCard } from '@animus-ui/test-ds';
import { Card as TestDsCard, GroupItem } from '@animus-ui/test-ds';

import {
ActiveItem,
Box,
Button,
Card,
Fade,
Family,
InlineSized,
KitPulse,
KitSized,
Pulse,
Stack,
StackItem,
Expand Down Expand Up @@ -56,8 +60,8 @@ export function App() {
</Family.Root>

{/*
Merged-config extraction witness (openspec: first-class-extension,
NS-1 / rust-system-loader › "Merged configuration is the extraction
Merged-config extraction witness (rust-system-loader › "Merged
configuration is the extraction
authority"): `top` and `zIndex` belong to the `positioning` group,
which ONLY test-ds registers — src/ds.ts deliberately does not
re-register it, so these usages emit CSS solely because
Expand All @@ -72,7 +76,43 @@ export function App() {
<Stack direction="row" gap={8}>
<Pulse>Pulse</Pulse>
<Fade>Fade In</Fade>
{/*
External keyframe-collection witness: KitPulse
animates with `kitMotion.pulse` from the test-ds package ENTRY —
rendered beside the app-local Pulse/Fade siblings so usage
reconciliation keeps all three animation references.
*/}
<KitPulse>Kit Pulse</KitPulse>
</Stack>

{/*
Binding-backed vs inline variant-map siblings: every option of BOTH
components renders so usage reconciliation keeps
the full option set on each — assertVariantDeclarationParity compares
the two per class.
*/}
<Stack direction="row" gap={8}>
<KitSized size="sm">Kit sm</KitSized>
<KitSized size="md">Kit md</KitSized>
<KitSized size="lg">Kit lg</KitSized>
<InlineSized size="sm">Inline sm</InlineSized>
<InlineSized size="md">Inline md</InlineSized>
<InlineSized size="lg">Inline lg</InlineSized>
</Stack>

{/*
Ancestor-subject witnesses: the wrapper carries
BOTH ancestor contexts — `data-active="true"` for the raw ancestor
keys (app ActiveItem + adjacent-sibling `& + &` pair) and the `group`
class for the kit GroupItem's registered `_groupHover` alias
(`.group:hover &`). GroupItem's `_dark` alias matches the
`[data-color-mode]` attribute the appearance bootstrap sets on :root.
*/}
<div className="group" data-active="true">
<ActiveItem>Active A</ActiveItem>
<ActiveItem>Active B</ActiveItem>
<GroupItem>Kit group item</GroupItem>
</div>
</Stack>
);
}
26 changes: 26 additions & 0 deletions e2e/vite-app/src/components/ActiveItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { ds } from '../ds';

// App-local ancestor/repeated-subject fixture (nested-selector-resolution):
// the raw ancestor key must emit with the composed class at the subject
// position (`[data-active="true"] .animus-ActiveItem-…`), and the
// repeated-subject key must substitute the class at EVERY unquoted `&`
// (`.animus-ActiveItem-… + .animus-ActiveItem-…`). The test-ds GroupItem
// covers the same family cross-package plus the registered-alias path; this
// sibling proves the raw keys work authored directly in the consumer app. No
// literal `&` may survive into the produced stylesheet.
export const ActiveItem = ds
.styles({
display: 'inline-flex',
alignItems: 'center',
px: 8,
py: 4,
borderRadius: '4px',
bg: 'surface',
color: 'text',
'[data-active="true"] &': {
bg: 'primary',
color: 'background',
},
'& + &': { ml: 8 },
})
.asElement('span');
2 changes: 1 addition & 1 deletion e2e/vite-app/src/components/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const Card = ds
_motionReduce: {
transition: 'none',
},
// UNREGISTERED built-in (inc 06 composite witness): this app never
// UNREGISTERED built-in (composite witness): this app never
// registers `_osDark` — it resolves through the DEFAULT built-in set via
// the full SystemBuilder → manifest → plugin glue → engine path. The
// assert lane pins its emission.
Expand Down
26 changes: 26 additions & 0 deletions e2e/vite-app/src/components/KitPulse.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { kitMotion } from '@animus-ui/test-ds';

import { ds } from '../ds';

// External keyframe-collection consumer (rust-extraction-pipeline › "External
// package collection discovered from its entry"): `kitMotion` is created and
// exported by the test-ds package ENTRY module, and this component references
// it through a plain named import — the app's ds.ts does NOT re-export it. The
// extractor's keyframes scan must discover the collection from the external
// entry, resolve `kitMotion.pulse` to its `animus-kf-<hash>` name, and emit the
// matching @keyframes block exactly once. Pulse.tsx (app-local
// `animations.pulse` from ds.ts) is the inline sibling in the same sheet;
// assertKeyframesUniqueBodies pins that no frame body is ever emitted twice.
export const KitPulse = ds
.styles({
bg: 'secondary',
color: 'background',
px: 16,
py: 8,
borderRadius: '4px',
animationName: kitMotion.pulse,
animationDuration: '2s',
animationTimingFunction: 'ease-in-out',
animationIterationCount: 'infinite',
})
.asElement('span');
35 changes: 35 additions & 0 deletions e2e/vite-app/src/components/Sized.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { kitSizes } from '@animus-ui/test-ds';

import { ds } from '../ds';

// Binding-backed vs inline variant-map siblings (semantic-const-resolution ›
// "Imported variant map across a package boundary"): KitSized references the
// kit's `as const` map through a plain named import; InlineSized authors the
// IDENTICAL map inline. The assert lane (assertVariantDeclarationParity) pins
// per-class declaration equality between the two — base class and every variant
// option class — so any resolver drift between the binding-backed and inline
// paths is a hard STOP.
const sizedBase = {
display: 'inline-flex',
alignItems: 'center',
borderRadius: '4px',
bg: 'surface',
color: 'text',
} as const;

export const KitSized = ds
.styles(sizedBase)
.variant({ prop: 'size', variants: kitSizes })
.asElement('span');

export const InlineSized = ds
.styles(sizedBase)
.variant({
prop: 'size',
variants: {
sm: { fontSize: 14, px: 8, py: 4 },
md: { fontSize: 16, px: 16, py: 8 },
lg: { fontSize: 20, px: 24, py: 12 },
},
})
.asElement('span');
3 changes: 3 additions & 0 deletions e2e/vite-app/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
export { ActiveItem } from './ActiveItem';
export { Box } from './Box';
export { Button } from './Button';
export { Card } from './Card';
export { Family } from './Family';
export { KitPulse } from './KitPulse';
export { Fade, Pulse } from './Pulse';
export { InlineSized, KitSized } from './Sized';
export { Stack } from './Stack';
export { StackItem } from './StackItem';
Loading