@reqvire-org/ads 0.3.8 → 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 (42) hide show
  1. package/dist/atomic-trace.mjs +3 -198
  2. package/dist/bootstrap-ds/scripts/build-mcp-previews.mjs +2 -1
  3. package/dist/bootstrap-ds/vite.config.ts +2 -1
  4. package/dist/check.mjs +12 -3
  5. package/dist/linaria/atomic-css.cjs +2 -0
  6. package/dist/linaria/atomic-styled.cjs +2 -0
  7. package/dist/linaria/preserve-values.cjs +32 -0
  8. package/dist/linaria.d.mts +1 -0
  9. package/dist/linaria.mjs +9 -0
  10. package/dist/mcp/preview-widget.bundle.js +1 -1
  11. package/dist/mcp/resource-loader.js +5 -2
  12. package/dist/mcp/server.mjs +3 -1
  13. package/dist/scenario-trace.mjs +149 -0
  14. package/dist/scenarios-compiler.mjs +2 -1
  15. package/dist/scenarios.mjs +11 -5
  16. package/dist/skills/ads-break-ui/SKILL.md +9 -2
  17. package/dist/skills/ads-break-ui/references/atomic-styles.md +50 -0
  18. package/dist/skills/ads-break-ui/references/scenarios.md +83 -0
  19. package/dist/skills/ads-designer/SKILL.md +12 -3
  20. package/dist/skills/ads-designer/assets/bootstrap-ds/scripts/build-mcp-previews.mjs +2 -1
  21. package/dist/skills/ads-designer/assets/bootstrap-ds/vite.config.ts +2 -1
  22. package/dist/skills/ads-designer/references/atomic-styles.md +50 -0
  23. package/dist/skills/ads-designer/references/scenarios.md +83 -0
  24. package/dist/skills/ads-import/SKILL.md +9 -2
  25. package/dist/skills/ads-import/references/atomic-styles.md +50 -0
  26. package/dist/skills/ads-import/references/scenarios.md +83 -0
  27. package/dist/skills/ads-migrate/SKILL.md +9 -2
  28. package/dist/skills/ads-migrate/references/atomic-styles.md +50 -0
  29. package/dist/skills/ads-migrate/references/scenarios.md +83 -0
  30. package/dist/skills/ads-motion/SKILL.md +9 -2
  31. package/dist/skills/ads-motion/references/atomic-styles.md +50 -0
  32. package/dist/skills/ads-motion/references/scenarios.md +83 -0
  33. package/dist/source-values.mjs +247 -0
  34. package/dist/verification/browser.mjs +21 -12
  35. package/dist/verification/playwright-reporter.cjs +6 -2
  36. package/dist/verification.mjs +8 -3
  37. package/docs/ADS.md +15 -5
  38. package/docs/ARCHITECTURE.md +16 -0
  39. package/docs/BEHAVIORS.md +11 -1
  40. package/docs/CHANGELOG.md +18 -1
  41. package/docs/MCP.md +12 -0
  42. package/package.json +10 -1
@@ -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
  }
