@reqvire-org/ads 0.3.3 → 0.3.6
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 +17 -5
- package/dist/behaviors.mjs +157 -0
- package/dist/bootstrap-ds/ADS.md +16 -3
- package/dist/bootstrap-ds/README.md +10 -0
- package/dist/bootstrap-ds/design-system/controllers/.gitkeep +0 -0
- package/dist/boundaries.mjs +3 -0
- package/dist/check.mjs +20 -2
- package/dist/cli.mjs +17 -0
- package/dist/config.mjs +23 -3
- package/dist/css-values.mjs +26 -0
- package/dist/index.mjs +2 -0
- package/dist/init.mjs +1 -1
- package/dist/mcp/build-status.mjs +17 -7
- package/dist/mcp/catalog.mjs +11 -8
- package/dist/mcp/context.mjs +3 -3
- package/dist/mcp/edit-preparation.mjs +111 -0
- package/dist/mcp/mutations.mjs +64 -38
- package/dist/mcp/preview-owner-guard.mjs +29 -0
- package/dist/mcp/preview-watch.mjs +38 -8
- package/dist/mcp/preview-widget.bundle.js +2 -2
- package/dist/mcp/prompts.mjs +2 -2
- package/dist/mcp/server.mjs +88 -41
- package/dist/mcp/showcase-host.mjs +9 -5
- 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 +39 -0
- package/dist/scenarios-compiler.mjs +41 -19
- package/dist/scenarios.mjs +72 -42
- package/dist/scoped-tokens.mjs +57 -0
- package/dist/skills/README.md +3 -3
- package/dist/skills/ads-break-ui/SKILL.md +26 -2
- 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 +27 -3
- package/dist/skills/ads-designer/assets/bootstrap-ds/ADS.md +16 -3
- package/dist/skills/ads-designer/assets/bootstrap-ds/README.md +10 -0
- package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/controllers/.gitkeep +0 -0
- package/dist/skills/ads-designer/references/cli.md +1 -1
- package/dist/skills/ads-designer/references/design-craft.md +1 -1
- package/dist/skills/ads-designer/references/guarded-editing.md +44 -0
- package/dist/skills/ads-import/SKILL.md +26 -2
- package/dist/skills/ads-import/references/cli.md +1 -1
- 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-assimilate → ads-migrate}/SKILL.md +32 -8
- package/dist/skills/ads-migrate/agents/openai.yaml +4 -0
- package/dist/skills/{ads-assimilate → ads-migrate}/references/cli.md +1 -1
- package/dist/skills/ads-migrate/references/guarded-editing.md +44 -0
- package/dist/skills/{ads-assimilate → ads-migrate}/references/native-implementation.md +11 -0
- package/dist/skills/ads-motion/SKILL.md +26 -2
- package/dist/skills/ads-motion/references/guarded-editing.md +44 -0
- package/dist/token-fixtures.mjs +30 -0
- package/dist/tokens.mjs +11 -4
- package/dist/units.mjs +35 -9
- package/dist/verification/browser.mjs +27 -0
- package/dist/verification/playwright-reporter.cjs +20 -0
- package/dist/verification.mjs +214 -0
- package/docs/ADS.md +72 -4
- package/docs/ARCHITECTURE.md +60 -1
- package/docs/BEHAVIORS.md +241 -0
- package/docs/CHANGELOG.md +56 -0
- package/docs/GUIDE.md +24 -4
- package/docs/MCP.md +106 -12
- package/package.json +4 -2
- package/dist/skills/ads-assimilate/agents/openai.yaml +0 -4
- /package/dist/skills/{ads-assimilate → ads-migrate}/references/lint-and-docs.md +0 -0
- /package/dist/skills/{ads-assimilate → ads-migrate}/references/testing.md +0 -0
|
@@ -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;
|
|
@@ -126,6 +131,16 @@ export async function runScenarioCompiler(directory, { watched = false, validate
|
|
|
126
131
|
cache.validate(stamp);
|
|
127
132
|
});
|
|
128
133
|
}
|
|
134
|
+
async function checkedBuild(stamp, build) {
|
|
135
|
+
const started = generation;
|
|
136
|
+
let checking = true, compiling = true;
|
|
137
|
+
function progress() { if (started === generation) reporter.progress(checking && compiling ? "validation-and-compilation" : checking ? "validation" : compiling ? "compilation" : "publication"); }
|
|
138
|
+
progress();
|
|
139
|
+
return Promise.allSettled([
|
|
140
|
+
gate(stamp).finally(() => { checking = false; progress(); }),
|
|
141
|
+
Promise.resolve().then(build).finally(() => { compiling = false; progress(); }),
|
|
142
|
+
]);
|
|
143
|
+
}
|
|
129
144
|
async function compile(entry) {
|
|
130
145
|
if (buildGraph) return compileGraph();
|
|
131
146
|
const revision = reporter.begin(entry.id);
|
|
@@ -154,16 +169,18 @@ export async function runScenarioCompiler(directory, { watched = false, validate
|
|
|
154
169
|
if (path.isAbsolute(file) && existsSync(file)) dependencies.add(file);
|
|
155
170
|
}
|
|
156
171
|
} };
|
|
157
|
-
const outcomes = await
|
|
172
|
+
const outcomes = await checkedBuild(stamp, () => reuse ? undefined : build(entry, options, track));
|
|
158
173
|
const failure = outcomes.find(outcome => outcome.status === "rejected");
|
|
159
174
|
if (failure) throw failure.reason;
|
|
160
175
|
cache.assertCompiler();
|
|
161
176
|
if (stopped || started !== generation || stamp !== cache.inputs()) return; // Watch queued a newer revision.
|
|
162
177
|
if (!reuse) cache.record(entry, dependencies);
|
|
163
178
|
reporter.ready(entry.id, revision);
|
|
179
|
+
reporter.progress("idle");
|
|
164
180
|
} catch (error) {
|
|
165
181
|
if (started !== generation) return;
|
|
166
182
|
reporter.error(entry.id, revision, error);
|
|
183
|
+
reporter.progress("idle");
|
|
167
184
|
console.error(error);
|
|
168
185
|
if (!watched) process.exitCode = 1;
|
|
169
186
|
}
|
|
@@ -190,9 +207,13 @@ export async function runScenarioCompiler(directory, { watched = false, validate
|
|
|
190
207
|
}
|
|
191
208
|
} };
|
|
192
209
|
cache.assertCompiler();
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
+
}
|
|
216
|
+
const outcomes = await checkedBuild(stamp, () => reusable ? null : buildGraph(entries, options, track));
|
|
196
217
|
const failure = outcomes.find(outcome => outcome.status === "rejected");
|
|
197
218
|
if (failure) throw failure.reason;
|
|
198
219
|
cache.assertCompiler();
|
|
@@ -200,18 +221,18 @@ export async function runScenarioCompiler(directory, { watched = false, validate
|
|
|
200
221
|
const publication = outcomes[1].value;
|
|
201
222
|
if (publication) {
|
|
202
223
|
publication.publish(reporter.session);
|
|
203
|
-
for (const entry of entries) cache.record(entry,
|
|
224
|
+
for (const entry of entries) cache.record(entry, dependencies);
|
|
225
|
+
cache.recordGraph(publication.revision);
|
|
204
226
|
} else {
|
|
205
|
-
|
|
206
|
-
const manifest = readPreviewManifest(options);
|
|
207
|
-
writeFileSync(`${file}.tmp`, JSON.stringify({ ...manifest, session: reporter.session }));
|
|
208
|
-
renameSync(`${file}.tmp`, file);
|
|
227
|
+
cached.publish(reporter.session);
|
|
209
228
|
}
|
|
210
229
|
for (const entry of entries) { pending.delete(entry.id); reporter.ready(entry.id, revisions.get(entry.id)); }
|
|
211
230
|
publishedStamp = stamp;
|
|
231
|
+
reporter.progress("idle");
|
|
212
232
|
} catch (error) {
|
|
213
233
|
if (started !== generation) return;
|
|
214
234
|
for (const entry of entries) { pending.delete(entry.id); reporter.error(entry.id, revisions.get(entry.id), error); }
|
|
235
|
+
reporter.progress("idle");
|
|
215
236
|
console.error(error);
|
|
216
237
|
if (!watched) process.exitCode = 1;
|
|
217
238
|
}
|
|
@@ -261,12 +282,13 @@ export async function runScenarioCompiler(directory, { watched = false, validate
|
|
|
261
282
|
clearTimeout(timer);
|
|
262
283
|
timer = setTimeout(() => {
|
|
263
284
|
++generation;
|
|
285
|
+
reporter.progress("queued");
|
|
264
286
|
validation = undefined;
|
|
265
287
|
const changes = [...changed]; changed.clear();
|
|
266
288
|
try {
|
|
267
289
|
entries = registry();
|
|
268
290
|
registryFailure = undefined;
|
|
269
|
-
reporter.setApps(
|
|
291
|
+
reporter.setApps(buildApps());
|
|
270
292
|
reporter.clearFailure();
|
|
271
293
|
request(false);
|
|
272
294
|
for (const id of requested) if (!entries.some(entry => entry.id === id)) { requested.delete(id); pending.delete(id); }
|
package/dist/scenarios.mjs
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
3
|
import ts from "typescript";
|
|
4
|
+
import { publicApi } from "./public-api.mjs";
|
|
4
5
|
import { uiOperation } from "./ui-framework.mjs";
|
|
5
6
|
import { isDevelopmentFile, isInside, isScenarioFile, sourceFiles } from "./config.mjs";
|
|
6
7
|
import { projectCompilerOptions } from "./boundaries.mjs";
|
|
7
8
|
import { inspectTokens } from "./tokens.mjs";
|
|
8
9
|
import { isSharedToken } from "./token-policy.mjs";
|
|
9
10
|
import { validTokenSamples } from "./token-samples.mjs";
|
|
11
|
+
import { scopedTokens } from "./scoped-tokens.mjs";
|
|
10
12
|
|
|
11
13
|
export const scenarioArtifact = (root, id) => path.join(root, ".ds/scenarios", id, "index.html");
|
|
12
14
|
|
|
@@ -27,54 +29,27 @@ function literal(node, checker, seen = new Set()) {
|
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
export function publicUi(program, options) {
|
|
30
|
-
|
|
31
|
-
const entry = program.getSourceFile(path.join(options.dsRoot, options.config.publicEntry));
|
|
32
|
-
const module = entry && checker.getSymbolAtLocation(entry);
|
|
33
|
-
const items = [];
|
|
34
|
-
for (const exported of module ? checker.getExportsOfModule(module) : []) {
|
|
35
|
-
let symbol = exported;
|
|
36
|
-
if (symbol.flags & ts.SymbolFlags.Alias) symbol = checker.getAliasedSymbol(symbol);
|
|
37
|
-
const declaration = symbol.valueDeclaration ?? symbol.declarations?.[0];
|
|
38
|
-
if (!declaration || !(symbol.flags & ts.SymbolFlags.Value)) continue;
|
|
39
|
-
const file = declaration.getSourceFile().fileName;
|
|
40
|
-
if (isDevelopmentFile(file, options)) continue;
|
|
41
|
-
const kind = isInside(path.join(options.dsRoot, "components"), file) ? "components" : isInside(path.join(options.dsRoot, "patterns"), file) ? "patterns" : null;
|
|
42
|
-
if (!kind) continue;
|
|
43
|
-
let renderable = false;
|
|
44
|
-
function visit(node) {
|
|
45
|
-
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node)) renderable = true;
|
|
46
|
-
if (ts.isCallExpression(node) && ["createElement", "forwardRef", "memo"].includes(uiOperation(node.expression, checker))) renderable = true;
|
|
47
|
-
if (ts.isTaggedTemplateExpression(node)) {
|
|
48
|
-
let tag = node.tag;
|
|
49
|
-
if (ts.isCallExpression(tag)) tag = tag.expression;
|
|
50
|
-
let base = tag;
|
|
51
|
-
while (ts.isPropertyAccessExpression(base)) base = base.expression;
|
|
52
|
-
const symbol = checker.getSymbolAtLocation(base);
|
|
53
|
-
if (symbol?.declarations?.some(item => ts.isImportSpecifier(item) && (item.propertyName ?? item.name).text === "styled" && item.parent.parent.parent.moduleSpecifier.text.startsWith("@linaria/") || ts.isNamespaceImport(item) && item.parent.parent.moduleSpecifier.text.startsWith("@linaria/") && /^\.styled(?:\.|$)/.test(tag.getText().slice(base.getText().length)))) renderable = true;
|
|
54
|
-
}
|
|
55
|
-
ts.forEachChild(node, visit);
|
|
56
|
-
}
|
|
57
|
-
visit(declaration);
|
|
58
|
-
items.push({ name: exported.name, kind, file, declaration, renderable });
|
|
59
|
-
}
|
|
60
|
-
return items;
|
|
32
|
+
return publicApi(program, options).filter(item => item.kind !== "controllers");
|
|
61
33
|
}
|
|
62
34
|
|
|
63
35
|
/** Discover metadata statically. The MCP process never imports scenario modules. */
|
|
64
36
|
export function inspectScenarios(options, suppliedProgram) {
|
|
65
37
|
const { root, dsRoot } = options;
|
|
66
38
|
const files = sourceFiles(dsRoot).filter(isScenarioFile);
|
|
67
|
-
const program = suppliedProgram ?? ts.createProgram([path.join(dsRoot, options.config.publicEntry), ...files], projectCompilerOptions(options));
|
|
39
|
+
const program = suppliedProgram ?? ts.createProgram([path.join(dsRoot, options.config.publicEntry), ...files], { ...projectCompilerOptions(options), allowJs: true });
|
|
68
40
|
const known = new Map(publicUi(program, options).map((item) => [item.name, item]));
|
|
69
41
|
const checker = program.getTypeChecker();
|
|
70
|
-
const
|
|
42
|
+
const behaviorApis = new Map(publicApi(program, options).filter(item => !item.renderable && item.behavioral).map(item => [item.name, item]));
|
|
43
|
+
const tokenInventory = inspectTokens(root);
|
|
44
|
+
const tokenNames = new Set(tokenInventory.tokens.map(token => token.name));
|
|
45
|
+
const scoped = scopedTokens(options);
|
|
71
46
|
const scenarios = [];
|
|
72
47
|
const diagnostics = [];
|
|
73
48
|
const ids = new Set();
|
|
74
49
|
function report(file, message) { diagnostics.push({ file: path.relative(root, file).replaceAll("\\", "/"), rule: "scenario-contract", line: 1, column: 1, message }); }
|
|
75
50
|
for (const file of files) {
|
|
76
51
|
const relative = path.relative(dsRoot, file).replaceAll("\\", "/");
|
|
77
|
-
|
|
52
|
+
let kind = isInside(path.join(dsRoot, options.config.tokenDir), file) ? "tokens" : relative.split("/")[0];
|
|
78
53
|
if (!["tokens", "components", "patterns", "composition"].includes(kind)) { report(file, "Scenarios live beside tokens/components/patterns or under composition/, not inside showcase pages."); continue; }
|
|
79
54
|
const id = relative.replace(/(?:\/Scenario|\.scenario)\.[^.]+$/, "");
|
|
80
55
|
if (id.length > 256) { report(file, "Scenario paths must fit within 256 characters."); continue; }
|
|
@@ -90,6 +65,11 @@ export function inspectScenarios(options, suppliedProgram) {
|
|
|
90
65
|
for (const declaration of node.declarationList.declarations) if (ts.isIdentifier(declaration.name) && declaration.name.text === "scenario") metadata = literal(declaration.initializer, checker);
|
|
91
66
|
}
|
|
92
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
|
+
}
|
|
93
73
|
if (!entry) report(file, "Export a default independently mountable scenario entrypoint.");
|
|
94
74
|
const module = checker.getSymbolAtLocation(source);
|
|
95
75
|
const defaultExport = module && checker.getExportsOfModule(module).find(item => item.name === "default");
|
|
@@ -105,21 +85,26 @@ export function inspectScenarios(options, suppliedProgram) {
|
|
|
105
85
|
}
|
|
106
86
|
const fixtureBase = path.basename(file).startsWith("Scenario.") ? path.join(path.dirname(file), "fixture") : file.replace(/\.scenario\.[^.]+$/, ".fixture");
|
|
107
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.");
|
|
108
|
-
if (!metadata || Object.keys(metadata).some((key) => !["title", "covers", "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)
|
|
88
|
+
if (!metadata || Object.keys(metadata).some((key) => !["title", "kind", "covers", "exercises", "tokens", "samples"].includes(key)) || typeof metadata.title !== "string" || !metadata.title.trim() || !Array.isArray(metadata.covers) || !metadata.covers.length || metadata.covers.some((name) => typeof name !== "string" || !known.get(name)?.renderable)
|
|
89
|
+
|| Object.hasOwn(metadata, "exercises") && (!Array.isArray(metadata.exercises) || metadata.exercises.some(name => typeof name !== "string" || !behaviorApis.has(name)))
|
|
109
90
|
|| Object.hasOwn(metadata, "tokens") && (!Array.isArray(metadata.tokens) || metadata.tokens.some(name => typeof name !== "string" || !name.startsWith("--")))
|
|
110
91
|
|| kind === "tokens" && (Object.hasOwn(metadata, "tokens") || !validTokenSamples(metadata.samples) || !metadata.covers.some(name => known.get(name)?.kind === "patterns"))
|
|
111
92
|
|| Object.hasOwn(metadata, "samples") && !validTokenSamples(metadata.samples)) {
|
|
112
|
-
report(file, "Export static metadata with nonempty covers. 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;
|
|
93
|
+
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;
|
|
113
94
|
}
|
|
114
95
|
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.");
|
|
115
96
|
const tokens = [...new Set([...(metadata.tokens ?? []), ...(metadata.samples ?? []).map(sample => sample.token)])];
|
|
116
|
-
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
|
|
117
|
-
|
|
97
|
+
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.` });
|
|
98
|
+
if (kind === "tokens") {
|
|
99
|
+
const owner = scoped.inventory.owner(relative);
|
|
100
|
+
for (const name of tokens) if (tokenInventory.tokens.find(token => token.name === name)?.owner !== owner) report(file, `Token ${name} needs a preview beside its defining owner. Shared tokens belong under the configured token directory; local tokens stay with their component/pattern.`);
|
|
101
|
+
}
|
|
102
|
+
scenarios.push({ id, kind, title: metadata.title, covers: [...new Set(metadata.covers)], ...(metadata.exercises ? { exercises: [...new Set(metadata.exercises)] } : {}), tokens, ...(metadata.samples ? { samples: metadata.samples } : {}), file: path.relative(root, file).replaceAll("\\", "/"), artifact: path.relative(root, scenarioArtifact(root, id)).replaceAll("\\", "/") });
|
|
118
103
|
}
|
|
119
104
|
return { scenarios: scenarios.sort((a, b) => a.id.localeCompare(b.id)), diagnostics };
|
|
120
105
|
}
|
|
121
106
|
|
|
122
|
-
export function inspectScenarioCoverage(options, program, report) {
|
|
107
|
+
export function inspectScenarioCoverage(options, program, report, advise = () => {}) {
|
|
123
108
|
const registry = inspectScenarios(options, program);
|
|
124
109
|
for (const item of registry.diagnostics) report(path.join(options.root, item.file), item.rule, item.message, item.line, item.column);
|
|
125
110
|
const checker = program.getTypeChecker();
|
|
@@ -153,7 +138,7 @@ export function inspectScenarioCoverage(options, program, report) {
|
|
|
153
138
|
return symbol.declarations?.length ? symbol : null;
|
|
154
139
|
}
|
|
155
140
|
const identities = new Map();
|
|
156
|
-
for (const item of
|
|
141
|
+
for (const item of publicApi(program, options)) {
|
|
157
142
|
if (!identities.has(item.declaration)) identities.set(item.declaration, new Set());
|
|
158
143
|
identities.get(item.declaration).add(item.name);
|
|
159
144
|
}
|
|
@@ -165,12 +150,16 @@ export function inspectScenarioCoverage(options, program, report) {
|
|
|
165
150
|
for (const scenario of registry.scenarios) {
|
|
166
151
|
const source = program.getSourceFile(path.join(options.root, scenario.file));
|
|
167
152
|
const rendered = new Set();
|
|
153
|
+
const exercised = new Set(), referencedApis = new Set();
|
|
168
154
|
const referencedTokens = new Set();
|
|
169
155
|
let samplesPassed = false;
|
|
170
156
|
const visited = new Set();
|
|
171
157
|
function visit(node, reached = false) {
|
|
172
158
|
if (!node || visited.has(node)) return;
|
|
173
|
-
if (!reached &&
|
|
159
|
+
if (!reached && ts.isFunctionDeclaration(node)) return;
|
|
160
|
+
// Calls/construction in a mounted adapter execute even when only a result
|
|
161
|
+
// property is read later. Unused literal fixture fields still do not count.
|
|
162
|
+
if (!reached && ts.isVariableDeclaration(node) && !(node.initializer && (ts.isCallExpression(node.initializer) || ts.isNewExpression(node.initializer)))) return;
|
|
174
163
|
if (!reached && (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) && !ts.isCallExpression(node.parent) && !ts.isJsxExpression(node.parent)) return;
|
|
175
164
|
visited.add(node);
|
|
176
165
|
if (ts.isStringLiteralLike(node) && (isSharedToken(node.text) || scenario.tokens.includes(node.text))) referencedTokens.add(node.text);
|
|
@@ -180,18 +169,25 @@ export function inspectScenarioCoverage(options, program, report) {
|
|
|
180
169
|
if (expression) {
|
|
181
170
|
const symbol = canonical(symbolOf(expression));
|
|
182
171
|
const names = identities.get(symbol?.valueDeclaration ?? symbol?.declarations?.[0]);
|
|
183
|
-
for (const name of names ?? []) rendered.add(name);
|
|
172
|
+
for (const name of names ?? []) { rendered.add(name); exercised.add(name); }
|
|
184
173
|
if (scenario.kind === "tokens" && [...names ?? []].some(name => scenario.covers.includes(name))) {
|
|
185
174
|
const attributes = ts.isCallExpression(node) ? literal(node.arguments[1], checker) : Object.assign({}, ...node.attributes.properties.map(attribute => ts.isJsxSpreadAttribute(attribute) ? literal(attribute.expression, checker) : ts.isJsxAttribute(attribute) && ts.isIdentifier(attribute.name) && attribute.initializer && ts.isJsxExpression(attribute.initializer) ? { [attribute.name.text]: literal(attribute.initializer.expression, checker) } : {}));
|
|
186
175
|
if (JSON.stringify(attributes?.samples) === JSON.stringify(scenario.samples)) samplesPassed = true;
|
|
187
176
|
}
|
|
188
177
|
}
|
|
178
|
+
if (ts.isCallExpression(node) || ts.isNewExpression(node)) {
|
|
179
|
+
let expression = node.expression;
|
|
180
|
+
if (ts.isPropertyAccessExpression(expression) && ["call", "apply"].includes(expression.name.text)) expression = expression.expression;
|
|
181
|
+
const symbol = canonical(symbolOf(expression));
|
|
182
|
+
for (const name of identities.get(symbol?.valueDeclaration ?? symbol?.declarations?.[0]) ?? []) exercised.add(name);
|
|
183
|
+
}
|
|
189
184
|
// Follow local adapters reachable from the default entry. Unused example
|
|
190
185
|
// functions do not satisfy coverage; production implementations are never
|
|
191
186
|
// traversed to manufacture coverage for their internal dependencies.
|
|
192
187
|
if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node) || ts.isIdentifier(node) && !(ts.isPropertyAccessExpression(node.parent) || ts.isElementAccessExpression(node.parent))) {
|
|
193
188
|
const symbol = canonical(symbolOf(node));
|
|
194
189
|
const declaration = symbol?.valueDeclaration;
|
|
190
|
+
for (const name of identities.get(declaration ?? symbol?.declarations?.[0]) ?? []) referencedApis.add(name);
|
|
195
191
|
if (declaration && isDevelopmentFile(declaration.getSourceFile().fileName, options)) visit(declaration, true);
|
|
196
192
|
}
|
|
197
193
|
ts.forEachChild(node, child => visit(child, reached && (ts.isExportAssignment(node) || ts.isVariableDeclaration(node))));
|
|
@@ -201,6 +197,10 @@ export function inspectScenarioCoverage(options, program, report) {
|
|
|
201
197
|
if (!rendered.has(name)) report(source.fileName, "scenario-render", `${name} is declared in covers but has no public render reference in this scenario. Render the real API; an import alone does not count.`);
|
|
202
198
|
else covered.add(name);
|
|
203
199
|
}
|
|
200
|
+
for (const name of scenario.exercises ?? []) {
|
|
201
|
+
if (!exercised.has(name) && !referencedApis.has(name)) report(source.fileName, "scenario-exercise", `${name} is declared in exercises but has no reachable behavioral use. Imports and unused adapters do not count.`);
|
|
202
|
+
else if (!exercised.has(name)) advise(source.fileName, "scenario-exercise-review", `${name} is referenced dynamically. Verify its invocation in the registered browser/behavior checks; a reference alone does not prove execution.`);
|
|
203
|
+
}
|
|
204
204
|
for (const name of referencedTokens) if (!declaredTokens.has(name)) report(source.fileName, "token-scenario-reference", `Reachable scenario input references undeclared token ${name}.`);
|
|
205
205
|
if (scenario.kind === "tokens" && !samplesPassed) report(source.fileName, "token-scenario-render", "Pass the declared fixture.samples into a rendered public review pattern's samples prop. Printing token names or reading unused fixtures is not sample coverage.");
|
|
206
206
|
for (const name of scenario.tokens) {
|
|
@@ -221,3 +221,33 @@ export function writeScenarioRegistry(options, write) {
|
|
|
221
221
|
write({ schemaVersion: 1, scenarios: registry.scenarios });
|
|
222
222
|
return registry.scenarios;
|
|
223
223
|
}
|
|
224
|
+
|
|
225
|
+
/** Locate the actual consumed array, rather than regenerating a whole fixture. */
|
|
226
|
+
export function tokenSampleSource(options, scenario) {
|
|
227
|
+
const file = path.join(options.root, scenario.file);
|
|
228
|
+
const program = ts.createProgram([file], { ...projectCompilerOptions(options), allowJs:true, resolveJsonModule:true });
|
|
229
|
+
const checker = program.getTypeChecker(), source = program.getSourceFile(file);
|
|
230
|
+
function resolve(node, seen = new Set()) {
|
|
231
|
+
while (node && (ts.isAsExpression(node) || ts.isSatisfiesExpression(node) || ts.isParenthesizedExpression(node))) node = node.expression;
|
|
232
|
+
if (!node || seen.has(node)) return null;
|
|
233
|
+
if (ts.isArrayLiteralExpression(node) || ts.isObjectLiteralExpression(node)) return node;
|
|
234
|
+
let symbol = checker.getSymbolAtLocation(ts.isPropertyAccessExpression(node) ? node.name : node);
|
|
235
|
+
if (symbol?.flags & ts.SymbolFlags.Alias) symbol = checker.getAliasedSymbol(symbol);
|
|
236
|
+
const declaration = symbol?.valueDeclaration;
|
|
237
|
+
return declaration?.initializer ? resolve(declaration.initializer, new Set([...seen,node])) : null;
|
|
238
|
+
}
|
|
239
|
+
for (const statement of source.statements) if (ts.isVariableStatement(statement)) for (const declaration of statement.declarationList.declarations) {
|
|
240
|
+
if (!ts.isIdentifier(declaration.name) || declaration.name.text !== "scenario") continue;
|
|
241
|
+
const metadata = resolve(declaration.initializer);
|
|
242
|
+
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");
|
|
243
|
+
const array = property && resolve(property.initializer);
|
|
244
|
+
if (array && ts.isArrayLiteralExpression(array)) {
|
|
245
|
+
const source = array.getSourceFile();
|
|
246
|
+
const scoped = scopedTokens(options), owner = scoped.inventory.owner(scoped.inventory.relative(scenario.file));
|
|
247
|
+
const relative = path.relative(options.root, source.fileName).replaceAll("\\", "/");
|
|
248
|
+
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.");
|
|
249
|
+
return {file:relative, content:source.text, start:array.getStart(source), end:array.end};
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
throw new Error("Cannot locate a literal consumed samples array. Keep fixture.samples statically inspectable; edit other fixture shapes with a normal owner patch.");
|
|
253
|
+
}
|
|
@@ -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
|
+
}
|
package/dist/skills/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
Install the skills you need with `npx skills`:
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
|
-
npx skills add reqvire-org/ads --skill ads-designer --skill ads-import --skill ads-
|
|
6
|
+
npx skills add reqvire-org/ads --skill ads-designer --skill ads-import --skill ads-migrate --skill ads-break-ui --skill ads-motion
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
For local development, install from this checkout:
|
|
10
10
|
|
|
11
11
|
```sh
|
|
12
|
-
npx skills add ./skills --skill ads-designer --skill ads-import --skill ads-
|
|
12
|
+
npx skills add ./skills --skill ads-designer --skill ads-import --skill ads-migrate --skill ads-break-ui --skill ads-motion
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Each skill includes its own instructions and references. `ads-designer` also
|
|
@@ -36,7 +36,7 @@ the browser, and provides a running URL or exact command for review. See the
|
|
|
36
36
|
|-------|-----|---------|
|
|
37
37
|
| [ads-designer](ads-designer/SKILL.md) | Bootstrap or build UI using the local ADS, with craft/mobile guidance | `$ads-designer build a settings page using this project's ADS` |
|
|
38
38
|
| [ads-import](ads-import/SKILL.md) | Import selected external ADS source, brand artwork, or compatible source token values | `$ads-import /path/to/source-repo component Toast` |
|
|
39
|
-
| [ads-
|
|
39
|
+
| [ads-migrate](ads-migrate/SKILL.md) | Study an external component library and implement a native counterpart | `$ads-migrate https://github.com/emilkowalski/sonner` |
|
|
40
40
|
| [ads-break-ui](ads-break-ui/SKILL.md) | Stress-test real components/patterns; optionally fix demonstrated failures | `$ads-break-ui AppShell +fix` |
|
|
41
41
|
| [ads-motion](ads-motion/SKILL.md) | Implement, review, audit, or profile motion | `$ads-motion review Toast` |
|
|
42
42
|
|
|
@@ -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,6 +45,25 @@ 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).
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
Controllers are UI behavior owners: private helpers stay with their component or
|
|
53
|
+
pattern; reusable controllers use discovered `controllers/<Name>` units. Use the
|
|
54
|
+
project's configured framework and declared dependencies, or plain TypeScript,
|
|
55
|
+
browser APIs and local helpers. MCP hosts supply no ad hoc runtime APIs. The
|
|
56
|
+
framework setting selects compilation/mounting; controller checks also validate
|
|
57
|
+
imports. For controller/behavior work, read `ads://guides/behaviors` (offline:
|
|
58
|
+
the installed ADS package's `docs/BEHAVIORS.md`). Discover registered guarantees
|
|
59
|
+
with `ads_behaviors`; use `ads_verify` for selected real checks. Optional
|
|
60
|
+
`exercises` links public behavioral APIs to scenarios; keep `covers` for rendered
|
|
61
|
+
UI. Registration and demonstration are not passing evidence. `behavior.json`,
|
|
62
|
+
scenarios, fixtures and checks stay out of production exports/imports. Verify
|
|
63
|
+
current results, including lifecycle setup/cleanup/setup and the actual browser
|
|
64
|
+
profile when promised. React StrictMode needs a development harness; wrapping a
|
|
65
|
+
production widget cannot replay development effects. Keep dynamic behavior and
|
|
66
|
+
capabilities intact rather than rewriting them only for static coverage.
|
|
48
67
|
|
|
49
68
|
Before trusting a preview after edits, check `ads_status.build` or
|
|
50
69
|
`ads://project/build`. Fix build diagnostics in owned source; an older view or
|
|
@@ -226,6 +245,11 @@ Do not copy an unchecked upstream preflight or turn a component repair into a re
|
|
|
226
245
|
Keep behavior and skin in their runtime owner. Put demo-only sizing and framing
|
|
227
246
|
in a public review/layout pattern; add component props for a deliberate runtime
|
|
228
247
|
API need. Preserve required capabilities while following ownership rules.
|
|
248
|
+
Use tokens for absolute/font-relative lengths, including `ch`, `ex`, `cap`, `ic`
|
|
249
|
+
and `lh`; calculations and fallback values follow the same rule. Structural
|
|
250
|
+
angle units remain available for orientation, skew and spin. Put tunable visual
|
|
251
|
+
angles in project tokens; static checks cannot infer that intent. Percentages,
|
|
252
|
+
grid fractions and viewport/container relationships retain structural use.
|
|
229
253
|
Browser review still verifies the effects.
|
|
230
254
|
|
|
231
255
|
Invalid metadata and source failures keep the managed compiler alive for repairs.
|
|
@@ -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.
|