@reqvire-org/ads 0.3.5 → 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.
- package/README.md +5 -1
- package/dist/atomic-composition.mjs +183 -0
- package/dist/bootstrap-ds/ADS.md +9 -3
- package/dist/bootstrap-ds/README.md +10 -0
- package/dist/boundaries.mjs +3 -0
- package/dist/check.mjs +16 -0
- package/dist/cli.mjs +1 -1
- package/dist/config.mjs +6 -2
- package/dist/mcp/build-status.mjs +7 -4
- package/dist/mcp/context.mjs +2 -2
- package/dist/mcp/edit-preparation.mjs +111 -0
- package/dist/mcp/mutations.mjs +54 -38
- package/dist/mcp/preview-watch.mjs +10 -4
- package/dist/mcp/server.mjs +36 -24
- package/dist/mcp/text-diff.mjs +19 -0
- package/dist/preview-resources.mjs +45 -8
- package/dist/preview-scope.mjs +30 -0
- package/dist/public-api.mjs +37 -1
- package/dist/scenarios-compiler.mjs +24 -17
- package/dist/scenarios.mjs +58 -6
- package/dist/scoped-tokens.mjs +57 -0
- package/dist/skills/ads-break-ui/SKILL.md +10 -2
- package/dist/skills/ads-break-ui/references/atomic-styles.md +121 -0
- package/dist/skills/ads-break-ui/references/guarded-editing.md +44 -0
- package/dist/skills/ads-break-ui/references/verification.md +6 -0
- package/dist/skills/ads-designer/SKILL.md +10 -2
- package/dist/skills/ads-designer/assets/bootstrap-ds/ADS.md +9 -3
- package/dist/skills/ads-designer/assets/bootstrap-ds/README.md +10 -0
- package/dist/skills/ads-designer/references/atomic-styles.md +121 -0
- package/dist/skills/ads-designer/references/guarded-editing.md +44 -0
- package/dist/skills/ads-import/SKILL.md +10 -2
- package/dist/skills/ads-import/references/atomic-styles.md +121 -0
- package/dist/skills/ads-import/references/guarded-editing.md +44 -0
- package/dist/skills/ads-import/references/tokens.md +4 -2
- package/dist/skills/ads-migrate/SKILL.md +10 -2
- package/dist/skills/ads-migrate/references/atomic-styles.md +121 -0
- package/dist/skills/ads-migrate/references/guarded-editing.md +44 -0
- package/dist/skills/ads-motion/SKILL.md +10 -2
- package/dist/skills/ads-motion/references/atomic-styles.md +121 -0
- package/dist/skills/ads-motion/references/guarded-editing.md +44 -0
- package/dist/token-fixtures.mjs +30 -0
- package/dist/tokens.mjs +8 -2
- package/dist/units.mjs +15 -0
- package/dist/verification.mjs +1 -1
- package/docs/ADS.md +59 -2
- package/docs/ARCHITECTURE.md +27 -0
- package/docs/BEHAVIORS.md +20 -0
- package/docs/CHANGELOG.md +32 -0
- package/docs/GUIDE.md +8 -3
- package/docs/MCP.md +70 -5
- package/package.json +4 -2
package/dist/public-api.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
|
@@ -4,7 +4,8 @@ import path from "node:path";
|
|
|
4
4
|
import { loadConfig, isInside } from "./config.mjs";
|
|
5
5
|
import { inspectScenarios } from "./scenarios.mjs";
|
|
6
6
|
import { artifactDigest, createBuildReporter } from "./mcp/build-status.mjs";
|
|
7
|
-
import { readPreviewManifest } from "./preview-resources.mjs";
|
|
7
|
+
import { cachedPreviewPublication, previewArtifactHtml, readPreviewManifest } from "./preview-resources.mjs";
|
|
8
|
+
import { previewArtifactPath, withPreviewSession } from "./preview-scope.mjs";
|
|
8
9
|
|
|
9
10
|
const digest = value => createHash("sha256").update(value).digest("hex");
|
|
10
11
|
const fileDigest = file => { try { return digest(readFileSync(file)); } catch { return null; } };
|
|
@@ -13,7 +14,7 @@ const fileDigest = file => { try { return digest(readFileSync(file)); } catch {
|
|
|
13
14
|
* Cache state is development output, never proof that a failed build is current. */
|
|
14
15
|
export function createPreviewCache(options, compilerFiles = []) {
|
|
15
16
|
const { root, config } = options;
|
|
16
|
-
const adapterFiles = [import.meta.filename, path.join(import.meta.dirname, "preview-resources.mjs"), path.join(import.meta.dirname, "scenarios-vite.mjs"), ...compilerFiles,
|
|
17
|
+
const adapterFiles = [import.meta.filename, path.join(import.meta.dirname, "preview-resources.mjs"), path.join(import.meta.dirname, "preview-scope.mjs"), path.join(import.meta.dirname, "scenarios-vite.mjs"), ...compilerFiles,
|
|
17
18
|
...["package.json", "package-lock.json", "bun.lock", "bun.lockb", "node_modules/.package-lock.json"].map(file => path.join(root, file))];
|
|
18
19
|
const adapterDigest = () => digest(JSON.stringify(adapterFiles.map(file => [file, fileDigest(file)])));
|
|
19
20
|
const loadedAdapter = adapterDigest();
|
|
@@ -62,14 +63,17 @@ export function createPreviewCache(options, compilerFiles = []) {
|
|
|
62
63
|
["scheduler", fileDigest(import.meta.filename)],
|
|
63
64
|
["node-runtime", `${process.version}/${process.platform}/${process.arch}`],
|
|
64
65
|
]));
|
|
65
|
-
function reusable(entry) {
|
|
66
|
+
function reusable(entry, expectedHtml) {
|
|
66
67
|
const cached = value.entries[entry.id];
|
|
67
68
|
if (!cached || cached.metadata !== JSON.stringify(entry) || cached.compiler !== compilerDigest()) return false;
|
|
68
69
|
if (!cached.dependencies || !Object.keys(cached.dependencies).length) return false;
|
|
69
70
|
if (Object.entries(cached.dependencies).some(([file, hash]) => fileDigest(path.resolve(root, file)) !== hash)) return false;
|
|
70
|
-
|
|
71
|
+
if (expectedHtml !== undefined && cached.artifact !== `sha256:${digest(expectedHtml)}`) return false;
|
|
72
|
+
const file = path.join(root, previewArtifactPath(options, entry.artifact));
|
|
73
|
+
if (expectedHtml !== undefined && !existsSync(file)) return true;
|
|
74
|
+
try { return cached.artifact === artifactDigest(file); } catch { return false; }
|
|
71
75
|
}
|
|
72
|
-
return { inputs, reusable, validated: stamp => value.validation === stamp,
|
|
76
|
+
return { inputs, reusable, graphRevision: () => value.graphRevision, recordGraph(revision) { value.graphRevision = revision; save(); }, validated: stamp => value.validation === stamp,
|
|
73
77
|
assertCompiler() {
|
|
74
78
|
if (adapterDigest() !== loadedAdapter) throw Object.assign(new Error("Preview compiler code or dependencies changed. Invoke the preview again to restart the builder and load current code."), { code: "ADS_PREVIEW_RESTART" });
|
|
75
79
|
},
|
|
@@ -78,7 +82,7 @@ export function createPreviewCache(options, compilerFiles = []) {
|
|
|
78
82
|
value.entries[entry.id] = { metadata: JSON.stringify(entry), compiler: compilerDigest(),
|
|
79
83
|
dependencies: Object.fromEntries([...new Set([path.join(root, entry.file), ...dependencies])]
|
|
80
84
|
.filter(file => isInside(root, file) && !file.includes(`${path.sep}node_modules${path.sep}`)).map(file => [path.relative(root, file), fileDigest(file)])),
|
|
81
|
-
artifact: artifactDigest(path.join(root, entry.artifact)) };
|
|
85
|
+
artifact: artifactDigest(path.join(root, previewArtifactPath(options, entry.artifact))) };
|
|
82
86
|
save();
|
|
83
87
|
},
|
|
84
88
|
};
|
|
@@ -87,11 +91,11 @@ export function createPreviewCache(options, compilerFiles = []) {
|
|
|
87
91
|
/** Shared scheduling and cache; the project supplies its own React/Preact build. */
|
|
88
92
|
export async function runScenarioCompiler(directory, { watched = false, validate, build, buildGraph, compilerFiles = [] }) {
|
|
89
93
|
const root = path.resolve(directory);
|
|
90
|
-
let options = loadConfig(root);
|
|
94
|
+
let options = withPreviewSession(loadConfig(root), process.env.ADS_MCP_BUILD_SESSION);
|
|
91
95
|
if (buildGraph && (!options.config.mcp.previewManifest || !options.config.framework)) throw new Error("Resource preview builds require an explicit framework and mcp.previewManifest in ds.config.json.");
|
|
92
96
|
const cache = createPreviewCache(options, compilerFiles);
|
|
93
97
|
function registry() {
|
|
94
|
-
options = loadConfig(root);
|
|
98
|
+
options = withPreviewSession(loadConfig(root), process.env.ADS_MCP_BUILD_SESSION);
|
|
95
99
|
const result = inspectScenarios(options);
|
|
96
100
|
if (result.diagnostics.length) throw Object.assign(new Error(result.diagnostics.map(item => `${item.file}: ${item.message}`).join("\n")), { scenarios: result.scenarios });
|
|
97
101
|
mkdirSync(path.join(root, ".ds/scenarios"), { recursive: true });
|
|
@@ -104,7 +108,8 @@ export async function runScenarioCompiler(directory, { watched = false, validate
|
|
|
104
108
|
if (!watched) throw error;
|
|
105
109
|
registryFailure = error; entries = error.scenarios ?? [];
|
|
106
110
|
}
|
|
107
|
-
const
|
|
111
|
+
const buildApps = () => entries.map(entry => ({ id: entry.id, file: previewArtifactPath(options, entry.artifact) }));
|
|
112
|
+
const reporter = createBuildReporter(root, { apps: buildApps() });
|
|
108
113
|
const requested = new Set();
|
|
109
114
|
const pending = new Set();
|
|
110
115
|
let active;
|
|
@@ -202,8 +207,12 @@ export async function runScenarioCompiler(directory, { watched = false, validate
|
|
|
202
207
|
}
|
|
203
208
|
} };
|
|
204
209
|
cache.assertCompiler();
|
|
205
|
-
|
|
206
|
-
|
|
210
|
+
const cached = cachedPreviewPublication(options, entries, cache.graphRevision());
|
|
211
|
+
let reusable = cached && entries.every(entry => cache.reusable(entry, previewArtifactHtml(entry.id, cached.revision)));
|
|
212
|
+
if (reusable) {
|
|
213
|
+
try { readPreviewManifest(options, { verify: true }); }
|
|
214
|
+
catch { reusable = false; }
|
|
215
|
+
}
|
|
207
216
|
const outcomes = await checkedBuild(stamp, () => reusable ? null : buildGraph(entries, options, track));
|
|
208
217
|
const failure = outcomes.find(outcome => outcome.status === "rejected");
|
|
209
218
|
if (failure) throw failure.reason;
|
|
@@ -212,12 +221,10 @@ export async function runScenarioCompiler(directory, { watched = false, validate
|
|
|
212
221
|
const publication = outcomes[1].value;
|
|
213
222
|
if (publication) {
|
|
214
223
|
publication.publish(reporter.session);
|
|
215
|
-
for (const entry of entries) cache.record(entry,
|
|
224
|
+
for (const entry of entries) cache.record(entry, dependencies);
|
|
225
|
+
cache.recordGraph(publication.revision);
|
|
216
226
|
} else {
|
|
217
|
-
|
|
218
|
-
const manifest = readPreviewManifest(options);
|
|
219
|
-
writeFileSync(`${file}.tmp`, JSON.stringify({ ...manifest, session: reporter.session }));
|
|
220
|
-
renameSync(`${file}.tmp`, file);
|
|
227
|
+
cached.publish(reporter.session);
|
|
221
228
|
}
|
|
222
229
|
for (const entry of entries) { pending.delete(entry.id); reporter.ready(entry.id, revisions.get(entry.id)); }
|
|
223
230
|
publishedStamp = stamp;
|
|
@@ -281,7 +288,7 @@ export async function runScenarioCompiler(directory, { watched = false, validate
|
|
|
281
288
|
try {
|
|
282
289
|
entries = registry();
|
|
283
290
|
registryFailure = undefined;
|
|
284
|
-
reporter.setApps(
|
|
291
|
+
reporter.setApps(buildApps());
|
|
285
292
|
reporter.clearFailure();
|
|
286
293
|
request(false);
|
|
287
294
|
for (const id of requested) if (!entries.some(entry => entry.id === id)) { requested.delete(id); pending.delete(id); }
|
package/dist/scenarios.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import { projectCompilerOptions } from "./boundaries.mjs";
|
|
|
8
8
|
import { inspectTokens } from "./tokens.mjs";
|
|
9
9
|
import { isSharedToken } from "./token-policy.mjs";
|
|
10
10
|
import { validTokenSamples } from "./token-samples.mjs";
|
|
11
|
+
import { scopedTokens } from "./scoped-tokens.mjs";
|
|
11
12
|
|
|
12
13
|
export const scenarioArtifact = (root, id) => path.join(root, ".ds/scenarios", id, "index.html");
|
|
13
14
|
|
|
@@ -39,14 +40,16 @@ export function inspectScenarios(options, suppliedProgram) {
|
|
|
39
40
|
const known = new Map(publicUi(program, options).map((item) => [item.name, item]));
|
|
40
41
|
const checker = program.getTypeChecker();
|
|
41
42
|
const behaviorApis = new Map(publicApi(program, options).filter(item => !item.renderable && item.behavioral).map(item => [item.name, item]));
|
|
42
|
-
const
|
|
43
|
+
const tokenInventory = inspectTokens(root);
|
|
44
|
+
const tokenNames = new Set(tokenInventory.tokens.map(token => token.name));
|
|
45
|
+
const scoped = scopedTokens(options);
|
|
43
46
|
const scenarios = [];
|
|
44
47
|
const diagnostics = [];
|
|
45
48
|
const ids = new Set();
|
|
46
49
|
function report(file, message) { diagnostics.push({ file: path.relative(root, file).replaceAll("\\", "/"), rule: "scenario-contract", line: 1, column: 1, message }); }
|
|
47
50
|
for (const file of files) {
|
|
48
51
|
const relative = path.relative(dsRoot, file).replaceAll("\\", "/");
|
|
49
|
-
|
|
52
|
+
let kind = isInside(path.join(dsRoot, options.config.tokenDir), file) ? "tokens" : relative.split("/")[0];
|
|
50
53
|
if (!["tokens", "components", "patterns", "composition"].includes(kind)) { report(file, "Scenarios live beside tokens/components/patterns or under composition/, not inside showcase pages."); continue; }
|
|
51
54
|
const id = relative.replace(/(?:\/Scenario|\.scenario)\.[^.]+$/, "");
|
|
52
55
|
if (id.length > 256) { report(file, "Scenario paths must fit within 256 characters."); continue; }
|
|
@@ -62,6 +65,11 @@ export function inspectScenarios(options, suppliedProgram) {
|
|
|
62
65
|
for (const declaration of node.declarationList.declarations) if (ts.isIdentifier(declaration.name) && declaration.name.text === "scenario") metadata = literal(declaration.initializer, checker);
|
|
63
66
|
}
|
|
64
67
|
}
|
|
68
|
+
if (metadata?.kind !== undefined) {
|
|
69
|
+
const owner = scoped.inventory.owner(relative);
|
|
70
|
+
if (metadata.kind !== "tokens" || !scoped.definitions.some(entry => entry.unit === owner)) { report(file, "Only an owner with registered local tokens may set kind: tokens for a colocated token preview."); continue; }
|
|
71
|
+
kind = "tokens";
|
|
72
|
+
}
|
|
65
73
|
if (!entry) report(file, "Export a default independently mountable scenario entrypoint.");
|
|
66
74
|
const module = checker.getSymbolAtLocation(source);
|
|
67
75
|
const defaultExport = module && checker.getExportsOfModule(module).find(item => item.name === "default");
|
|
@@ -77,17 +85,31 @@ export function inspectScenarios(options, suppliedProgram) {
|
|
|
77
85
|
}
|
|
78
86
|
const fixtureBase = path.basename(file).startsWith("Scenario.") ? path.join(path.dirname(file), "fixture") : file.replace(/\.scenario\.[^.]+$/, ".fixture");
|
|
79
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.");
|
|
80
|
-
if (!metadata || Object.keys(metadata).some((key) => !["title", "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)
|
|
81
|
-
|| Object.hasOwn(metadata, "exercises") && (!Array.isArray(metadata.exercises) || metadata.exercises.some(name => typeof name !== "string" || !behaviorApis.has(name)))
|
|
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)
|
|
82
89
|
|| Object.hasOwn(metadata, "tokens") && (!Array.isArray(metadata.tokens) || metadata.tokens.some(name => typeof name !== "string" || !name.startsWith("--")))
|
|
83
90
|
|| kind === "tokens" && (Object.hasOwn(metadata, "tokens") || !validTokenSamples(metadata.samples) || !metadata.covers.some(name => known.get(name)?.kind === "patterns"))
|
|
84
91
|
|| Object.hasOwn(metadata, "samples") && !validTokenSamples(metadata.samples)) {
|
|
85
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;
|
|
86
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
|
+
}
|
|
87
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.");
|
|
88
106
|
const tokens = [...new Set([...(metadata.tokens ?? []), ...(metadata.samples ?? []).map(sample => sample.token)])];
|
|
89
|
-
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
|
|
90
|
-
|
|
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.` });
|
|
108
|
+
if (kind === "tokens") {
|
|
109
|
+
const owner = scoped.inventory.owner(relative);
|
|
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.`);
|
|
111
|
+
}
|
|
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("\\", "/") });
|
|
91
113
|
}
|
|
92
114
|
return { scenarios: scenarios.sort((a, b) => a.id.localeCompare(b.id)), diagnostics };
|
|
93
115
|
}
|
|
@@ -209,3 +231,33 @@ export function writeScenarioRegistry(options, write) {
|
|
|
209
231
|
write({ schemaVersion: 1, scenarios: registry.scenarios });
|
|
210
232
|
return registry.scenarios;
|
|
211
233
|
}
|
|
234
|
+
|
|
235
|
+
/** Locate the actual consumed array, rather than regenerating a whole fixture. */
|
|
236
|
+
export function tokenSampleSource(options, scenario) {
|
|
237
|
+
const file = path.join(options.root, scenario.file);
|
|
238
|
+
const program = ts.createProgram([file], { ...projectCompilerOptions(options), allowJs:true, resolveJsonModule:true });
|
|
239
|
+
const checker = program.getTypeChecker(), source = program.getSourceFile(file);
|
|
240
|
+
function resolve(node, seen = new Set()) {
|
|
241
|
+
while (node && (ts.isAsExpression(node) || ts.isSatisfiesExpression(node) || ts.isParenthesizedExpression(node))) node = node.expression;
|
|
242
|
+
if (!node || seen.has(node)) return null;
|
|
243
|
+
if (ts.isArrayLiteralExpression(node) || ts.isObjectLiteralExpression(node)) return node;
|
|
244
|
+
let symbol = checker.getSymbolAtLocation(ts.isPropertyAccessExpression(node) ? node.name : node);
|
|
245
|
+
if (symbol?.flags & ts.SymbolFlags.Alias) symbol = checker.getAliasedSymbol(symbol);
|
|
246
|
+
const declaration = symbol?.valueDeclaration;
|
|
247
|
+
return declaration?.initializer ? resolve(declaration.initializer, new Set([...seen,node])) : null;
|
|
248
|
+
}
|
|
249
|
+
for (const statement of source.statements) if (ts.isVariableStatement(statement)) for (const declaration of statement.declarationList.declarations) {
|
|
250
|
+
if (!ts.isIdentifier(declaration.name) || declaration.name.text !== "scenario") continue;
|
|
251
|
+
const metadata = resolve(declaration.initializer);
|
|
252
|
+
const property = metadata && ts.isObjectLiteralExpression(metadata) && metadata.properties.find(node => ts.isPropertyAssignment(node) && (ts.isIdentifier(node.name) || ts.isStringLiteralLike(node.name)) && node.name.text === "samples");
|
|
253
|
+
const array = property && resolve(property.initializer);
|
|
254
|
+
if (array && ts.isArrayLiteralExpression(array)) {
|
|
255
|
+
const source = array.getSourceFile();
|
|
256
|
+
const scoped = scopedTokens(options), owner = scoped.inventory.owner(scoped.inventory.relative(scenario.file));
|
|
257
|
+
const relative = path.relative(options.root, source.fileName).replaceAll("\\", "/");
|
|
258
|
+
if (!isDevelopmentFile(source.fileName, options) || scoped.inventory.owner(scoped.inventory.relative(relative)) !== owner) throw new Error("The consumed samples array must live in this scenario's own development fixture.");
|
|
259
|
+
return {file:relative, content:source.text, start:array.getStart(source), end:array.end};
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
throw new Error("Cannot locate a literal consumed samples array. Keep fixture.samples statically inspectable; edit other fixture shapes with a normal owner patch.");
|
|
263
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import postcss from "postcss";
|
|
4
|
+
import ts from "typescript";
|
|
5
|
+
import { inspectUnits } from "./units.mjs";
|
|
6
|
+
import { isDevelopmentFile } from "./config.mjs";
|
|
7
|
+
import { foundationSelector } from "./foundation.mjs";
|
|
8
|
+
import { isSharedToken, tokenContract } from "./token-policy.mjs";
|
|
9
|
+
|
|
10
|
+
/** An explicit owner contract, never a heuristic based on a token's name. */
|
|
11
|
+
export function scopedTokens(options) {
|
|
12
|
+
if (options.scopedTokens) return options.scopedTokens;
|
|
13
|
+
const inventory = inspectUnits(options), definitions = [], diagnostics = [];
|
|
14
|
+
const report = (file, message, line = 1) => diagnostics.push({rule:"scoped-token", file, message, line, column:1});
|
|
15
|
+
for (const unit of inventory.units.filter(unit => ["components", "patterns"].includes(unit.kind))) {
|
|
16
|
+
for (const manifest of unit.files.filter(file => /(?:^|\/)tokens\.json$|\.tokens\.json$/.test(file))) {
|
|
17
|
+
const absolute = path.join(options.root, manifest);
|
|
18
|
+
try {
|
|
19
|
+
if (statSync(absolute).size > 65536 || realpathSync(absolute) !== absolute) throw new Error("Token manifests must be ordinary files no larger than 64 KiB.");
|
|
20
|
+
const data = JSON.parse(readFileSync(absolute, "utf8"));
|
|
21
|
+
const stylesheet = manifest.replace(/\.json$/, ".css");
|
|
22
|
+
if (!data || data.schemaVersion !== 1 || Object.keys(data).some(key => !["schemaVersion", "namespace"].includes(key))
|
|
23
|
+
|| typeof data.namespace !== "string" || !/^--[a-z][a-z0-9-]*-$/.test(data.namespace)
|
|
24
|
+
|| /^(?:--ds-|--ux-|--showcase-)/.test(data.namespace) || isSharedToken(data.namespace + "value")
|
|
25
|
+
|| tokenContract.naming.prefixes.some(prefix => prefix.startsWith(data.namespace))
|
|
26
|
+
|| [...tokenContract.naming.names,...Object.keys(tokenContract.tokens)].some(name => name.startsWith(data.namespace))) throw new Error("Use {schemaVersion:1, namespace:\"--<owner>-\"}; shared and runtime-variable prefixes are reserved.");
|
|
27
|
+
if (definitions.some(entry => entry.unit === unit.id || entry.namespace.startsWith(data.namespace) || data.namespace.startsWith(entry.namespace))) throw new Error("Each unit has one token namespace; namespaces must not overlap other owners.");
|
|
28
|
+
if (!unit.files.includes(stylesheet) || !existsSync(path.join(options.root, stylesheet))) throw new Error(`Colocate the definition-only stylesheet ${stylesheet}.`);
|
|
29
|
+
const file = path.join(options.root, stylesheet);
|
|
30
|
+
if (realpathSync(file) !== file) throw new Error("Scoped token stylesheets cannot resolve through symlinks.");
|
|
31
|
+
const css = postcss.parse(readFileSync(file, "utf8"), {from:file});
|
|
32
|
+
let count = 0;
|
|
33
|
+
css.walk(node => {
|
|
34
|
+
if (node.type === "comment" || node.type === "root") return;
|
|
35
|
+
if (node.type === "atrule" && ["media", "supports", "layer"].includes(node.name) && node.nodes) return;
|
|
36
|
+
if (node.type === "rule" && node.parent.type !== "rule" && !foundationSelector(node.selector, {definitions:true})) return;
|
|
37
|
+
if (node.type === "decl" && node.parent.type === "rule" && node.prop.startsWith(data.namespace) && node.prop.length > data.namespace.length && !node.important) { count++; return; }
|
|
38
|
+
report(stylesheet, "Owner token sheets contain only namespaced custom-property definitions under root/theme conditions. UI selectors, imports, font faces, animations and ordinary declarations belong in their runtime owners.", node.source?.start.line);
|
|
39
|
+
});
|
|
40
|
+
if (!count) report(stylesheet, "Define at least one real namespaced token.");
|
|
41
|
+
const imported = unit.files.filter(file => /\.[cm]?[jt]sx?$/.test(file) && !isDevelopmentFile(path.join(options.root, file), options)).some(source => {
|
|
42
|
+
const ast = ts.createSourceFile(source, readFileSync(path.join(options.root, source), "utf8"), ts.ScriptTarget.Latest, true);
|
|
43
|
+
return ast.statements.some(node => ts.isImportDeclaration(node) && !node.importClause && ts.isStringLiteralLike(node.moduleSpecifier) && path.resolve(options.root, path.dirname(source), node.moduleSpecifier.text) === file);
|
|
44
|
+
});
|
|
45
|
+
if (!imported) report(manifest, `Import ./${path.basename(stylesheet)} from this owner's runtime source so its real preview and product consume the same definitions.`);
|
|
46
|
+
definitions.push({unit:unit.id, namespace:data.namespace, manifest, file:stylesheet});
|
|
47
|
+
} catch (error) { report(manifest, error.reason ?? error.message, error.line); }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const result = {definitions, diagnostics, inventory};
|
|
51
|
+
options.scopedTokens = result;
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function scopedTokenOwner(options, name) {
|
|
56
|
+
return scopedTokens(options).definitions.find(entry => name.startsWith(entry.namespace));
|
|
57
|
+
}
|
|
@@ -36,8 +36,8 @@ and `boundaries.styleWrites` entries together with its source. Unused permission
|
|
|
36
36
|
fail the core checks; do not pre-register a boundary without its implementation.
|
|
37
37
|
Use `dryRun: true` to
|
|
38
38
|
inspect validation; applying re-runs the gates. Fix rejected patches in their
|
|
39
|
-
owner.
|
|
40
|
-
direct source writes. Shared styles/helpers use `foundation`, root guides use
|
|
39
|
+
owner. Use `ads_apply_transaction` for genuinely coupled owner patches;
|
|
40
|
+
never bypass a rejection with direct source writes. Shared styles/helpers use `foundation`, root guides use
|
|
41
41
|
`documentation`, and branding/fonts use their discovered `assets/*` unit.
|
|
42
42
|
Intentional configuration, rule exceptions, registered guard code and compiler
|
|
43
43
|
changes use `configuration`; validate the proposed settings and keep mandatory
|
|
@@ -45,8 +45,16 @@ ADS gates. For binary/large assets, generate `git diff --binary --full-index` in
|
|
|
45
45
|
`.ds/patches/<name>.patch` and send `patchFile` instead of inline `patch`, so bytes
|
|
46
46
|
stay out of model context. Binary changes require Git. When editing is disabled,
|
|
47
47
|
use the client's normal source-editing tools.
|
|
48
|
+
For owner drafts, token-sample reconciliation, coupled transactions and local
|
|
49
|
+
constant namespaces, read [guarded-editing.md](references/guarded-editing.md).
|
|
48
50
|
|
|
49
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
|
+
|
|
50
58
|
Controllers are UI behavior owners: private helpers stay with their component or
|
|
51
59
|
pattern; reusable controllers use discovered `controllers/<Name>` units. Use the
|
|
52
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.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Advanced guarded editing
|
|
2
|
+
|
|
3
|
+
Use `ads_apply_patch` for independent owner edits. `ads_prepare_patch` can create a
|
|
4
|
+
local draft of one owner and its companions; edit that directory with local file
|
|
5
|
+
tools, request `action:"diff"`, then apply the returned patchFile. Discard the
|
|
6
|
+
draft afterward. Remote clients can keep sending inline Git diffs. Neither draft
|
|
7
|
+
preparation nor diff generation validates or changes live source.
|
|
8
|
+
|
|
9
|
+
For genuinely coupled changes, `ads_apply_transaction` accepts 2–8 distinct owner
|
|
10
|
+
patches (32 distinct files and 16 MiB combined). Each patch keeps its ownership and
|
|
11
|
+
companion restrictions. Patches apply in order; later shared-entry hunks match the
|
|
12
|
+
earlier candidate. All guards, registered checks and affected behavior checks run
|
|
13
|
+
on the final candidate before live source is written. Dry-run first. Rejection,
|
|
14
|
+
conflict and cancellation publish nothing. Dependency installation remains local;
|
|
15
|
+
the candidate uses installed node_modules. Never bypass a rejected gate directly.
|
|
16
|
+
|
|
17
|
+
A component/pattern can own constants without expanding the shared vocabulary:
|
|
18
|
+
colocate `tokens.json` with `{ "schemaVersion":1, "namespace":"--<owner>-" }`
|
|
19
|
+
and a definition-only `tokens.css`, imported by its runtime source. Flat owners use
|
|
20
|
+
`<Owner>.tokens.json`/`<Owner>.tokens.css`. Namespaces are unique and cannot overlap
|
|
21
|
+
shared or --ds-/--ux-/--showcase- variables. Only that owner consumes/defines its
|
|
22
|
+
local tokens; aliases can use shared or own tokens. UI styles stay in Linaria.
|
|
23
|
+
Add a colocated token scenario with `kind:"tokens"`, real fixture `samples` and a
|
|
24
|
+
public review pattern. These inputs belong to the same owner and need real sample
|
|
25
|
+
coverage. Manifests and fixtures stay out of the product bundle.
|
|
26
|
+
|
|
27
|
+
`ads_prepare_token_samples({id, bindings?})` preserves existing project samples,
|
|
28
|
+
removes undeclared names and returns an owner patchFile. Supply explicit
|
|
29
|
+
`{token,property,probe}` bindings for new samples; unresolved tokens stay reported.
|
|
30
|
+
ADS does not infer visual meaning or fabricate renderers. Apply through the guard,
|
|
31
|
+
then inspect the actual App sample effects.
|
|
32
|
+
|
|
33
|
+
An empty .gitkeep can be deleted alongside new source beneath it in the selected
|
|
34
|
+
owner. Nonempty markers and other hidden files stay protected. A redundant
|
|
35
|
+
Scenario.tsx can be deleted when real coverage remains elsewhere; removing a
|
|
36
|
+
scenario does not erase ownership of the unit's runtime source.
|
|
37
|
+
|
|
38
|
+
Keep contextual public parts public when they are intended consumer API. Demonstrate
|
|
39
|
+
them through their actual public composition/customization path and list their real
|
|
40
|
+
render references in covers. Do not count internal production renders as public API
|
|
41
|
+
coverage. Behavioral APIs use exercises and registered guarantees instead.
|
|
42
|
+
Scenario controls still use public UI; native link/button markup belongs in an
|
|
43
|
+
owned public primitive/pattern or an owner-local test, including polymorphic/slot
|
|
44
|
+
adaptation. Do not add demo-only component props or global styles for review glue.
|
|
@@ -26,6 +26,12 @@ The showcase keeps a notification stream open. Wait for document loading and the
|
|
|
26
26
|
selected App's ready/error state; `networkidle` is not a readiness condition.
|
|
27
27
|
For tall previews, scroll the App document to its last example and verify it is
|
|
28
28
|
reachable without the host shell scrolling or clipping the preview.
|
|
29
|
+
Compare references under equivalent framing. With `content-visibility`, App
|
|
30
|
+
readiness can precede off-screen layout: bring measured content into view and
|
|
31
|
+
wait for observable geometry to settle. Record initial placeholder behavior
|
|
32
|
+
separately when relevant; a framed/top-level difference alone does not identify
|
|
33
|
+
an ADS defect. Wait for actual focus/positioning readiness for keyboard checks;
|
|
34
|
+
exercise rapid input as a separate case instead of adding arbitrary delays.
|
|
29
35
|
Capture the fixture, viewport/container size, theme, zoom, and actions alongside
|
|
30
36
|
each failure. CSS viewport emulation is useful; it is not evidence of real
|
|
31
37
|
mobile keyboard, browser chrome, or assistive-technology behavior.
|
|
@@ -68,8 +68,8 @@ and `boundaries.styleWrites` entries together with its source. Unused permission
|
|
|
68
68
|
fail the core checks; do not pre-register a boundary without its implementation.
|
|
69
69
|
Use `dryRun: true` to
|
|
70
70
|
inspect validation; applying re-runs the gates. Fix rejected patches in their
|
|
71
|
-
owner.
|
|
72
|
-
direct source writes. Shared styles/helpers use `foundation`, root guides use
|
|
71
|
+
owner. Use `ads_apply_transaction` for genuinely coupled owner patches;
|
|
72
|
+
never bypass a rejection with direct source writes. Shared styles/helpers use `foundation`, root guides use
|
|
73
73
|
`documentation`, and branding/fonts use their discovered `assets/*` unit.
|
|
74
74
|
Intentional configuration, rule exceptions, registered guard code and compiler
|
|
75
75
|
changes use `configuration`; validate the proposed settings and keep mandatory
|
|
@@ -77,6 +77,14 @@ ADS gates. For binary/large assets, generate `git diff --binary --full-index` in
|
|
|
77
77
|
`.ds/patches/<name>.patch` and send `patchFile` instead of inline `patch`, so bytes
|
|
78
78
|
stay out of model context. Binary changes require Git. When editing is disabled,
|
|
79
79
|
use the client's normal source-editing tools.
|
|
80
|
+
For owner drafts, token-sample reconciliation, coupled transactions and local
|
|
81
|
+
constant namespaces, read [guarded-editing.md](references/guarded-editing.md).
|
|
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.
|
|
80
88
|
|
|
81
89
|
Controllers are UI behavior owners: private helpers stay with their component or
|
|
82
90
|
pattern; reusable controllers use discovered `controllers/<Name>` units. Use the
|
|
@@ -36,8 +36,14 @@ Projects sharing this ADS use the same token names, meanings, and value types;
|
|
|
36
36
|
their concrete values and theme overrides can differ. Reuse existing local values
|
|
37
37
|
when importing components/patterns, add missing shared definitions, and import
|
|
38
38
|
source token values only when adopting that theme is requested.
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
Shared token names and prefixes follow the ADS contract. A component/pattern may
|
|
40
|
+
register one unique local namespace in colocated `tokens.json` and define it in
|
|
41
|
+
`tokens.css`, imported by its runtime. Flat owners use `<Owner>.tokens.json` and
|
|
42
|
+
`<Owner>.tokens.css`. These definition-only sheets contain root/theme custom
|
|
43
|
+
properties, not UI selectors or ordinary styles. Other owners consume shared
|
|
44
|
+
tokens; they cannot override or consume a peer's local namespace. A colocated
|
|
45
|
+
`kind:"tokens"` scenario needs real samples for every local token. Shared
|
|
46
|
+
assignments remain in the shared token files. Components own
|
|
41
47
|
`--ds-*`, patterns `--ux-*`, and the showcase `--showcase-*`; token files cannot
|
|
42
48
|
assign those local variables or override component state.
|
|
43
49
|
|
|
@@ -183,7 +189,7 @@ mounting, correct props, every branch/state or visual parity. The older
|
|
|
183
189
|
|
|
184
190
|
`token-scenario-reference` rejects undeclared token names in metadata and reachable
|
|
185
191
|
inputs. `token-scenario-render` rejects claimed tokens with no reachable input
|
|
186
|
-
reference; `token-scenario-coverage` requires every discovered
|
|
192
|
+
reference; `token-scenario-coverage` requires every discovered shared or owner-local token
|
|
187
193
|
to have coverage. A new or removed token therefore needs a scenario/fixture
|
|
188
194
|
update. All three are errors in `ads check`, run by `lint:ds` and the build.
|
|
189
195
|
Coverage is static reachability, not proof that every branch actually renders
|
|
@@ -62,6 +62,10 @@ the public `TokenPreview` pattern owns their presentation and applies the real C
|
|
|
62
62
|
variables. Add/remove samples alongside token definitions. `ads check` rejects
|
|
63
63
|
missing coverage and invented names; the App verifies the declared CSS bindings
|
|
64
64
|
in the browser. Extend or replace the project-owned pattern as your tokens evolve.
|
|
65
|
+
`ads_prepare_token_samples` can prepare a fixture patch from explicit project
|
|
66
|
+
property/probe bindings. It preserves existing samples and reports unmapped tokens;
|
|
67
|
+
it does not guess how a new value should look. Component/pattern constants can use
|
|
68
|
+
the colocated namespace contract in [ADS.md](ADS.md), with their own token scenario.
|
|
65
69
|
|
|
66
70
|
Create a component under `design-system/components/`, export it through `@ds`,
|
|
67
71
|
and colocate `Scenario.tsx` plus `fixture.ts` (or `<Owner>.scenario.tsx` and
|
|
@@ -79,6 +83,8 @@ stale retained output. Switching or rebuilding a scenario unmounts its previous
|
|
|
79
83
|
state. `showcase/mcp/main.tsx` exports `mountScenario(Scenario, container)` and
|
|
80
84
|
returns an unmount callback; it is not the shell. Keep the saved `framework`,
|
|
81
85
|
compiler and mount adapter consistent.
|
|
86
|
+
Managed sessions own their graph output; standalone prebuilds cannot replace it.
|
|
87
|
+
Verified generated graph snapshots preserve warm-start reuse across sessions.
|
|
82
88
|
|
|
83
89
|
The application and previews use the same foundation CSS and Linaria pipeline.
|
|
84
90
|
The source barrel loads the foundation once. The private `build:lib` script uses
|
|
@@ -98,3 +104,7 @@ from a user-selected external repository, preserving revisions and licenses.
|
|
|
98
104
|
inspection builds it automatically, then loads selected entries. Browser review
|
|
99
105
|
still checks layout, accessibility, keyboard/focus, states, themes and motion. A clean
|
|
100
106
|
build or delivered HTML alone does not establish those behaviors.
|
|
107
|
+
Opt-in MCP editing uses owner Git patches. `ads_prepare_patch` creates a temporary
|
|
108
|
+
owner draft/diff; `ads_apply_transaction` validates bounded coupled owner patches
|
|
109
|
+
against one final candidate. Preparation changes no live source. Keep all guards
|
|
110
|
+
and review the rendered result after applying.
|