@reqvire-org/ads 0.3.7 → 0.3.8

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.
@@ -1,7 +1,9 @@
1
1
  import path from "node:path";
2
2
  import ts from "typescript";
3
3
  import postcss from "postcss";
4
+ import valueParser from "postcss-value-parser";
4
5
  import { isInside } from "./config.mjs";
6
+ import { createAtomicTrace } from "./atomic-trace.mjs";
5
7
 
6
8
  export const atomicStyleGuide = "ads://guides/atomic-styles";
7
9
 
@@ -62,6 +64,67 @@ function overlaps(a, b) {
62
64
  return intersects(longhands.get(a) ?? [a], longhands.get(b) ?? [b]);
63
65
  }
64
66
 
67
+ const modes = ["horizontal-tb", "vertical-rl", "vertical-lr"].flatMap(mode => ["ltr", "rtl"].map(direction => ({ mode, direction })));
68
+ function physicalSlots(property, { mode, direction }) {
69
+ const value = footprint(property);
70
+ if (!value) return null;
71
+ if (value.whole) return sides;
72
+ const horizontal = mode === "horizontal-tb";
73
+ const axes = {
74
+ "block-start": horizontal ? "top" : mode === "vertical-rl" ? "right" : "left",
75
+ "block-end": horizontal ? "bottom" : mode === "vertical-rl" ? "left" : "right",
76
+ "inline-start": horizontal ? direction === "ltr" ? "left" : "right" : direction === "ltr" ? "top" : "bottom",
77
+ "inline-end": horizontal ? direction === "ltr" ? "right" : "left" : direction === "ltr" ? "bottom" : "top",
78
+ "inline-size": horizontal ? "width" : "height", "block-size": horizontal ? "height" : "width",
79
+ inline: horizontal ? "x" : "y", block: horizontal ? "y" : "x",
80
+ };
81
+ return value.slots.map(slot => value.family === "radius" && slot ? (/^(start|end)-/.test(slot) ? [axes[`block-${slot.split("-")[0]}`], axes[`inline-${slot.split("-")[1]}`]] : slot.split("-")).sort().join("-") : axes[slot] ?? slot);
82
+ }
83
+ function writingModeDependent(a, b) {
84
+ const first = footprint(a), second = footprint(b);
85
+ if (!first || !second || first.family !== second.family || first.whole || second.whole || first.logical === second.logical) return false;
86
+ const matches = modes.map(mode => intersects(physicalSlots(a, mode), physicalSlots(b, mode)));
87
+ return matches.some(Boolean) && !matches.every(Boolean);
88
+ }
89
+ function words(value) {
90
+ const nodes = valueParser(value).nodes;
91
+ return nodes.every(node => ["word", "space", "comment"].includes(node.type)) ? nodes.filter(node => node.type === "word").map(node => node.value.toLowerCase()) : null;
92
+ }
93
+ function boxValues(entry, mode) {
94
+ const value = footprint(entry.property), parts = words(entry.value);
95
+ if (!value || !["padding", "margin", "scroll-padding", "scroll-margin", "inset"].includes(value.family) || !parts?.length || parts.length > 4 || parts.some(part => ["inherit", "initial", "unset", "revert", "revert-layer"].includes(part))) return null;
96
+ const slots = physicalSlots(entry.property, mode);
97
+ if (!value.whole && parts.length > slots.length) return null;
98
+ const expanded = value.whole ? [parts[0], parts[1] ?? parts[0], parts[2] ?? parts[0], parts[3] ?? parts[1] ?? parts[0]] : slots.map((_slot, index) => parts[index] ?? parts[0]);
99
+ return new Map(slots.map((slot, index) => [slot, expanded[index]]));
100
+ }
101
+ function equivalent(a, b) {
102
+ // Only suppress values whose overlap can be established from literal source.
103
+ // var(), calc(), inheritance and token aliases still need rendered review.
104
+ const first = footprint(a.property), second = footprint(b.property);
105
+ if (first && second && first.family === second.family && ["padding", "margin", "scroll-padding", "scroll-margin", "inset"].includes(first.family)) {
106
+ return modes.every(mode => {
107
+ const left = boxValues(a, mode), right = boxValues(b, mode);
108
+ return left && right && [...left].every(([slot, value]) => !right.has(slot) || right.get(slot) === value);
109
+ });
110
+ }
111
+ const flex = a.property === "flex" ? a : b.property === "flex" ? b : null;
112
+ const field = flex === a ? b : a;
113
+ if (flex && ["flex-grow", "flex-shrink", "flex-basis"].includes(field.property)) {
114
+ const parts = words(flex.value);
115
+ return parts?.length === 3 && /^\d+(?:\.\d+)?$/.test(parts[0]) && /^\d+(?:\.\d+)?$/.test(parts[1]) && /^(?:auto|0|\d+(?:\.\d+)?(?:px|rem|em|%))$/.test(parts[2]) && parts[["flex-grow", "flex-shrink", "flex-basis"].indexOf(field.property)] === field.value.trim().toLowerCase();
116
+ }
117
+ return false;
118
+ }
119
+
120
+ function valueModeDependent(a, b) {
121
+ const equal = modes.map(mode => {
122
+ const left = boxValues(a, mode), right = boxValues(b, mode);
123
+ return left && right ? [...left].every(([slot, value]) => !right.has(slot) || right.get(slot) === value) : null;
124
+ });
125
+ return !equal.includes(null) && equal.some(Boolean) && !equal.every(Boolean);
126
+ }
127
+
65
128
  function unwrap(node) {
66
129
  while (node && (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isSatisfiesExpression(node) || ts.isNonNullExpression(node))) node = node.expression;
67
130
  return node;
@@ -70,11 +133,6 @@ function unwrap(node) {
70
133
  /** Review only: valid CSS can intentionally overlap. Never rewrite project CSS. */
71
134
  export function createAtomicCompositionReview(program, options) {
72
135
  const checker = program.getTypeChecker(), templates = new Map();
73
- function declaration(node) {
74
- let symbol = checker.getSymbolAtLocation(ts.isPropertyAccessExpression(node) ? node.name : node);
75
- if (symbol?.flags & ts.SymbolFlags.Alias) symbol = checker.getAliasedSymbol(symbol);
76
- return symbol?.valueDeclaration;
77
- }
78
136
  function imported(node, names, modules, seen = new Set()) {
79
137
  node = unwrap(node);
80
138
  if (!node || seen.has(node)) return false;
@@ -117,67 +175,40 @@ export function createAtomicCompositionReview(program, options) {
117
175
  } catch { /* The core CSS syntax diagnostic owns malformed templates. */ }
118
176
  return result;
119
177
  }
120
- function object(node, seen = new Set()) {
121
- node = unwrap(node);
122
- if (!node || seen.has(node)) return null;
123
- if (ts.isObjectLiteralExpression(node) || ts.isArrayLiteralExpression(node)) return node;
124
- const owner = declaration(node);
125
- return owner?.initializer ? object(owner.initializer, new Set([...seen, node])) : null;
126
- }
127
- function styles(node, seen = new Set()) {
128
- node = unwrap(node);
129
- if (!node || seen.has(node)) return [];
130
- seen = new Set([...seen, node]);
131
- if (ts.isTaggedTemplateExpression(node) && isAtomic(node.tag)) return entries(node);
132
- if (ts.isConditionalExpression(node)) return [...styles(node.whenTrue, seen), ...styles(node.whenFalse, seen)];
133
- if (ts.isBinaryExpression(node) && [ts.SyntaxKind.AmpersandAmpersandToken, ts.SyntaxKind.BarBarToken, ts.SyntaxKind.QuestionQuestionToken].includes(node.operatorToken.kind)) return [...(node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken ? [] : styles(node.left, seen)), ...styles(node.right, seen)];
134
- if (ts.isCallExpression(node) && isCx(node.expression)) return node.arguments.flatMap(argument => styles(argument, seen));
135
- if (ts.isSpreadElement(node)) {
136
- const value = object(node.expression);
137
- return value && ts.isArrayLiteralExpression(value) ? value.elements.flatMap(element => styles(element, seen)) : [];
138
- }
139
- const owner = declaration(node);
140
- if (owner?.initializer) return styles(owner.initializer, seen);
141
- if (ts.isElementAccessExpression(node) || ts.isPropertyAccessExpression(node)) {
142
- const value = object(node.expression);
143
- if (!value || !ts.isObjectLiteralExpression(value)) return [];
144
- const key = ts.isPropertyAccessExpression(node) ? node.name.text : node.argumentExpression && ts.isStringLiteralLike(node.argumentExpression) ? node.argumentExpression.text : null;
145
- return value.properties.flatMap(property => {
146
- const name = property.name && (ts.isIdentifier(property.name) || ts.isStringLiteralLike(property.name)) ? property.name.text : null;
147
- return key !== null && name !== key ? [] : ts.isPropertyAssignment(property) ? styles(property.initializer, seen) : ts.isShorthandPropertyAssignment(property) ? styles(property.name, seen) : [];
148
- });
149
- }
150
- return [];
151
- }
178
+ const trace = createAtomicTrace(program, options, { entries, isAtomic, isCx });
152
179
  return source => {
153
- const warnings = [], seen = new Set();
154
- function compare(first, second, location) {
180
+ const warnings = new Map(), limits = new Map();
181
+ const relative = entry => ({ ...entry, file: path.relative(options.root, entry.file).replaceAll("\\", "/") });
182
+ function compare(first, second, location, context = {}) {
155
183
  for (const a of first) for (const b of second) {
156
- if (a === b || a.selectors !== b.selectors || a.conditions !== b.conditions || a.important !== b.important || a.value === "0" && b.value === "0" || !overlaps(a.property, b.property)) continue;
184
+ if (a === b || a.selectors !== b.selectors || a.conditions !== b.conditions || a.important !== b.important || !overlaps(a.property, b.property) || equivalent(a, b)) continue;
157
185
  const key = JSON.stringify([a, b].map(entry => [entry.file, entry.line, entry.column]).sort());
158
- if (seen.has(key)) continue;
159
- seen.add(key);
186
+ const composition = [...new Map([...(context.path ?? []), ...(a.via ?? []), ...(b.via ?? [])].map(entry => [JSON.stringify(entry), relative(entry)])).values()].slice(0, 16);
187
+ const previous = warnings.get(key);
188
+ if (previous && (previous.target || !context.target) && previous.composition.length >= composition.length) continue;
160
189
  const position = source.getLineAndCharacterOfPosition(location.getStart(source));
161
- warnings.push({ rule: "atomic-composition-review", severity: "warning", line: position.line + 1, column: position.character + 1,
162
- message: `Atomic declarations ${a.property}: ${previewValue(a.value)} and ${b.property}: ${previewValue(b.value)} may overlap without matching cx merge keys. Their winner can depend on extraction order, specificity or writing mode. Use consistent property names/granularity across the base and variants, compose with Linaria cx, and verify computed styles in the built preview. Review all affected variants; valid tokens do not establish cascade correctness. See ${atomicStyleGuide}.`,
163
- guidanceUri: atomicStyleGuide, related: [a, b].map(({ selectors: _selectors, conditions: _conditions, important: _important, ...entry }) => ({...entry,value:previewValue(entry.value)})) });
190
+ const dependent = writingModeDependent(a.property, b.property) || valueModeDependent(a, b);
191
+ warnings.set(key, { rule: "atomic-composition-review", severity: "warning", line: position.line + 1, column: position.character + 1,
192
+ message: `Atomic declarations ${a.property}: ${previewValue(a.value)} and ${b.property}: ${previewValue(b.value)} may overlap without matching cx merge keys.${context.target ? ` Source tracing reaches the same <${context.target.name}> className.` : " This is a source composition risk, not a confirmed rendered conflict."}${dependent ? " The overlap depends on writing mode or direction." : ""} Their winner can depend on extraction order, specificity or writing mode. Use consistent property names/granularity across the base and variants, compose with Linaria cx, and verify computed styles in the built preview. Review all affected variants; valid tokens do not establish cascade correctness. See ${atomicStyleGuide}.`,
193
+ guidanceUri: atomicStyleGuide, writingModeDependent: dependent, composition, ...(context.target ? { target: relative(context.target) } : {}), ...(context.limits?.length ? { analysisLimits: [...new Set(context.limits)].slice(0, 5) } : {}),
194
+ related: [a, b].map(({ selectors: _selectors, conditions: _conditions, important: _important, via: _via, ...entry }) => ({...entry,value:previewValue(entry.value)})) });
164
195
  }
165
196
  }
197
+ function limit(node, reasons, composition) {
198
+ const key = reasons.join("\n");
199
+ if (limits.has(key) || limits.size >= 5) return;
200
+ const position = source.getLineAndCharacterOfPosition(node.getStart(source));
201
+ limits.set(key, { rule: "atomic-composition-limit", severity: "info", line: position.line + 1, column: position.character + 1,
202
+ message: `Atomic source review is incomplete: ${[...new Set(reasons)].slice(0, 3).join(" ")} Review the actual DOM classes and computed styles in a built scenario. An unresolved path is not evidence that composition is safe. See ${atomicStyleGuide}.`, guidanceUri: atomicStyleGuide, composition: composition.slice(0, 16).map(relative) });
203
+ }
166
204
  function visit(node) {
167
205
  if (ts.isTaggedTemplateExpression(node) && isAtomic(node.tag)) {
168
206
  const declarations = entries(node);
169
207
  for (let index = 0; index < declarations.length; index++) compare([declarations[index]], declarations.slice(index + 1), node);
170
208
  }
171
- if (ts.isCallExpression(node) && isCx(node.expression)) {
172
- const groups = node.arguments.flatMap(argument => {
173
- const value = ts.isSpreadElement(argument) ? object(argument.expression) : null;
174
- return value && ts.isArrayLiteralExpression(value) ? value.elements.map(element => styles(element)) : [styles(argument)];
175
- });
176
- for (let index = 0; index < groups.length; index++) for (let next = index + 1; next < groups.length; next++) compare(groups[index], groups[next], node);
177
- }
178
209
  ts.forEachChild(node, visit);
179
210
  }
180
- if (isInside(options.dsRoot, source.fileName)) visit(source);
181
- return warnings;
211
+ if (isInside(options.dsRoot, source.fileName)) { visit(source); trace(source, { compare, limit }); }
212
+ return [...warnings.values(), ...limits.values()];
182
213
  };
183
214
  }
@@ -0,0 +1,287 @@
1
+ import ts from "typescript";
2
+ import { isInside } from "./config.mjs";
3
+ import { uiOperation } from "./ui-framework.mjs";
4
+
5
+ const unknown = reason => ({ kind: "unknown", reason });
6
+ const literal = value => ({ kind: "literal", value });
7
+ const reference = (node, env = new Map(), via = []) => node ? { kind: "node", node, env, via } : literal(undefined);
8
+ const union = values => values.length === 1 ? values[0] : { kind: "union", values };
9
+ 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
+ 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 createAtomicTrace(program, options, { entries, isAtomic, isCx }) {
18
+ const checker = program.getTypeChecker();
19
+ const symbol = node => checker.getSymbolAtLocation(ts.isPropertyAccessExpression(node) ? node.name : node);
20
+ function declaration(node) {
21
+ let value = symbol(node);
22
+ if (value?.flags & ts.SymbolFlags.Alias) value = checker.getAliasedSymbol(value);
23
+ return value?.valueDeclaration;
24
+ }
25
+ const state = () => ({ budget: { steps: 0 }, depth: 0, calls: new Set() });
26
+ function next(context) { return { ...context, depth: context.depth + 1 }; }
27
+ function exhausted(context) { return ++context.budget.steps > 4000 || context.depth > 48; }
28
+ function at(node, kind, name) {
29
+ const source = node.getSourceFile(), position = source.getLineAndCharacterOfPosition(node.getStart(source));
30
+ return { file: source.fileName, line: position.line + 1, column: position.character + 1, kind, name: name.slice(0, 100) };
31
+ }
32
+ function withVia(value, via) {
33
+ if (value.kind === "union") return { ...value, values: value.values.map(item => withVia(item, via)) };
34
+ return value.kind === "node" ? { ...value, via: [...new Map([...via, ...value.via].map(item => [JSON.stringify(item), item])).values()].slice(0, 16) } : value;
35
+ }
36
+ function withDefault(value, fallback, context) {
37
+ if (!fallback) return value;
38
+ value = resolve(value, next(context));
39
+ if (value.kind === "literal") return value.value === undefined ? fallback : value;
40
+ if (value.kind === "unknown") return union([value, fallback]);
41
+ if (value.kind === "union") return union(value.values.map(item => withDefault(item, fallback, next(context))));
42
+ return value;
43
+ }
44
+ function bind(pattern, value, env, via, context) {
45
+ if (ts.isIdentifier(pattern)) { env.set(symbol(pattern), value); return; }
46
+ if (ts.isObjectBindingPattern(pattern)) {
47
+ const excluded = new Set();
48
+ for (const part of pattern.elements) {
49
+ const key = nameOf(part.propertyName ?? part.name);
50
+ if (part.dotDotDotToken) bind(part.name, { kind: "rest", value, excluded: new Set(excluded) }, env, via, next(context));
51
+ else {
52
+ if (key === undefined) continue;
53
+ excluded.add(key);
54
+ bind(part.name, withDefault(get(value, key, next(context)), part.initializer && reference(part.initializer, env, via), context), env, via, next(context));
55
+ }
56
+ }
57
+ } else if (ts.isArrayBindingPattern(pattern)) {
58
+ pattern.elements.forEach((part, index) => {
59
+ 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));
60
+ });
61
+ }
62
+ }
63
+ function boundElement(node, env, via, context) {
64
+ let owner = node;
65
+ while (ts.isBindingElement(owner) || ts.isObjectBindingPattern(owner) || ts.isArrayBindingPattern(owner)) owner = owner.parent;
66
+ if (!ts.isVariableDeclaration(owner) || !owner.initializer || !(owner.parent.flags & ts.NodeFlags.Const)) return unknown();
67
+ const local = new Map(env);
68
+ bind(owner.name, reference(owner.initializer, env, via), local, via, next(context));
69
+ return local.get(symbol(node.name)) ?? unknown();
70
+ }
71
+ function get(value, key, context) {
72
+ value = resolve(value, next(context));
73
+ if (value.kind === "union") return union(value.values.map(item => get(item, key, next(context))));
74
+ if (value.kind === "rest") return value.excluded.has(key) ? literal(undefined) : get(value.value, key, next(context));
75
+ const layers = value.kind === "object" ? value.layers : [];
76
+ if (value.kind === "node" && ts.isObjectLiteralExpression(value.node)) {
77
+ for (const property of value.node.properties) {
78
+ if (ts.isSpreadAssignment(property)) layers.push(reference(property.expression, value.env, value.via));
79
+ else {
80
+ const name = nameOf(property.name);
81
+ if (name !== undefined) layers.push(new Map([[name, reference(ts.isShorthandPropertyAssignment(property) ? property.name : ts.isMethodDeclaration(property) ? property : property.initializer, value.env, value.via)]]));
82
+ }
83
+ }
84
+ } else if (value.kind === "node" && ts.isArrayLiteralExpression(value.node)) {
85
+ return reference(value.node.elements[Number(key)], value.env, value.via);
86
+ } else if (value.kind !== "object") return unknown(value.reason);
87
+ for (let index = layers.length - 1; index >= 0; index--) {
88
+ const layer = layers[index];
89
+ if (layer instanceof Map) { if (layer.has(key)) return layer.get(key); }
90
+ else {
91
+ const selected = get(layer, key, next(context));
92
+ if (selected.kind === "literal" && selected.value === undefined) continue;
93
+ // An opaque later spread may replace an earlier explicit property.
94
+ if (selected.kind === "unknown" || selected.kind === "union") return union([selected, get({ kind: "object", layers: layers.slice(0, index) }, key, next(context))]);
95
+ return selected;
96
+ }
97
+ }
98
+ return literal(undefined);
99
+ }
100
+ function keys(value, context) {
101
+ value = resolve(value, next(context));
102
+ if (value.kind === "union") return [...new Set(value.values.flatMap(item => keys(item, next(context))))];
103
+ 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) ?? []);
104
+ if (value.kind === "object") return [...new Set(value.layers.flatMap(layer => layer instanceof Map ? [...layer.keys()] : keys(layer, next(context))))];
105
+ return [];
106
+ }
107
+ function returns(fn, args, context, kind = "helper") {
108
+ if (context.calls.has(fn.node)) return [unknown("Recursive local call could not be fully traced.")];
109
+ 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.")];
110
+ const env = new Map(fn.env), via = [...fn.via, at(fn.node, kind, fn.node.name?.getText() ?? "local function")];
111
+ const inner = { ...next(context), calls: new Set([...context.calls, fn.node]) };
112
+ for (const [index, parameter] of fn.node.parameters.entries()) {
113
+ if (parameter.dotDotDotToken) { bind(parameter.name, { kind: "array", values: args.slice(index) }, env, via, inner); break; }
114
+ bind(parameter.name, withDefault(resolve(args[index] ?? literal(undefined), inner), parameter.initializer && reference(parameter.initializer, env, via), inner), env, via, inner);
115
+ }
116
+ const outputs = [];
117
+ function statement(node) {
118
+ if (ts.isReturnStatement(node)) { outputs.push(reference(node.expression, env, via)); return true; }
119
+ if (ts.isBlock(node)) { for (const child of node.statements) if (statement(child)) return true; }
120
+ else if (ts.isIfStatement(node)) {
121
+ const condition = resolve(reference(node.expression, env, via), inner);
122
+ if (condition.kind === "literal") return condition.value ? statement(node.thenStatement) : node.elseStatement ? statement(node.elseStatement) : false;
123
+ const left = statement(node.thenStatement), right = node.elseStatement ? statement(node.elseStatement) : false;
124
+ return left && right;
125
+ } else if (ts.isThrowStatement(node)) return true;
126
+ 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."));
127
+ return false;
128
+ }
129
+ if (ts.isBlock(fn.node.body)) statement(fn.node.body);
130
+ else outputs.push(reference(fn.node.body, env, via));
131
+ // Keep call stacks with lazy return references, including nested helpers.
132
+ return outputs.length ? outputs.map(value => ({ ...value, context: inner })) : [literal(undefined)];
133
+ }
134
+ function resolve(value, context) {
135
+ if (!value) return literal(undefined);
136
+ context = value.context ?? context;
137
+ if (exhausted(context)) return unknown("Atomic source tracing reached its depth or work limit.");
138
+ if (value.kind !== "node") return value;
139
+ const node = unwrap(value.node), { env, via } = value;
140
+ if (ts.isStringLiteralLike(node) || ts.isNumericLiteral(node)) return literal(ts.isNumericLiteral(node) ? Number(node.text) : node.text);
141
+ if (node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword) return literal(node.kind === ts.SyntaxKind.TrueKeyword);
142
+ if (node.kind === ts.SyntaxKind.NullKeyword) return literal(null);
143
+ if (ts.isIdentifier(node)) {
144
+ if (env.has(symbol(node))) return resolve(env.get(symbol(node)), next(context));
145
+ const owner = declaration(node);
146
+ if (owner && isInside(options.dsRoot, owner.getSourceFile().fileName)) {
147
+ if (isFunction(owner)) return reference(owner, env, via);
148
+ if (ts.isVariableDeclaration(owner) && owner.initializer && owner.parent.flags & ts.NodeFlags.Const) return resolve(reference(owner.initializer, env, via), next(context));
149
+ if (ts.isBindingElement(owner)) return resolve(boundElement(owner, env, via, context), next(context));
150
+ }
151
+ return node.text === "undefined" && !owner ? literal(undefined) : unknown();
152
+ }
153
+ if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node)) {
154
+ const key = ts.isPropertyAccessExpression(node) ? literal(node.name.text) : resolve(reference(node.argumentExpression, env, via), next(context));
155
+ const object = reference(node.expression, env, via);
156
+ if (key.kind === "literal") {
157
+ const selected = get(object, String(key.value), next(context));
158
+ const owner = declaration(node);
159
+ if (selected.kind === "unknown" && owner && isInside(options.dsRoot, owner.getSourceFile().fileName)) {
160
+ if (isFunction(owner)) return reference(owner, env, via);
161
+ if (ts.isVariableDeclaration(owner) && owner.initializer && owner.parent.flags & ts.NodeFlags.Const) return resolve(reference(owner.initializer, env, via), next(context));
162
+ }
163
+ return resolve(selected, next(context));
164
+ }
165
+ const names = keys(object, next(context));
166
+ return names.length ? union(names.map(name => get(object, name, next(context)))) : unknown();
167
+ }
168
+ if (ts.isConditionalExpression(node)) {
169
+ const condition = resolve(reference(node.condition, env, via), next(context));
170
+ 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)]);
171
+ }
172
+ if (ts.isBinaryExpression(node)) {
173
+ const left = resolve(reference(node.left, env, via), next(context)), right = reference(node.right, env, via), operator = node.operatorToken.kind;
174
+ if (operator === ts.SyntaxKind.AmpersandAmpersandToken) return left.kind === "literal" ? left.value ? right : literal(undefined) : union([literal(undefined), right]);
175
+ if (operator === ts.SyntaxKind.BarBarToken) return left.kind === "literal" ? left.value ? left : right : union([left, right]);
176
+ if (operator === ts.SyntaxKind.QuestionQuestionToken) return left.kind === "literal" ? left.value == null ? right : left : union([left, right]);
177
+ const other = resolve(right, next(context));
178
+ 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);
179
+ }
180
+ if (ts.isPrefixUnaryExpression(node) && node.operator === ts.SyntaxKind.ExclamationToken) {
181
+ const operand = resolve(reference(node.operand, env, via), next(context));
182
+ return operand.kind === "literal" ? literal(!operand.value) : unknown();
183
+ }
184
+ if (ts.isCallExpression(node) && !isCx(node.expression)) {
185
+ if (["memo", "forwardRef"].includes(uiOperation(node.expression, checker))) return resolve(reference(node.arguments[0], env, via), next(context));
186
+ const fn = resolve(reference(node.expression, env, via), next(context));
187
+ if (fn.kind === "node" && isFunction(fn.node)) {
188
+ const outputs = returns(fn, node.arguments.map(argument => reference(argument, env, via)), next(context));
189
+ return withVia(resolve(union(outputs), next(context)), outputs.flatMap(item => item.via ?? []));
190
+ }
191
+ return unknown(`Call ${node.expression.getText().slice(0, 80)} could not be traced to a local function.`);
192
+ }
193
+ return { ...value, node };
194
+ }
195
+ function arrays(value, context) {
196
+ value = resolve(value, next(context));
197
+ if (value.kind === "array") return value.values;
198
+ if (value.kind === "node" && ts.isArrayLiteralExpression(value.node)) return value.node.elements.map(node => reference(node, value.env, value.via));
199
+ return null;
200
+ }
201
+
202
+ return function review(source, { compare, limit }) {
203
+ const maxBranches = 32;
204
+ const unique = branches => [...new Map(branches.map(branch => [JSON.stringify(branch.map(entry => [entry.file, entry.line, entry.column])), branch])).values()];
205
+ function styles(input, context) {
206
+ const value = resolve(input, next(context));
207
+ context = value.context ?? input.context ?? context;
208
+ if (value.kind === "unknown") return { branches: [[]], limits: value.reason ? [value.reason] : [] };
209
+ if (value.kind === "union") {
210
+ const results = value.values.slice(0, maxBranches).map(item => styles(item, next(context)));
211
+ const branches = unique(results.flatMap(item => item.branches));
212
+ return { branches: branches.slice(0, maxBranches), limits: [...results.flatMap(item => item.limits), ...(value.values.length > maxBranches || branches.length > maxBranches ? ["Atomic source tracing reached its alternative limit."] : [])] };
213
+ }
214
+ if (value.kind !== "node") return { branches: [[]], limits: [] };
215
+ const { node, env, via } = value;
216
+ if (ts.isTaggedTemplateExpression(node) && isAtomic(node.tag)) return { branches: [entries(node).map(entry => ({ ...entry, via }))], limits: [] };
217
+ if (ts.isCallExpression(node) && isCx(node.expression)) {
218
+ const inputs = node.arguments.flatMap(argument => ts.isSpreadElement(argument) ? arrays(reference(argument.expression, env, via), context) ?? [unknown("A dynamic class argument spread could not be traced.")] : [reference(argument, env, via)]);
219
+ let branches = [[]]; const limits = [];
220
+ for (const argument of inputs) {
221
+ const result = styles(argument, next(context));
222
+ limits.push(...result.limits);
223
+ const combined = new Map(); let truncated = false;
224
+ for (const first of branches) for (const second of result.branches) {
225
+ // cx replaces exact property/selector/condition keys; shorthand
226
+ // expansion remains the CSS overlap review's responsibility.
227
+ const merged = new Map([...first, ...second.map(entry => ({ ...entry, via: [...via, ...(entry.via ?? [])] }))].map(entry => [JSON.stringify([entry.property, entry.selectors, entry.conditions]), entry]));
228
+ const branch = [...merged.values()], key = JSON.stringify(branch.map(entry => [entry.file, entry.line, entry.column]));
229
+ if (!combined.has(key) && combined.size >= maxBranches) { truncated = true; continue; }
230
+ combined.set(key, branch);
231
+ }
232
+ if (truncated) limits.push("Atomic source tracing reached its alternative limit.");
233
+ branches = [...combined.values()];
234
+ }
235
+ return { branches, limits };
236
+ }
237
+ return { branches: [[]], limits: [] };
238
+ }
239
+ function inspect(value, node, context, target, path = []) {
240
+ const result = styles(value, context);
241
+ for (const branch of result.branches) for (let index = 0; index < branch.length; index++) compare([branch[index]], branch.slice(index + 1), node, { target, path, limits: result.limits });
242
+ if (result.limits.length && (result.branches.some(branch => branch.length) || ts.isCallExpression(node) && node.arguments.some(argument => styles(reference(argument), next(context)).branches.some(branch => branch.length)))) limit(node, result.limits, path);
243
+ return result;
244
+ }
245
+ function props(node, env, via) {
246
+ 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)]])) };
247
+ }
248
+ function render(input, location, context, path = [], pendingStyles = false) {
249
+ const value = resolve(input, next(context));
250
+ context = value.context ?? input.context ?? context;
251
+ if (value.kind === "union") { for (const item of value.values.slice(0, maxBranches)) render(item, location, next(context), path, pendingStyles); return; }
252
+ if (value.kind !== "node") { if (value.reason && pendingStyles) limit(location, [value.reason], path); return; }
253
+ const { node, env, via } = value;
254
+ if (ts.isJsxExpression(node)) { render(reference(node.expression, env, via), location, next(context), path, pendingStyles); return; }
255
+ if (ts.isJsxFragment(node)) { for (const child of node.children) render(reference(child, env, via), location, next(context), path); return; }
256
+ if (ts.isArrayLiteralExpression(node)) { for (const child of node.elements) render(reference(child, env, via), location, next(context), path); return; }
257
+ if (!ts.isJsxElement(node) && !ts.isJsxSelfClosingElement(node)) return;
258
+ const opening = ts.isJsxElement(node) ? node.openingElement : node;
259
+ const attributes = props(opening, env, via), classValue = get(attributes, "className", next(context));
260
+ const tag = opening.tagName.getText();
261
+ if (/^[a-z]/.test(tag) && !tag.includes(".")) {
262
+ inspect(classValue, location, context, at(opening, "element", tag), [...path, ...via]);
263
+ if (ts.isJsxElement(node)) for (const child of node.children) render(reference(child, env, via), location, next(context), path);
264
+ return;
265
+ }
266
+ function componentTarget(component) {
267
+ if (component.kind === "union") { for (const alternative of component.values.slice(0, maxBranches)) componentTarget(resolve(alternative, next(context))); return; }
268
+ if (component.kind === "literal" && typeof component.value === "string") {
269
+ inspect(classValue, location, context, at(opening, "element", component.value), [...path, ...via]);
270
+ } else if (component.kind === "node" && isFunction(component.node)) {
271
+ const passed = pendingStyles || styles(classValue, context).branches.some(branch => branch.length);
272
+ for (const output of returns(component, [attributes], next(context), "component")) render(output, location, output.context ?? next(context), [...path, at(opening, "component", tag)], passed);
273
+ } else {
274
+ const passed = styles(classValue, context);
275
+ if (passed.branches.some(branch => branch.length)) limit(location, [`Component ${tag.slice(0, 80)} could not be traced to its DOM className target.`, ...passed.limits], path);
276
+ }
277
+ }
278
+ componentTarget(resolve(reference(opening.tagName, env, via), next(context)));
279
+ }
280
+ function visit(node) {
281
+ if (ts.isCallExpression(node)) inspect(reference(node), node, state());
282
+ if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) render(reference(node), node, state());
283
+ ts.forEachChild(node, visit);
284
+ }
285
+ visit(source);
286
+ };
287
+ }
package/dist/check.mjs CHANGED
@@ -262,5 +262,7 @@ export function checkProject(directory, { trackingRoot } = {}) {
262
262
  }
