@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.
- package/dist/atomic-composition.mjs +85 -54
- package/dist/atomic-trace.mjs +287 -0
- package/dist/check.mjs +2 -0
- package/dist/mcp/server.mjs +1 -1
- package/dist/scenarios.mjs +11 -1
- package/dist/skills/ads-break-ui/SKILL.md +2 -1
- package/dist/skills/ads-break-ui/references/atomic-styles.md +72 -7
- package/dist/skills/ads-designer/SKILL.md +2 -1
- package/dist/skills/ads-designer/references/atomic-styles.md +72 -7
- package/dist/skills/ads-import/SKILL.md +2 -1
- package/dist/skills/ads-import/references/atomic-styles.md +72 -7
- package/dist/skills/ads-migrate/SKILL.md +2 -1
- package/dist/skills/ads-migrate/references/atomic-styles.md +72 -7
- package/dist/skills/ads-motion/SKILL.md +2 -1
- package/dist/skills/ads-motion/references/atomic-styles.md +72 -7
- package/docs/ADS.md +3 -0
- package/docs/ARCHITECTURE.md +3 -0
- package/docs/BEHAVIORS.md +4 -0
- package/docs/CHANGELOG.md +10 -0
- package/docs/MCP.md +6 -0
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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 =
|
|
154
|
-
|
|
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.
|
|
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
|
-
|
|
159
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
}
|
package/dist/mcp/server.mjs
CHANGED
|
@@ -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
|
|
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
|
} },
|
package/dist/scenarios.mjs
CHANGED
|
@@ -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
|
|
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,
|
|
112
|
-
expressions and variant-object
|
|
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
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
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,
|
|
112
|
-
expressions and variant-object
|
|
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
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
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,
|
|
112
|
-
expressions and variant-object
|
|
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
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
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,
|
|
112
|
-
expressions and variant-object
|
|
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
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
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,
|
|
112
|
-
expressions and variant-object
|
|
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
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -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
|
|