@@ -6,15 +6,19 @@ module.exports = class AdsBehaviorReporter {
6
6
  constructor() {
7
7
  this.request = JSON.parse(readFileSync(process.env.ADS_VERIFY_REQUEST, "utf8"));
8
8
  this.cases = [];
9
+ this.errors = [];
9
10
  }
10
11
  onTestEnd(test, result) {
11
12
  const file = path.relative(this.request.projectRoot, test.location.file).replaceAll("\\", "/");
12
13
  for (const tag of test.tags ?? []) {
13
14
  if (!tag.startsWith("@ads:")) continue;
14
- this.cases.push({ id: tag.slice(5), file, status: result.status === "passed" && test.expectedStatus === "passed" ? "passed" : ["skipped", "interrupted"].includes(result.status) ? "skipped" : "failed", message: result.errors?.map(error => error.message).join("\n").slice(0, 4000) });
15
+ this.cases.push({ id: tag.slice(5), file, status: result.status === "passed" && test.expectedStatus === "passed" ? "passed" : ["skipped", "interrupted"].includes(result.status) ? "skipped" : "failed", message: result.errors?.map(error => error.message || error.stack || error.value || "The browser runner reported an error without details.").join("\n").slice(0, 4000) });
15
16
  }
16
17
  }
18
+ onError(error) {
19
+ if (this.errors.length < 5) this.errors.push(String(error.message || error.stack || error.value || "The browser runner reported a suite error without details.").slice(0, 4000));
20
+ }
17
21
  onEnd() {
18
- writeFileSync(process.env.ADS_VERIFY_REPORT, JSON.stringify({ schemaVersion: 1, runId: this.request.runId, cases: this.cases.slice(0, 500) }));
22
+ writeFileSync(process.env.ADS_VERIFY_REPORT, JSON.stringify({ schemaVersion: 1, runId: this.request.runId, cases: this.cases.slice(0, 500), errors: this.errors }));
19
23
  }
20
24
  };
@@ -182,19 +182,24 @@ export function createVerificationController(root) {
182
182
  if (!existsSync(report) || realpathSync(report) !== report || lstatSync(report).size > 1024 * 1024) throw new Error("Runner produced no valid bounded verification report. A successful exit alone is not passing evidence.");
183
183
  reportValue = JSON.parse(readFileSync(report, "utf8"));
184
184
  if (reportValue.schemaVersion !== 1 || reportValue.runId !== runId || !Array.isArray(reportValue.cases) || reportValue.cases.length > 500) throw new Error("Runner report has the wrong run ID/schema or too many cases.");
185
- } catch (error) { runnerError = error; diagnostics.push({ rule: "behavior-runner", message: error.message, output: error.output?.slice(-4000), profile: profileName }); }
185
+ } catch (error) { runnerError = error; reportValue = undefined; diagnostics.push({ rule: "behavior-runner", message: error.message, output: error.output?.slice(-4000), profile: profileName }); }
186
+ const suiteErrors = Array.isArray(reportValue?.errors) ? reportValue.errors.slice(0, 5).map(error => String(error).slice(0, 1000)) : [];
187
+ const runnerFailure = run && (run.code !== 0 || suiteErrors.length) ? `Registered runner ${profileName} ${run.code !== 0 ? `exited with code ${run.code ?? "unknown"}` : "reported a suite error"}. ${suiteErrors.join("; ") || reportValue?.cases.filter(item => item?.status === "failed").slice(0, 3).map(item => `${item.id}: ${item.message || "failed without a case diagnostic"}`).join("; ") || "Inspect the runner output and verification report."}`.slice(0, 3000) : undefined;
188
+ if (runnerFailure) diagnostics.push({ rule: "behavior-runner", message: runnerFailure, output: run.output?.slice(-4000), profile: profileName });
186
189
  const afterOptions = loadConfig(root), afterContext = inputContext(afterOptions);