263
263
  visit(source);
264
264
  }
265
+ // Bounded clients should see actionable warnings before source-analysis limits.
266
+ advisories.sort((first, second) => Number(first.severity === "info") - Number(second.severity === "info"));
265
267
  return { ok: diagnostics.length === 0, diagnostics, ...(advisories.length ? { advisories } : {}) };
266
268
  }
@@ -205,7 +205,7 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
205
205
  const matches = inventory.tokens.filter((token) => token.name.toLowerCase().includes(search.toLowerCase()));
206
206
  return json({ total: matches.length, tokens: matches.slice(0, limit), categories: tokenScenarios(), diagnosticCount: inventory.diagnostics.length, resourceUri: `${PROJECT_URI}/tokens` });
207
207
  } },
208
- { name: "ads_check", description: "Run read-only core ADS architecture checks; returns blocking diagnostics separately from nonblocking review advisories. Atomic composition warnings identify overlapping properties and link ads://guides/atomic-styles for causes and fixes. Project Oxlint, typecheck, build and browser tests remain separate.", schema: z.object({ limit: z.number().int().min(1).max(50).default(20) }).strict(), call: ({ limit }) => {
208
+ { name: "ads_check", description: "Run read-only core ADS architecture checks; returns blocking diagnostics separately from nonblocking advisories, with warnings before informational limits. Atomic warnings include helper/forwarded-class paths and ads://guides/atomic-styles guidance; unresolved paths do not establish safe composition. Project Oxlint, typecheck, build and browser tests remain separate.", schema: z.object({ limit: z.number().int().min(1).max(50).default(20) }).strict(), call: ({ limit }) => {
209
209
  const result = checkProject(root);
210
210
  return json({ ok: result.ok, total: result.diagnostics.length, diagnostics: result.diagnostics.slice(0, limit), advisoryCount: result.advisories?.length ?? 0, advisories: (result.advisories ?? []).slice(0, limit), resourceUri: `${PROJECT_URI}/checks` });
211
211
  } },
@@ -147,10 +147,11 @@ export function inspectScenarioCoverage(options, program, report, advise = () =>
147
147
  }
148
148
  return symbol.declarations?.length ? symbol : null;
149
149
  }
150
- const identities = new Map();
150
+ const identities = new Map(), behaviorIdentities = new Set();
151
151
  for (const item of publicApi(program, options)) {
152
152
  if (!identities.has(item.declaration)) identities.set(item.declaration, new Set());
153
153
  identities.get(item.declaration).add(item.name);
154
+ if (item.behavioral && !item.renderable) behaviorIdentities.add(item.declaration);
154
155
  }
155
156
 
156
157
  const covered = new Set();
@@ -190,6 +191,15 @@ export function inspectScenarioCoverage(options, program, report, advise = () =>
190
191
  if (ts.isPropertyAccessExpression(expression) && ["call", "apply"].includes(expression.name.text)) expression = expression.expression;
191
192
  const symbol = canonical(symbolOf(expression));
192
193
  for (const name of identities.get(symbol?.valueDeclaration ?? symbol?.declarations?.[0]) ?? []) exercised.add(name);
194
+ // A call on an attached method exercises its public behavioral receiver.
195
+ // Follow symbol identities through aliases, namespaces and re-exports;
196
+ // a property read or an unused callback still does not establish a call.
197
+ while (ts.isPropertyAccessExpression(expression) || ts.isElementAccessExpression(expression) && expression.argumentExpression && ts.isStringLiteralLike(expression.argumentExpression)) {
198
+ expression = expression.expression;
199
+ const receiver = canonical(symbolOf(expression));
200
+ const owner = receiver?.valueDeclaration ?? receiver?.declarations?.[0];
201
+ if (behaviorIdentities.has(owner)) for (const name of identities.get(owner) ?? []) exercised.add(name);
202
+ }
193
203
  }
194
204
  // Follow local adapters reachable from the default entry. Unused example
195
205
  // functions do not satisfy coverage; production implementations are never
@@ -49,7 +49,8 @@ For owner drafts, token-sample reconciliation, coupled transactions and local
49
49
  constant namespaces, read [guarded-editing.md](references/guarded-editing.md).
50
50
 
51
51
 
52
- For atomic base/variant styling or an `atomic-composition-review` warning, read
52
+ For atomic base/variant styling, helper/wrapper class composition, or an
53
+ `atomic-composition-review` / `atomic-composition-limit` advisory, read
53
54
  [atomic style composition](references/atomic-styles.md). The same guide is served
54
55
  as `ads://guides/atomic-styles`. It explains shorthand/logical conflicts and
55
56
  matching-property fixes. Review warnings in the real `cx` composition and verify
@@ -81,6 +81,41 @@ 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
+ ## Follow helpers and forwarded classes
85
+
86
+ Composition can cross a helper or a component boundary. For example, in component
87
+ implementation source:
88
+
89
+ ```tsx
90
+ function baseClasses() { return base; }
91
+ function Primitive({ className, ...props }) {
92
+ return <button {...props} className={cx(baseClasses(), className)} />;
93
+ }
94
+ function CompactPrimitive(props) {
95
+ return <Primitive {...props} className={small} />;
96
+ }
97
+ ```
98
+
99
+ The base and small atoms can reach the same button. Inspect both owners when
100
+ correcting their property vocabulary. The wrapper's class does not style every
101
+ descendant: a class on a child or sibling element is a separate composition.
102
+ An imported stylesheet alone does not show that its classes co-apply.
103
+
104
+ Spread order matters. `<button className={base} {...props} />` can replace the
105
+ entire className; `<button {...props} className={cx(base, className)} />` composes
106
+ it explicitly. Keep the product's intended forwarding behavior when fixing an
107
+ overlap. Do not change the public API simply to make tracing easier.
108
+
109
+ Warnings include a bounded `composition` path through traced helpers and
110
+ components. A `target` identifies a statically reached DOM element. This
111
+ establishes source composition, not that a scenario mounted it or that its
112
+ computed result is wrong. `writingModeDependent: true` identifies overlaps that
113
+ change with direction or writing mode; evaluate those against supported modes.
114
+ Literal overlaps with provably equal values are omitted where the analyzer can
115
+ expand them safely. Token formulas, inheritance and runtime values still need
116
+ review. A margin shorthand and `margin-inline: auto` may agree horizontally but
117
+ disagree in vertical text, so horizontal equality alone is insufficient.
118
+
84
119
  ## Respond to a warning
85
120
 
86
121
  1. Follow the warning's related file/line locations and inspect the actual `cx`
@@ -101,6 +136,24 @@ the conflict. Prefer resolving the composition itself. If an overlap is
101
136
  intentional, document why its precedence is stable and verify the resulting
102
137
  behavior rather than changing product capabilities to silence a warning.
103
138
 
139
+ Use existing behavior contracts and `ads_verify` for important geometry. Register
140
+ a check beside its owner, link its real scenario and use the project's browser
141
+ profile. An assertion can inspect the actual control in the App frame:
142
+
143
+ ```ts
144
+ const frame = await openBehaviorScenario(page, "compact-spacing");
145
+ await expect(frame.getByRole("button", { name: "Compact" }))
146
+ .toHaveCSS("padding-inline-start", "8px");
147
+ ```
148
+
149
+ The name, check ID and expected value above are examples. Derive assertions from
150
+ the project's intended API and design, with relevant variants, open/hover/focus
151
+ states, themes and supported writing modes. Wait for observable readiness; an
152
+ event-stream connection can prevent `networkidle` from settling. A generic
153
+ overlap scan cannot decide which dimensions the product intended. A failed
154
+ registered geometry assertion is blocking through the existing verification
155
+ contract; an overlap warning remains a review signal.
156
+
104
157
  ## What ADS checks
105
158
 
106
159
  `atomic-composition-review` is a nonblocking warning, returned under `advisories`
@@ -108,14 +161,26 @@ with `severity: "warning"`. CLI checks still exit successfully when there are no
108
161
  errors; MCP checks and mutations preserve the warning and its guidance URI.
109
162
 
110
163
  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
164
+ `cx` arguments, including aliases, local imports/re-exports, local helper returns,
165
+ arguments/defaults, nested composition, conditional expressions and variant-object
166
+ lookups. It follows explicit `className` forwarding through local function
167
+ components, props/rest spreads and imported React/Preact `memo`/`forwardRef`
168
+ wrappers to an intrinsic JSX element. It checks known overlaps involving box
113
169
  spacing, inset, borders/corners, dimensions, overflow, backgrounds, typography,
114
170
  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.
171
+ within one conditional or lookup are not assumed to apply together. Matching
172
+ atomic property/selector/condition keys are replaced during traced `cx`
173
+ composition before comparing the surviving declarations.
116
174
 
117
175
  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.
176
+ does not execute helpers or import project modules, enumerate every external
177
+ class, or model arbitrary runtime mutation, component rendering, selectors or
178
+ overlapping media queries. Recursive calls, unsupported call/control-flow paths,
179
+ unresolved DOM targets and traversal limits produce bounded
180
+ `atomic-composition-limit` informational advisories where known atomic styles
181
+ reach an unresolved composition. Ordinary externally supplied className values
182
+ remain open inputs, not guaranteed safe styles. Depth, work and alternative
183
+ limits keep source inspection bounded; an unreported path or an empty warning
184
+ list does not establish visual correctness. Actual failed registered
185
+ browser/behavior checks remain blocking; potentially intentional source overlaps
186
+ remain warnings.
@@ -80,7 +80,8 @@ use the client's normal source-editing tools.
80
80
  For owner drafts, token-sample reconciliation, coupled transactions and local
81
81
  constant namespaces, read [guarded-editing.md](references/guarded-editing.md).
82
82
 
83
- For atomic base/variant styling or an `atomic-composition-review` warning, read
83
+ For atomic base/variant styling, helper/wrapper class composition, or an
84
+ `atomic-composition-review` / `atomic-composition-limit` advisory, read
84
85
  [atomic style composition](references/atomic-styles.md). The same guide is served
85
86
  as `ads://guides/atomic-styles`. It explains shorthand/logical conflicts and
86
87
  matching-property fixes. Review warnings in the real `cx` composition and verify
@@ -81,6 +81,41 @@ 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
+ ## Follow helpers and forwarded classes
85
+
86
+ Composition can cross a helper or a component boundary. For example, in component
87
+ implementation source:
88
+
89
+ ```tsx
90
+ function baseClasses() { return base; }
91
+ function Primitive({ className, ...props }) {
92
+ return <button {...props} className={cx(baseClasses(), className)} />;
93
+ }
94
+ function CompactPrimitive(props) {
95
+ return <Primitive {...props} className={small} />;
96
+ }
97
+ ```
98
+
99
+ The base and small atoms can reach the same button. Inspect both owners when
100
+ correcting their property vocabulary. The wrapper's class does not style every
101
+ descendant: a class on a child or sibling element is a separate composition.
102
+ An imported stylesheet alone does not show that its classes co-apply.
103
+
104
+ Spread order matters. `<button className={base} {...props} />` can replace the
105
+ entire className; `<button {...props} className={cx(base, className)} />` composes
106
+ it explicitly. Keep the product's intended forwarding behavior when fixing an
107
+ overlap. Do not change the public API simply to make tracing easier.
108
+
109
+ Warnings include a bounded `composition` path through traced helpers and
110
+ components. A `target` identifies a statically reached DOM element. This
111
+ establishes source composition, not that a scenario mounted it or that its
112
+ computed result is wrong. `writingModeDependent: true` identifies overlaps that
113
+ change with direction or writing mode; evaluate those against supported modes.
114
+ Literal overlaps with provably equal values are omitted where the analyzer can
115
+ expand them safely. Token formulas, inheritance and runtime values still need
116
+ review. A margin shorthand and `margin-inline: auto` may agree horizontally but
117
+ disagree in vertical text, so horizontal equality alone is insufficient.
118
+
84
119
  ## Respond to a warning
85
120
 
86
121
  1. Follow the warning's related file/line locations and inspect the actual `cx`
@@ -101,6 +136,24 @@ the conflict. Prefer resolving the composition itself. If an overlap is
101
136
  intentional, document why its precedence is stable and verify the resulting
102
137
  behavior rather than changing product capabilities to silence a warning.
103
138
 
139
+ Use existing behavior contracts and `ads_verify` for important geometry. Register
140
+ a check beside its owner, link its real scenario and use the project's browser
141
+ profile. An assertion can inspect the actual control in the App frame:
142
+
143
+ ```ts
144
+ const frame = await openBehaviorScenario(page, "compact-spacing");
145
+ await expect(frame.getByRole("button", { name: "Compact" }))
146
+ .toHaveCSS("padding-inline-start", "8px");
147
+ ```
148
+
149
+ The name, check ID and expected value above are examples. Derive assertions from
150
+ the project's intended API and design, with relevant variants, open/hover/focus
151
+ states, themes and supported writing modes. Wait for observable readiness; an
152
+ event-stream connection can prevent `networkidle` from settling. A generic
153
+ overlap scan cannot decide which dimensions the product intended. A failed
154
+ registered geometry assertion is blocking through the existing verification
155
+ contract; an overlap warning remains a review signal.
156
+
104
157
  ## What ADS checks
105
158
 
106
159
  `atomic-composition-review` is a nonblocking warning, returned under `advisories`
@@ -108,14 +161,26 @@ with `severity: "warning"`. CLI checks still exit successfully when there are no
108
161
  errors; MCP checks and mutations preserve the warning and its guidance URI.
109
162
 
110
163
  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
164
+ `cx` arguments, including aliases, local imports/re-exports, local helper returns,
165
+ arguments/defaults, nested composition, conditional expressions and variant-object
166
+ lookups. It follows explicit `className` forwarding through local function
167
+ components, props/rest spreads and imported React/Preact `memo`/`forwardRef`
168
+ wrappers to an intrinsic JSX element. It checks known overlaps involving box
113
169
  spacing, inset, borders/corners, dimensions, overflow, backgrounds, typography,
114
170
  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.
171
+ within one conditional or lookup are not assumed to apply together. Matching
172
+ atomic property/selector/condition keys are replaced during traced `cx`
173
+ composition before comparing the surviving declarations.
116
174
 
117
175
  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.
176
+ does not execute helpers or import project modules, enumerate every external
177
+ class, or model arbitrary runtime mutation, component rendering, selectors or
178
+ overlapping media queries. Recursive calls, unsupported call/control-flow paths,
179
+ unresolved DOM targets and traversal limits produce bounded
180
+ `atomic-composition-limit` informational advisories where known atomic styles
181
+ reach an unresolved composition. Ordinary externally supplied className values
182
+ remain open inputs, not guaranteed safe styles. Depth, work and alternative
183
+ limits keep source inspection bounded; an unreported path or an empty warning
184
+ list does not establish visual correctness. Actual failed registered
185
+ browser/behavior checks remain blocking; potentially intentional source overlaps
186
+ remain warnings.
@@ -44,7 +44,8 @@ use the client's normal source-editing tools.
44
44
  For owner drafts, token-sample reconciliation, coupled transactions and local
45
45
  constant namespaces, read [guarded-editing.md](references/guarded-editing.md).
46
46
 
47
- For atomic base/variant styling or an `atomic-composition-review` warning, read
47
+ For atomic base/variant styling, helper/wrapper class composition, or an
48
+ `atomic-composition-review` / `atomic-composition-limit` advisory, read
48
49
  [atomic style composition](references/atomic-styles.md). The same guide is served
49
50
  as `ads://guides/atomic-styles`. It explains shorthand/logical conflicts and
50
51
  matching-property fixes. Review warnings in the real `cx` composition and verify
@@ -81,6 +81,41 @@ 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
+ ## Follow helpers and forwarded classes
85
+
86
+ Composition can cross a helper or a component boundary. For example, in component
87
+ implementation source:
88
+
89
+ ```tsx
90
+ function baseClasses() { return base; }
91
+ function Primitive({ className, ...props }) {
92
+ return <button {...props} className={cx(baseClasses(), className)} />;
93
+ }
94
+ function CompactPrimitive(props) {
95
+ return <Primitive {...props} className={small} />;
96
+ }
97
+ ```
98
+
99
+ The base and small atoms can reach the same button. Inspect both owners when
100
+ correcting their property vocabulary. The wrapper's class does not style every
101
+ descendant: a class on a child or sibling element is a separate composition.
102
+ An imported stylesheet alone does not show that its classes co-apply.
103
+
104
+ Spread order matters. `<button className={base} {...props} />` can replace the
105
+ entire className; `<button {...props} className={cx(base, className)} />` composes
106
+ it explicitly. Keep the product's intended forwarding behavior when fixing an
107
+ overlap. Do not change the public API simply to make tracing easier.
108
+
109
+ Warnings include a bounded `composition` path through traced helpers and
110
+ components. A `target` identifies a statically reached DOM element. This
111
+ establishes source composition, not that a scenario mounted it or that its
112
+ computed result is wrong. `writingModeDependent: true` identifies overlaps that
113
+ change with direction or writing mode; evaluate those against supported modes.
114
+ Literal overlaps with provably equal values are omitted where the analyzer can
115
+ expand them safely. Token formulas, inheritance and runtime values still need
116
+ review. A margin shorthand and `margin-inline: auto` may agree horizontally but
117
+ disagree in vertical text, so horizontal equality alone is insufficient.
118
+
84
119
  ## Respond to a warning
85
120
 
86
121
  1. Follow the warning's related file/line locations and inspect the actual `cx`
@@ -101,6 +136,24 @@ the conflict. Prefer resolving the composition itself. If an overlap is
101
136
  intentional, document why its precedence is stable and verify the resulting
102
137
  behavior rather than changing product capabilities to silence a warning.
103
138
 
139
+ Use existing behavior contracts and `ads_verify` for important geometry. Register
140
+ a check beside its owner, link its real scenario and use the project's browser
141
+ profile. An assertion can inspect the actual control in the App frame:
142
+
143
+ ```ts
144
+ const frame = await openBehaviorScenario(page, "compact-spacing");
145
+ await expect(frame.getByRole("button", { name: "Compact" }))
146
+ .toHaveCSS("padding-inline-start", "8px");
147
+ ```
148
+
149
+ The name, check ID and expected value above are examples. Derive assertions from
150
+ the project's intended API and design, with relevant variants, open/hover/focus
151
+ states, themes and supported writing modes. Wait for observable readiness; an
152
+ event-stream connection can prevent `networkidle` from settling. A generic
153
+ overlap scan cannot decide which dimensions the product intended. A failed
154
+ registered geometry assertion is blocking through the existing verification
155
+ contract; an overlap warning remains a review signal.
156
+
104
157
  ## What ADS checks
105
158
 
106
159
  `atomic-composition-review` is a nonblocking warning, returned under `advisories`
@@ -108,14 +161,26 @@ with `severity: "warning"`. CLI checks still exit successfully when there are no
108
161
  errors; MCP checks and mutations preserve the warning and its guidance URI.
109
162
 
110
163
  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
164
+ `cx` arguments, including aliases, local imports/re-exports, local helper returns,
165
+ arguments/defaults, nested composition, conditional expressions and variant-object
166
+ lookups. It follows explicit `className` forwarding through local function
167
+ components, props/rest spreads and imported React/Preact `memo`/`forwardRef`
168
+ wrappers to an intrinsic JSX element. It checks known overlaps involving box
113
169
  spacing, inset, borders/corners, dimensions, overflow, backgrounds, typography,
114
170
  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.
171
+ within one conditional or lookup are not assumed to apply together. Matching
172
+ atomic property/selector/condition keys are replaced during traced `cx`
173
+ composition before comparing the surviving declarations.
116
174
 
117
175
  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.
176
+ does not execute helpers or import project modules, enumerate every external
177
+ class, or model arbitrary runtime mutation, component rendering, selectors or
178
+ overlapping media queries. Recursive calls, unsupported call/control-flow paths,
179
+ unresolved DOM targets and traversal limits produce bounded
180
+ `atomic-composition-limit` informational advisories where known atomic styles
181
+ reach an unresolved composition. Ordinary externally supplied className values
182
+ remain open inputs, not guaranteed safe styles. Depth, work and alternative
183
+ limits keep source inspection bounded; an unreported path or an empty warning
184
+ list does not establish visual correctness. Actual failed registered
185
+ browser/behavior checks remain blocking; potentially intentional source overlaps
186
+ remain warnings.
@@ -46,7 +46,8 @@ For owner drafts, token-sample reconciliation, coupled transactions and local
46
46
  constant namespaces, read [guarded-editing.md](references/guarded-editing.md).
47
47
 
48
48
 
49
- For atomic base/variant styling or an `atomic-composition-review` warning, read
49
+ For atomic base/variant styling, helper/wrapper class composition, or an
50
+ `atomic-composition-review` / `atomic-composition-limit` advisory, read
50
51
  [atomic style composition](references/atomic-styles.md). The same guide is served
51
52
  as `ads://guides/atomic-styles`. It explains shorthand/logical conflicts and
52
53
  matching-property fixes. Review warnings in the real `cx` composition and verify
@@ -81,6 +81,41 @@ 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
+ ## Follow helpers and forwarded classes
85
+
86
+ Composition can cross a helper or a component boundary. For example, in component
87
+ implementation source:
88
+
89
+ ```tsx
90
+ function baseClasses() { return base; }
91
+ function Primitive({ className, ...props }) {
92
+ return <button {...props} className={cx(baseClasses(), className)} />;
93
+ }
94
+ function CompactPrimitive(props) {
95
+ return <Primitive {...props} className={small} />;
96
+ }
97
+ ```
98
+
99
+ The base and small atoms can reach the same button. Inspect both owners when
100
+ correcting their property vocabulary. The wrapper's class does not style every
101
+ descendant: a class on a child or sibling element is a separate composition.
102
+ An imported stylesheet alone does not show that its classes co-apply.
103
+
104
+ Spread order matters. `<button className={base} {...props} />` can replace the
105
+ entire className; `<button {...props} className={cx(base, className)} />` composes
106
+ it explicitly. Keep the product's intended forwarding behavior when fixing an
107
+ overlap. Do not change the public API simply to make tracing easier.
108
+
109
+ Warnings include a bounded `composition` path through traced helpers and
110
+ components. A `target` identifies a statically reached DOM element. This
111
+ establishes source composition, not that a scenario mounted it or that its
112
+ computed result is wrong. `writingModeDependent: true` identifies overlaps that
113
+ change with direction or writing mode; evaluate those against supported modes.
114
+ Literal overlaps with provably equal values are omitted where the analyzer can
115
+ expand them safely. Token formulas, inheritance and runtime values still need
116
+ review. A margin shorthand and `margin-inline: auto` may agree horizontally but
117
+ disagree in vertical text, so horizontal equality alone is insufficient.
118
+
84
119
  ## Respond to a warning
85
120
 
86
121
  1. Follow the warning's related file/line locations and inspect the actual `cx`
@@ -101,6 +136,24 @@ the conflict. Prefer resolving the composition itself. If an overlap is
101
136
  intentional, document why its precedence is stable and verify the resulting
102
137
  behavior rather than changing product capabilities to silence a warning.
103
138
 
139
+ Use existing behavior contracts and `ads_verify` for important geometry. Register
140
+ a check beside its owner, link its real scenario and use the project's browser
141
+ profile. An assertion can inspect the actual control in the App frame:
142
+
143
+ ```ts
144
+ const frame = await openBehaviorScenario(page, "compact-spacing");
145
+ await expect(frame.getByRole("button", { name: "Compact" }))
146
+ .toHaveCSS("padding-inline-start", "8px");
147
+ ```
148
+
149
+ The name, check ID and expected value above are examples. Derive assertions from
150
+ the project's intended API and design, with relevant variants, open/hover/focus
151
+ states, themes and supported writing modes. Wait for observable readiness; an
152
+ event-stream connection can prevent `networkidle` from settling. A generic
153
+ overlap scan cannot decide which dimensions the product intended. A failed
154
+ registered geometry assertion is blocking through the existing verification
155
+ contract; an overlap warning remains a review signal.
156
+
104
157
  ## What ADS checks
105
158
 
106
159
  `atomic-composition-review` is a nonblocking warning, returned under `advisories`
@@ -108,14 +161,26 @@ with `severity: "warning"`. CLI checks still exit successfully when there are no
108
161
  errors; MCP checks and mutations preserve the warning and its guidance URI.
109
162
 
110
163
  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
164
+ `cx` arguments, including aliases, local imports/re-exports, local helper returns,
165
+ arguments/defaults, nested composition, conditional expressions and variant-object
166
+ lookups. It follows explicit `className` forwarding through local function
167
+ components, props/rest spreads and imported React/Preact `memo`/`forwardRef`
168
+ wrappers to an intrinsic JSX element. It checks known overlaps involving box
113
169
  spacing, inset, borders/corners, dimensions, overflow, backgrounds, typography,
114
170
  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.
171
+ within one conditional or lookup are not assumed to apply together. Matching
172
+ atomic property/selector/condition keys are replaced during traced `cx`
173
+ composition before comparing the surviving declarations.
116
174
 
117
175
  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.
176
+ does not execute helpers or import project modules, enumerate every external
177
+ class, or model arbitrary runtime mutation, component rendering, selectors or
178
+ overlapping media queries. Recursive calls, unsupported call/control-flow paths,
179
+ unresolved DOM targets and traversal limits produce bounded
180
+ `atomic-composition-limit` informational advisories where known atomic styles
181
+ reach an unresolved composition. Ordinary externally supplied className values
182
+ remain open inputs, not guaranteed safe styles. Depth, work and alternative
183
+ limits keep source inspection bounded; an unreported path or an empty warning
184
+ list does not establish visual correctness. Actual failed registered
185
+ browser/behavior checks remain blocking; potentially intentional source overlaps
186
+ remain warnings.
@@ -66,7 +66,8 @@ 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 atomic base/variant styling, helper/wrapper class composition, or an
70
+ `atomic-composition-review` / `atomic-composition-limit` advisory, read
70
71
  [atomic style composition](references/atomic-styles.md). The same guide is served
71
72
  as `ads://guides/atomic-styles`. It explains shorthand/logical conflicts and
72
73
  matching-property fixes. Review warnings in the real `cx` composition and verify
@@ -81,6 +81,41 @@ 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
+ ## Follow helpers and forwarded classes
85
+
86
+ Composition can cross a helper or a component boundary. For example, in component
87
+ implementation source:
88
+
89
+ ```tsx
90
+ function baseClasses() { return base; }
91
+ function Primitive({ className, ...props }) {
92
+ return <button {...props} className={cx(baseClasses(), className)} />;
93
+ }
94
+ function CompactPrimitive(props) {
95
+ return <Primitive {...props} className={small} />;
96
+ }
97
+ ```
98
+
99
+ The base and small atoms can reach the same button. Inspect both owners when
100
+ correcting their property vocabulary. The wrapper's class does not style every
101
+ descendant: a class on a child or sibling element is a separate composition.
102
+ An imported stylesheet alone does not show that its classes co-apply.
103
+
104
+ Spread order matters. `<button className={base} {...props} />` can replace the
105
+ entire className; `<button {...props} className={cx(base, className)} />` composes
106
+ it explicitly. Keep the product's intended forwarding behavior when fixing an
107
+ overlap. Do not change the public API simply to make tracing easier.
108
+
109
+ Warnings include a bounded `composition` path through traced helpers and
110
+ components. A `target` identifies a statically reached DOM element. This
111
+ establishes source composition, not that a scenario mounted it or that its
112
+ computed result is wrong. `writingModeDependent: true` identifies overlaps that
113
+ change with direction or writing mode; evaluate those against supported modes.
114
+ Literal overlaps with provably equal values are omitted where the analyzer can
115
+ expand them safely. Token formulas, inheritance and runtime values still need
116
+ review. A margin shorthand and `margin-inline: auto` may agree horizontally but
117
+ disagree in vertical text, so horizontal equality alone is insufficient.
118
+
84
119
  ## Respond to a warning
85
120
 
86
121
  1. Follow the warning's related file/line locations and inspect the actual `cx`
@@ -101,6 +136,24 @@ the conflict. Prefer resolving the composition itself. If an overlap is
101
136
  intentional, document why its precedence is stable and verify the resulting
102
137
  behavior rather than changing product capabilities to silence a warning.
103
138
 
139
+ Use existing behavior contracts and `ads_verify` for important geometry. Register
140
+ a check beside its owner, link its real scenario and use the project's browser
141
+ profile. An assertion can inspect the actual control in the App frame:
142
+
143
+ ```ts
144
+ const frame = await openBehaviorScenario(page, "compact-spacing");
145
+ await expect(frame.getByRole("button", { name: "Compact" }))
146
+ .toHaveCSS("padding-inline-start", "8px");
147
+ ```
148
+
149
+ The name, check ID and expected value above are examples. Derive assertions from
150
+ the project's intended API and design, with relevant variants, open/hover/focus
151
+ states, themes and supported writing modes. Wait for observable readiness; an
152
+ event-stream connection can prevent `networkidle` from settling. A generic
153
+ overlap scan cannot decide which dimensions the product intended. A failed
154
+ registered geometry assertion is blocking through the existing verification
155
+ contract; an overlap warning remains a review signal.
156
+
104
157
  ## What ADS checks
105
158
 
106
159
  `atomic-composition-review` is a nonblocking warning, returned under `advisories`
@@ -108,14 +161,26 @@ with `severity: "warning"`. CLI checks still exit successfully when there are no
108
161
  errors; MCP checks and mutations preserve the warning and its guidance URI.
109
162
 
110
163
  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
164
+ `cx` arguments, including aliases, local imports/re-exports, local helper returns,
165
+ arguments/defaults, nested composition, conditional expressions and variant-object
166
+ lookups. It follows explicit `className` forwarding through local function
167
+ components, props/rest spreads and imported React/Preact `memo`/`forwardRef`
168
+ wrappers to an intrinsic JSX element. It checks known overlaps involving box
113
169
  spacing, inset, borders/corners, dimensions, overflow, backgrounds, typography,
114
170
  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.
171
+ within one conditional or lookup are not assumed to apply together. Matching
172
+ atomic property/selector/condition keys are replaced during traced `cx`
173
+ composition before comparing the surviving declarations.
116
174
 
117
175
  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.
176
+ does not execute helpers or import project modules, enumerate every external
177
+ class, or model arbitrary runtime mutation, component rendering, selectors or
178
+ overlapping media queries. Recursive calls, unsupported call/control-flow paths,
179
+ unresolved DOM targets and traversal limits produce bounded
180
+ `atomic-composition-limit` informational advisories where known atomic styles
181
+ reach an unresolved composition. Ordinary externally supplied className values
182
+ remain open inputs, not guaranteed safe styles. Depth, work and alternative
183
+ limits keep source inspection bounded; an unreported path or an empty warning
184
+ list does not establish visual correctness. Actual failed registered
185
+ browser/behavior checks remain blocking; potentially intentional source overlaps
186
+ remain warnings.
package/docs/ADS.md CHANGED
@@ -373,6 +373,9 @@ guarded mutations: valid CSS can intentionally overlap. Read
373
373
  [atomic style composition](../skills/ads-designer/references/atomic-styles.md) for causes, matching-property fixes
374
374
  and the source review's limits. Registered rendered checks can enforce a
375
375
  project's intended computed geometry; token validity alone cannot establish it.
376
+ Bounded tracing follows local helpers and explicit `className` forwarding;
377
+ diagnostics distinguish a reached DOM target from informational analysis limits.
378
+ Neither a trace nor an absent warning establishes the intended rendered result.
376
379
 
377
380
  Document a new ownership exception with its guard/configuration change and
378
381
  tests. Passing static checks establishes only the policies and source formats
@@ -127,6 +127,9 @@ every shorthand/logical-property overlap. ADS reports detectable composition
127
127
  risks as nonblocking warnings with guidance, while registered rendered checks
128
128
  can enforce actual geometry. See the skill-local
129
129
  [atomic style composition guide](../skills/ads-designer/references/atomic-styles.md).
130
+ Bounded source tracing follows local class helpers and explicit component
131
+ `className` forwarding to DOM targets. Separate elements remain separate;
132
+ unsupported paths are informational limits, not evidence of safe composition.
130
133
 
131
134
  Airbnb's 2022 [migration report](https://medium.com/airbnb-engineering/airbnbs-trip-to-linaria-dc169230bd12)
132
135
  provides practical evidence for this choice: performance was its leading
package/docs/BEHAVIORS.md CHANGED
@@ -103,6 +103,10 @@ values lazily and shares that type discovery within the check. Untyped `any`
103
103
  values and objects containing methods do not establish a callable API.
104
104
  An invalid `exercises` entry names the offending value and fails validation while
105
105
  retaining independent render evidence for `covers`.
106
+ Calls to a callable's attached methods also exercise that public receiver,
107
+ including statically named member calls inside reachable event-handler closures.
108
+ Aliases, namespace access and re-exports retain the receiver identity. Reading a
109
+ method or defining an unused callback does not establish an invocation.
106
110
 
107
111
  Public APIs appear in `ads_catalog` with `apiKind: "ui"` or `"behavior"` and their
108
112
  scenario links. `ads_behaviors` joins contracts to demonstrations and evidence.
package/docs/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.3.8
6
+
7
+ - Trace local atomic class helpers and explicit component class forwarding,
8
+ preserving alternatives and spread order. Report composition paths, DOM
9
+ targets, writing-mode risks and bounded informational analysis limits;
10
+ suppress safely established literal-equivalent overlaps. Keep review warnings
11
+ nonblocking and use registered rendered checks for intended geometry.
12
+ - Count calls to attached methods of public callable behavioral APIs through
13
+ aliases, namespace imports and re-exports, including mounted event handlers.
14
+
5
15
  ## 0.3.7
6
16
 
7
17
  - Warn about statically traceable atomic CSS property overlaps without blocking
package/docs/MCP.md CHANGED
@@ -197,6 +197,12 @@ component-level fixes. The full guide lives in every ADS skill as
197
197
  Guarded mutation results preserve these warnings without rejecting valid source.
198
198
  An overlap warning is a review signal; failed registered rendered checks remain
199
199
  blocking.
200
+ Warnings can carry a `composition` source path, a statically reached DOM `target`
201
+ and `writingModeDependent`. Unresolved paths appear as bounded
202
+ `atomic-composition-limit` advisories with `severity: "info"`; neither an
203
+ informational limit nor a traced target proves a rendered failure or success.
204
+ Bounded check results put warnings before informational limits; the checks
205
+ resource retains complete results.
200
206
 
201
207
  ### Behavioral verification
202
208
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reqvire-org/ads",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
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": {