@reqvire-org/ads 0.3.6 → 0.3.7

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.
@@ -0,0 +1,183 @@
1
+ import path from "node:path";
2
+ import ts from "typescript";
3
+ import postcss from "postcss";
4
+ import { isInside } from "./config.mjs";
5
+
6
+ export const atomicStyleGuide = "ads://guides/atomic-styles";
7
+
8
+ const sides = ["top", "right", "bottom", "left"];
9
+ const logicalSides = ["block-start", "block-end", "inline-start", "inline-end"];
10
+ const intersects = (a, b) => a.some(value => b.includes(value));
11
+ const previewValue = value => value.length > 200 ? value.slice(0, 197) + "..." : value;
12
+ const shorthands = {
13
+ background: ["background-attachment", "background-clip", "background-color", "background-image", "background-origin", "background-position", "background-repeat", "background-size"],
14
+ "background-position": ["background-position-x", "background-position-y"],
15
+ "background-repeat": ["background-repeat-x", "background-repeat-y"],
16
+ font: ["font-family", "font-size", "font-stretch", "font-style", "font-variant", "font-weight", "line-height"],
17
+ animation: ["animation-name", "animation-duration", "animation-timing-function", "animation-delay", "animation-iteration-count", "animation-direction", "animation-fill-mode", "animation-play-state"],
18
+ transition: ["transition-property", "transition-duration", "transition-timing-function", "transition-delay", "transition-behavior"],
19
+ flex: ["flex-grow", "flex-shrink", "flex-basis"],
20
+ "flex-flow": ["flex-direction", "flex-wrap"],
21
+ gap: ["row-gap", "column-gap"],
22
+ outline: ["outline-color", "outline-style", "outline-width"],
23
+ columns: ["column-count", "column-width"],
24
+ "column-rule": ["column-rule-color", "column-rule-style", "column-rule-width"],
25
+ "list-style": ["list-style-image", "list-style-position", "list-style-type"],
26
+ "text-decoration": ["text-decoration-color", "text-decoration-style", "text-decoration-line", "text-decoration-thickness"],
27
+ };
28
+ const expand = property => shorthands[property]?.flatMap(expand) ?? [property];
29
+ const longhands = new Map(Object.keys(shorthands).map(property => [property, expand(property)]));
30
+
31
+ // CSS-language relationships, independent of components, tokens and frameworks.
32
+ function footprint(property) {
33
+ const box = /^(padding|margin|scroll-padding|scroll-margin|inset)(?:-(top|right|bottom|left|block|inline|block-start|block-end|inline-start|inline-end))?$/.exec(property);
34
+ if (box) {
35
+ const side = box[2];
36
+ const logical = !!side && !sides.includes(side);
37
+ return { family: box[1], logical, slots: !side ? logicalSides : /^(block|inline)$/.test(side) ? [`${side}-start`, `${side}-end`] : [side], whole: !side };
38
+ }
39
+ if (sides.includes(property)) return { family: "inset", logical: false, slots: [property] };
40
+ const border = /^border(?:-(top|right|bottom|left|block|inline|block-start|block-end|inline-start|inline-end))?(?:-(color|style|width))?$/.exec(property);
41
+ if (border) {
42
+ const side = border[1];
43
+ return { family: "border", logical: !!side && !sides.includes(side), slots: !side ? logicalSides : /^(block|inline)$/.test(side) ? [`${side}-start`, `${side}-end`] : [side], whole: !side, fields: border[2] ? [border[2]] : ["color", "style", "width"] };
44
+ }
45
+ const corner = /^border-(?:(top-left|top-right|bottom-left|bottom-right|start-start|start-end|end-start|end-end)-)?radius$/.exec(property);
46
+ if (corner) return { family: "radius", logical: /^(start|end)-/.test(corner[1] ?? ""), whole: !corner[1], slots: [corner[1]] };
47
+ const dimension = /^(min-|max-)?(width|height|inline-size|block-size)$/.exec(property);
48
+ if (dimension) return { family: `${dimension[1] ?? ""}size`, logical: dimension[2].endsWith("-size"), slots: [dimension[2]] };
49
+ const overflow = /^(overflow|overscroll-behavior)(?:-(x|y|block|inline))?$/.exec(property);
50
+ if (overflow) return { family: overflow[1], logical: ["block", "inline"].includes(overflow[2]), whole: !overflow[2], slots: [overflow[2]] };
51
+ return null;
52
+ }
53
+
54
+ function overlaps(a, b) {
55
+ if (a === b || a.startsWith("--") || b.startsWith("--")) return false;
56
+ if ([a, b].includes("all")) return ![a, b].some(value => ["direction", "unicode-bidi"].includes(value));
57
+ const first = footprint(a), second = footprint(b);
58
+ if (first && second && first.family === second.family) {
59
+ if (first.fields && !intersects(first.fields, second.fields)) return false;
60
+ return first.whole || second.whole || first.logical !== second.logical || intersects(first.slots, second.slots);
61
+ }
62
+ return intersects(longhands.get(a) ?? [a], longhands.get(b) ?? [b]);
63
+ }
64
+
65
+ function unwrap(node) {
66
+ while (node && (ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isSatisfiesExpression(node) || ts.isNonNullExpression(node))) node = node.expression;
67
+ return node;
68
+ }
69
+
70
+ /** Review only: valid CSS can intentionally overlap. Never rewrite project CSS. */
71
+ export function createAtomicCompositionReview(program, options) {
72
+ 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
+ function imported(node, names, modules, seen = new Set()) {
79
+ node = unwrap(node);
80
+ if (!node || seen.has(node)) return false;
81
+ seen = new Set([...seen, node]);
82
+ const base = ts.isPropertyAccessExpression(node) ? node.expression : node;
83
+ const symbol = checker.getSymbolAtLocation(base);
84
+ for (const item of symbol?.declarations ?? []) {
85
+ if (ts.isImportSpecifier(item) && names.includes((item.propertyName ?? item.name).text) && modules.includes(item.parent.parent.parent.moduleSpecifier.text)) return true;
86
+ if (ts.isNamespaceImport(item) && ts.isPropertyAccessExpression(node) && names.includes(node.name.text) && modules.includes(item.parent.parent.moduleSpecifier.text)) return true;
87
+ if (ts.isVariableDeclaration(item) && imported(item.initializer, names, modules, seen)) return true;
88
+ }
89
+ return false;
90
+ }
91
+ const isCx = node => imported(node, ["cx"], ["@linaria/atomic", "@linaria/core"]);
92
+ function isAtomic(tag) {
93
+ if (ts.isCallExpression(tag)) tag = tag.expression;
94
+ if (imported(tag, ["css", "styled"], ["@linaria/atomic"])) return true;
95
+ return ts.isPropertyAccessExpression(tag) && imported(tag.expression, ["styled"], ["@linaria/atomic"]);
96
+ }
97
+ function entries(node) {
98
+ if (templates.has(node)) return templates.get(node);
99
+ const result = [];
100
+ templates.set(node, result);
101
+ const source = node.getSourceFile(), template = node.template;
102
+ const text = ts.isNoSubstitutionTemplateLiteral(template) ? template.text : template.head.text + template.templateSpans.map(span => `__ads_expression__${"\n".repeat(span.expression.getText(source).split("\n").length - 1)}${span.literal.text}`).join("");
103
+ const position = source.getLineAndCharacterOfPosition(template.getStart(source) + 1);
104
+ try {
105
+ postcss.parse(text).walkDecls(decl => {
106
+ const selectors = [], conditions = [];
107
+ for (let parent = decl.parent; parent?.type !== "root"; parent = parent?.parent) {
108
+ if (!parent) break;
109
+ if (parent.type === "rule") selectors.unshift(parent.selector.trim().replace(/\s+/g, " "));
110
+ if (parent.type === "atrule") conditions.unshift(`${parent.name} ${parent.params}`.trim().replace(/\s+/g, " "));
111
+ }
112
+ // Animation frames describe different instants, not combined classes.
113
+ if (conditions.some(condition => /^(?:-\w+-)?keyframes\b/.test(condition))) return;
114
+ result.push({ property: decl.prop.toLowerCase(), value: decl.value, important: !!decl.important, selectors: JSON.stringify(selectors), conditions: JSON.stringify(conditions),
115
+ file: path.relative(options.root, source.fileName).replaceAll("\\", "/"), line: position.line + decl.source.start.line, column: decl.source.start.column + (decl.source.start.line === 1 ? position.character : 0) });
116
+ });
117
+ } catch { /* The core CSS syntax diagnostic owns malformed templates. */ }
118
+ return result;
119
+ }
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
+ }
152
+ return source => {
153
+ const warnings = [], seen = new Set();
154
+ function compare(first, second, location) {
155
+ 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;
157
+ 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);
160
+ 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)})) });
164
+ }
165
+ }
166
+ function visit(node) {
167
+ if (ts.isTaggedTemplateExpression(node) && isAtomic(node.tag)) {
168
+ const declarations = entries(node);
169
+ for (let index = 0; index < declarations.length; index++) compare([declarations[index]], declarations.slice(index + 1), node);
170
+ }
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
+ ts.forEachChild(node, visit);
179
+ }
180
+ if (isInside(options.dsRoot, source.fileName)) visit(source);
181
+ return warnings;
182
+ };
183
+ }
package/dist/check.mjs CHANGED
@@ -19,6 +19,7 @@ import { inspectBehaviors, inspectControllers } from "./behaviors.mjs";
19
19
  import { numericCssUnit, tokenLengthLiteral } from "./css-values.mjs";