187
190
  for (const check of group) {
188
191
  const file = slash(path.relative(root, path.resolve(root, path.dirname(check.contract.file), check.file)));
189
192
  const matches = reportValue?.cases.filter(item => item && item.id === check.case && item.file === file) ?? [];
190
- let status = !run || run.code !== 0 ? "failed" : matches.length && matches.every(item => item.status === "passed") ? "passed" : matches.length && matches.every(item => item.status === "skipped") ? "skipped" : "failed";
193
+ const reportedStatus = matches.length && matches.every(item => item.status === "passed") ? "passed" : matches.length && matches.every(item => item.status === "skipped") ? "skipped" : "failed";
194
+ let status = !run || runnerFailure ? "failed" : reportedStatus;
191
195
  if (runnerError?.code === "ENOENT") status = "unavailable";
192
196
  const current = behaviorInputs(afterOptions, check.contract, check, afterContext).revision === check.inputRevision;
193
197
  if (!current) status = "stale";
194
198
  if (signal.aborted) status = "cancelled";
195
199
  results.push({ unit: check.unit, check: check.id, file: check.file, case: check.case, profile: profileName, status, current, revision: check.inputRevision, runId, completedAt: new Date().toISOString(),
196
200
  previewRevision: profile.mode === "browser" ? host?.previewRevision() ?? readPreviewManifest(options)?.revision ?? null : null,
197
- message: runnerError?.message ?? (!matches.length ? "The selected case did not execute/report. Missing and skipped cases do not pass." : matches.filter(item => item.status !== "passed").map(item => String(item.message ?? item.status).slice(0, 2000)).join("\n")), report: slash(path.relative(root, report)) });
201
+ ...(runnerFailure ? { reportedStatus, runnerExitCode: run.code } : {}),
202
+ message: runnerError?.message ?? (matches.filter(item => item.status !== "passed").map(item => String(item.message || `Case reported ${item.status} without a diagnostic.`).slice(0, 2000)).join("\n") || (!matches.length ? "The selected case did not execute/report. Missing and skipped cases do not pass." : runnerFailure ? `This case reported ${reportedStatus}, but the complete run did not pass. ${runnerFailure}` : "")), report: slash(path.relative(root, report)) });
198
203
  }
199
204
  if (signal.aborted) break;
200
205
  }
package/docs/ADS.md CHANGED
@@ -141,7 +141,11 @@ TSX or JSON equivalents are supported. An empty fixture is valid without inputs.
141
141
  `scenario-contract`, `scenario-render` and `scenario-coverage` checks require valid
142
142
  entries and reachable public render references for every public renderable
143
143
  component/pattern, including unused exports and Linaria styled exports. Types, constants and utility functions
144
- are excluded. Coverage is a static source policy; it does not prove runtime
144
+ are excluded. Explicit `covers` entries can receive transitive evidence through a
145
+ rendered parent's returned JSX, fixture props/defaults and known framework
146
+ wrappers/providers. Unknown branches contribute only their common public parts;
147
+ unused JSX and callbacks contribute none. Bounded tracing does not automatically
148
+ add dependency APIs to `covers`. Coverage is a static source policy; it does not prove runtime
145
149
  mounting, correct props, every branch/state or visual parity. The older
146
150
  `showcase-coverage` policy also checks application render references.
147
151
 
@@ -156,6 +160,11 @@ or applies the right CSS property. Browser review still verifies the samples.
156
160
  Scenarios and fixture helpers are development-only wherever colocated. Runtime
157
161
  source and the public barrel cannot import or re-export them. Showcase/scenario
158
162
  code defines no CSS, custom classes or native controls; use public ADS APIs.
163
+ Scenarios may consume public class helpers whose returned styles trace to owned
164
+ Linaria. Stable hooks in that owner may accompany real styles; strings forwarded
165
+ from scenarios, hook-only returns and untraceable outputs remain invalid.
166
+ `exercises` does not authorize classes. See the skill-local
167
+ [scenario contract guide](../skills/ads-designer/references/scenarios.md).
159
168
  Neither ADS implementations nor scenarios import application code. Live business
160
169
  data belongs to application adapters; ADS receives inputs or injected callbacks.
161
170
  Direct global `fetch`, XHR, WebSocket and EventSource use is rejected. Renderer
@@ -400,10 +409,11 @@ Validate accepted and rejected cases, configuration, and false-positive limits.
400
409
 
401
410
  ## Bootstrap and updates
402
411
 
403
- The atomic composition guide lives in each skill's `references/atomic-styles.md`.
404
- The maintained copy is in `skills/ads-designer/references/atomic-styles.md`;
405
- MCP serves that packaged skill reference. `npm run sync:skill-references` copies
406
- it to the other skills for standalone use; `npm run check:skill-references`
412
+ The atomic composition and scenario contract guides live in each skill's
413
+ `references/atomic-styles.md` and `references/scenarios.md`. The maintained copies
414
+ are in `skills/ads-designer/references/`; MCP serves those packaged references.
415
+ `npm run sync:skill-references` copies them to the other skills for standalone use;
416
+ `npm run check:skill-references`
407
417
  rejects drift during the tool build. Published packages include these references
