@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.
Files changed (68) hide show
  1. package/README.md +17 -5
  2. package/dist/behaviors.mjs +157 -0
  3. package/dist/bootstrap-ds/ADS.md +16 -3
  4. package/dist/bootstrap-ds/README.md +10 -0
  5. package/dist/bootstrap-ds/design-system/controllers/.gitkeep +0 -0
  6. package/dist/boundaries.mjs +3 -0
  7. package/dist/check.mjs +20 -2
  8. package/dist/cli.mjs +17 -0
  9. package/dist/config.mjs +23 -3
  10. package/dist/css-values.mjs +26 -0
  11. package/dist/index.mjs +2 -0
  12. package/dist/init.mjs +1 -1
  13. package/dist/mcp/build-status.mjs +17 -7
  14. package/dist/mcp/catalog.mjs +11 -8
  15. package/dist/mcp/context.mjs +3 -3
  16. package/dist/mcp/edit-preparation.mjs +111 -0
  17. package/dist/mcp/mutations.mjs +64 -38
  18. package/dist/mcp/preview-owner-guard.mjs +29 -0
  19. package/dist/mcp/preview-watch.mjs +38 -8
  20. package/dist/mcp/preview-widget.bundle.js +2 -2
  21. package/dist/mcp/prompts.mjs +2 -2
  22. package/dist/mcp/server.mjs +88 -41
  23. package/dist/mcp/showcase-host.mjs +9 -5
  24. package/dist/mcp/text-diff.mjs +19 -0
  25. package/dist/preview-resources.mjs +45 -8
  26. package/dist/preview-scope.mjs +30 -0
  27. package/dist/public-api.mjs +39 -0
  28. package/dist/scenarios-compiler.mjs +41 -19
  29. package/dist/scenarios.mjs +72 -42
  30. package/dist/scoped-tokens.mjs +57 -0
  31. package/dist/skills/README.md +3 -3
  32. package/dist/skills/ads-break-ui/SKILL.md +26 -2
  33. package/dist/skills/ads-break-ui/references/guarded-editing.md +44 -0
  34. package/dist/skills/ads-break-ui/references/verification.md +6 -0
  35. package/dist/skills/ads-designer/SKILL.md +27 -3
  36. package/dist/skills/ads-designer/assets/bootstrap-ds/ADS.md +16 -3
  37. package/dist/skills/ads-designer/assets/bootstrap-ds/README.md +10 -0
  38. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/controllers/.gitkeep +0 -0
  39. package/dist/skills/ads-designer/references/cli.md +1 -1
  40. package/dist/skills/ads-designer/references/design-craft.md +1 -1
  41. package/dist/skills/ads-designer/references/guarded-editing.md +44 -0
  42. package/dist/skills/ads-import/SKILL.md +26 -2
  43. package/dist/skills/ads-import/references/cli.md +1 -1
  44. package/dist/skills/ads-import/references/guarded-editing.md +44 -0
  45. package/dist/skills/ads-import/references/tokens.md +4 -2
  46. package/dist/skills/{ads-assimilate → ads-migrate}/SKILL.md +32 -8
  47. package/dist/skills/ads-migrate/agents/openai.yaml +4 -0
  48. package/dist/skills/{ads-assimilate → ads-migrate}/references/cli.md +1 -1
  49. package/dist/skills/ads-migrate/references/guarded-editing.md +44 -0
  50. package/dist/skills/{ads-assimilate → ads-migrate}/references/native-implementation.md +11 -0
  51. package/dist/skills/ads-motion/SKILL.md +26 -2
  52. package/dist/skills/ads-motion/references/guarded-editing.md +44 -0
  53. package/dist/token-fixtures.mjs +30 -0
  54. package/dist/tokens.mjs +11 -4
  55. package/dist/units.mjs +35 -9
  56. package/dist/verification/browser.mjs +27 -0
  57. package/dist/verification/playwright-reporter.cjs +20 -0
  58. package/dist/verification.mjs +214 -0
  59. package/docs/ADS.md +72 -4
  60. package/docs/ARCHITECTURE.md +60 -1
  61. package/docs/BEHAVIORS.md +241 -0
  62. package/docs/CHANGELOG.md +56 -0
  63. package/docs/GUIDE.md +24 -4
  64. package/docs/MCP.md +106 -12
  65. package/package.json +4 -2
  66. package/dist/skills/ads-assimilate/agents/openai.yaml +0 -4
  67. /package/dist/skills/{ads-assimilate → ads-migrate}/references/lint-and-docs.md +0 -0
  68. /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
- try { return cached.artifact === artifactDigest(path.join(root, entry.artifact)); } catch { return false; }
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 reporter = createBuildReporter(root, { apps: entries.map(entry => ({ id: entry.id, file: entry.artifact })) });
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 Promise.allSettled([gate(stamp), reuse ? Promise.resolve() : build(entry, options, track)]);
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
- let reusable = entries.every(entry => cache.reusable(entry));
194
- if (reusable) { try { reusable = Boolean(readPreviewManifest(options, { verify: true })); } catch { reusable = false; } }
195
- const outcomes = await Promise.allSettled([gate(stamp), reusable ? Promise.resolve(null) : buildGraph(entries, options, track)]);
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, [...dependencies, ...publication.assetFiles]);
224
+ for (const entry of entries) cache.record(entry, dependencies);
225
+ cache.recordGraph(publication.revision);
204
226
  } else {
205
- const file = path.join(root, options.config.mcp.previewManifest);
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(entries.map(entry => ({ id: entry.id, file: entry.artifact })));
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); }
@@ -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
- const checker = program.getTypeChecker();
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 tokenNames = new Set(inspectTokens(root).tokens.map(token => token.name));
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
- const kind = isInside(path.join(dsRoot, options.config.tokenDir), file) ? "tokens" : relative.split("/")[0];
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 foundation token.` });
117
- scenarios.push({ id, kind, title: metadata.title, covers: [...new Set(metadata.covers)], tokens, ...(metadata.samples ? { samples: metadata.samples } : {}), file: path.relative(root, file).replaceAll("\\", "/"), artifact: path.relative(root, scenarioArtifact(root, id)).replaceAll("\\", "/") });
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 ui) {
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 && (ts.isFunctionDeclaration(node) || ts.isVariableDeclaration(node))) return;
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
+ }
@@ -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-assimilate --skill ads-break-ui --skill ads-motion
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-assimilate --skill ads-break-ui --skill ads-motion
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-assimilate](ads-assimilate/SKILL.md) | Study an external component library and implement a native counterpart | `$ads-assimilate https://github.com/emilkowalski/sonner` |
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. Report unsupported coupled changes instead of bypassing a rejection with
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.