@reqvire-org/ads 0.3.8 → 0.3.10

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-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/preview-resources.mjs +2 -1
  14. package/dist/scenario-trace.mjs +149 -0
  15. package/dist/scenarios-compiler.mjs +2 -1
  16. package/dist/scenarios.mjs +13 -6
  17. package/dist/skills/ads-break-ui/SKILL.md +9 -2
  18. package/dist/skills/ads-break-ui/references/atomic-styles.md +50 -0
  19. package/dist/skills/ads-break-ui/references/scenarios.md +98 -0
  20. package/dist/skills/ads-designer/SKILL.md +12 -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 +50 -0
  24. package/dist/skills/ads-designer/references/scenarios.md +98 -0
  25. package/dist/skills/ads-import/SKILL.md +9 -2
  26. package/dist/skills/ads-import/references/atomic-styles.md +50 -0
  27. package/dist/skills/ads-import/references/scenarios.md +98 -0
  28. package/dist/skills/ads-migrate/SKILL.md +9 -2
  29. package/dist/skills/ads-migrate/references/atomic-styles.md +50 -0
  30. package/dist/skills/ads-migrate/references/scenarios.md +98 -0
  31. package/dist/skills/ads-motion/SKILL.md +9 -2
  32. package/dist/skills/ads-motion/references/atomic-styles.md +50 -0
  33. package/dist/skills/ads-motion/references/scenarios.md +98 -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 +13 -4
  38. package/docs/ADS.md +20 -5
  39. package/docs/ARCHITECTURE.md +18 -0
  40. package/docs/BEHAVIORS.md +11 -1
  41. package/docs/CHANGELOG.md +27 -1
  42. package/docs/MCP.md +16 -0
  43. 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
  };
@@ -170,7 +170,11 @@ export function createVerificationController(root) {
170
170
  if (profile.mode === "browser" && group.some(check => check.scenario)) {
171
171
  const { startAdsMcp } = await import("./mcp/server.mjs");
172
172
  host ??= await startAdsMcp(root, { stdio: false, showcase: true });
173
- for (const check of group.filter(check => check.scenario)) { const url = new URL(host.url); url.searchParams.set("scenario", check.scenario); scenarios[check.scenario] = url.href; }
173
+ for (const check of group.filter(check => check.scenario)) {
174
+ const url = new URL(host.url), scenario = context.scenarios.find(entry => entry.id === check.scenario);
175
+ url.searchParams.set(scenario?.kind === "tokens" ? "category" : "scenario", check.scenario);
176
+ scenarios[check.scenario] = url.href;
177
+ }
174
178
  }
175
179
  const requestContext = { schemaVersion: 1, runId, projectRoot: root, framework: options.config.framework, profile: profileName, mode: profile.mode ?? "unit", checks: group.map(({ contract: _contract, ...check }) => check), scenarios };
176
180
  writeFileSync(request, JSON.stringify(requestContext));
@@ -182,19 +186,24 @@ export function createVerificationController(root) {
182
186
  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
187
  reportValue = JSON.parse(readFileSync(report, "utf8"));
184
188
  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 }); }
189
+ } catch (error) { runnerError = error; reportValue = undefined; diagnostics.push({ rule: "behavior-runner", message: error.message, output: error.output?.slice(-4000), profile: profileName }); }
190
+ const suiteErrors = Array.isArray(reportValue?.errors) ? reportValue.errors.slice(0, 5).map(error => String(error).slice(0, 1000)) : [];
191
+ 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;
192
+ if (runnerFailure) diagnostics.push({ rule: "behavior-runner", message: runnerFailure, output: run.output?.slice(-4000), profile: profileName });
186
193
  const afterOptions = loadConfig(root), afterContext = inputContext(afterOptions);
187
194
  for (const check of group) {
188
195
  const file = slash(path.relative(root, path.resolve(root, path.dirname(check.contract.file), check.file)));
189
196
  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";
197
+ const reportedStatus = matches.length && matches.every(item => item.status === "passed") ? "passed" : matches.length && matches.every(item => item.status === "skipped") ? "skipped" : "failed";
198
+ let status = !run || runnerFailure ? "failed" : reportedStatus;
191
199
  if (runnerError?.code === "ENOENT") status = "unavailable";
192
200
  const current = behaviorInputs(afterOptions, check.contract, check, afterContext).revision === check.inputRevision;
193
201
  if (!current) status = "stale";
194
202
  if (signal.aborted) status = "cancelled";
195
203
  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
204
  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)) });
205
+ ...(runnerFailure ? { reportedStatus, runnerExitCode: run.code } : {}),
206
+ 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
207
  }
199
208
  if (signal.aborted) break;
200
209
  }
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
@@ -200,6 +209,11 @@ Measured runtime variables retain their existing exact `styleWrites` boundary.
200
209
 
201
210
  A colocated scenario sets `kind:"tokens"` and declares real fixture `samples`
202
211
  consumed by a public review pattern. Its ID still derives from its source path.
