@reqvire-org/ads 0.3.7 → 0.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/atomic-composition.mjs +85 -54
  2. package/dist/atomic-trace.mjs +92 -0
  3. package/dist/bootstrap-ds/scripts/build-mcp-previews.mjs +2 -1
  4. package/dist/bootstrap-ds/vite.config.ts +2 -1
  5. package/dist/check.mjs +14 -3
  6. package/dist/linaria/atomic-css.cjs +2 -0
  7. package/dist/linaria/atomic-styled.cjs +2 -0
  8. package/dist/linaria/preserve-values.cjs +32 -0
  9. package/dist/linaria.d.mts +1 -0
  10. package/dist/linaria.mjs +9 -0
  11. package/dist/mcp/preview-widget.bundle.js +1 -1
  12. package/dist/mcp/resource-loader.js +5 -2
  13. package/dist/mcp/server.mjs +4 -2
  14. package/dist/scenario-trace.mjs +149 -0
  15. package/dist/scenarios-compiler.mjs +2 -1
  16. package/dist/scenarios.mjs +22 -6
  17. package/dist/skills/ads-break-ui/SKILL.md +10 -2
  18. package/dist/skills/ads-break-ui/references/atomic-styles.md +122 -7
  19. package/dist/skills/ads-break-ui/references/scenarios.md +83 -0
  20. package/dist/skills/ads-designer/SKILL.md +13 -3
  21. package/dist/skills/ads-designer/assets/bootstrap-ds/scripts/build-mcp-previews.mjs +2 -1
  22. package/dist/skills/ads-designer/assets/bootstrap-ds/vite.config.ts +2 -1
  23. package/dist/skills/ads-designer/references/atomic-styles.md +122 -7
  24. package/dist/skills/ads-designer/references/scenarios.md +83 -0
  25. package/dist/skills/ads-import/SKILL.md +10 -2
  26. package/dist/skills/ads-import/references/atomic-styles.md +122 -7
  27. package/dist/skills/ads-import/references/scenarios.md +83 -0
  28. package/dist/skills/ads-migrate/SKILL.md +10 -2
  29. package/dist/skills/ads-migrate/references/atomic-styles.md +122 -7
  30. package/dist/skills/ads-migrate/references/scenarios.md +83 -0
  31. package/dist/skills/ads-motion/SKILL.md +10 -2
  32. package/dist/skills/ads-motion/references/atomic-styles.md +122 -7
  33. package/dist/skills/ads-motion/references/scenarios.md +83 -0
  34. package/dist/source-values.mjs +247 -0
  35. package/dist/verification/browser.mjs +21 -12
  36. package/dist/verification/playwright-reporter.cjs +6 -2
  37. package/dist/verification.mjs +8 -3
  38. package/docs/ADS.md +18 -5
  39. package/docs/ARCHITECTURE.md +19 -0
  40. package/docs/BEHAVIORS.md +15 -1
  41. package/docs/CHANGELOG.md +28 -1
  42. package/docs/MCP.md +18 -0
  43. package/package.json +10 -1
@@ -0,0 +1,83 @@
1
+ # Scenario render evidence and public class helpers
2
+
3
+ Use this reference for `scenario-render`, `scenario-coverage` or `showcase-class`
4
+ diagnostics. MCP serves the same guide as `ads://guides/scenarios`.
5
+
6
+ ## Public children can share their parent's scenario
7
+
8
+ Declare the public parts actually demonstrated in `covers`, even when their
9
+ parent renders them internally:
10
+
11
+ ```tsx
12
+ // Runtime owner
13
+ export function Part() { return <span>Content</span>; }
14
+ export function Frame({ visible = true } = {}) {
15
+ return <section>{visible && <Part />}</section>;
16
+ }
17
+
18
+ // Colocated Scenario.tsx
19
+ import { Frame } from "@ds";
20
+ export const scenario = { title: "Frame", covers: ["Frame", "Part"] };
21
+ export default function Scenario() { return <Frame />; }
22
+ ```
23
+
24
+ ADS follows returned JSX through local components/helpers, aliases, registered
25
+ framework factories, memo/ref wrappers and context providers. Fixture props,
26
+ spreads and destructuring defaults select the child path. An explicit
27
+ `visible={false}` does not cover `Part`. When a condition remains unknown, only
28
+ parts returned on every possible path receive transitive credit. Imports, unused
29
+ JSX, event-handler bodies and uncalled functions do not establish child rendering.
30
+ Only the explicitly declared `covers` entries acquire coverage; dependencies are
31
+ not silently added to metadata or the catalogue.
32
+
33
+ Tracing is bounded and does not execute the project. Recursion, written/escaped inputs,
34
+ opaque external wrappers and unsupported control flow can prevent proof. The
35
+ diagnostic explains known limits. Use meaningful fixture states or another real
36
+ scenario; do not make a valid public part private or duplicate it in a parent
37
+ just to satisfy a guard. Static evidence does not establish successful mounting,
38
+ visibility, interaction, every state or visual parity. Verify those in the built
39
+ App and registered browser checks.
40
+
41
+ ## Scenarios may consume verified public style helpers
42
+
43
+ A deliberately public helper can expose its owner's Linaria treatment:
44
+
45
+ ```tsx
46
+ // Runtime owner
47
+ const treatment = css`padding-inline: var(--space-4);`;
48
+ export function controlClasses() {
49
+ return cx("control-hook", treatment);
50
+ }
51
+
52
+ // Scenario: consume the public API; define no styles here
53
+ <PublicLink className={controlClasses()}>Documentation</PublicLink>
54
+ ```
55
+
56
+ The sample token and APIs are illustrative; use the project's real exports and
57
+ tokens. Import helpers through the public ADS entry. ADS traces helper calls,
58
+ returns, local aliases, finite known selections and Linaria `cx` composition.
59
+ Styling must originate in component/pattern Linaria through a public callable
60
+ helper. A literal stable hook authored in that runtime owner may accompany the
61
+ verified styles. Each nonempty return path needs actual Linaria declarations;
62
+ empty templates and hook-only/string-only returns do not qualify.
63
+
64
+ These are still rejected:
65
+
66
+ ```tsx
67
+ <PublicLink className="custom-layout" />
68
+ <PublicLink className={arbitraryPublicFunctionReturningAString()} />
69
+ <PublicLink className={controlClasses({ className: "custom-layout" })} />
70
+ ```
71
+
72
+ Scenario-supplied strings, generated styles in fixtures, private style imports,
73
+ spoofed Linaria functions and untraceable results acquire no permission by being
74
+ passed through a public function. Known className values in props/spreads receive
75
+ the same provenance check. Opaque spreads remain outside complete source proof.
76
+ An `exercises` entry records behavioral use; it never grants styling permission.
77
+
78
+ Keep helper signatures compatible with the product API. Fix ownership/provenance
79
+ in the runtime owner when genuinely wrong; retain intentional public APIs when
80
+ reporting an unsupported trace. Demo-only sizing, framing and arrangement still
81
+ belong in public review/layout patterns. A style helper is not permission to
82
+ author a second design in the scenario. For cascade risks and extraction issues,
83
+ read [atomic-styles.md](atomic-styles.md).
@@ -66,10 +66,18 @@ For owner drafts, token-sample reconciliation, coupled transactions and local
66
66
  constant namespaces, read [guarded-editing.md](references/guarded-editing.md).