408
418
  under `dist/skills/`.
409
419
 
@@ -146,6 +146,14 @@ of every unused variant. Atomic composition still requires care with the cascade
146
146
  Apply extraction to application and project preview builds, preserve intentional
147
147
  foundation CSS, and inspect compiled output when performance matters. The
148
148
  [implementation guide](../design-system/README.md#bundling-notes) covers these details.
149
+ Both extraction pipelines use the build-only `@reqvire-org/ads/linaria` tag
150
+ resolver. It delegates atomization and nesting to the project's installed Linaria
151
+ processor while preserving custom-property token streams around that pass.
152
+ Arithmetic becomes meaningful after `var()` substitution, so stripping whitespace
153
+ can invalidate otherwise valid source. Verify emitted CSS and computed browser
154
+ values; source guards alone cannot establish extraction correctness. The
155
+ skill-local [atomic styling guide](../skills/ads-designer/references/atomic-styles.md#preserve-custom-property-arithmetic-during-extraction)
156
+ documents integration and source-workaround semantics.
149
157
 
150
158
  ## Public APIs preserve the boundary
151
159
 
@@ -286,6 +294,14 @@ their required parent. Every declared token and every public renderable componen
286
294
  and pattern needs addressable coverage. Token references must exist in the real
287
295
  foundation; claimed token/UI coverage must be reachable from the scenario entry.
288
296
  Types, constants and utilities do not require UI scenarios.
297
+ Explicitly declared public children can share a parent's scenario when bounded
298
+ source tracing reaches them through its returned JSX and selected props/defaults.
299
+ Opaque conditional branches do not grant speculative coverage. Scenarios may
300
+ consume deliberate public class helpers when their returned styles retain
301
+ runtime-owned Linaria provenance; stable owner-authored hooks may accompany real
302
+ styles. This preserves ownership rather than granting scenario styling freedom.
303
+ The skill-local [scenario guide](../skills/ads-designer/references/scenarios.md)
304
+ explains evidence, limits and diagnostics.
289
305
 
290
306
  After source changes, validation and compilation must both succeed before a new
291
307
  graph becomes current. MCP announces build/resource updates. The built-in client
package/docs/BEHAVIORS.md CHANGED
@@ -140,7 +140,17 @@ and any required test setup in that project. The ADS tooling host gains no
140
140
  framework or browser dependency from a profile.
141
141
 
142
142
  The Playwright adapter appends selected file paths, its reporter and a generated
143
- artifact directory. Use native test tags for stable case identity:
143
+ artifact directory.
144
+
145
+ Runner failures remain blocking even when some selected cases reported passed.
146
+ Those results retain `reportedStatus` and `runnerExitCode`, and explain the suite
147
+ failure rather than returning an empty message. Suite-level browser errors and
148
+ bounded runner output appear in diagnostics. `openBehaviorScenario` includes the
149
+ scenario ID and failed browser requests in startup failures; App resource errors
150
+ identify the resource URI. Failed assertions and startup errors are not retried
151
+ automatically. Fix or investigate the failure before explicitly verifying again.
152
+
153
+ Use native test tags for stable case identity:
144
154
 
145
155
  ```ts
146
156
  import { test, expect } from "@playwright/test";
package/docs/CHANGELOG.md CHANGED
@@ -1,6 +1,23 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.3.9
4
+
5
+ - Credit explicitly declared public child renders reached through parents,
6
+ fixture props/defaults and known framework wrappers/providers. Share bounded
7
+ source evaluation with atomic review; unresolved paths, unused JSX and
8
+ callbacks do not manufacture child coverage.
9
+ - Allow scenario class helpers with proven runtime Linaria provenance and stable
10
+ owner-authored hooks alongside real styles. Reject consumer strings, private
11
+ styles, empty templates and untraceable returns; document the contract in all
12
+ five portable skills and `ads://guides/scenarios`.
13
+ - Preserve custom-property token streams through Linaria atomic extraction with
14
+ the build-only `@reqvire-org/ads/linaria` resolver. Integrate the resolver in
15
+ product and MCP preview starter builds; verify extracted arithmetic and real
16
+ browser transforms using generic fixtures with and without CSS minification.
17
+ - Explain suite-wide verification failures on otherwise passing cases, retain
18
+ their reported status, and surface runner-level errors. Include scenario IDs,
19
+ failed requests and App resource URIs in startup diagnostics without retrying
20
+ failed checks automatically.
4
21
 
5
22
  ## 0.3.8
6
23
 
package/docs/MCP.md CHANGED
@@ -441,6 +441,12 @@ project's normal lint/build gates.
441
441
 
442
442
  `ads_scenarios` discovers path-derived IDs for token, component, pattern and
443
443
  composition scenarios. `ads_catalog` links public exports to their coverage.
444
+ `covers` can include public children reached through the rendered parent's
445
+ returned JSX with known props/defaults. Public class helpers require verified
446
+ runtime Linaria provenance; `exercises` alone grants no class permission. The
447
+ `ads://guides/scenarios` resource and each skill's
448
+ [local scenario reference](../skills/ads-designer/references/scenarios.md) describe
449
+ the bounded checks and diagnostics.
444
450
  `ads_show_scenario({id})` selects an independently mountable entry with its own
445
451
  fixtures. `ads_show_tokens({category})` selects a discovered token scenario;
446
452
  omitting category selects the first token entry. The tool contains no project
@@ -557,6 +563,12 @@ framework compilation and Linaria extraction. The starter's
557
563
  - Keep `preserveEntrySignatures: "strict"`, `cssCodeSplit: true`,
558
564
  `assetsInlineLimit: 0`, `modulePreload: false`, and `write: false`. Include the
559
565
  supplied dependency-tracking plugin and the project's Linaria/framework plugins.
566
+ - Use `linariaTagResolver` from `@reqvire-org/ads/linaria` in WyW's `tagResolver`
567
+ option in both the product build and preview builder. It preserves opaque
568
+ custom-property values through atomic extraction, including whitespace needed
569
+ by arithmetic later substituted into `calc()`. Existing custom builders need
570
+ this integration explicitly; disabling minification does not fix extraction.
571
+ See the skill-local [atomic styling guide](../skills/ads-designer/references/atomic-styles.md#preserve-custom-property-arithmetic-during-extraction).
560
572
  - `showcase/mcp/main.tsx` exports `mountScenario(Scenario, container)` and returns
561
573
  an unmount callback. It imports the same project foundation used in production.
562
574
  It creates no App bridge; ADS supplies that bridge once.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reqvire-org/ads",
3
- "version": "0.3.8",
3
+ "version": "0.3.9",
4
4
  "type": "module",
5
5
  "description": "A design system your agent can see and shape. Your components. Your look. Your code.",
6
6
  "repository": {
@@ -87,6 +87,14 @@
87
87
  "vite": "^6.0.7"
88
88
  },
89
89
  "license": "MIT",
90
+ "peerDependencies": {
91
+ "@linaria/atomic": "^8.0.0"
92
+ },
93
+ "peerDependenciesMeta": {
94
+ "@linaria/atomic": {
95
+ "optional": true
96
+ }
97
+ },
90
98
  "engines": {
91
99
  "node": ">=22.12.0"
92
100
  },
@@ -96,6 +104,7 @@
96
104
  "exports": {
97
105
  ".": "./dist/index.mjs",
98
106
  "./oxlint": "./dist/oxlint.mjs",
107
+ "./linaria": "./dist/linaria.mjs",
99
108
  "./tokens": "./dist/contracts/tokens.json",
100
109
  "./mcp": "./dist/mcp/server.mjs",
101
110
  "./mcp/build": "./dist/mcp/build-status.mjs",