212
+ Discovery attaches its owner's registered definition sheet as `stylesheets` on
213
+ the generated scenario record; `previewEntriesPlugin` imports it directly into
214
+ that preview's module graph. CSS-only package side effects can then tree-shake
215
+ the runtime component without dropping the token definitions. Keep the runtime
216
+ CSS import for product use; the token scenario need not import or render its owner.
203
217
  Every local token needs this real sample coverage, just like shared tokens.
204
218
  Definitions, metadata, fixture and token scenario belong to the same edit unit.
205
219
  Manifests/fixtures are development inputs; production includes the runtime's
@@ -400,10 +414,11 @@ Validate accepted and rejected cases, configuration, and false-positive limits.
400
414
 
401
415
  ## Bootstrap and updates
402
416
 
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`
417
+ The atomic composition and scenario contract guides live in each skill's
418
+ `references/atomic-styles.md` and `references/scenarios.md`. The maintained copies
419
+ are in `skills/ads-designer/references/`; MCP serves those packaged references.
420
+ `npm run sync:skill-references` copies them to the other skills for standalone use;
421
+ `npm run check:skill-references`
407
422
  rejects drift during the tool build. Published packages include these references
408
423
  under `dist/skills/`.
409
424
 
@@ -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
@@ -418,6 +434,8 @@ verification, and the browser check does not judge design quality.
418
434
  An optional owner-local token namespace keeps component/pattern constants beside
419
435
  their consumer. Its definition-only stylesheet contains root/theme custom
420
436
  properties and is imported by the runtime owner; UI styling remains in Linaria.
437
+ Independent owner-token preview entries also import the registered definition
438
+ sheet directly, so tree-shaking an unused runtime owner cannot remove their values.
421
439
  Namespaces, aliases, ownership and actual colocated sample coverage are checked.
422
440
  The shared foundation remains the cross-owner vocabulary. See the
423
441
  [owner-token contract](ADS.md#owner-local-tokens).
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,32 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.3.10
4
+
5
+ - Attach registered owner-local token sheets to independently compiled token
6
+ previews, preserving definitions under CSS-only package side effects without
7
+ retaining runtime owners or loading peer sheets. Verify resource closure in
8
+ React/Preact and rendered samples in a sandboxed App in both themes.
9
+ - Route managed browser checks for owner-local token scenarios through the
10
+ discovered token category, independent of their component/pattern file paths.
11
+
12
+ ## 0.3.9
13
+
14
+ - Credit explicitly declared public child renders reached through parents,
15
+ fixture props/defaults and known framework wrappers/providers. Share bounded
16
+ source evaluation with atomic review; unresolved paths, unused JSX and
17
+ callbacks do not manufacture child coverage.
18
+ - Allow scenario class helpers with proven runtime Linaria provenance and stable
19
+ owner-authored hooks alongside real styles. Reject consumer strings, private
20
+ styles, empty templates and untraceable returns; document the contract in all
21
+ five portable skills and `ads://guides/scenarios`.
22
+ - Preserve custom-property token streams through Linaria atomic extraction with
23
+ the build-only `@reqvire-org/ads/linaria` resolver. Integrate the resolver in
24
+ product and MCP preview starter builds; verify extracted arithmetic and real
25
+ browser transforms using generic fixtures with and without CSS minification.
26
+ - Explain suite-wide verification failures on otherwise passing cases, retain
27
+ their reported status, and surface runner-level errors. Include scenario IDs,
28
+ failed requests and App resource URIs in startup diagnostics without retrying
29
+ failed checks automatically.
4
30
 
5
31
  ## 0.3.8
6
32
 
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
@@ -554,9 +560,19 @@ framework compilation and Linaria extraction. The starter's
554
560
  - Its `buildGraph(entries, options, track)` adapter runs one multi-entry Vite build
555
561
  with `previewEntriesPlugin()` and `createPreviewResources()` from
556
562
  `@reqvire-org/ads/scenarios/vite`.
563
+ Generated owner-token entries include `stylesheets` derived from the registered
564
+ owner manifest. Pass the entries intact: the plugin imports those definition
565
+ sheets even when the runtime owner is tree-shaken. A replacement entry plugin
566
+ must preserve these dependencies without importing unrelated owners.
557
567
  - Keep `preserveEntrySignatures: "strict"`, `cssCodeSplit: true`,
558
568
  `assetsInlineLimit: 0`, `modulePreload: false`, and `write: false`. Include the
559
569
  supplied dependency-tracking plugin and the project's Linaria/framework plugins.
570
+ - Use `linariaTagResolver` from `@reqvire-org/ads/linaria` in WyW's `tagResolver`
571
+ option in both the product build and preview builder. It preserves opaque
572
+ custom-property values through atomic extraction, including whitespace needed
573
+ by arithmetic later substituted into `calc()`. Existing custom builders need
574
+ this integration explicitly; disabling minification does not fix extraction.
575
+ See the skill-local [atomic styling guide](../skills/ads-designer/references/atomic-styles.md#preserve-custom-property-arithmetic-during-extraction).
560
576
  - `showcase/mcp/main.tsx` exports `mountScenario(Scenario, container)` and returns
561
577
  an unmount callback. It imports the same project foundation used in production.
562
578
  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.10",
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",