20
20
  import { foundationDeclaration, foundationSelector } from "./foundation.mjs";
21
21
  import { scopedTokens, scopedTokenOwner } from "./scoped-tokens.mjs";
22
+ import { createAtomicCompositionReview } from "./atomic-composition.mjs";
22
23
 
23
24
  const scenarioLayoutGuidance = "Put demo-only sizing and framing in a public review/layout pattern. Keep required behavior in the runtime owner; add component props for deliberate product API needs.";
24
25
  const inlineStyleGuidance = `Own visual styles in component/pattern Linaria. ${scenarioLayoutGuidance} Reserve inlineStyleFiles for documented measured geometry, not demo layout.`;
@@ -50,6 +51,7 @@ export function checkProject(directory, { trackingRoot } = {}) {
50
51
  const compiler = projectCompilerOptions(options);
51
52
  const program = ts.createProgram(scriptFiles, { ...compiler, paths: { ...compiler.paths, [config.publicAlias]: [path.join(dsRoot, config.publicEntry)] }, allowJs: true, noResolve: true, noLib: true, types: [], target: ts.ScriptTarget.Latest, module: ts.ModuleKind.ESNext, jsx: ts.JsxEmit.Preserve });
52
53
  const checker = program.getTypeChecker();
54
+ const atomicCompositionReview = createAtomicCompositionReview(program, options);
53
55
  function report(file, rule, message, line = 1, column = 1) {
54
56
  diagnostics.push({ file: path.relative(root, file).replaceAll("\\", "/"), rule, message, line, column });
55
57
  }
@@ -149,6 +151,7 @@ export function checkProject(directory, { trackingRoot } = {}) {
149
151
  const source = program.getSourceFile(file);
150
152
  if (!source) continue;
151
153
  advisories.push(...pointerCaptureAdvisories(source).map(entry => ({ file: path.relative(root, file).replaceAll("\\", "/"), ...entry })));
154
+ advisories.push(...atomicCompositionReview(source).map(entry => ({ file: path.relative(root, file).replaceAll("\\", "/"), ...entry })));
152
155
  for (const diagnostic of program.getSyntacticDiagnostics(source)) {
153
156
  const position = source.getLineAndCharacterOfPosition(diagnostic.start ?? 0);
154
157
  report(file, "source-syntax", ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"), position.line + 1, position.character + 1);
package/dist/cli.mjs CHANGED
@@ -113,7 +113,7 @@ async function main(args) {
113
113
  if (!json && result.diagnostics?.length) {
114
114
  for (const diagnostic of result.diagnostics) console.error(`${diagnostic.file}:${diagnostic.line}:${diagnostic.column} [${diagnostic.rule}] ${diagnostic.message}`);
115
115
  }
116
- if (!json) for (const advisory of result.advisories ?? []) console.error(`${advisory.file}:${advisory.line}:${advisory.column} [advisory:${advisory.rule}] ${advisory.message}`);
116
+ if (!json) for (const advisory of result.advisories ?? []) console.error(`${advisory.file}:${advisory.line}:${advisory.column} [${advisory.severity ?? "advisory"}:${advisory.rule}] ${advisory.message}`);
117
117
  if (result.ok === false || result.diagnostics?.length) process.exitCode = 1;
118
118
  }
119
119
 
@@ -85,6 +85,7 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
85
85
  return [
86
86
  { uri: GUIDE_URI, name: "Start here: design-system workflow", description: "Read first for UI/ADS tasks, especially when only resource access is exposed. Explains the exact workspace, tool sequence, skill selection, previews, project commands and fallback.", mimeType: "text/markdown" },
87
87
  { uri: "ads://guides/behaviors", name: "Controller and behavioral verification guide", mimeType: "text/markdown" },
88
+ { uri: "ads://guides/atomic-styles", name: "Atomic style composition: conflicts, prevention and fixes", mimeType: "text/markdown" },
88
89
  { uri: `${PROJECT_URI}/status`, name: "ADS project status", mimeType: "application/json" },
89
90
  { uri: BUILD_URI, name: "Preview build status and diagnostics", mimeType: "application/json" },
90
91
  { uri: `${PROJECT_URI}/tokens`, name: "Project token values and themes", mimeType: "application/json" },
@@ -92,13 +93,13 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
92
93
  { uri: `${PROJECT_URI}/scenarios`, name: "Independently mountable scenarios", mimeType: "application/json" },
93
94
  { uri: BEHAVIOR_URI, name: "Behavior contracts, demonstrations and current verification evidence", mimeType: "application/json" },
94
95
  { uri: `${PROJECT_URI}/checks`, name: "Current ADS architecture diagnostics", mimeType: "application/json" },
96
+ ...documents.filter(document => promoted.has(document.uri)).sort((a, b) => Number(b.uri === context.publicEntryUri) - Number(a.uri === context.publicEntryUri)).map(({ file: _file, ...document }) => document),
95
97
  { uri: TOKENS_APP_URI, name: "Experimental token widget", mimeType: APP_MIME },
96
98
  { uri: SCENARIO_APP_URI, name: "Experimental scenario widget", mimeType: APP_MIME },
97
99
  ...(options.config.mcp.previewManifest ? [{ uri: PREVIEW_APP_URI, name: "Shared-resource preview App", mimeType: APP_MIME }, { uri: PREVIEW_MANIFEST_URI, name: "Preview dependency graph", mimeType: "application/json" }] : []),
98
100
  ...(options.config.mcp.previewManifest ? resourceAssets() : []),
99
101
  ...allApps().map((app) => ({ uri: appUri(app.id), name: app.title, description: app.description, mimeType: APP_MIME })),
100
102
  ...skills.skills.map(({ uri, frontmatter }) => ({ uri, name: frontmatter.name, description: frontmatter.description, mimeType: "text/markdown" })),
101
- ...documents.filter((document) => promoted.has(document.uri)).map(({ file: _file, ...document }) => document),
102
103
  ...documents.filter((document) => !promoted.has(document.uri)).map(({ file: _file, ...document }) => document),
103
104
  ...[...skills.files.values()].filter((file) => !skillEntries.has(file.uri)).map(({ uri, name, mimeType }) => ({ uri, name, mimeType })),
104
105
  ];
@@ -121,6 +122,7 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
121
122
  async function read(uri, signal) {
122
123
  if (uri === GUIDE_URI) return { contents: [{ uri, mimeType: "text/markdown", text: projectGuide(project(), [...previews(), ...scenarioApps()]) }] };
123
124
  if (uri === "ads://guides/behaviors") return { contents: [{ uri, mimeType: "text/markdown", text: readFileSync(new URL("../../docs/BEHAVIORS.md", import.meta.url), "utf8") }] };
125
+ if (uri === "ads://guides/atomic-styles") return { contents: [{ uri, mimeType: "text/markdown", text: readFileSync(new URL("../skills/ads-designer/references/atomic-styles.md", import.meta.url), "utf8") }] };
124
126
  if (skills.files.has(uri)) return { contents: [skills.read(uri)] };
125
127
  let value;
126
128
  if (uri === `${PROJECT_URI}/status`) value = status();
@@ -203,7 +205,7 @@ export function createAdsMcp(directory, { era = "legacy", skillDirectory, pollIn
203
205
  const matches = inventory.tokens.filter((token) => token.name.toLowerCase().includes(search.toLowerCase()));
204
206
  return json({ total: matches.length, tokens: matches.slice(0, limit), categories: tokenScenarios(), diagnosticCount: inventory.diagnostics.length, resourceUri: `${PROJECT_URI}/tokens` });
205
207
  } },
206
- { name: "ads_check", description: "Run read-only core ADS architecture checks; returns blocking diagnostics separately from nonblocking review advisories. 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 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 }) => {
207
209
  const result = checkProject(root);
208
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` });
209
211
  } },
@@ -2,6 +2,38 @@ import path from "node:path";
2
2
  import ts from "typescript";
3
3
  import { isDevelopmentFile, isInside } from "./config.mjs";
4
4
  import { uiOperation } from "./ui-framework.mjs";
5
+ import { projectCompilerOptions } from "./boundaries.mjs";
6
+
7
+ const resolvedCallables = new WeakMap();
8
+
9
+ // The syntax guard omits libraries and dependency types. Resolve an ambiguous
10
+ // public value lazily against the project's actual types, without importing it.
11
+ // Keep the result with this immutable program so repeated discovery shares it.
12
+ function resolvedCallable(program, options, name) {
13
+ if (!resolvedCallables.has(program)) {
14
+ const { dsRoot, config } = options;
15
+ const entryFile = path.join(dsRoot, config.publicEntry);
16
+ const compiler = projectCompilerOptions(options);
17
+ const semantic = ts.createProgram([entryFile], {
18
+ ...compiler, allowJs: true,
19
+ paths: { ...compiler.paths, [config.publicAlias]: [entryFile] },
20
+ types: compiler.types ?? [],
21
+ });
22
+ const checker = semantic.getTypeChecker();
23
+ const entry = semantic.getSourceFile(entryFile);
24
+ const module = entry && checker.getSymbolAtLocation(entry);
25
+ const callables = new Map();
26
+ for (const exported of module ? checker.getExportsOfModule(module) : []) {
27
+ const symbol = exported.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(exported) : exported;
28
+ const declaration = symbol.valueDeclaration ?? symbol.declarations?.[0];
29
+ if (!declaration || !(symbol.flags & ts.SymbolFlags.Value)) continue;
30
+ const type = checker.getTypeOfSymbolAtLocation(symbol, declaration);
31
+ callables.set(exported.name, !!(type.getCallSignatures().length || type.getConstructSignatures().length));
32
+ }
33
+ resolvedCallables.set(program, callables);
34
+ }
35
+ return resolvedCallables.get(program).get(name) ?? false;
36
+ }
5
37
 
6
38
  export function publicApi(program, options) {
7
39
  const checker = program.getTypeChecker();
@@ -33,7 +65,11 @@ export function publicApi(program, options) {
33
65
  }
34
66
  visit(declaration);
35
67
  const type = checker.getTypeOfSymbolAtLocation(symbol, declaration);
36
- items.push({ name: exported.name, kind, file, declaration, renderable: kind !== "controllers" && renderable, behavioral: !!(type.getCallSignatures().length || type.getConstructSignatures().length) });
68
+ const behavioral = !!(type.getCallSignatures().length || type.getConstructSignatures().length
69
+ || !renderable && !!(type.flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown))
70
+ && (program.getCompilerOptions().noLib || program.getCompilerOptions().noResolve)
71
+ && resolvedCallable(program, options, exported.name));
72
+ items.push({ name: exported.name, kind, file, declaration, renderable: kind !== "controllers" && renderable, behavioral });
37
73
  }
38
74
  return items;
39
75
  }
@@ -86,12 +86,22 @@ export function inspectScenarios(options, suppliedProgram) {
86
86
  const fixtureBase = path.basename(file).startsWith("Scenario.") ? path.join(path.dirname(file), "fixture") : file.replace(/\.scenario\.[^.]+$/, ".fixture");
87
87
  if (![".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", ".mjs", ".cjs", ".json"].some(extension => existsSync(fixtureBase + extension))) report(file, "Keep this scenario's inputs beside it: fixture.ts (directory entry) or <Owner>.fixture.ts (flat entry). An empty fixture is valid for a component without inputs.");
88
88
  if (!metadata || Object.keys(metadata).some((key) => !["title", "kind", "covers", "exercises", "tokens", "samples"].includes(key)) || typeof metadata.title !== "string" || !metadata.title.trim() || !Array.isArray(metadata.covers) || !metadata.covers.length || metadata.covers.some((name) => typeof name !== "string" || !known.get(name)?.renderable)
89
- || Object.hasOwn(metadata, "exercises") && (!Array.isArray(metadata.exercises) || metadata.exercises.some(name => typeof name !== "string" || !behaviorApis.has(name)))
90
89
  || Object.hasOwn(metadata, "tokens") && (!Array.isArray(metadata.tokens) || metadata.tokens.some(name => typeof name !== "string" || !name.startsWith("--")))
91
90
  || kind === "tokens" && (Object.hasOwn(metadata, "tokens") || !validTokenSamples(metadata.samples) || !metadata.covers.some(name => known.get(name)?.kind === "patterns"))
92
91
  || Object.hasOwn(metadata, "samples") && !validTokenSamples(metadata.samples)) {
93
92
  report(file, "Export static metadata with nonempty covers naming rendered public UI. Optional exercises names public behavioral APIs, not rendered UI. Token scenarios must render a project-owned public review pattern and declare samples: fixture.samples, a literal list of { token, property, probe } CSS samples. A plain value list or covers: [] is incomplete."); continue;
94
93
  }
94
+ // A bad behavioral entry must fail validation without erasing independent
95
+ // render evidence and manufacturing missing-coverage errors for real UI.
96
+ const exercises = [];
97
+ if (Object.hasOwn(metadata, "exercises")) {
98
+ if (!Array.isArray(metadata.exercises)) report(file, "Scenario exercises must be a static array naming public behavioral APIs.");
99
+ else for (const [index, name] of metadata.exercises.entries()) {
100
+ if (typeof name !== "string") report(file, `Scenario exercises[${index}] must name a public behavioral API, not ${JSON.stringify(name) ?? "an unresolved value"}.`);
101
+ else if (!behaviorApis.has(name)) report(file, `Scenario exercises[${index}] ${JSON.stringify(name)} is not a recognized public behavioral API. Use a public callable function, hook or controller; rendered UI belongs in covers. Discover the project's exports before declaring exercises.`);
102
+ else exercises.push(name);
103
+ }
104
+ }
95
105
  if (!["composition", "tokens"].includes(kind) && metadata.covers.some((name) => known.get(name).kind !== kind)) report(file, "Component scenarios cover primitives; pattern scenarios cover patterns. Use composition/ for larger interfaces.");
96
106
  const tokens = [...new Set([...(metadata.tokens ?? []), ...(metadata.samples ?? []).map(sample => sample.token)])];
97
107
  for (const name of tokens) if (!tokenNames.has(name)) diagnostics.push({ file: path.relative(root, file).replaceAll("\\", "/"), rule: "token-scenario-reference", line: 1, column: 1, message: `Scenario references undeclared token ${name}. Use a real token from its defining owner.` });
@@ -99,7 +109,7 @@ export function inspectScenarios(options, suppliedProgram) {
99
109
  const owner = scoped.inventory.owner(relative);
100
110
  for (const name of tokens) if (tokenInventory.tokens.find(token => token.name === name)?.owner !== owner) report(file, `Token ${name} needs a preview beside its defining owner. Shared tokens belong under the configured token directory; local tokens stay with their component/pattern.`);
101
111
  }
102
- scenarios.push({ id, kind, title: metadata.title, covers: [...new Set(metadata.covers)], ...(metadata.exercises ? { exercises: [...new Set(metadata.exercises)] } : {}), tokens, ...(metadata.samples ? { samples: metadata.samples } : {}), file: path.relative(root, file).replaceAll("\\", "/"), artifact: path.relative(root, scenarioArtifact(root, id)).replaceAll("\\", "/") });
112
+ scenarios.push({ id, kind, title: metadata.title, covers: [...new Set(metadata.covers)], ...(Object.hasOwn(metadata, "exercises") ? { exercises: [...new Set(exercises)] } : {}), tokens, ...(metadata.samples ? { samples: metadata.samples } : {}), file: path.relative(root, file).replaceAll("\\", "/"), artifact: path.relative(root, scenarioArtifact(root, id)).replaceAll("\\", "/") });
103
113
  }
104
114
  return { scenarios: scenarios.sort((a, b) => a.id.localeCompare(b.id)), diagnostics };
105
115
  }
@@ -49,6 +49,12 @@ 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
53
+ [atomic style composition](references/atomic-styles.md). The same guide is served
54
+ as `ads://guides/atomic-styles`. It explains shorthand/logical conflicts and
55
+ matching-property fixes. Review warnings in the real `cx` composition and verify
56
+ computed styles across affected sizes/states; retain intentional product behavior.
57
+
52
58
  Controllers are UI behavior owners: private helpers stay with their component or
53
59
  pattern; reusable controllers use discovered `controllers/<Name>` units. Use the
54
60
  project's configured framework and declared dependencies, or plain TypeScript,
@@ -0,0 +1,121 @@
1
+ # Atomic style composition
2
+
3
+ Use this guide when creating or migrating component/pattern styles, changing
4
+ base/size/state combinations, or reviewing `atomic-composition-review` warnings.
5
+ It is also available as `ads://guides/atomic-styles` through ADS MCP.
6
+
7
+ ## Why valid tokens can still render incorrectly
8
+
9
+ Atomic Linaria extracts declarations into reusable classes. `cx` from
10
+ `@linaria/atomic` or `@linaria/core` replaces earlier atoms with the same property
11
+ and selector/condition key. It does not expand every CSS shorthand or resolve
12
+ every overlap between physical and logical properties.
13
+
14
+ For example, these declarations have different atomic keys:
15
+
16
+ ```tsx
17
+ const base = css`
18
+ padding: 0;
19
+ `;
20
+ const small = css`
21
+ padding-inline: var(--space-6);
22
+ `;
23
+
24
+ cx(base, small);
25
+ ```
26
+
27
+ Both classes survive. Both declarations can affect horizontal padding in a
28
+ horizontal writing mode. In the inspected Linaria implementation, `padding` and
29
+ `padding-inline` receive equal specificity. If a reset atom is emitted later,
30
+ it can override the intended size. Shared atom reuse and CSS splitting can make
31
+ this visible in a larger build even when an isolated view appears correct.
32
+ The order of class names in HTML does not decide equal-specificity rule order.
33
+ Valid tokens, successful extraction and a passing source check do not establish
34
+ the intended computed value.
35
+
36
+ See Linaria's [class merging implementation](https://github.com/callstack/linaria/blob/master/packages/core/src/cx.ts)
37
+ and [property priority implementation](https://github.com/callstack/linaria/blob/master/packages/atomic/src/processors/helpers/propertyPriority.ts).
38
+ Inspect the project's installed version when investigating its output.
39
+
40
+ ## Prefer matching properties across composed blocks
41
+
42
+ Express the reset and overrides using the same property vocabulary:
43
+
44
+ ```tsx
45
+ const base = css`
46
+ padding-block: 0;
47
+ padding-inline: 0;
48
+ `;
49
+ const small = css`
50
+ padding-inline: var(--space-6);
51
+ `;
52
+ const regular = css`
53
+ padding-block: var(--space-4);
54
+ padding-inline: var(--space-8);
55
+ `;
56
+
57
+ cx(base, compact ? small : regular);
58
+ ```
59
+
60
+ Now each selected size replaces the corresponding base atom through `cx`.
61
+ Review every size, including the default and icon-only variants. Changing only
62
+ one size can leave another size mixing shorthand and logical properties.
63
+ The names and tokens above are illustrative: use the project's real API and
64
+ declared tokens. Preserve its intended dimensions and supported writing modes.
65
+
66
+ The same reasoning applies to margin, inset, logical borders/corners, dimensions
67
+ and overflow. For example, prefer `margin-inline: 0` plus a size's
68
+ `margin-inline: var(...)` over combining `margin: 0` with that logical override.
69
+ Choose consistent physical properties when the product deliberately positions
70
+ against physical edges; logical properties are not mandatory.
71
+
72
+ An intentional state override may have a different selector or condition:
73
+
74
+ ```tsx
75
+ const treatment = css`
76
+ padding-inline: var(--space-6);
77
+ &:hover { padding-inline: var(--space-8); }
78
+ `;
79
+ ```
80
+
81
+ This is a distinct state, not two competing base atoms. Whether changing padding
82
+ on hover is desirable remains a product decision.
83
+
84
+ ## Respond to a warning
85
+
86
+ 1. Follow the warning's related file/line locations and inspect the actual `cx`
87
+ composition. Variant lookups may contribute several possible combinations.
88
+ 2. In a current built preview, inspect computed properties and the winning CSS
89
+ rules. Establish whether extraction order, specificity, writing mode or an
90
+ intentional override explains the result.
91
+ 3. For accidental overlap, align property names/granularity across the base and
92
+ affected variants. Use Linaria's `cx`, preserve tokens and fix the owning
93
+ component/pattern. Reordering class strings does not remove overlapping keys.
94
+ 4. Verify the affected sizes/states and supported directions in the normal app
95
+ and preview build. Add a registered rendered check when a specific geometry
96
+ or computed-style guarantee matters.
97
+
98
+ Increasing specificity, adding `!important`, copying styles into scenarios or
99
+ moving component styling into `base.css` can hide the symptom while retaining
100
+ the conflict. Prefer resolving the composition itself. If an overlap is
101
+ intentional, document why its precedence is stable and verify the resulting
102
+ behavior rather than changing product capabilities to silence a warning.
103
+
104
+ ## What ADS checks
105
+
106
+ `atomic-composition-review` is a nonblocking warning, returned under `advisories`
107
+ with `severity: "warning"`. CLI checks still exit successfully when there are no
108
+ errors; MCP checks and mutations preserve the warning and its guidance URI.
109
+
110
+ 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
113
+ spacing, inset, borders/corners, dimensions, overflow, backgrounds, typography,
114
+ 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.
116
+
117
+ 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.
@@ -80,6 +80,12 @@ 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
84
+ [atomic style composition](references/atomic-styles.md). The same guide is served
85
+ as `ads://guides/atomic-styles`. It explains shorthand/logical conflicts and
86
+ matching-property fixes. Review warnings in the real `cx` composition and verify
87
+ computed styles across affected sizes/states; retain intentional product behavior.
88
+
83
89
  Controllers are UI behavior owners: private helpers stay with their component or
84
90
  pattern; reusable controllers use discovered `controllers/<Name>` units. Use the
85
91
  project's configured framework and declared dependencies, or plain TypeScript,
@@ -0,0 +1,121 @@
1
+ # Atomic style composition
2
+
3
+ Use this guide when creating or migrating component/pattern styles, changing
4
+ base/size/state combinations, or reviewing `atomic-composition-review` warnings.
5
+ It is also available as `ads://guides/atomic-styles` through ADS MCP.
6
+
7
+ ## Why valid tokens can still render incorrectly
8
+
9
+ Atomic Linaria extracts declarations into reusable classes. `cx` from
10
+ `@linaria/atomic` or `@linaria/core` replaces earlier atoms with the same property
11
+ and selector/condition key. It does not expand every CSS shorthand or resolve
12
+ every overlap between physical and logical properties.
13
+
14
+ For example, these declarations have different atomic keys:
15
+
16
+ ```tsx
17
+ const base = css`
18
+ padding: 0;
19
+ `;
20
+ const small = css`
21
+ padding-inline: var(--space-6);
22
+ `;
23
+
24
+ cx(base, small);
25
+ ```
26
+
27
+ Both classes survive. Both declarations can affect horizontal padding in a
28
+ horizontal writing mode. In the inspected Linaria implementation, `padding` and
29
+ `padding-inline` receive equal specificity. If a reset atom is emitted later,
30
+ it can override the intended size. Shared atom reuse and CSS splitting can make
31
+ this visible in a larger build even when an isolated view appears correct.
32
+ The order of class names in HTML does not decide equal-specificity rule order.
33
+ Valid tokens, successful extraction and a passing source check do not establish
34
+ the intended computed value.
35
+
36
+ See Linaria's [class merging implementation](https://github.com/callstack/linaria/blob/master/packages/core/src/cx.ts)
37
+ and [property priority implementation](https://github.com/callstack/linaria/blob/master/packages/atomic/src/processors/helpers/propertyPriority.ts).
38
+ Inspect the project's installed version when investigating its output.
39
+
40
+ ## Prefer matching properties across composed blocks
41
+
42
+ Express the reset and overrides using the same property vocabulary:
43
+
44
+ ```tsx
45
+ const base = css`
46
+ padding-block: 0;
47
+ padding-inline: 0;
48
+ `;
49
+ const small = css`
50
+ padding-inline: var(--space-6);
51
+ `;
52
+ const regular = css`
53
+ padding-block: var(--space-4);
54
+ padding-inline: var(--space-8);
55
+ `;
56
+
57
+ cx(base, compact ? small : regular);
58
+ ```
59
+
60
+ Now each selected size replaces the corresponding base atom through `cx`.
61
+ Review every size, including the default and icon-only variants. Changing only
62
+ one size can leave another size mixing shorthand and logical properties.
63
+ The names and tokens above are illustrative: use the project's real API and
64
+ declared tokens. Preserve its intended dimensions and supported writing modes.
65
+
66
+ The same reasoning applies to margin, inset, logical borders/corners, dimensions
67
+ and overflow. For example, prefer `margin-inline: 0` plus a size's
68
+ `margin-inline: var(...)` over combining `margin: 0` with that logical override.
69
+ Choose consistent physical properties when the product deliberately positions
70
+ against physical edges; logical properties are not mandatory.
71
+
72
+ An intentional state override may have a different selector or condition:
73
+
74
+ ```tsx
75
+ const treatment = css`
76
+ padding-inline: var(--space-6);
77
+ &:hover { padding-inline: var(--space-8); }
78
+ `;
79
+ ```
80
+
81
+ This is a distinct state, not two competing base atoms. Whether changing padding
82
+ on hover is desirable remains a product decision.
83
+
84
+ ## Respond to a warning
85
+
86
+ 1. Follow the warning's related file/line locations and inspect the actual `cx`
87
+ composition. Variant lookups may contribute several possible combinations.
88
+ 2. In a current built preview, inspect computed properties and the winning CSS
89
+ rules. Establish whether extraction order, specificity, writing mode or an
90
+ intentional override explains the result.
91
+ 3. For accidental overlap, align property names/granularity across the base and
92
+ affected variants. Use Linaria's `cx`, preserve tokens and fix the owning
93
+ component/pattern. Reordering class strings does not remove overlapping keys.
94
+ 4. Verify the affected sizes/states and supported directions in the normal app
95
+ and preview build. Add a registered rendered check when a specific geometry
96
+ or computed-style guarantee matters.
97
+
98
+ Increasing specificity, adding `!important`, copying styles into scenarios or
99
+ moving component styling into `base.css` can hide the symptom while retaining
100
+ the conflict. Prefer resolving the composition itself. If an overlap is
101
+ intentional, document why its precedence is stable and verify the resulting
102
+ behavior rather than changing product capabilities to silence a warning.
103
+
104
+ ## What ADS checks
105
+
106
+ `atomic-composition-review` is a nonblocking warning, returned under `advisories`
107
+ with `severity: "warning"`. CLI checks still exit successfully when there are no
108
+ errors; MCP checks and mutations preserve the warning and its guidance URI.
109
+
110
+ 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
113
+ spacing, inset, borders/corners, dimensions, overflow, backgrounds, typography,
114
+ 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.
116
+
117
+ 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.
@@ -44,6 +44,12 @@ 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
48
+ [atomic style composition](references/atomic-styles.md). The same guide is served
49
+ as `ads://guides/atomic-styles`. It explains shorthand/logical conflicts and
50
+ matching-property fixes. Review warnings in the real `cx` composition and verify
51
+ computed styles across affected sizes/states; retain intentional product behavior.
52
+
47
53
  Controllers are UI behavior owners: private helpers stay with their component or
48
54
  pattern; reusable controllers use discovered `controllers/<Name>` units. Use the
49
55
  project's configured framework and declared dependencies, or plain TypeScript,
@@ -0,0 +1,121 @@
1
+ # Atomic style composition
2
+
3
+ Use this guide when creating or migrating component/pattern styles, changing
4
+ base/size/state combinations, or reviewing `atomic-composition-review` warnings.
5
+ It is also available as `ads://guides/atomic-styles` through ADS MCP.
6
+
7
+ ## Why valid tokens can still render incorrectly
8
+
9
+ Atomic Linaria extracts declarations into reusable classes. `cx` from
10
+ `@linaria/atomic` or `@linaria/core` replaces earlier atoms with the same property
11
+ and selector/condition key. It does not expand every CSS shorthand or resolve
12
+ every overlap between physical and logical properties.
13
+
14
+ For example, these declarations have different atomic keys:
15
+
16
+ ```tsx
17
+ const base = css`
18
+ padding: 0;
19
+ `;
20
+ const small = css`
21
+ padding-inline: var(--space-6);
22
+ `;
23
+
24
+ cx(base, small);
25
+ ```
26
+
27
+ Both classes survive. Both declarations can affect horizontal padding in a
28
+ horizontal writing mode. In the inspected Linaria implementation, `padding` and
29
+ `padding-inline` receive equal specificity. If a reset atom is emitted later,
30
+ it can override the intended size. Shared atom reuse and CSS splitting can make
31
+ this visible in a larger build even when an isolated view appears correct.
32
+ The order of class names in HTML does not decide equal-specificity rule order.
33
+ Valid tokens, successful extraction and a passing source check do not establish
34
+ the intended computed value.
35
+
36
+ See Linaria's [class merging implementation](https://github.com/callstack/linaria/blob/master/packages/core/src/cx.ts)
37
+ and [property priority implementation](https://github.com/callstack/linaria/blob/master/packages/atomic/src/processors/helpers/propertyPriority.ts).
38
+ Inspect the project's installed version when investigating its output.
39
+
40
+ ## Prefer matching properties across composed blocks
41
+
42
+ Express the reset and overrides using the same property vocabulary:
43
+
44
+ ```tsx
45
+ const base = css`
46
+ padding-block: 0;
47
+ padding-inline: 0;
48
+ `;
49
+ const small = css`
50
+ padding-inline: var(--space-6);
51
+ `;
52
+ const regular = css`
53
+ padding-block: var(--space-4);
54
+ padding-inline: var(--space-8);
55
+ `;
56
+
57
+ cx(base, compact ? small : regular);
58
+ ```
59
+
60
+ Now each selected size replaces the corresponding base atom through `cx`.
61
+ Review every size, including the default and icon-only variants. Changing only
62
+ one size can leave another size mixing shorthand and logical properties.
63
+ The names and tokens above are illustrative: use the project's real API and
64
+ declared tokens. Preserve its intended dimensions and supported writing modes.
65
+
66
+ The same reasoning applies to margin, inset, logical borders/corners, dimensions
67
+ and overflow. For example, prefer `margin-inline: 0` plus a size's
68
+ `margin-inline: var(...)` over combining `margin: 0` with that logical override.
69
+ Choose consistent physical properties when the product deliberately positions
70
+ against physical edges; logical properties are not mandatory.
71
+
72
+ An intentional state override may have a different selector or condition:
73
+
74
+ ```tsx
75
+ const treatment = css`
76
+ padding-inline: var(--space-6);
77
+ &:hover { padding-inline: var(--space-8); }
78
+ `;
79
+ ```
80
+
81
+ This is a distinct state, not two competing base atoms. Whether changing padding
82
+ on hover is desirable remains a product decision.
83
+
84
+ ## Respond to a warning
85
+
86
+ 1. Follow the warning's related file/line locations and inspect the actual `cx`
87
+ composition. Variant lookups may contribute several possible combinations.
88
+ 2. In a current built preview, inspect computed properties and the winning CSS
89
+ rules. Establish whether extraction order, specificity, writing mode or an
90
+ intentional override explains the result.
91
+ 3. For accidental overlap, align property names/granularity across the base and
92
+ affected variants. Use Linaria's `cx`, preserve tokens and fix the owning
93
+ component/pattern. Reordering class strings does not remove overlapping keys.
94
+ 4. Verify the affected sizes/states and supported directions in the normal app
95
+ and preview build. Add a registered rendered check when a specific geometry
96
+ or computed-style guarantee matters.
97
+
98
+ Increasing specificity, adding `!important`, copying styles into scenarios or
99
+ moving component styling into `base.css` can hide the symptom while retaining
100
+ the conflict. Prefer resolving the composition itself. If an overlap is
101
+ intentional, document why its precedence is stable and verify the resulting
102
+ behavior rather than changing product capabilities to silence a warning.
103
+
104
+ ## What ADS checks
105
+
106
+ `atomic-composition-review` is a nonblocking warning, returned under `advisories`
107
+ with `severity: "warning"`. CLI checks still exit successfully when there are no
108
+ errors; MCP checks and mutations preserve the warning and its guidance URI.
109
+
110
+ 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
113
+ spacing, inset, borders/corners, dimensions, overflow, backgrounds, typography,
114
+ 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.
116
+
117
+ 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.
@@ -46,6 +46,12 @@ 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
50
+ [atomic style composition](references/atomic-styles.md). The same guide is served
51
+ as `ads://guides/atomic-styles`. It explains shorthand/logical conflicts and
52
+ matching-property fixes. Review warnings in the real `cx` composition and verify
53
+ computed styles across affected sizes/states; retain intentional product behavior.
54
+
49
55
  Controllers are UI behavior owners: private helpers stay with their component or
50
56
  pattern; reusable controllers use discovered `controllers/<Name>` units. Use the
51
57
  project's configured framework and declared dependencies, or plain TypeScript,
@@ -0,0 +1,121 @@
1
+ # Atomic style composition
2
+
3
+ Use this guide when creating or migrating component/pattern styles, changing
4
+ base/size/state combinations, or reviewing `atomic-composition-review` warnings.
5
+ It is also available as `ads://guides/atomic-styles` through ADS MCP.
6
+
7
+ ## Why valid tokens can still render incorrectly
8
+
9
+ Atomic Linaria extracts declarations into reusable classes. `cx` from
10
+ `@linaria/atomic` or `@linaria/core` replaces earlier atoms with the same property
11
+ and selector/condition key. It does not expand every CSS shorthand or resolve
12
+ every overlap between physical and logical properties.
13
+
14
+ For example, these declarations have different atomic keys:
15
+
16
+ ```tsx
17
+ const base = css`
18
+ padding: 0;
19
+ `;
20
+ const small = css`
21
+ padding-inline: var(--space-6);
22
+ `;
23
+
24
+ cx(base, small);
25
+ ```
26
+
27
+ Both classes survive. Both declarations can affect horizontal padding in a
28
+ horizontal writing mode. In the inspected Linaria implementation, `padding` and
29
+ `padding-inline` receive equal specificity. If a reset atom is emitted later,
30
+ it can override the intended size. Shared atom reuse and CSS splitting can make
31
+ this visible in a larger build even when an isolated view appears correct.
32
+ The order of class names in HTML does not decide equal-specificity rule order.
33
+ Valid tokens, successful extraction and a passing source check do not establish
34
+ the intended computed value.
35
+
36
+ See Linaria's [class merging implementation](https://github.com/callstack/linaria/blob/master/packages/core/src/cx.ts)
37
+ and [property priority implementation](https://github.com/callstack/linaria/blob/master/packages/atomic/src/processors/helpers/propertyPriority.ts).
38
+ Inspect the project's installed version when investigating its output.
39
+
40
+ ## Prefer matching properties across composed blocks
41
+
42
+ Express the reset and overrides using the same property vocabulary:
43
+
44
+ ```tsx
45
+ const base = css`
46
+ padding-block: 0;
47
+ padding-inline: 0;
48
+ `;
49
+ const small = css`
50
+ padding-inline: var(--space-6);
51
+ `;
52
+ const regular = css`
53
+ padding-block: var(--space-4);
54
+ padding-inline: var(--space-8);
55
+ `;
56
+
57
+ cx(base, compact ? small : regular);
58
+ ```
59
+
60
+ Now each selected size replaces the corresponding base atom through `cx`.
61
+ Review every size, including the default and icon-only variants. Changing only
62
+ one size can leave another size mixing shorthand and logical properties.
63
+ The names and tokens above are illustrative: use the project's real API and
64
+ declared tokens. Preserve its intended dimensions and supported writing modes.
65
+
66
+ The same reasoning applies to margin, inset, logical borders/corners, dimensions
67
+ and overflow. For example, prefer `margin-inline: 0` plus a size's
68
+ `margin-inline: var(...)` over combining `margin: 0` with that logical override.
69
+ Choose consistent physical properties when the product deliberately positions
70
+ against physical edges; logical properties are not mandatory.
71
+
72
+ An intentional state override may have a different selector or condition:
73
+
74
+ ```tsx
75
+ const treatment = css`
76
+ padding-inline: var(--space-6);
77
+ &:hover { padding-inline: var(--space-8); }
78
+ `;
79
+ ```
80
+
81
+ This is a distinct state, not two competing base atoms. Whether changing padding
82
+ on hover is desirable remains a product decision.
83
+
84
+ ## Respond to a warning
85
+
86
+ 1. Follow the warning's related file/line locations and inspect the actual `cx`
87
+ composition. Variant lookups may contribute several possible combinations.
88
+ 2. In a current built preview, inspect computed properties and the winning CSS
89
+ rules. Establish whether extraction order, specificity, writing mode or an
90
+ intentional override explains the result.
91
+ 3. For accidental overlap, align property names/granularity across the base and
92
+ affected variants. Use Linaria's `cx`, preserve tokens and fix the owning
93
+ component/pattern. Reordering class strings does not remove overlapping keys.
94
+ 4. Verify the affected sizes/states and supported directions in the normal app
95
+ and preview build. Add a registered rendered check when a specific geometry
96
+ or computed-style guarantee matters.
97
+
98
+ Increasing specificity, adding `!important`, copying styles into scenarios or
99
+ moving component styling into `base.css` can hide the symptom while retaining
100
+ the conflict. Prefer resolving the composition itself. If an overlap is
101
+ intentional, document why its precedence is stable and verify the resulting
102
+ behavior rather than changing product capabilities to silence a warning.
103
+
104
+ ## What ADS checks
105
+
106
+ `atomic-composition-review` is a nonblocking warning, returned under `advisories`
107
+ with `severity: "warning"`. CLI checks still exit successfully when there are no
108
+ errors; MCP checks and mutations preserve the warning and its guidance URI.
109
+
110
+ 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
113
+ spacing, inset, borders/corners, dimensions, overflow, backgrounds, typography,
114
+ 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.
116
+
117
+ 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.
@@ -66,6 +66,12 @@ 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
70
+ [atomic style composition](references/atomic-styles.md). The same guide is served
71
+ as `ads://guides/atomic-styles`. It explains shorthand/logical conflicts and
72
+ matching-property fixes. Review warnings in the real `cx` composition and verify
73
+ computed styles across affected sizes/states; retain intentional product behavior.
74
+
69
75
  Controllers are UI behavior owners: private helpers stay with their component or
70
76
  pattern; reusable controllers use discovered `controllers/<Name>` units. Use the
71
77
  project's configured framework and declared dependencies, or plain TypeScript,
@@ -0,0 +1,121 @@
1
+ # Atomic style composition
2
+
3
+ Use this guide when creating or migrating component/pattern styles, changing
4
+ base/size/state combinations, or reviewing `atomic-composition-review` warnings.
5
+ It is also available as `ads://guides/atomic-styles` through ADS MCP.
6
+
7
+ ## Why valid tokens can still render incorrectly
8
+
9
+ Atomic Linaria extracts declarations into reusable classes. `cx` from
10
+ `@linaria/atomic` or `@linaria/core` replaces earlier atoms with the same property
11
+ and selector/condition key. It does not expand every CSS shorthand or resolve
12
+ every overlap between physical and logical properties.
13
+
14
+ For example, these declarations have different atomic keys:
15
+
16
+ ```tsx
17
+ const base = css`
18
+ padding: 0;
19
+ `;
20
+ const small = css`
21
+ padding-inline: var(--space-6);
22
+ `;
23
+
24
+ cx(base, small);
25
+ ```
26
+
27
+ Both classes survive. Both declarations can affect horizontal padding in a
28
+ horizontal writing mode. In the inspected Linaria implementation, `padding` and
29
+ `padding-inline` receive equal specificity. If a reset atom is emitted later,
30
+ it can override the intended size. Shared atom reuse and CSS splitting can make
31
+ this visible in a larger build even when an isolated view appears correct.
32
+ The order of class names in HTML does not decide equal-specificity rule order.
33
+ Valid tokens, successful extraction and a passing source check do not establish
34
+ the intended computed value.
35
+
36
+ See Linaria's [class merging implementation](https://github.com/callstack/linaria/blob/master/packages/core/src/cx.ts)
37
+ and [property priority implementation](https://github.com/callstack/linaria/blob/master/packages/atomic/src/processors/helpers/propertyPriority.ts).
38
+ Inspect the project's installed version when investigating its output.
39
+
40
+ ## Prefer matching properties across composed blocks
41
+
42
+ Express the reset and overrides using the same property vocabulary:
43
+
44
+ ```tsx
45
+ const base = css`
46
+ padding-block: 0;
47
+ padding-inline: 0;
48
+ `;
49
+ const small = css`
50
+ padding-inline: var(--space-6);
51
+ `;
52
+ const regular = css`
53
+ padding-block: var(--space-4);
54
+ padding-inline: var(--space-8);
55
+ `;
56
+
57
+ cx(base, compact ? small : regular);
58
+ ```
59
+
60
+ Now each selected size replaces the corresponding base atom through `cx`.
61
+ Review every size, including the default and icon-only variants. Changing only
62
+ one size can leave another size mixing shorthand and logical properties.
63
+ The names and tokens above are illustrative: use the project's real API and
64
+ declared tokens. Preserve its intended dimensions and supported writing modes.
65
+
66
+ The same reasoning applies to margin, inset, logical borders/corners, dimensions
67
+ and overflow. For example, prefer `margin-inline: 0` plus a size's
68
+ `margin-inline: var(...)` over combining `margin: 0` with that logical override.
69
+ Choose consistent physical properties when the product deliberately positions
70
+ against physical edges; logical properties are not mandatory.
71
+
72
+ An intentional state override may have a different selector or condition:
73
+
74
+ ```tsx
75
+ const treatment = css`
76
+ padding-inline: var(--space-6);
77
+ &:hover { padding-inline: var(--space-8); }
78
+ `;
79
+ ```
80
+
81
+ This is a distinct state, not two competing base atoms. Whether changing padding
82
+ on hover is desirable remains a product decision.
83
+
84
+ ## Respond to a warning
85
+
86
+ 1. Follow the warning's related file/line locations and inspect the actual `cx`
87
+ composition. Variant lookups may contribute several possible combinations.
88
+ 2. In a current built preview, inspect computed properties and the winning CSS
89
+ rules. Establish whether extraction order, specificity, writing mode or an
90
+ intentional override explains the result.
91
+ 3. For accidental overlap, align property names/granularity across the base and
92
+ affected variants. Use Linaria's `cx`, preserve tokens and fix the owning
93
+ component/pattern. Reordering class strings does not remove overlapping keys.
94
+ 4. Verify the affected sizes/states and supported directions in the normal app
95
+ and preview build. Add a registered rendered check when a specific geometry
96
+ or computed-style guarantee matters.
97
+
98
+ Increasing specificity, adding `!important`, copying styles into scenarios or
99
+ moving component styling into `base.css` can hide the symptom while retaining
100
+ the conflict. Prefer resolving the composition itself. If an overlap is
101
+ intentional, document why its precedence is stable and verify the resulting
102
+ behavior rather than changing product capabilities to silence a warning.
103
+
104
+ ## What ADS checks
105
+
106
+ `atomic-composition-review` is a nonblocking warning, returned under `advisories`
107
+ with `severity: "warning"`. CLI checks still exit successfully when there are no
108
+ errors; MCP checks and mutations preserve the warning and its guidance URI.
109
+
110
+ 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
113
+ spacing, inset, borders/corners, dimensions, overflow, backgrounds, typography,
114
+ 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.
116
+
117
+ 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.
package/docs/ADS.md CHANGED
@@ -367,6 +367,13 @@ Pointer capture without locally visible capture-loss cleanup produces a
367
367
  nonblocking advisory. Review delegated cleanup and browser behavior; neither a
368
368
  warning nor a handler's presence establishes lifecycle correctness.
369
369
 
370
+ Potential atomic CSS overlaps produce `atomic-composition-review` warnings with
371
+ related declarations and a guidance URI. They are nonblocking, including during
372
+ guarded mutations: valid CSS can intentionally overlap. Read
373
+ [atomic style composition](../skills/ads-designer/references/atomic-styles.md) for causes, matching-property fixes
374
+ and the source review's limits. Registered rendered checks can enforce a
375
+ project's intended computed geometry; token validity alone cannot establish it.
376
+
370
377
  Document a new ownership exception with its guard/configuration change and
371
378
  tests. Passing static checks establishes only the policies and source formats
372
379
  they cover. Browser review and behavior tests still verify accessibility,
@@ -390,6 +397,13 @@ Validate accepted and rejected cases, configuration, and false-positive limits.
390
397
 
391
398
  ## Bootstrap and updates
392
399
 
400
+ The atomic composition guide lives in each skill's `references/atomic-styles.md`.
401
+ The maintained copy is in `skills/ads-designer/references/atomic-styles.md`;
402
+ MCP serves that packaged skill reference. `npm run sync:skill-references` copies
403
+ it to the other skills for standalone use; `npm run check:skill-references`
404
+ rejects drift during the tool build. Published packages include these references
405
+ under `dist/skills/`.
406
+
393
407
  Root [bootstrap-ds/](../bootstrap-ds/README.md) is the canonical empty scaffold.
394
408
  Its identical copy under `skills/ads-designer/assets/bootstrap-ds/` makes the
395
409
  skill self-contained. Run `npm run sync:skill-starter` after edits and
@@ -122,6 +122,12 @@ Linaria's [colocation and extraction model](https://github.com/callstack/linaria
122
122
  and [atomic composition model](https://github.com/callstack/linaria/blob/master/docs/ATOMIC_CSS.md)
123
123
  support this combination of local ownership and compiled styling.
124
124
 
125
+ Atomic merging matches property/selector/condition keys; it does not resolve
126
+ every shorthand/logical-property overlap. ADS reports detectable composition
127
+ risks as nonblocking warnings with guidance, while registered rendered checks
128
+ can enforce actual geometry. See the skill-local
129
+ [atomic style composition guide](../skills/ads-designer/references/atomic-styles.md).
130
+
125
131
  Airbnb's 2022 [migration report](https://medium.com/airbnb-engineering/airbnbs-trip-to-linaria-dc169230bd12)
126
132
  provides practical evidence for this choice: performance was its leading
127
133
  criterion, with separate CSS caching and colocated authoring among the benefits.
package/docs/BEHAVIORS.md CHANGED
@@ -95,6 +95,15 @@ metadata string does not count. Dynamically passed behavior references produce a
95
95
  review advisory when a direct call cannot be established; verify those through
96
96
  registered execution checks rather than changing the API for the analyzer.
97
97
 
98
+ Callable exports may be functions, callable objects with attached methods, or
99
+ factory results. ADS uses their TypeScript call/construct signatures, including
100
+ the project's library declarations; it does not execute modules or maintain a
101
+ list of recognized factory names. The fast source guard resolves ambiguous public
102
+ values lazily and shares that type discovery within the check. Untyped `any`
103
+ values and objects containing methods do not establish a callable API.
104
+ An invalid `exercises` entry names the offending value and fails validation while
105
+ retaining independent render evidence for `covers`.
106
+
98
107
  Public APIs appear in `ads_catalog` with `apiKind: "ui"` or `"behavior"` and their
99
108
  scenario links. `ads_behaviors` joins contracts to demonstrations and evidence.
100
109
  A demonstrated API is not automatically a verified guarantee.
package/docs/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.3.7
6
+
7
+ - Warn about statically traceable atomic CSS property overlaps without blocking
8
+ checks or guarded edits. Include source pairs, concrete correction guidance and
9
+ a guide inside every ADS skill's references folder, also served by MCP.
10
+ Keep regression cases
11
+ generic and validate computed geometry in a dummy compiled preview.
12
+
13
+ - Preserve callable-object and factory-result recognition in the fast source
14
+ guard using lazy project type discovery. Name invalid scenario `exercises`
15
+ entries without discarding independent UI render coverage.
16
+
5
17
  ## 0.3.6
6
18
 
7
19
  - Isolate managed resource-graph output by session; reuse verified compiled
package/docs/MCP.md CHANGED
@@ -188,6 +188,16 @@ resources. No arbitrary filesystem reader is exposed. Prefer bounded tools for
188
188
  large catalogues; explicit resource reads can enter model context and a client
189
189
  may truncate large payloads.
190
190
 
191
+ `ads_check` and `ads://project/checks` return nonblocking atomic composition
192
+ warnings under `advisories` with `severity: "warning"`, related source declarations
193
+ and `guidanceUri: "ads://guides/atomic-styles"`. Read that guide for causes and
194
+ component-level fixes. The full guide lives in every ADS skill as
195
+ `references/atomic-styles.md` for standalone clients; MCP serves the packaged
196
+ [skill-local reference](../skills/ads-designer/references/atomic-styles.md).
197
+ Guarded mutation results preserve these warnings without rejecting valid source.
198
+ An overlap warning is a review signal; failed registered rendered checks remain
199
+ blocking.
200
+
191
201
  ### Behavioral verification
192
202
 
193
203
  Read `ads://guides/behaviors` or the [behavior guide](BEHAVIORS.md) for the contract
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reqvire-org/ads",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
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": {
@@ -34,8 +34,10 @@
34
34
  "generate:icons": "node scripts/generate-icons.mjs",
35
35
  "sync:skill-starter": "node scripts/sync-skill-starter.mjs",
36
36
  "check:skill-starter": "node scripts/sync-skill-starter.mjs --check",
37
+ "sync:skill-references": "node scripts/sync-skill-references.mjs",
38
+ "check:skill-references": "node scripts/sync-skill-references.mjs --check",
37
39
  "lint": "npm run lint:tool",
38
- "lint:tool": "npm run check:skill-starter && oxlint --deny-warnings src preview-client showcase-client mcp-explorer tests/*.test.mjs scripts/build-ads-tool.mjs scripts/pack-ads-tool.mjs",
40
+ "lint:tool": "npm run check:skill-starter && npm run check:skill-references && oxlint --deny-warnings src preview-client showcase-client mcp-explorer tests/*.test.mjs scripts/build-ads-tool.mjs scripts/pack-ads-tool.mjs scripts/sync-skill-references.mjs",
39
41
  "lint:project": "npm run lint:ds && npm run lint:style && npm run check:skill-starter",
40
42
  "lint:ds": "node dist/cli.mjs lint-config && node dist/cli.mjs check && oxlint --deny-warnings --config .ds/oxlintrc.json src design-system",
41
43
  "lint:style": "biome lint --config-path=biome.css.json .",