@reqvire-org/ads 0.3.8 → 0.3.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/atomic-trace.mjs +3 -198
- package/dist/bootstrap-ds/scripts/build-mcp-previews.mjs +2 -1
- package/dist/bootstrap-ds/vite.config.ts +2 -1
- package/dist/check.mjs +12 -3
- package/dist/linaria/atomic-css.cjs +2 -0
- package/dist/linaria/atomic-styled.cjs +2 -0
- package/dist/linaria/preserve-values.cjs +32 -0
- package/dist/linaria.d.mts +1 -0
- package/dist/linaria.mjs +9 -0
- package/dist/mcp/preview-widget.bundle.js +1 -1
- package/dist/mcp/resource-loader.js +5 -2
- package/dist/mcp/server.mjs +3 -1
- package/dist/preview-resources.mjs +2 -1
- package/dist/scenario-trace.mjs +149 -0
- package/dist/scenarios-compiler.mjs +2 -1
- package/dist/scenarios.mjs +13 -6
- package/dist/skills/ads-break-ui/SKILL.md +9 -2
- package/dist/skills/ads-break-ui/references/atomic-styles.md +50 -0
- package/dist/skills/ads-break-ui/references/scenarios.md +98 -0
- package/dist/skills/ads-designer/SKILL.md +12 -3
- package/dist/skills/ads-designer/assets/bootstrap-ds/scripts/build-mcp-previews.mjs +2 -1
- package/dist/skills/ads-designer/assets/bootstrap-ds/vite.config.ts +2 -1
- package/dist/skills/ads-designer/references/atomic-styles.md +50 -0
- package/dist/skills/ads-designer/references/scenarios.md +98 -0
- package/dist/skills/ads-import/SKILL.md +9 -2
- package/dist/skills/ads-import/references/atomic-styles.md +50 -0
- package/dist/skills/ads-import/references/scenarios.md +98 -0
- package/dist/skills/ads-migrate/SKILL.md +9 -2
- package/dist/skills/ads-migrate/references/atomic-styles.md +50 -0
- package/dist/skills/ads-migrate/references/scenarios.md +98 -0
- package/dist/skills/ads-motion/SKILL.md +9 -2
- package/dist/skills/ads-motion/references/atomic-styles.md +50 -0
- package/dist/skills/ads-motion/references/scenarios.md +98 -0
- package/dist/source-values.mjs +247 -0
- package/dist/verification/browser.mjs +21 -12
- package/dist/verification/playwright-reporter.cjs +6 -2
- package/dist/verification.mjs +13 -4
- package/docs/ADS.md +20 -5
- package/docs/ARCHITECTURE.md +18 -0
- package/docs/BEHAVIORS.md +11 -1
- package/docs/CHANGELOG.md +27 -1
- package/docs/MCP.md +16 -0
- package/package.json +10 -1
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
import postcss from "postcss";
|
|
4
|
+
import { isDevelopmentFile, isInside } from "./config.mjs";
|
|
5
|
+
import { publicApi } from "./public-api.mjs";
|
|
6
|
+
import { uiOperation } from "./ui-framework.mjs";
|
|
7
|
+
import { inspectUnits } from "./units.mjs";
|
|
8
|
+
import { createSourceValues, reference, union, isFunction } from "./source-values.mjs";
|
|
9
|
+
|
|
10
|
+
/** Source evidence only. No project modules execute, and unknown paths grant no
|
|
11
|
+
* permission. Coverage uses intersections across unresolved render branches;
|
|
12
|
+
* class helpers must retain provenance through every possible returned value. */
|
|
13
|
+
export function createScenarioTrace(program, options) {
|
|
14
|
+
const checker = program.getTypeChecker(), maxBranches = 32;
|
|
15
|
+
function linaria(expression, name, seen = new Set()) {
|
|
16
|
+
if (!expression || seen.has(expression)) return false;
|
|
17
|
+
seen.add(expression);
|
|
18
|
+
const base = ts.isPropertyAccessExpression(expression) ? expression.expression : expression;
|
|
19
|
+
const symbol = checker.getSymbolAtLocation(base);
|
|
20
|
+
for (const declaration of symbol?.declarations ?? []) {
|
|
21
|
+
if (ts.isImportSpecifier(declaration) && (declaration.propertyName ?? declaration.name).text === name && ["@linaria/atomic", "@linaria/core"].includes(declaration.parent.parent.parent.moduleSpecifier.text)) return true;
|
|
22
|
+
if (ts.isNamespaceImport(declaration) && ts.isPropertyAccessExpression(expression) && expression.name.text === name && ["@linaria/atomic", "@linaria/core"].includes(declaration.parent.parent.moduleSpecifier.text)) return true;
|
|
23
|
+
if (ts.isVariableDeclaration(declaration) && declaration.parent.flags & ts.NodeFlags.Const && linaria(declaration.initializer, name, seen)) return true;
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
const isCx = expression => linaria(expression, "cx");
|
|
28
|
+
const { state, next, at, resolve, get, arrays, returns, props } = createSourceValues(program, options, { preserveCall: expression => isCx(expression) || uiOperation(expression, checker) === "createElement", strict: true });
|
|
29
|
+
const publicNodes = new Map(), helpers = new Map(), inventory = inspectUnits(options);
|
|
30
|
+
const locationKey = value => JSON.stringify([value.file, value.line, value.column]);
|
|
31
|
+
function runtimeOwner(file) {
|
|
32
|
+
return ["components", "patterns"].some(folder => isInside(path.join(options.dsRoot, folder), file)) && !isDevelopmentFile(file, options)
|
|
33
|
+
? inventory.owner(inventory.relative(path.relative(options.root, file))) : null;
|
|
34
|
+
}
|
|
35
|
+
for (const item of publicApi(program, options)) {
|
|
36
|
+
const value = resolve(reference(item.declaration.name ?? item.declaration), state());
|
|
37
|
+
if (value.kind !== "node") continue;
|
|
38
|
+
if (item.renderable) {
|
|
39
|
+
if (!publicNodes.has(value.node)) publicNodes.set(value.node, new Set());
|
|
40
|
+
publicNodes.get(value.node).add(item.name);
|
|
41
|
+
} else if (item.behavioral && item.kind !== "controllers" && isFunction(value.node)) helpers.set(locationKey(at(value.node, "helper", "")), runtimeOwner(item.file));
|
|
42
|
+
}
|
|
43
|
+
const combine = sets => new Set(sets.flatMap(value => [...value]));
|
|
44
|
+
const common = sets => new Set(sets[0] ? [...sets[0]].filter(name => sets.every(set => set.has(name))) : []);
|
|
45
|
+
|
|
46
|
+
function element(node, env, via) {
|
|
47
|
+
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) {
|
|
48
|
+
const opening = ts.isJsxElement(node) ? node.openingElement : node;
|
|
49
|
+
const attributes = props(opening, env, via);
|
|
50
|
+
const children = ts.isJsxElement(node) ? node.children.filter(child => !ts.isJsxText(child) || child.text.trim()) : [];
|
|
51
|
+
if (children.length) attributes.layers.push(new Map([["children", children.length === 1 ? reference(children[0], env, via) : { kind: "array", values: children.map(child => reference(child, env, via)) }]]));
|
|
52
|
+
return { tag: reference(opening.tagName, env, via), attributes, intrinsic: ts.isIdentifier(opening.tagName) && /^[a-z]/.test(opening.tagName.text) };
|
|
53
|
+
}
|
|
54
|
+
if (ts.isCallExpression(node) && uiOperation(node.expression, checker) === "createElement") {
|
|
55
|
+
const attributes = { kind: "object", layers: node.arguments[1] && node.arguments[1].kind !== ts.SyntaxKind.NullKeyword ? [reference(node.arguments[1], env, via)] : [] };
|
|
56
|
+
const children = node.arguments.slice(2).map(child => reference(child, env, via));
|
|
57
|
+
if (children.length) attributes.layers.push(new Map([["children", children.length === 1 ? children[0] : { kind: "array", values: children }]]));
|
|
58
|
+
return { tag: reference(node.arguments[0], env, via), attributes, intrinsic: !!node.arguments[0] && ts.isStringLiteralLike(node.arguments[0]) };
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function traceRenders(site) {
|
|
64
|
+
const limits = new Set();
|
|
65
|
+
function render(input, context) {
|
|
66
|
+
const value = resolve(input, next(context));
|
|
67
|
+
context = value.context ?? input.context ?? context;
|
|
68
|
+
if (value.kind === "unknown") { if (value.reason) limits.add(value.reason); return new Set(); }
|
|
69
|
+
if (value.kind === "union") {
|
|
70
|
+
if (value.values.length > maxBranches) { limits.add("Scenario render tracing reached its alternative limit."); return new Set(); }
|
|
71
|
+
return common(value.values.map(item => render(item, next(context))));
|
|
72
|
+
}
|
|
73
|
+
if (value.kind === "array") return combine(value.values.map(item => render(item, next(context))));
|
|
74
|
+
if (value.kind !== "node") return new Set();
|
|
75
|
+
const { node, env, via } = value;
|
|
76
|
+
if (ts.isJsxExpression(node)) return render(reference(node.expression, env, via), next(context));
|
|
77
|
+
if (ts.isJsxFragment(node)) return combine(node.children.map(child => render(reference(child, env, via), next(context))));
|
|
78
|
+
if (ts.isArrayLiteralExpression(node)) return combine(node.elements.map(child => render(reference(ts.isSpreadElement(child) ? child.expression : child, env, via), next(context))));
|
|
79
|
+
const view = element(node, env, via);
|
|
80
|
+
if (!view) return new Set();
|
|
81
|
+
if (view.intrinsic) return render(get(view.attributes, "children", next(context)), next(context));
|
|
82
|
+
function component(input) {
|
|
83
|
+
const target = resolve(input, next(context));
|
|
84
|
+
if (["provider", "context"].includes(target.kind)) return render(get(view.attributes, "children", next(context)), next(context));
|
|
85
|
+
if (target.kind === "union") return target.values.length <= maxBranches ? common(target.values.map(component)) : new Set();
|
|
86
|
+
if (target.kind !== "node") return new Set();
|
|
87
|
+
const names = new Set(publicNodes.get(target.node) ?? []);
|
|
88
|
+
if (isFunction(target.node) && !isDevelopmentFile(target.node.getSourceFile().fileName, options)) {
|
|
89
|
+
const outputs = returns(target, [view.attributes], next(context), "component");
|
|
90
|
+
for (const name of render(union(outputs), next(context))) names.add(name);
|
|
91
|
+
}
|
|
92
|
+
return names;
|
|
93
|
+
}
|
|
94
|
+
return component(view.tag);
|
|
95
|
+
}
|
|
96
|
+
return { rendered: render(reference(site), state()), limits: [...limits].slice(0, 3) };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function classValue(expression, property = false) {
|
|
100
|
+
const reasons = new Set();
|
|
101
|
+
const result = (ok, atoms = false, hooks = false) => ({ ok, branches: [{ atoms, hooks }] });
|
|
102
|
+
const owned = (file, via) => {
|
|
103
|
+
const owner = runtimeOwner(file);
|
|
104
|
+
return !!owner && via?.some(location => helpers.get(locationKey(location)) === owner);
|
|
105
|
+
};
|
|
106
|
+
function verify(input, context) {
|
|
107
|
+
const value = resolve(input, next(context));
|
|
108
|
+
context = value.context ?? input.context ?? context;
|
|
109
|
+
if (value.kind === "union") {
|
|
110
|
+
if (value.values.length > maxBranches) { reasons.add("Class provenance reached its alternative limit."); return result(false); }
|
|
111
|
+
const branches = value.values.map(item => verify(item, next(context)));
|
|
112
|
+
return { ok: branches.every(item => item.ok), branches: branches.flatMap(item => item.branches) };
|
|
113
|
+
}
|
|
114
|
+
if (value.kind === "literal" && !value.value) return result(true);
|
|
115
|
+
if (value.kind === "literal" && typeof value.value === "string" && value.origin && owned(value.origin.getSourceFile().fileName, value.via)) return result(true, false, true);
|
|
116
|
+
if (value.kind === "node") {
|
|
117
|
+
const { node, env, via } = value;
|
|
118
|
+
if (ts.isTaggedTemplateExpression(node) && linaria(node.tag, "css")) {
|
|
119
|
+
const source = node.getSourceFile().fileName;
|
|
120
|
+
let declarations = 0;
|
|
121
|
+
const template = node.template;
|
|
122
|
+
const text = ts.isNoSubstitutionTemplateLiteral(template) ? template.text : template.head.text + template.templateSpans.map(span => `__ads_expression__${span.literal.text}`).join("");
|
|
123
|
+
try { postcss.parse(text).walkDecls(() => ++declarations); } catch { /* Source CSS guards own malformed templates. */ }
|
|
124
|
+
if (owned(source, via) && declarations) return result(true, true);
|
|
125
|
+
reasons.add("The returned style must originate in runtime component/pattern Linaria through a public callable helper.");
|
|
126
|
+
return result(false);
|
|
127
|
+
}
|
|
128
|
+
if (ts.isCallExpression(node) && isCx(node.expression)) {
|
|
129
|
+
const arguments_ = node.arguments.flatMap(argument => ts.isSpreadElement(argument) ? arrays(reference(argument.expression, env, via), context) ?? [reference(argument.expression, env, via)] : [reference(argument, env, via)]);
|
|
130
|
+
const values = arguments_.map(argument => verify(argument, next(context)));
|
|
131
|
+
let branches = [{ atoms: false, hooks: false }];
|
|
132
|
+
for (const value of values) branches = [...new Map(branches.flatMap(first => value.branches.map(second => ({ atoms: first.atoms || second.atoms, hooks: first.hooks || second.hooks }))).map(branch => [JSON.stringify(branch), branch])).values()];
|
|
133
|
+
return { ok: values.every(item => item.ok), branches };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
reasons.add(value.reason || "A returned class value is an arbitrary string or cannot be traced to owned Linaria.");
|
|
137
|
+
return result(false);
|
|
138
|
+
}
|
|
139
|
+
const context = state();
|
|
140
|
+
const input = property ? get(reference(expression), "className", next(context)) : reference(expression);
|
|
141
|
+
const value = resolve(input, next(context));
|
|
142
|
+
if (property && value.kind === "literal" && value.value === undefined) return { present: false, ok: true, reasons: [] };
|
|
143
|
+
const proof = verify(input, context);
|
|
144
|
+
const ok = proof.ok && proof.branches.some(branch => branch.atoms) && proof.branches.every(branch => !branch.hooks || branch.atoms);
|
|
145
|
+
if (!ok && !reasons.size) reasons.add("Each nonempty return path needs owned Linaria styles; stable hooks alone are not styling evidence.");
|
|
146
|
+
return { present: !property || value.kind !== "unknown", ok, reasons: [...reasons].slice(0, 3) };
|
|
147
|
+
}
|
|
148
|
+
return { traceRenders, classValue };
|
|
149
|
+
}
|
|
@@ -14,7 +14,7 @@ const fileDigest = file => { try { return digest(readFileSync(file)); } catch {
|
|
|
14
14
|
* Cache state is development output, never proof that a failed build is current. */
|
|
15
15
|
export function createPreviewCache(options, compilerFiles = []) {
|
|
16
16
|
const { root, config } = options;
|
|
17
|
-
const adapterFiles = [import.meta.filename,
|
|
17
|
+
const adapterFiles = [import.meta.filename, ...["preview-resources.mjs", "preview-scope.mjs", "scenarios-vite.mjs", "linaria.mjs", "linaria/atomic-css.cjs", "linaria/atomic-styled.cjs", "linaria/preserve-values.cjs"].map(file => path.join(import.meta.dirname, file)), ...compilerFiles,
|
|
18
18
|
...["package.json", "package-lock.json", "bun.lock", "bun.lockb", "node_modules/.package-lock.json"].map(file => path.join(root, file))];
|
|
19
19
|
const adapterDigest = () => digest(JSON.stringify(adapterFiles.map(file => [file, fileDigest(file)])));
|
|
20
20
|
const loadedAdapter = adapterDigest();
|
|
@@ -46,6 +46,7 @@ export function createPreviewCache(options, compilerFiles = []) {
|
|
|
46
46
|
for (const name of readdirSync(import.meta.dirname).filter(name => name.endsWith(".mjs"))) {
|
|
47
47
|
rows.push([`ads-tool/${name}`, fileDigest(path.join(import.meta.dirname, name))]);
|
|
48
48
|
}
|
|
49
|
+
for (const name of readdirSync(path.join(import.meta.dirname, "linaria"))) rows.push([`ads-tool/linaria/${name}`, fileDigest(path.join(import.meta.dirname, "linaria", name))]);
|
|
49
50
|
return digest(JSON.stringify(rows.sort((a, b) => a[0].localeCompare(b[0]))));
|
|
50
51
|
}
|
|
51
52
|
function save() {
|
package/dist/scenarios.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import { inspectTokens } from "./tokens.mjs";
|
|
|
9
9
|
import { isSharedToken } from "./token-policy.mjs";
|
|
10
10
|
import { validTokenSamples } from "./token-samples.mjs";
|
|
11
11
|
import { scopedTokens } from "./scoped-tokens.mjs";
|
|
12
|
+
import { createScenarioTrace } from "./scenario-trace.mjs";
|
|
12
13
|
|
|
13
14
|
export const scenarioArtifact = (root, id) => path.join(root, ".ds/scenarios", id, "index.html");
|
|
14
15
|
|
|
@@ -109,12 +110,13 @@ export function inspectScenarios(options, suppliedProgram) {
|
|
|
109
110
|
const owner = scoped.inventory.owner(relative);
|
|
110
111
|
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.`);
|
|
111
112
|
}
|
|
112
|
-
|
|
113
|
+
const stylesheets = kind === "tokens" ? scoped.definitions.filter(entry => entry.unit === scoped.inventory.owner(relative)).map(entry => entry.file) : [];
|
|
114
|
+
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 } : {}), ...(stylesheets.length ? { stylesheets } : {}), file: path.relative(root, file).replaceAll("\\", "/"), artifact: path.relative(root, scenarioArtifact(root, id)).replaceAll("\\", "/") });
|
|
113
115
|
}
|
|
114
116
|
return { scenarios: scenarios.sort((a, b) => a.id.localeCompare(b.id)), diagnostics };
|
|
115
117
|
}
|
|
116
118
|
|
|
117
|
-
export function inspectScenarioCoverage(options, program, report, advise = () => {}) {
|
|
119
|
+
export function inspectScenarioCoverage(options, program, report, advise = () => {}, trace = createScenarioTrace(program, options)) {
|
|
118
120
|
const registry = inspectScenarios(options, program);
|
|
119
121
|
for (const item of registry.diagnostics) report(path.join(options.root, item.file), item.rule, item.message, item.line, item.column);
|
|
120
122
|
const checker = program.getTypeChecker();
|
|
@@ -164,6 +166,7 @@ export function inspectScenarioCoverage(options, program, report, advise = () =>
|
|
|
164
166
|
const exercised = new Set(), referencedApis = new Set();
|
|
165
167
|
const referencedTokens = new Set();
|
|
166
168
|
let samplesPassed = false;
|
|
169
|
+
const renderLimits = new Set();
|
|
167
170
|
const visited = new Set();
|
|
168
171
|
function visit(node, reached = false) {
|
|
169
172
|
if (!node || visited.has(node)) return;
|
|
@@ -178,6 +181,10 @@ export function inspectScenarioCoverage(options, program, report, advise = () =>
|
|
|
178
181
|
if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) expression = node.tagName;
|
|
179
182
|
if (ts.isCallExpression(node) && uiOperation(node.expression, checker) === "createElement") expression = node.arguments[0];
|
|
180
183
|
if (expression) {
|
|
184
|
+
const site = ts.isJsxOpeningElement(node) ? node.parent : node;
|
|
185
|
+
const evidence = trace.traceRenders(site);
|
|
186
|
+
for (const name of evidence.rendered) rendered.add(name);
|
|
187
|
+
for (const reason of evidence.limits) renderLimits.add(reason);
|
|
181
188
|
const symbol = canonical(symbolOf(expression));
|
|
182
189
|
const names = identities.get(symbol?.valueDeclaration ?? symbol?.declarations?.[0]);
|
|
183
190
|
for (const name of names ?? []) { rendered.add(name); exercised.add(name); }
|
|
@@ -201,9 +208,9 @@ export function inspectScenarioCoverage(options, program, report, advise = () =>
|
|
|
201
208
|
if (behaviorIdentities.has(owner)) for (const name of identities.get(owner) ?? []) exercised.add(name);
|
|
202
209
|
}
|
|
203
210
|
}
|
|
204
|
-
// Follow local adapters reachable from the default entry.
|
|
205
|
-
//
|
|
206
|
-
//
|
|
211
|
+
// Follow local adapters reachable from the default entry. Production
|
|
212
|
+
// children get evidence only from bounded returned-JSX tracing above,
|
|
213
|
+
// never by scanning every reference in their implementation.
|
|
207
214
|
if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node) || ts.isIdentifier(node) && !(ts.isPropertyAccessExpression(node.parent) || ts.isElementAccessExpression(node.parent))) {
|
|
208
215
|
const symbol = canonical(symbolOf(node));
|
|
209
216
|
const declaration = symbol?.valueDeclaration;
|
|
@@ -214,7 +221,7 @@ export function inspectScenarioCoverage(options, program, report, advise = () =>
|
|
|
214
221
|
}
|
|
215
222
|
for (const node of source.statements) if (ts.isExportAssignment(node) || node.modifiers?.some(item => item.kind === ts.SyntaxKind.DefaultKeyword)) visit(node, true);
|
|
216
223
|
for (const name of scenario.covers) {
|
|
217
|
-
if (!rendered.has(name)) report(source.fileName, "scenario-render", `${name} is declared in covers but has no public render reference
|
|
224
|
+
if (!rendered.has(name)) report(source.fileName, "scenario-render", `${name} is declared in covers but has no reachable public render reference. Render it directly or through a traced parent with fixture props/defaults selecting that child. Imports, unused JSX and unknown conditional paths do not establish coverage.${renderLimits.size ? ` Source tracing could not establish: ${[...renderLimits].slice(0, 2).join(" ")}` : ""} Review the real scenario in a browser; do not hide a valid public part just to avoid coverage. See ads://guides/scenarios.`);
|
|
218
225
|
else covered.add(name);
|
|
219
226
|
}
|
|
220
227
|
for (const name of scenario.exercises ?? []) {
|
|
@@ -49,11 +49,18 @@ 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
|
|
52
|
+
For parent/child `covers`, public style-helper use in scenarios, or
|
|
53
|
+
`scenario-render` / `showcase-class` findings, read
|
|
54
|
+
[scenario contracts](references/scenarios.md). Traced parents can cover declared
|
|
55
|
+
public children; verified public Linaria helpers do not grant custom scenario CSS.
|
|
56
|
+
|
|
57
|
+
For atomic base/variant styling, helper/wrapper class composition, extracted
|
|
58
|
+
custom-property arithmetic, or an
|
|
53
59
|
`atomic-composition-review` / `atomic-composition-limit` advisory, read
|
|
54
60
|
[atomic style composition](references/atomic-styles.md). The same guide is served
|
|
55
61
|
as `ads://guides/atomic-styles`. It explains shorthand/logical conflicts and
|
|
56
|
-
matching-property fixes
|
|
62
|
+
matching-property fixes, plus the build resolver for preserving opaque custom
|
|
63
|
+
properties through extraction. Review warnings in the real `cx` composition and verify
|
|
57
64
|
computed styles across affected sizes/states; retain intentional product behavior.
|
|
58
65
|
|
|
59
66
|
Controllers are UI behavior owners: private helpers stay with their component or
|
|
@@ -81,6 +81,56 @@ 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
|
+
## Preserve custom-property arithmetic during extraction
|
|
85
|
+
|
|
86
|
+
Custom properties can hold an unevaluated token stream that becomes arithmetic
|
|
87
|
+
only after substitution into a later `calc()`. For example:
|
|
88
|
+
|
|
89
|
+
```css
|
|
90
|
+
--i: 2;
|
|
91
|
+
--scale: var(--i) * 0.05 + 1;
|
|
92
|
+
transform: scale(calc(-1 * var(--scale)));
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The result is scale `0.9`: substitution does not add parentheses around the
|
|
96
|
+
custom property. Some Linaria/Stylis extraction versions strip the whitespace
|
|
97
|
+
around the bare `+`, leaving `0.05+1`. The browser then treats the transform as
|
|
98
|
+
invalid. This can happen with CSS minification disabled. Source guards cannot
|
|
99
|
+
prove what a later extractor emits.
|
|
100
|
+
|
|
101
|
+
Use ADS's build-only resolver in **both** the application's Linaria configuration
|
|
102
|
+
and its separately configured MCP preview builder:
|
|
103
|
+
|
|
104
|
+
```js
|
|
105
|
+
import wyw from "@wyw-in-js/vite";
|
|
106
|
+
import { linariaTagResolver } from "@reqvire-org/ads/linaria";
|
|
107
|
+
|
|
108
|
+
// In the project's Vite plugins:
|
|
109
|
+
wyw({ tagResolver: linariaTagResolver });
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The resolver keeps the project's installed atomic processor and preserves opaque
|
|
113
|
+
custom-property values through its nesting/atomization pass. It does not add a
|
|
114
|
+
browser runtime or alter token ownership. Existing projects must adopt it in
|
|
115
|
+
each extraction pipeline; upgrading ADS alone does not rewrite custom builders.
|
|
116
|
+
If an existing `tagResolver` handles other tags, delegate atomic imports to
|
|
117
|
+
`linariaTagResolver(source, tag)` and retain the existing fallback. WyW config
|
|
118
|
+
files also support this resolver when a build integration requires serializable
|
|
119
|
+
inline options.
|
|
120
|
+
|
|
121
|
+
For a source workaround, write a self-contained formula and consume it directly:
|
|
122
|
+
|
|
123
|
+
```css
|
|
124
|
+
--scale: calc(1 - var(--i) * 0.05);
|
|
125
|
+
transform: scale(var(--scale));
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
This also produces `0.9`. Simply wrapping the old value in `calc()` while keeping
|
|
129
|
+
the outer `-1 *` changes its grouping and result. Verify extracted CSS text and a
|
|
130
|
+
computed style after any rewrite, including nested `var()` fallbacks/transforms.
|
|
131
|
+
Do not globally insert spaces into already extracted CSS: that can change valid
|
|
132
|
+
signed numbers, identifiers, strings or URLs.
|
|
133
|
+
|
|
84
134
|
## Follow helpers and forwarded classes
|
|
85
135
|
|
|
86
136
|
Composition can cross a helper or a component boundary. For example, in component
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Scenario rendering, token previews and public class helpers
|
|
2
|
+
|
|
3
|
+
Use this reference for `scenario-render`, `scenario-coverage` or `showcase-class`
|
|
4
|
+
diagnostics. MCP serves the same guide as `ads://guides/scenarios`.
|
|
5
|
+
|
|
6
|
+
## Owner-local token previews load their own definitions
|
|
7
|
+
|
|
8
|
+
Keep the registered token sheet imported by its runtime owner for product use.
|
|
9
|
+
A colocated `kind: "tokens"` scenario may render only the public review pattern
|
|
10
|
+
with its fixture samples. Discovery adds the registered owner sheet to the
|
|
11
|
+
generated record's `stylesheets`; `previewEntriesPlugin` imports those paths
|
|
12
|
+
into the token preview independently of the runtime component. Custom builders
|
|
13
|
+
that replace this plugin must preserve these explicit stylesheet dependencies.
|
|
14
|
+
Do not render the runtime owner merely to retain its CSS or add unrelated sheets.
|
|
15
|
+
|
|
16
|
+
Open the exact token scenario in a fresh App and verify resolved values and sample
|
|
17
|
+
effects in both themes. A successful ordinary component preview does not prove
|
|
18
|
+
the independently bundled token preview loads the same definitions. Shared
|
|
19
|
+
foundation styling still follows the project's mount/build integration.
|
|
20
|
+
|
|
21
|
+
## Public children can share their parent's scenario
|
|
22
|
+
|
|
23
|
+
Declare the public parts actually demonstrated in `covers`, even when their
|
|
24
|
+
parent renders them internally:
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
// Runtime owner
|
|
28
|
+
export function Part() { return <span>Content</span>; }
|
|
29
|
+
export function Frame({ visible = true } = {}) {
|
|
30
|
+
return <section>{visible && <Part />}</section>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Colocated Scenario.tsx
|
|
34
|
+
import { Frame } from "@ds";
|
|
35
|
+
export const scenario = { title: "Frame", covers: ["Frame", "Part"] };
|
|
36
|
+
export default function Scenario() { return <Frame />; }
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
ADS follows returned JSX through local components/helpers, aliases, registered
|
|
40
|
+
framework factories, memo/ref wrappers and context providers. Fixture props,
|
|
41
|
+
spreads and destructuring defaults select the child path. An explicit
|
|
42
|
+
`visible={false}` does not cover `Part`. When a condition remains unknown, only
|
|
43
|
+
parts returned on every possible path receive transitive credit. Imports, unused
|
|
44
|
+
JSX, event-handler bodies and uncalled functions do not establish child rendering.
|
|
45
|
+
Only the explicitly declared `covers` entries acquire coverage; dependencies are
|
|
46
|
+
not silently added to metadata or the catalogue.
|
|
47
|
+
|
|
48
|
+
Tracing is bounded and does not execute the project. Recursion, written/escaped inputs,
|
|
49
|
+
opaque external wrappers and unsupported control flow can prevent proof. The
|
|
50
|
+
diagnostic explains known limits. Use meaningful fixture states or another real
|
|
51
|
+
scenario; do not make a valid public part private or duplicate it in a parent
|
|
52
|
+
just to satisfy a guard. Static evidence does not establish successful mounting,
|
|
53
|
+
visibility, interaction, every state or visual parity. Verify those in the built
|
|
54
|
+
App and registered browser checks.
|
|
55
|
+
|
|
56
|
+
## Scenarios may consume verified public style helpers
|
|
57
|
+
|
|
58
|
+
A deliberately public helper can expose its owner's Linaria treatment:
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
// Runtime owner
|
|
62
|
+
const treatment = css`padding-inline: var(--space-4);`;
|
|
63
|
+
export function controlClasses() {
|
|
64
|
+
return cx("control-hook", treatment);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Scenario: consume the public API; define no styles here
|
|
68
|
+
<PublicLink className={controlClasses()}>Documentation</PublicLink>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The sample token and APIs are illustrative; use the project's real exports and
|
|
72
|
+
tokens. Import helpers through the public ADS entry. ADS traces helper calls,
|
|
73
|
+
returns, local aliases, finite known selections and Linaria `cx` composition.
|
|
74
|
+
Styling must originate in component/pattern Linaria through a public callable
|
|
75
|
+
helper. A literal stable hook authored in that runtime owner may accompany the
|
|
76
|
+
verified styles. Each nonempty return path needs actual Linaria declarations;
|
|
77
|
+
empty templates and hook-only/string-only returns do not qualify.
|
|
78
|
+
|
|
79
|
+
These are still rejected:
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
<PublicLink className="custom-layout" />
|
|
83
|
+
<PublicLink className={arbitraryPublicFunctionReturningAString()} />
|
|
84
|
+
<PublicLink className={controlClasses({ className: "custom-layout" })} />
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Scenario-supplied strings, generated styles in fixtures, private style imports,
|
|
88
|
+
spoofed Linaria functions and untraceable results acquire no permission by being
|
|
89
|
+
passed through a public function. Known className values in props/spreads receive
|
|
90
|
+
the same provenance check. Opaque spreads remain outside complete source proof.
|
|
91
|
+
An `exercises` entry records behavioral use; it never grants styling permission.
|
|
92
|
+
|
|
93
|
+
Keep helper signatures compatible with the product API. Fix ownership/provenance
|
|
94
|
+
in the runtime owner when genuinely wrong; retain intentional public APIs when
|
|
95
|
+
reporting an unsupported trace. Demo-only sizing, framing and arrangement still
|
|
96
|
+
belong in public review/layout patterns. A style helper is not permission to
|
|
97
|
+
author a second design in the scenario. For cascade risks and extraction issues,
|
|
98
|
+
read [atomic-styles.md](atomic-styles.md).
|
|
@@ -80,11 +80,18 @@ 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
|
|
83
|
+
For parent/child `covers`, public style-helper use in scenarios, or
|
|
84
|
+
`scenario-render` / `showcase-class` findings, read
|
|
85
|
+
[scenario contracts](references/scenarios.md). Traced parents can cover declared
|
|
86
|
+
public children; verified public Linaria helpers do not grant custom scenario CSS.
|
|
87
|
+
|
|
88
|
+
For atomic base/variant styling, helper/wrapper class composition, extracted
|
|
89
|
+
custom-property arithmetic, or an
|
|
84
90
|
`atomic-composition-review` / `atomic-composition-limit` advisory, read
|
|
85
91
|
[atomic style composition](references/atomic-styles.md). The same guide is served
|
|
86
92
|
as `ads://guides/atomic-styles`. It explains shorthand/logical conflicts and
|
|
87
|
-
matching-property fixes
|
|
93
|
+
matching-property fixes, plus the build resolver for preserving opaque custom
|
|
94
|
+
properties through extraction. Review warnings in the real `cx` composition and verify
|
|
88
95
|
computed styles across affected sizes/states; retain intentional product behavior.
|
|
89
96
|
|
|
90
97
|
Controllers are UI behavior owners: private helpers stay with their component or
|
|
@@ -328,7 +335,9 @@ Screen reuse remains skill guidance, not a dedicated screen-parity lint.
|
|
|
328
335
|
Resolve `scenario-contract`, `scenario-render` and `scenario-coverage` findings
|
|
329
336
|
with meaningful entries. Every public renderable component and pattern needs an
|
|
330
337
|
addressable scenario, including unused exports; types/constants/utilities do not.
|
|
331
|
-
|
|
338
|
+
Direct and traced parent/child public render references reachable from the default
|
|
339
|
+
entry count. Declared children need returned JSX selected by known props/defaults;
|
|
340
|
+
unresolved branches do not grant speculative coverage. Tests and
|
|
332
341
|
unused imports/functions do not. Static reachability does not prove successful
|
|
333
342
|
mounting, correct content, every branch/state or visual parity: open each affected
|
|
334
343
|
entry independently and verify real behavior at narrow/wide sizes in both themes.
|
|
@@ -4,6 +4,7 @@ import { execFile } from "node:child_process";
|
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
5
|
import { build } from "vite";
|
|
6
6
|
import wyw from "@wyw-in-js/vite";
|
|
7
|
+
import { linariaTagResolver } from "@reqvire-org/ads/linaria";
|
|
7
8
|
import react from "@vitejs/plugin-react";
|
|
8
9
|
import { runScenarioCompiler } from "@reqvire-org/ads/scenarios/compiler";
|
|
9
10
|
import { createPreviewResources, previewEntriesPlugin } from "@reqvire-org/ads/scenarios/vite";
|
|
@@ -22,7 +23,7 @@ await runScenarioCompiler(root, {
|
|
|
22
23
|
async buildGraph(entries, options, track) {
|
|
23
24
|
const mount = path.join(options.dsRoot, "showcase/mcp/main.tsx");
|
|
24
25
|
const output = await build({ configFile: false, root, base: "./", publicDir: false,
|
|
25
|
-
plugins: [previewEntriesPlugin(entries.map(entry => ({ ...entry, root })), mount), wyw({ include: ["**/*.{ts,tsx}"], exclude: ["**/node_modules/**"] }), react(), track],
|
|
26
|
+
plugins: [previewEntriesPlugin(entries.map(entry => ({ ...entry, root })), mount), wyw({ tagResolver: linariaTagResolver, include: ["**/*.{ts,tsx}"], exclude: ["**/node_modules/**"] }), react(), track],
|
|
26
27
|
resolve: { alias: { "@ds": path.join(options.dsRoot, options.config.publicEntry) } },
|
|
27
28
|
worker: { format: "iife" },
|
|
28
29
|
build: { write: false, cssCodeSplit: true, assetsInlineLimit: 0, modulePreload: false,
|
|
@@ -2,9 +2,10 @@ import path from "node:path";
|
|
|
2
2
|
import { defineConfig } from "vite";
|
|
3
3
|
import react from "@vitejs/plugin-react";
|
|
4
4
|
import wyw from "@wyw-in-js/vite";
|
|
5
|
+
import { linariaTagResolver } from "@reqvire-org/ads/linaria";
|
|
5
6
|
|
|
6
7
|
export default defineConfig({
|
|
7
|
-
plugins: [wyw(), react()],
|
|
8
|
+
plugins: [wyw({ tagResolver: linariaTagResolver }), react()],
|
|
8
9
|
resolve: {
|
|
9
10
|
alias: { "@ds": path.resolve(import.meta.dirname, "design-system/index.ts") },
|
|
10
11
|
},
|
|
@@ -81,6 +81,56 @@ 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
|
+
## Preserve custom-property arithmetic during extraction
|
|
85
|
+
|
|
86
|
+
Custom properties can hold an unevaluated token stream that becomes arithmetic
|
|
87
|
+
only after substitution into a later `calc()`. For example:
|
|
88
|
+
|
|
89
|
+
```css
|
|
90
|
+
--i: 2;
|
|
91
|
+
--scale: var(--i) * 0.05 + 1;
|
|
92
|
+
transform: scale(calc(-1 * var(--scale)));
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
The result is scale `0.9`: substitution does not add parentheses around the
|
|
96
|
+
custom property. Some Linaria/Stylis extraction versions strip the whitespace
|
|
97
|
+
around the bare `+`, leaving `0.05+1`. The browser then treats the transform as
|
|
98
|
+
invalid. This can happen with CSS minification disabled. Source guards cannot
|
|
99
|
+
prove what a later extractor emits.
|
|
100
|
+
|
|
101
|
+
Use ADS's build-only resolver in **both** the application's Linaria configuration
|
|
102
|
+
and its separately configured MCP preview builder:
|
|
103
|
+
|
|
104
|
+
```js
|
|
105
|
+
import wyw from "@wyw-in-js/vite";
|
|
106
|
+
import { linariaTagResolver } from "@reqvire-org/ads/linaria";
|
|
107
|
+
|
|
108
|
+
// In the project's Vite plugins:
|
|
109
|
+
wyw({ tagResolver: linariaTagResolver });
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The resolver keeps the project's installed atomic processor and preserves opaque
|
|
113
|
+
custom-property values through its nesting/atomization pass. It does not add a
|
|
114
|
+
browser runtime or alter token ownership. Existing projects must adopt it in
|
|
115
|
+
each extraction pipeline; upgrading ADS alone does not rewrite custom builders.
|
|
116
|
+
If an existing `tagResolver` handles other tags, delegate atomic imports to
|
|
117
|
+
`linariaTagResolver(source, tag)` and retain the existing fallback. WyW config
|
|
118
|
+
files also support this resolver when a build integration requires serializable
|
|
119
|
+
inline options.
|
|
120
|
+
|
|
121
|
+
For a source workaround, write a self-contained formula and consume it directly:
|
|
122
|
+
|
|
123
|
+
```css
|
|
124
|
+
--scale: calc(1 - var(--i) * 0.05);
|
|
125
|
+
transform: scale(var(--scale));
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
This also produces `0.9`. Simply wrapping the old value in `calc()` while keeping
|
|
129
|
+
the outer `-1 *` changes its grouping and result. Verify extracted CSS text and a
|
|
130
|
+
computed style after any rewrite, including nested `var()` fallbacks/transforms.
|
|
131
|
+
Do not globally insert spaces into already extracted CSS: that can change valid
|
|
132
|
+
signed numbers, identifiers, strings or URLs.
|
|
133
|
+
|
|
84
134
|
## Follow helpers and forwarded classes
|
|
85
135
|
|
|
86
136
|
Composition can cross a helper or a component boundary. For example, in component
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Scenario rendering, token previews and public class helpers
|
|
2
|
+
|
|
3
|
+
Use this reference for `scenario-render`, `scenario-coverage` or `showcase-class`
|
|
4
|
+
diagnostics. MCP serves the same guide as `ads://guides/scenarios`.
|
|
5
|
+
|
|
6
|
+
## Owner-local token previews load their own definitions
|
|
7
|
+
|
|
8
|
+
Keep the registered token sheet imported by its runtime owner for product use.
|
|
9
|
+
A colocated `kind: "tokens"` scenario may render only the public review pattern
|
|
10
|
+
with its fixture samples. Discovery adds the registered owner sheet to the
|
|
11
|
+
generated record's `stylesheets`; `previewEntriesPlugin` imports those paths
|
|
12
|
+
into the token preview independently of the runtime component. Custom builders
|
|
13
|
+
that replace this plugin must preserve these explicit stylesheet dependencies.
|
|
14
|
+
Do not render the runtime owner merely to retain its CSS or add unrelated sheets.
|
|
15
|
+
|
|
16
|
+
Open the exact token scenario in a fresh App and verify resolved values and sample
|
|
17
|
+
effects in both themes. A successful ordinary component preview does not prove
|
|
18
|
+
the independently bundled token preview loads the same definitions. Shared
|
|
19
|
+
foundation styling still follows the project's mount/build integration.
|
|
20
|
+
|
|
21
|
+
## Public children can share their parent's scenario
|
|
22
|
+
|
|
23
|
+
Declare the public parts actually demonstrated in `covers`, even when their
|
|
24
|
+
parent renders them internally:
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
// Runtime owner
|
|
28
|
+
export function Part() { return <span>Content</span>; }
|
|
29
|
+
export function Frame({ visible = true } = {}) {
|
|
30
|
+
return <section>{visible && <Part />}</section>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Colocated Scenario.tsx
|
|
34
|
+
import { Frame } from "@ds";
|
|
35
|
+
export const scenario = { title: "Frame", covers: ["Frame", "Part"] };
|
|
36
|
+
export default function Scenario() { return <Frame />; }
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
ADS follows returned JSX through local components/helpers, aliases, registered
|
|
40
|
+
framework factories, memo/ref wrappers and context providers. Fixture props,
|
|
41
|
+
spreads and destructuring defaults select the child path. An explicit
|
|
42
|
+
`visible={false}` does not cover `Part`. When a condition remains unknown, only
|
|
43
|
+
parts returned on every possible path receive transitive credit. Imports, unused
|
|
44
|
+
JSX, event-handler bodies and uncalled functions do not establish child rendering.
|
|
45
|
+
Only the explicitly declared `covers` entries acquire coverage; dependencies are
|
|
46
|
+
not silently added to metadata or the catalogue.
|
|
47
|
+
|
|
48
|
+
Tracing is bounded and does not execute the project. Recursion, written/escaped inputs,
|
|
49
|
+
opaque external wrappers and unsupported control flow can prevent proof. The
|
|
50
|
+
diagnostic explains known limits. Use meaningful fixture states or another real
|
|
51
|
+
scenario; do not make a valid public part private or duplicate it in a parent
|
|
52
|
+
just to satisfy a guard. Static evidence does not establish successful mounting,
|
|
53
|
+
visibility, interaction, every state or visual parity. Verify those in the built
|
|
54
|
+
App and registered browser checks.
|
|
55
|
+
|
|
56
|
+
## Scenarios may consume verified public style helpers
|
|
57
|
+
|
|
58
|
+
A deliberately public helper can expose its owner's Linaria treatment:
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
// Runtime owner
|
|
62
|
+
const treatment = css`padding-inline: var(--space-4);`;
|
|
63
|
+
export function controlClasses() {
|
|
64
|
+
return cx("control-hook", treatment);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Scenario: consume the public API; define no styles here
|
|
68
|
+
<PublicLink className={controlClasses()}>Documentation</PublicLink>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The sample token and APIs are illustrative; use the project's real exports and
|
|
72
|
+
tokens. Import helpers through the public ADS entry. ADS traces helper calls,
|
|
73
|
+
returns, local aliases, finite known selections and Linaria `cx` composition.
|
|
74
|
+
Styling must originate in component/pattern Linaria through a public callable
|
|
75
|
+
helper. A literal stable hook authored in that runtime owner may accompany the
|
|
76
|
+
verified styles. Each nonempty return path needs actual Linaria declarations;
|
|
77
|
+
empty templates and hook-only/string-only returns do not qualify.
|
|
78
|
+
|
|
79
|
+
These are still rejected:
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
<PublicLink className="custom-layout" />
|
|
83
|
+
<PublicLink className={arbitraryPublicFunctionReturningAString()} />
|
|
84
|
+
<PublicLink className={controlClasses({ className: "custom-layout" })} />
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Scenario-supplied strings, generated styles in fixtures, private style imports,
|
|
88
|
+
spoofed Linaria functions and untraceable results acquire no permission by being
|
|
89
|
+
passed through a public function. Known className values in props/spreads receive
|
|
90
|
+
the same provenance check. Opaque spreads remain outside complete source proof.
|
|
91
|
+
An `exercises` entry records behavioral use; it never grants styling permission.
|
|
92
|
+
|
|
93
|
+
Keep helper signatures compatible with the product API. Fix ownership/provenance
|
|
94
|
+
in the runtime owner when genuinely wrong; retain intentional public APIs when
|
|
95
|
+
reporting an unsupported trace. Demo-only sizing, framing and arrangement still
|
|
96
|
+
belong in public review/layout patterns. A style helper is not permission to
|
|
97
|
+
author a second design in the scenario. For cascade risks and extraction issues,
|
|
98
|
+
read [atomic-styles.md](atomic-styles.md).
|