67
67
 
68
68
 
69
- For atomic base/variant styling or an `atomic-composition-review` warning, read
69
+ For parent/child `covers`, public style-helper use in scenarios, or
70
+ `scenario-render` / `showcase-class` findings, read
71
+ [scenario contracts](references/scenarios.md). Traced parents can cover declared
72
+ public children; verified public Linaria helpers do not grant custom scenario CSS.
73
+
74
+ For atomic base/variant styling, helper/wrapper class composition, extracted
75
+ custom-property arithmetic, or an
76
+ `atomic-composition-review` / `atomic-composition-limit` advisory, read
70
77
  [atomic style composition](references/atomic-styles.md). The same guide is served
71
78
  as `ads://guides/atomic-styles`. It explains shorthand/logical conflicts and
72
- matching-property fixes. Review warnings in the real `cx` composition and verify
79
+ matching-property fixes, plus the build resolver for preserving opaque custom
80
+ properties through extraction. Review warnings in the real `cx` composition and verify
73
81
  computed styles across affected sizes/states; retain intentional product behavior.
74
82
 
75
83
  Controllers are UI behavior owners: private helpers stay with their component or
@@ -81,6 +81,91 @@ const treatment = css`
81
81
  This is a distinct state, not two competing base atoms. Whether changing padding
82
82
  on hover is desirable remains a product decision.
83
83
 
84
+ ## Preserve custom-property arithmetic during extraction
85
+
86
+ Custom properties can hold an unevaluated token stream that becomes arithmetic
87
+ only after substitution into a later `calc()`. For example:
88
+
89
+ ```css
90
+ --i: 2;
91
+ --scale: var(--i) * 0.05 + 1;
92
+ transform: scale(calc(-1 * var(--scale)));
93
+ ```
94
+
95
+ The result is scale `0.9`: substitution does not add parentheses around the
96
+ custom property. Some Linaria/Stylis extraction versions strip the whitespace
97
+ around the bare `+`, leaving `0.05+1`. The browser then treats the transform as
98
+ invalid. This can happen with CSS minification disabled. Source guards cannot
99
+ prove what a later extractor emits.
100
+
101
+ Use ADS's build-only resolver in **both** the application's Linaria configuration
102
+ and its separately configured MCP preview builder:
103
+
104
+ ```js
105
+ import wyw from "@wyw-in-js/vite";
106
+ import { linariaTagResolver } from "@reqvire-org/ads/linaria";
107
+
108
+ // In the project's Vite plugins:
109
+ wyw({ tagResolver: linariaTagResolver });
110
+ ```
111
+
112
+ The resolver keeps the project's installed atomic processor and preserves opaque
113
+ custom-property values through its nesting/atomization pass. It does not add a
114
+ browser runtime or alter token ownership. Existing projects must adopt it in
115
+ each extraction pipeline; upgrading ADS alone does not rewrite custom builders.
116
+ If an existing `tagResolver` handles other tags, delegate atomic imports to
117
+ `linariaTagResolver(source, tag)` and retain the existing fallback. WyW config
118
+ files also support this resolver when a build integration requires serializable
119
+ inline options.
120
+
121
+ For a source workaround, write a self-contained formula and consume it directly:
122
+
123
+ ```css
124
+ --scale: calc(1 - var(--i) * 0.05);
125
+ transform: scale(var(--scale));
126
+ ```
127
+
128
+ This also produces `0.9`. Simply wrapping the old value in `calc()` while keeping
129
+ the outer `-1 *` changes its grouping and result. Verify extracted CSS text and a
130
+ computed style after any rewrite, including nested `var()` fallbacks/transforms.
131
+ Do not globally insert spaces into already extracted CSS: that can change valid
132
+ signed numbers, identifiers, strings or URLs.
133
+
134
+ ## Follow helpers and forwarded classes
135
+
136
+ Composition can cross a helper or a component boundary. For example, in component
137
+ implementation source:
138
+
139
+ ```tsx
140
+ function baseClasses() { return base; }
141
+ function Primitive({ className, ...props }) {
142
+ return <button {...props} className={cx(baseClasses(), className)} />;
143
+ }
144
+ function CompactPrimitive(props) {
145
+ return <Primitive {...props} className={small} />;
146
+ }
147
+ ```
148
+
149
+ The base and small atoms can reach the same button. Inspect both owners when
150
+ correcting their property vocabulary. The wrapper's class does not style every
151
+ descendant: a class on a child or sibling element is a separate composition.
152
+ An imported stylesheet alone does not show that its classes co-apply.
153
+
154
+ Spread order matters. `<button className={base} {...props} />` can replace the
155
+ entire className; `<button {...props} className={cx(base, className)} />` composes
156
+ it explicitly. Keep the product's intended forwarding behavior when fixing an
157
+ overlap. Do not change the public API simply to make tracing easier.
158
+
159
+ Warnings include a bounded `composition` path through traced helpers and
160
+ components. A `target` identifies a statically reached DOM element. This
161
+ establishes source composition, not that a scenario mounted it or that its
162
+ computed result is wrong. `writingModeDependent: true` identifies overlaps that
163
+ change with direction or writing mode; evaluate those against supported modes.
164
+ Literal overlaps with provably equal values are omitted where the analyzer can
165
+ expand them safely. Token formulas, inheritance and runtime values still need
166
+ review. A margin shorthand and `margin-inline: auto` may agree horizontally but
167
+ disagree in vertical text, so horizontal equality alone is insufficient.
168
+
84
169
  ## Respond to a warning
85
170
 
86
171
  1. Follow the warning's related file/line locations and inspect the actual `cx`
@@ -101,6 +186,24 @@ the conflict. Prefer resolving the composition itself. If an overlap is
101
186
  intentional, document why its precedence is stable and verify the resulting
102
187
  behavior rather than changing product capabilities to silence a warning.
103
188
 
189
+ Use existing behavior contracts and `ads_verify` for important geometry. Register
190
+ a check beside its owner, link its real scenario and use the project's browser
191
+ profile. An assertion can inspect the actual control in the App frame:
192
+
193
+ ```ts
194
+ const frame = await openBehaviorScenario(page, "compact-spacing");
195
+ await expect(frame.getByRole("button", { name: "Compact" }))
196
+ .toHaveCSS("padding-inline-start", "8px");
197
+ ```
198
+
199
+ The name, check ID and expected value above are examples. Derive assertions from
200
+ the project's intended API and design, with relevant variants, open/hover/focus
201
+ states, themes and supported writing modes. Wait for observable readiness; an
202
+ event-stream connection can prevent `networkidle` from settling. A generic
203
+ overlap scan cannot decide which dimensions the product intended. A failed
204
+ registered geometry assertion is blocking through the existing verification
205
+ contract; an overlap warning remains a review signal.
206
+
104
207
  ## What ADS checks
105
208
 
106
209
  `atomic-composition-review` is a nonblocking warning, returned under `advisories`
@@ -108,14 +211,26 @@ with `severity: "warning"`. CLI checks still exit successfully when there are no
108
211
  errors; MCP checks and mutations preserve the warning and its guidance URI.
109
212
 
110
213
  The source review examines atomic Linaria templates and statically traceable
111
- `cx` arguments, including aliases, local imports, nested composition, conditional
112
- expressions and variant-object lookups. It checks known overlaps involving box
214
+ `cx` arguments, including aliases, local imports/re-exports, local helper returns,
215
+ arguments/defaults, nested composition, conditional expressions and variant-object
216
+ lookups. It follows explicit `className` forwarding through local function
217
+ components, props/rest spreads and imported React/Preact `memo`/`forwardRef`
218
+ wrappers to an intrinsic JSX element. It checks known overlaps involving box
113
219
  spacing, inset, borders/corners, dimensions, overflow, backgrounds, typography,
114
220
  motion and common layout shorthands, plus `all` resets. It compares shared selector/condition contexts; separate alternatives
115
- within one conditional or lookup are not assumed to apply together.
221
+ within one conditional or lookup are not assumed to apply together. Matching
222
+ atomic property/selector/condition keys are replaced during traced `cx`
223
+ composition before comparing the surviving declarations.
116
224
 
117
225
  This is a bounded source review, not a CSS engine or a proof of precedence. It
118
- does not enumerate every external class, runtime function result, selector
119
- interaction or overlapping media query. An empty warning list does not establish
120
- visual correctness. Actual failed registered browser/behavior checks remain
121
- blocking; potentially intentional source overlaps remain warnings.
226
+ does not execute helpers or import project modules, enumerate every external
227
+ class, or model arbitrary runtime mutation, component rendering, selectors or
228
+ overlapping media queries. Recursive calls, unsupported call/control-flow paths,
229
+ unresolved DOM targets and traversal limits produce bounded
230
+ `atomic-composition-limit` informational advisories where known atomic styles
231
+ reach an unresolved composition. Ordinary externally supplied className values
232
+ remain open inputs, not guaranteed safe styles. Depth, work and alternative
233
+ limits keep source inspection bounded; an unreported path or an empty warning
234
+ list does not establish visual correctness. Actual failed registered
235
+ browser/behavior checks remain blocking; potentially intentional source overlaps
236
+ remain warnings.
@@ -0,0 +1,83 @@
1
+ # Scenario render evidence and public class helpers
2
+
3
+ Use this reference for `scenario-render`, `scenario-coverage` or `showcase-class`
4
+ diagnostics. MCP serves the same guide as `ads://guides/scenarios`.
5
+
6
+ ## Public children can share their parent's scenario
7
+
8
+ Declare the public parts actually demonstrated in `covers`, even when their
9
+ parent renders them internally:
10
+
11
+ ```tsx
12
+ // Runtime owner
13
+ export function Part() { return <span>Content</span>; }
14
+ export function Frame({ visible = true } = {}) {
15
+ return <section>{visible && <Part />}</section>;
16
+ }
17
+
18
+ // Colocated Scenario.tsx
19
+ import { Frame } from "@ds";
20
+ export const scenario = { title: "Frame", covers: ["Frame", "Part"] };
21
+ export default function Scenario() { return <Frame />; }
22
+ ```
23
+
24
+ ADS follows returned JSX through local components/helpers, aliases, registered
25
+ framework factories, memo/ref wrappers and context providers. Fixture props,
26
+ spreads and destructuring defaults select the child path. An explicit
27
+ `visible={false}` does not cover `Part`. When a condition remains unknown, only
28
+ parts returned on every possible path receive transitive credit. Imports, unused
29
+ JSX, event-handler bodies and uncalled functions do not establish child rendering.
30
+ Only the explicitly declared `covers` entries acquire coverage; dependencies are
31
+ not silently added to metadata or the catalogue.
32
+
33
+ Tracing is bounded and does not execute the project. Recursion, written/escaped inputs,
34
+ opaque external wrappers and unsupported control flow can prevent proof. The
35
+ diagnostic explains known limits. Use meaningful fixture states or another real
36
+ scenario; do not make a valid public part private or duplicate it in a parent
37
+ just to satisfy a guard. Static evidence does not establish successful mounting,
38
+ visibility, interaction, every state or visual parity. Verify those in the built
39
+ App and registered browser checks.
40
+
41
+ ## Scenarios may consume verified public style helpers
42
+
43
+ A deliberately public helper can expose its owner's Linaria treatment:
44
+
45
+ ```tsx
46
+ // Runtime owner
47
+ const treatment = css`padding-inline: var(--space-4);`;
48
+ export function controlClasses() {
49
+ return cx("control-hook", treatment);
50
+ }
51
+
52
+ // Scenario: consume the public API; define no styles here
53
+ <PublicLink className={controlClasses()}>Documentation</PublicLink>
54
+ ```
55
+
56
+ The sample token and APIs are illustrative; use the project's real exports and
57
+ tokens. Import helpers through the public ADS entry. ADS traces helper calls,
58
+ returns, local aliases, finite known selections and Linaria `cx` composition.
59
+ Styling must originate in component/pattern Linaria through a public callable
60
+ helper. A literal stable hook authored in that runtime owner may accompany the
61
+ verified styles. Each nonempty return path needs actual Linaria declarations;
62
+ empty templates and hook-only/string-only returns do not qualify.
63
+
64
+ These are still rejected:
65
+
66
+ ```tsx
67
+ <PublicLink className="custom-layout" />
68
+ <PublicLink className={arbitraryPublicFunctionReturningAString()} />
69
+ <PublicLink className={controlClasses({ className: "custom-layout" })} />
70
+ ```
71
+
72
+ Scenario-supplied strings, generated styles in fixtures, private style imports,
73
+ spoofed Linaria functions and untraceable results acquire no permission by being
74
+ passed through a public function. Known className values in props/spreads receive
75
+ the same provenance check. Opaque spreads remain outside complete source proof.
76
+ An `exercises` entry records behavioral use; it never grants styling permission.
77
+
78
+ Keep helper signatures compatible with the product API. Fix ownership/provenance
79
+ in the runtime owner when genuinely wrong; retain intentional public APIs when
80
+ reporting an unsupported trace. Demo-only sizing, framing and arrangement still
81
+ belong in public review/layout patterns. A style helper is not permission to
82
+ author a second design in the scenario. For cascade risks and extraction issues,
83
+ read [atomic-styles.md](atomic-styles.md).
@@ -0,0 +1,247 @@
1
+ import ts from "typescript";
2
+ import { isInside } from "./config.mjs";
3
+ import { uiOperation } from "./ui-framework.mjs";
4
+
5
+ export const unknown = reason => ({ kind: "unknown", reason });
6
+ export const literal = value => ({ kind: "literal", value });
7
+ export const reference = (node, env = new Map(), via = []) => node ? { kind: "node", node, env, via } : literal(undefined);
8
+ export const union = values => values.length === 1 ? values[0] : { kind: "union", values };
9
+ export const isFunction = node => node && (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node));
10
+ const nameOf = node => node && (ts.isIdentifier(node) || ts.isStringLiteralLike(node) || ts.isNumericLiteral(node)) ? node.text : undefined;
11
+ export function unwrap(node) {
12
+ while (node && (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isSatisfiesExpression(node) || ts.isNonNullExpression(node))) node = node.expression;
13
+ return node;
14
+ }
15
+
16
+ /** Bounded symbolic source traversal. No project code is imported or executed. */
17
+ export function createSourceValues(program, options, { preserveCall = () => false, strict = false } = {}) {
18
+ const checker = program.getTypeChecker();
19
+ const symbol = node => {
20
+ const value = checker.getSymbolAtLocation(ts.isPropertyAccessExpression(node) ? node.name : node);
21
+ return value?.valueDeclaration && ts.isShorthandPropertyAssignment(value.valueDeclaration) ? checker.getShorthandAssignmentValueSymbol(value.valueDeclaration) ?? value : value;
22
+ };
23
+ function declaration(node) {
24
+ let value = symbol(node);
25
+ if (value?.flags & ts.SymbolFlags.Alias) value = checker.getAliasedSymbol(value);
26
+ return value?.valueDeclaration;
27
+ }
28
+ const writes = new Set(), escapes = new Set(), aliases = [];
29
+ if (strict) {
30
+ function root(node) {
31
+ node = unwrap(node);
32
+ while (node && (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node))) node = unwrap(node.expression);
33
+ return node && ts.isIdentifier(node) ? symbol(node) : null;
34
+ }
35
+ function scan(node) {
36
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && node.operatorToken.kind <= ts.SyntaxKind.LastAssignment) writes.add(root(node.left));
37
+ if ((ts.isPrefixUnaryExpression(node) || ts.isPostfixUnaryExpression(node)) && [ts.SyntaxKind.PlusPlusToken, ts.SyntaxKind.MinusMinusToken].includes(node.operator)) writes.add(root(node.operand));
38
+ if (ts.isDeleteExpression(node)) writes.add(root(node.expression));
39
+ if (ts.isCallExpression(node) || ts.isNewExpression(node)) for (const argument of node.arguments ?? []) {
40
+ const value = unwrap(argument);
41
+ if (ts.isIdentifier(value)) escapes.add(symbol(value));
42
+ }
43
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && root(node.initializer)) aliases.push([symbol(node.name), root(node.initializer)]);
44
+ ts.forEachChild(node, scan);
45
+ }
46
+ for (const file of program.getSourceFiles()) if (isInside(options.dsRoot, file.fileName)) scan(file);
47
+ const targets = new Map();
48
+ for (const [alias, target] of aliases) { if (!targets.has(alias)) targets.set(alias, []); targets.get(alias).push(target); }
49
+ for (const affected of [writes, escapes]) {
50
+ const queue = [...affected];
51
+ for (let index = 0; index < queue.length; ++index) for (const target of targets.get(queue[index]) ?? []) if (!affected.has(target)) { affected.add(target); queue.push(target); }
52
+ }
53
+ }
54
+ const state = () => ({ budget: { steps: 0 }, depth: 0, calls: new Set() });
55
+ function next(context) { return { ...context, depth: context.depth + 1 }; }
56
+ function exhausted(context) { return ++context.budget.steps > 4000 || context.depth > 48; }
57
+ function at(node, kind, name) {
58
+ const source = node.getSourceFile(), position = source.getLineAndCharacterOfPosition(node.getStart(source));
59
+ return { file: source.fileName, line: position.line + 1, column: position.character + 1, kind, name: name.slice(0, 100) };
60
+ }
61
+ function withVia(value, via) {
62
+ if (value.kind === "union") return { ...value, values: value.values.map(item => withVia(item, via)) };
63
+ return value.kind === "node" ? { ...value, via: [...new Map([...via, ...value.via].map(item => [JSON.stringify(item), item])).values()].slice(0, 16) } : value;
64
+ }
65
+ function withDefault(value, fallback, context) {
66
+ if (!fallback) return value;
67
+ value = resolve(value, next(context));
68
+ if (value.kind === "literal") return value.value === undefined ? fallback : value;
69
+ if (value.kind === "unknown") return union([value, fallback]);
70
+ if (value.kind === "union") return union(value.values.map(item => withDefault(item, fallback, next(context))));
71
+ return value;
72
+ }
73
+ function bind(pattern, value, env, via, context) {
74
+ if (ts.isIdentifier(pattern)) { env.set(symbol(pattern), value); return; }
75
+ if (ts.isObjectBindingPattern(pattern)) {
76
+ const excluded = new Set();
77
+ for (const part of pattern.elements) {
78
+ const key = nameOf(part.propertyName ?? part.name);
79
+ if (part.dotDotDotToken) bind(part.name, { kind: "rest", value, excluded: new Set(excluded) }, env, via, next(context));
80
+ else {
81
+ if (key === undefined) continue;
82
+ excluded.add(key);
83
+ bind(part.name, withDefault(get(value, key, next(context)), part.initializer && reference(part.initializer, env, via), context), env, via, next(context));
84
+ }
85
+ }
86
+ } else if (ts.isArrayBindingPattern(pattern)) {
87
+ pattern.elements.forEach((part, index) => {
88
+ if (ts.isBindingElement(part) && !part.dotDotDotToken) bind(part.name, withDefault(get(value, String(index), next(context)), part.initializer && reference(part.initializer, env, via), context), env, via, next(context));
89
+ });
90
+ }
91
+ }
92
+ function boundElement(node, env, via, context) {
93
+ let owner = node;
94
+ while (ts.isBindingElement(owner) || ts.isObjectBindingPattern(owner) || ts.isArrayBindingPattern(owner)) owner = owner.parent;
95
+ if (!ts.isVariableDeclaration(owner) || !owner.initializer || !(owner.parent.flags & ts.NodeFlags.Const)) return unknown();
96
+ const local = new Map(env);
97
+ bind(owner.name, reference(owner.initializer, env, via), local, via, next(context));
98
+ return local.get(symbol(node.name)) ?? unknown();
99
+ }
100
+ function get(value, key, context) {
101
+ value = resolve(value, next(context));
102
+ if (strict && value.kind === "context" && key === "Provider") return { kind: "provider" };
103
+ if (value.kind === "union") return union(value.values.map(item => get(item, key, next(context))));
104
+ if (value.kind === "rest") return value.excluded.has(key) ? literal(undefined) : get(value.value, key, next(context));
105
+ const layers = value.kind === "object" ? value.layers : [];
106
+ if (value.kind === "node" && ts.isObjectLiteralExpression(value.node)) {
107
+ for (const property of value.node.properties) {
108
+ if (ts.isSpreadAssignment(property)) layers.push(reference(property.expression, value.env, value.via));
109
+ else {
110
+ const name = nameOf(property.name);
111
+ if (name !== undefined) layers.push(new Map([[name, strict && (ts.isGetAccessorDeclaration(property) || ts.isSetAccessorDeclaration(property)) ? unknown("An accessor is not a statically known property.") : reference(ts.isShorthandPropertyAssignment(property) ? property.name : ts.isMethodDeclaration(property) ? property : property.initializer, value.env, value.via)]]));
112
+ else if (strict) layers.push(unknown("A computed object key may replace the selected property."));
113
+ }
114
+ }
115
+ } else if (value.kind === "node" && ts.isArrayLiteralExpression(value.node)) {
116
+ return reference(value.node.elements[Number(key)], value.env, value.via);
117
+ } else if (value.kind !== "object") return unknown(value.reason);
118
+ for (let index = layers.length - 1; index >= 0; index--) {
119
+ const layer = layers[index];
120
+ if (layer instanceof Map) { if (layer.has(key)) return layer.get(key); }
121
+ else {
122
+ const selected = get(layer, key, next(context));
123
+ if (selected.kind === "literal" && selected.value === undefined) continue;
124
+ // An opaque later spread may replace an earlier explicit property.
125
+ if (selected.kind === "unknown" || selected.kind === "union") return union([selected, get({ kind: "object", layers: layers.slice(0, index) }, key, next(context))]);
126
+ return selected;
127
+ }
128
+ }
129
+ return literal(undefined);
130
+ }
131
+ function keys(value, context) {
132
+ value = resolve(value, next(context));
133
+ if (value.kind === "union") return [...new Set(value.values.flatMap(item => keys(item, next(context))))];
134
+ if (value.kind === "node" && ts.isObjectLiteralExpression(value.node)) return value.node.properties.flatMap(property => ts.isSpreadAssignment(property) ? keys(reference(property.expression, value.env, value.via), next(context)) : nameOf(property.name) ?? []);
135
+ if (value.kind === "object") return [...new Set(value.layers.flatMap(layer => layer instanceof Map ? [...layer.keys()] : keys(layer, next(context))))];
136
+ return [];
137
+ }
138
+ function returns(fn, args, context, kind = "helper") {
139
+ if (context.calls.has(fn.node)) return [unknown("Recursive local call could not be fully traced.")];
140
+ if (!fn.node.body || fn.node.asteriskToken || fn.node.modifiers?.some(item => item.kind === ts.SyntaxKind.AsyncKeyword)) return [unknown("Async, generator or declaration-only calls are outside atomic source tracing.")];
141
+ const env = new Map(fn.env), via = [...fn.via, at(fn.node, kind, fn.node.name?.getText() ?? "local function")];
142
+ const inner = { ...next(context), calls: new Set([...context.calls, fn.node]) };
143
+ for (const [index, parameter] of fn.node.parameters.entries()) {
144
+ if (parameter.dotDotDotToken) { bind(parameter.name, { kind: "array", values: args.slice(index) }, env, via, inner); break; }
145
+ bind(parameter.name, withDefault(resolve(args[index] ?? literal(undefined), inner), parameter.initializer && reference(parameter.initializer, env, via), inner), env, via, inner);
146
+ }
147
+ const outputs = [];
148
+ function statement(node) {
149
+ if (ts.isReturnStatement(node)) { outputs.push(reference(node.expression, env, via)); return true; }
150
+ if (ts.isBlock(node)) { for (const child of node.statements) if (statement(child)) return true; }
151
+ else if (ts.isIfStatement(node)) {
152
+ const condition = resolve(reference(node.expression, env, via), inner);
153
+ if (condition.kind === "literal") return condition.value ? statement(node.thenStatement) : node.elseStatement ? statement(node.elseStatement) : false;
154
+ const left = statement(node.thenStatement), right = node.elseStatement ? statement(node.elseStatement) : false;
155
+ return left && right;
156
+ } else if (ts.isThrowStatement(node)) { if (strict) outputs.push(unknown("A throwing branch does not establish a returned value.")); return true; }
157
+ else if (ts.isIterationStatement(node, false) || ts.isSwitchStatement(node) || ts.isTryStatement(node)) outputs.push(unknown("Loop, switch or try control flow could not be fully traced."));
158
+ return false;
159
+ }
160
+ if (ts.isBlock(fn.node.body)) { if (!statement(fn.node.body) && strict) outputs.push(literal(undefined)); }
161
+ else outputs.push(reference(fn.node.body, env, via));
162
+ // Keep call stacks with lazy return references, including nested helpers.
163
+ return outputs.length ? outputs.map(value => ({ ...value, context: inner })) : [literal(undefined)];
164
+ }
165
+ function resolve(value, context) {
166
+ if (!value) return literal(undefined);
167
+ context = value.context ?? context;
168
+ if (exhausted(context)) return unknown("Atomic source tracing reached its depth or work limit.");
169
+ if (value.kind !== "node") return value;
170
+ const node = unwrap(value.node), { env, via } = value;
171
+ if (ts.isStringLiteralLike(node) || ts.isNumericLiteral(node)) return { ...literal(ts.isNumericLiteral(node) ? Number(node.text) : node.text), ...(strict ? { origin: node, via } : {}) };
172
+ if (node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword) return literal(node.kind === ts.SyntaxKind.TrueKeyword);
173
+ if (node.kind === ts.SyntaxKind.NullKeyword) return literal(null);
174
+ if (ts.isIdentifier(node)) {
175
+ if (strict && writes.has(symbol(node))) return unknown("A written binding or object is not a statically known value.");
176
+ if (strict && ["Fragment", "StrictMode"].includes(uiOperation(node, checker))) return { kind: "provider" };
177
+ if (env.has(symbol(node))) {
178
+ const result = resolve(env.get(symbol(node)), next(context));
179
+ return strict && escapes.has(symbol(node)) && (result.kind === "object" || result.kind === "array" || result.kind === "node" && (ts.isObjectLiteralExpression(result.node) || ts.isArrayLiteralExpression(result.node))) ? unknown("An object passed to another call may have been mutated.") : result;
180
+ }
181
+ const owner = declaration(node);
182
+ if (owner && isInside(options.dsRoot, owner.getSourceFile().fileName)) {
183
+ if (isFunction(owner)) return reference(owner, env, via);
184
+ if (ts.isVariableDeclaration(owner) && owner.initializer && owner.parent.flags & ts.NodeFlags.Const) {
185
+ const result = resolve(reference(owner.initializer, env, via), next(context));
186
+ return strict && escapes.has(symbol(node)) && (result.kind === "object" || result.kind === "array" || result.kind === "node" && (ts.isObjectLiteralExpression(result.node) || ts.isArrayLiteralExpression(result.node))) ? unknown("An object passed to another call may have been mutated.") : result;
187
+ }
188
+ if (ts.isBindingElement(owner)) return resolve(boundElement(owner, env, via, context), next(context));
189
+ }
190
+ return node.text === "undefined" && !owner ? literal(undefined) : unknown();
191
+ }
192
+ if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) {
193
+ const key = ts.isPropertyAccessExpression(node) ? literal(node.name.text) : resolve(reference(node.argumentExpression, env, via), next(context));
194
+ const object = reference(node.expression, env, via);
195
+ if (key.kind === "literal") {
196
+ const selected = get(object, String(key.value), next(context));
197
+ const owner = declaration(node);
198
+ if (selected.kind === "unknown" && owner && isInside(options.dsRoot, owner.getSourceFile().fileName)) {
199
+ if (isFunction(owner)) return reference(owner, env, via);
200
+ if (ts.isVariableDeclaration(owner) && owner.initializer && owner.parent.flags & ts.NodeFlags.Const) return resolve(reference(owner.initializer, env, via), next(context));
201
+ }
202
+ return resolve(selected, next(context));
203
+ }
204
+ const names = keys(object, next(context));
205
+ if (strict) return unknown("A dynamic property selection is not a statically known value.");
206
+ return names.length ? union(names.map(name => get(object, name, next(context)))) : unknown();
207
+ }
208
+ if (ts.isConditionalExpression(node)) {
209
+ const condition = resolve(reference(node.condition, env, via), next(context));
210
+ return condition.kind === "literal" ? resolve(reference(condition.value ? node.whenTrue : node.whenFalse, env, via), next(context)) : union([reference(node.whenTrue, env, via), reference(node.whenFalse, env, via)]);
211
+ }
212
+ if (ts.isBinaryExpression(node)) {
213
+ const left = resolve(reference(node.left, env, via), next(context)), right = reference(node.right, env, via), operator = node.operatorToken.kind;
214
+ if (operator === ts.SyntaxKind.AmpersandAmpersandToken) return left.kind === "literal" ? left.value ? right : literal(undefined) : union([literal(undefined), right]);
215
+ if (operator === ts.SyntaxKind.BarBarToken) return left.kind === "literal" ? left.value ? left : right : union([left, right]);
216
+ if (operator === ts.SyntaxKind.QuestionQuestionToken) return left.kind === "literal" ? left.value == null ? right : left : union([left, right]);
217
+ const other = resolve(right, next(context));
218
+ if (left.kind === "literal" && other.kind === "literal" && [ts.SyntaxKind.EqualsEqualsEqualsToken, ts.SyntaxKind.ExclamationEqualsEqualsToken].includes(operator)) return literal(operator === ts.SyntaxKind.EqualsEqualsEqualsToken ? left.value === other.value : left.value !== other.value);
219
+ }
220
+ if (ts.isPrefixUnaryExpression(node) && node.operator === ts.SyntaxKind.ExclamationToken) {
221
+ const operand = resolve(reference(node.operand, env, via), next(context));
222
+ return operand.kind === "literal" ? literal(!operand.value) : unknown();
223
+ }
224
+ if (ts.isCallExpression(node) && !preserveCall(node.expression)) {
225
+ if (strict && uiOperation(node.expression, checker) === "createContext") return { kind: "context" };
226
+ if (["memo", "forwardRef"].includes(uiOperation(node.expression, checker))) return resolve(reference(node.arguments[0], env, via), next(context));
227
+ const fn = resolve(reference(node.expression, env, via), next(context));
228
+ if (fn.kind === "node" && isFunction(fn.node)) {
229
+ const outputs = returns(fn, node.arguments.map(argument => reference(argument, env, via)), next(context));
230
+ return withVia(resolve(union(outputs), next(context)), outputs.flatMap(item => item.via ?? []));
231
+ }
232
+ return unknown(`Call ${node.expression.getText().slice(0, 80)} could not be traced to a local function.`);
233
+ }
234
+ return { ...value, node };
235
+ }
236
+ function arrays(value, context) {
237
+ value = resolve(value, next(context));
238
+ if (value.kind === "array") return value.values;
239
+ if (value.kind === "node" && ts.isArrayLiteralExpression(value.node)) return value.node.elements.map(node => reference(node, value.env, value.via));
240
+ return null;
241
+ }
242
+
243
+ function props(node, env = new Map(), via = []) {
244
+ return { kind: "object", layers: node.attributes.properties.map(attribute => ts.isJsxSpreadAttribute(attribute) ? reference(attribute.expression, env, via) : new Map([[attribute.name.text, attribute.initializer ? ts.isJsxExpression(attribute.initializer) ? reference(attribute.initializer.expression, env, via) : reference(attribute.initializer, env, via) : literal(true)]])) };
245
+ }
246
+ return { state, next, at, resolve, get, arrays, returns, props, declaration };
247
+ }
@@ -11,17 +11,26 @@ export async function openBehaviorScenario(page, checkId, { timeout = 30000 } =
11
11
  const check = request.checks.find(check => check.id === checkId);
12
12
  const url = check && request.scenarios[check.scenario];
13
13
  if (!url) throw new Error("This check has no managed browser scenario. StrictMode profiles must supply a project development harness; production preview bundles do not replay React development effects.");
14
- await page.goto(url, { waitUntil: "load", timeout });
15
- const end = Date.now() + timeout;
16
- while (Date.now() < end) {
17
- for (const frame of page.frames().filter(frame => frame !== page.mainFrame())) {
18
- const state = await frame.evaluate(() => ({ state: document.documentElement.dataset.adsPreviewState, id: document.documentElement.dataset.adsScenario, message: document.getElementById("ads-preview-status")?.textContent })).catch(() => ({}));
19
- if (state.id === check.scenario && state.state === "ready") return frame;
20
- if (state.state === "error") throw new Error(state.message || "The MCP App failed before verification.");
14
+ const failures = [];
15
+ const failed = request => {
16
+ if (failures.length < 5) failures.push(`${request.method()} ${request.url().slice(0, 300)}: ${request.failure()?.errorText ?? "request failed"}`);
17
+ };
18
+ page.on("requestfailed", failed);
19
+ try {
20
+ await page.goto(url, { waitUntil: "load", timeout });
21
+ const end = Date.now() + timeout;
22
+ while (Date.now() < end) {
23
+ for (const frame of page.frames().filter(frame => frame !== page.mainFrame())) {
24
+ const state = await frame.evaluate(() => ({ state: document.documentElement.dataset.adsPreviewState, id: document.documentElement.dataset.adsScenario, message: document.getElementById("ads-preview-status")?.textContent })).catch(() => ({}));
25
+ if (state.id === check.scenario && state.state === "ready") return frame;
26
+ if (state.id === check.scenario && state.state === "error") throw new Error(state.message || "The MCP App failed before verification.");
27
+ }
28
+ const error = await page.evaluate(() => document.querySelector('#status[data-error="true"]')?.textContent).catch(() => null);
29
+ if (error) throw new Error(error);
30
+ await page.waitForTimeout(50);
21
31
  }
22
- const error = await page.evaluate(() => document.querySelector('#status[data-error="true"]')?.textContent).catch(() => null);
23
- if (error) throw new Error(error);
24
- await page.waitForTimeout(50);
25
- }
26
- throw new Error("Timed out waiting for the selected MCP App scenario to become current and ready. Inspect ads_status and ads://project/build.");
32
+ throw new Error("Timed out waiting for the selected MCP App scenario to become current and ready. Inspect ads_status and ads://project/build.");
33
+ } catch (error) {
34
+ throw new Error(`Scenario ${check.scenario}: ${error.message}${failures.length ? `\nFailed browser requests:\n${failures.join("\n")}` : ""}`, { cause: error });
35
+ } finally { page.off("requestfailed", failed); }
27
36
  }