@reqvire-org/ads 0.3.18 → 0.3.21

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 (104) hide show
  1. package/README.md +2 -0
  2. package/dist/behaviors.mjs +19 -9
  3. package/dist/bootstrap-ds/ADS.md +2 -2
  4. package/dist/bootstrap-ds/README.md +2 -2
  5. package/dist/bootstrap-ds/design-system/index.ts +0 -1
  6. package/dist/bootstrap-ds/design-system/review/index.ts +1 -0
  7. package/dist/bootstrap-ds/design-system/tokens/{Foundation.scenario.tsx → Foundation.ads.scenario.tsx} +2 -2
  8. package/dist/bootstrap-ds/ds.config.json +1 -1
  9. package/dist/bootstrap-ds/tsconfig.build.json +9 -2
  10. package/dist/bootstrap-ds/tsconfig.json +18 -3
  11. package/dist/bootstrap-ds/vite.config.ts +1 -1
  12. package/dist/boundaries.mjs +6 -2
  13. package/dist/check.mjs +44 -17
  14. package/dist/config.mjs +33 -5
  15. package/dist/contract-migration.mjs +110 -0
  16. package/dist/contracts.d.mts +22 -0
  17. package/dist/evidence.mjs +1 -1
  18. package/dist/index.mjs +1 -0
  19. package/dist/init.mjs +18 -15
  20. package/dist/layers.mjs +288 -0
  21. package/dist/lint-config.mjs +1 -1
  22. package/dist/mcp/catalog.mjs +9 -2
  23. package/dist/mcp/context.mjs +2 -1
  24. package/dist/mcp/edit-preparation.mjs +9 -6
  25. package/dist/mcp/mutations.mjs +1 -1
  26. package/dist/mcp/preview-widget.bundle.js +41 -39
  27. package/dist/mcp/prompts.mjs +1 -1
  28. package/dist/mcp/server.mjs +9 -3
  29. package/dist/mcp/widget.bundle.js +1 -1
  30. package/dist/oxlint.mjs +3 -1
  31. package/dist/paint-contracts.mjs +124 -0
  32. package/dist/preview-resources.mjs +12 -2
  33. package/dist/public-api.mjs +13 -6
  34. package/dist/public-props.mjs +2 -2
  35. package/dist/runtime-contracts.mjs +225 -0
  36. package/dist/scenario-trace.mjs +37 -9
  37. package/dist/scenarios-vite.mjs +3 -2
  38. package/dist/scenarios.mjs +89 -30
  39. package/dist/scoped-tokens.mjs +9 -4
  40. package/dist/showcase/client.bundle.js +1 -1
  41. package/dist/showcase/sandbox.bundle.js +1 -1
  42. package/dist/skills/ads-break-ui/SKILL.md +25 -6
  43. package/dist/skills/ads-break-ui/references/contracts.md +250 -0
  44. package/dist/skills/ads-break-ui/references/guarded-editing.md +1 -1
  45. package/dist/skills/ads-break-ui/references/layers.md +123 -0
  46. package/dist/skills/ads-break-ui/references/review.md +96 -0
  47. package/dist/skills/ads-break-ui/references/scenarios.md +24 -3
  48. package/dist/skills/ads-designer/SKILL.md +26 -7
  49. package/dist/skills/ads-designer/assets/bootstrap-ds/ADS.md +2 -2
  50. package/dist/skills/ads-designer/assets/bootstrap-ds/README.md +2 -2
  51. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/index.ts +0 -1
  52. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/review/index.ts +1 -0
  53. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/{Foundation.scenario.tsx → Foundation.ads.scenario.tsx} +2 -2
  54. package/dist/skills/ads-designer/assets/bootstrap-ds/ds.config.json +1 -1
  55. package/dist/skills/ads-designer/assets/bootstrap-ds/tsconfig.build.json +9 -2
  56. package/dist/skills/ads-designer/assets/bootstrap-ds/tsconfig.json +18 -3
  57. package/dist/skills/ads-designer/assets/bootstrap-ds/vite.config.ts +1 -1
  58. package/dist/skills/ads-designer/references/contracts.md +250 -0
  59. package/dist/skills/ads-designer/references/guarded-editing.md +1 -1
  60. package/dist/skills/ads-designer/references/layers.md +123 -0
  61. package/dist/skills/ads-designer/references/review.md +96 -0
  62. package/dist/skills/ads-designer/references/scenarios.md +24 -3
  63. package/dist/skills/ads-import/SKILL.md +25 -6
  64. package/dist/skills/ads-import/references/contracts.md +250 -0
  65. package/dist/skills/ads-import/references/guarded-editing.md +1 -1
  66. package/dist/skills/ads-import/references/layers.md +123 -0
  67. package/dist/skills/ads-import/references/review.md +96 -0
  68. package/dist/skills/ads-import/references/scenarios.md +24 -3
  69. package/dist/skills/ads-migrate/SKILL.md +25 -6
  70. package/dist/skills/ads-migrate/references/contracts.md +250 -0
  71. package/dist/skills/ads-migrate/references/guarded-editing.md +1 -1
  72. package/dist/skills/ads-migrate/references/layers.md +123 -0
  73. package/dist/skills/ads-migrate/references/review.md +96 -0
  74. package/dist/skills/ads-migrate/references/scenarios.md +24 -3
  75. package/dist/skills/ads-motion/SKILL.md +26 -7
  76. package/dist/skills/ads-motion/references/audit.md +1 -1
  77. package/dist/skills/ads-motion/references/contracts.md +250 -0
  78. package/dist/skills/ads-motion/references/guarded-editing.md +1 -1
  79. package/dist/skills/ads-motion/references/layers.md +123 -0
  80. package/dist/skills/ads-motion/references/motion-review.md +35 -0
  81. package/dist/skills/ads-motion/references/review.md +96 -35
  82. package/dist/skills/ads-motion/references/scenarios.md +24 -3
  83. package/dist/token-origins.mjs +24 -0
  84. package/dist/tokens.mjs +4 -3
  85. package/dist/unit-contracts.mjs +203 -0
  86. package/dist/units.mjs +26 -17
  87. package/dist/verification/browser.mjs +156 -26
  88. package/dist/verification/playwright-reporter.cjs +13 -3
  89. package/docs/ADS.md +40 -6
  90. package/docs/ARCHITECTURE.md +73 -4
  91. package/docs/BEHAVIORS.md +15 -0
  92. package/docs/CHANGELOG.md +42 -0
  93. package/docs/CONTRACTS.md +250 -0
  94. package/docs/GUIDE.md +29 -3
  95. package/docs/MCP.md +42 -6
  96. package/package.json +4 -1
  97. package/dist/bootstrap-ds/design-system/patterns/token-preview/Scenario.tsx +0 -4
  98. package/dist/bootstrap-ds/design-system/patterns/token-preview/fixture.ts +0 -8
  99. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/patterns/token-preview/Scenario.tsx +0 -4
  100. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/patterns/token-preview/fixture.ts +0 -8
  101. /package/dist/bootstrap-ds/design-system/{patterns → review}/token-preview/TokenPreview.tsx +0 -0
  102. /package/dist/bootstrap-ds/design-system/tokens/{Foundation.fixture.ts → Foundation.ads.fixture.ts} +0 -0
  103. /package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/{patterns → review}/token-preview/TokenPreview.tsx +0 -0
  104. /package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/{Foundation.fixture.ts → Foundation.ads.fixture.ts} +0 -0
package/dist/units.mjs CHANGED
@@ -2,7 +2,7 @@ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import ts from "typescript";
4
4
  import postcss from "postcss";
5
- import { isDevelopmentFile, isInside } from "./config.mjs";
5
+ import { adsFileRole, isDevelopmentFile, isInside } from "./config.mjs";
6
6
  import { importResolver } from "./boundaries.mjs";
7
7
  import { documentationFilePolicy, isProjectDocument, projectDocumentFiles } from "./project-documents.mjs";
8
8
  import { foundationFiles, foundationImportTarget } from "./token-policy.mjs";
@@ -25,7 +25,7 @@ export function configurationFiles(options) {
25
25
  // fixtures to configuration. Those remain in the behavior owner's patch.
26
26
  if (isInside(dsRoot, file) && isDevelopmentFile(file, options)) return;
27
27
  if (!entrypoint && isInside(sourceRoot, file) && !isInside(dsRoot, file)) return;
28
- if (!entrypoint && isInside(dsRoot, file) && /^(?:components|patterns|controllers|composition|tokens)[\\/]/.test(path.relative(dsRoot, file))) {
28
+ if (!entrypoint && isInside(dsRoot, file) && /^(?:components|patterns|controllers|composition|review|tokens)[\\/]/.test(path.relative(dsRoot, file))) {
29
29
  if (isInside(path.join(dsRoot, "controllers"), file) || isDevelopmentFile(file, options)) return;
30
30
  const folder = path.dirname(file), stem = path.basename(file).split(".")[0];
31
31
  const anchored = ["index", path.basename(folder)].some(name => [".ts", ".tsx", ".js", ".jsx", ".mjs", ".mts"].some(extension => existsSync(path.join(folder, name + extension))));
@@ -98,34 +98,39 @@ export function inspectUnits(options) {
98
98
  // Runtime entry conventions keep directory ownership stable when an optional
99
99
  // preview is removed. A flat scenario family still denotes separate owners.
100
100
  const sameName = name => name.replace(/[-_]/g, "").toLowerCase();
101
- const flatFamilies = new Set(files.filter(file => /\.scenario\.[cm]?[jt]sx?$/.test(file)).map(file => path.posix.dirname(file)));
101
+ const flatFamilies = new Set(files.filter(file => !adsFileRole(file) && /\.scenario\.[cm]?[jt]sx?$/.test(file)).map(file => path.posix.dirname(file)));
102
102
  const directories = new Set(files.filter(file => /(?:^|\/)Scenario\.[cm]?[jt]sx?$/.test(file)).map(file => slash(path.dirname(file))));
103
103
  for (const file of files.filter(file => /^(?:components|patterns)\/.+\/tokens\.json$/.test(file))) directories.add(path.posix.dirname(file));
104
+ for (const file of files.filter(file => /\.ads\.contract\.[cm]?ts$/.test(file))) {
105
+ const directory=path.posix.dirname(file);
106
+ if(directory.includes("/")&&sameName(path.basename(file).split(".")[0])===sameName(path.posix.basename(directory))&&!flatFamilies.has(directory))directories.add(directory);
107
+ }
104
108
  for (const file of files) {
105
- if (!/^(?:components|patterns|controllers|composition)\//.test(file) || !source.test(file)) continue;
109
+ if (!/^(?:components|patterns|controllers|composition|review)\//.test(file) || !source.test(file)) continue;
106
110
  const directory = slash(path.dirname(file)), name = path.basename(file).replace(source, "");
107
- if (!directory.includes("/") || /\.(?:scenario|fixture|scenario-adapter)$/.test(name)) continue;
111
+ if (!directory.includes("/") || adsFileRole(file) || /\.(?:scenario|fixture|scenario-adapter)$/.test(name)) continue;
108
112
  if (flatFamilies.has(directory)) continue;
109
113
  if (name === "index" || sameName(name) === sameName(path.posix.basename(directory)) || name === "fixture") directories.add(directory);
110
114
  }
111
115
  const directoryUnits = [...directories].sort((a, b) => b.length - a.length);
112
- const flatUnits = [...new Set(files.filter(file => /^(?:components|patterns|controllers|composition)\//.test(file) && source.test(file) && !directoryUnits.some(directory => file.startsWith(directory + "/"))).map(file => {
116
+ const flatUnits = [...new Set(files.filter(file => /^(?:components|patterns|controllers|composition|review)\//.test(file) && source.test(file) && !directoryUnits.some(directory => file.startsWith(directory + "/"))).map(file => {
113
117
  const base = path.basename(file).split(".")[0];
114
118
  return slash(path.join(path.dirname(file), base));
115
119
  }))].sort((a, b) => b.length - a.length);
116
120
  const publicEntry = slash(path.relative(dsRoot, path.join(dsRoot, config.publicEntry)));
121
+ const reviewEntry = slash(path.relative(dsRoot, path.join(dsRoot, config.reviewEntry)));
117
122
  const tokenDir = slash(path.relative(dsRoot, path.join(dsRoot, config.tokenDir)));
118
123
  const stylesEntry = slash(path.relative(dsRoot, path.join(dsRoot, config.stylesEntry)));
119
124
  function owner(relative) {
120
125
  const projectFile = slash(path.relative(root, path.resolve(dsRoot, relative)));
121
126
  if (setup.has(projectFile)) return "configuration";
122
127
  if (!isInside(dsRoot, path.resolve(dsRoot, relative))) return isProjectDocument(projectFile) ? "documentation" : null;
123
- if (relative === publicEntry) return null; // Controlled re-export bookkeeping.
128
+ if (relative === publicEntry || relative === reviewEntry) return null; // Controlled re-export bookkeeping.
124
129
  if (relative.startsWith(tokenDir + "/")) return "tokens";
125
130
  if (relative.startsWith("assets/")) return relative.split("/").length > 2 ? relative.split("/").slice(0, 2).join("/") : "assets";
126
131
  if (isProjectDocument(relative)) return "documentation";
127
132
  if (!relative.includes("/")) return /(?:\.(?:md|txt)$|^LICENSE$)/i.test(relative) ? "documentation" : "foundation";
128
- if (!/^(?:components|patterns|controllers|composition)\//.test(relative)) return relative.split("/")[0] === "documentation" ? null : relative.split("/")[0];
133
+ if (!/^(?:components|patterns|controllers|composition|review)\//.test(relative)) return relative.split("/")[0] === "documentation" ? null : relative.split("/")[0];
129
134
  for (const directory of directoryUnits) if (relative.startsWith(directory + "/")) return directory;
130
135
  for (const unit of flatUnits) if (relative.startsWith(unit + "/") || relative.startsWith(unit + ".")) return unit;
131
136
  const base = path.basename(relative).split(".")[0];
@@ -146,22 +151,25 @@ export function inspectUnits(options) {
146
151
  if (!units.get(id).files.includes(file)) units.get(id).files.push(file);
147
152
  }
148
153
  for (const unit of units.values()) if (boundaryCompanion(unit.id)) {
154
+ const contracts=unit.files.filter(file=>/\.ads\.contract\.[cm]?ts$/.test(file));
155
+ if(contracts.length===1)unit.contractFile=contracts[0];
156
+ if (unit.id.startsWith("review/")) (unit.companionFiles ??= []).push({ path: slash(path.relative(root, path.join(dsRoot, reviewEntry))), allowedChanges: "Only this review owner's re-exports in the separate development entry; never the production public entry." });
149
157
  (unit.companionFiles ??= []).push({ path: "ds.config.json", allowedChanges: "Only this owner's inlineStyleFiles and boundaries.styleWrites entries, together with its source change; preserve all other configuration and peer permissions." });
150
- if (/^(?:components|patterns|controllers|composition)\//.test(unit.id)) {
158
+ if (/^(?:components|patterns|controllers|composition|review)\//.test(unit.id)) {
151
159
  for (let directory = unit.id; directory !== "."; directory = path.posix.dirname(directory)) {
152
160
  const marker = path.join(dsRoot, directory, ".gitkeep");
153
161
  if (existsSync(marker) && readFileSync(marker).length === 0) (unit.companionFiles ??= []).push({ path: slash(path.relative(root, marker)), allowedChanges: "Delete this empty placeholder only while adding real source in the selected owner below it." });
154
162
  }
155
163
  }
156
164
  }
157
- return { units: [...units.values()].sort((a, b) => a.id.localeCompare(b.id)), owner, publicEntry, stylesEntry, tokenDir,
165
+ return { units: [...units.values()].sort((a, b) => a.id.localeCompare(b.id)), owner, publicEntry, reviewEntry, stylesEntry, tokenDir,
158
166
  relative: file => slash(path.relative(dsRoot, path.resolve(root, file))) };
159
167
  }
160
168
 
161
169
  function boundaryCompanion(unit) { return !["configuration", "documentation", "assets"].includes(unit.split("/")[0]); }
162
170
 
163
171
  export function validateUnitId(unit) {
164
- if (typeof unit !== "string" || unit.length > 256 || !/^[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*$/.test(unit) || ["components", "patterns", "controllers", "composition"].includes(unit)) throw fail("Select one ADS unit ID from ads_units or an unoccupied owner path following its colocation convention.");
172
+ if (typeof unit !== "string" || unit.length > 256 || !/^[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*$/.test(unit) || ["components", "patterns", "controllers", "composition", "review"].includes(unit)) throw fail("Select one ADS unit ID from ads_units or an unoccupied owner path following its colocation convention.");
165
173
  }
166
174
 
167
175
  /** Reject arbitrary file batches, even when every path is inside the ADS. */
@@ -171,7 +179,7 @@ export function checkUnitFiles(options, unit, changes, inventory = inspectUnits(
171
179
  const relative = inventory.relative(change.path);
172
180
  if (path.posix.basename(relative) === ".gitkeep") {
173
181
  const directory = path.posix.dirname(relative);
174
- if (change.originalContent !== "" || change.content !== null || !/^(?:components|patterns|controllers|composition)\//.test(relative)
182
+ if (change.originalContent !== "" || change.content !== null || !/^(?:components|patterns|controllers|composition|review)\//.test(relative)
175
183
  || !(unit === directory || unit.startsWith(directory + "/"))
176
184
  || !changes.some(item => item.originalContent === null && item.content !== null && source.test(item.path)
177
185
  && (inventory.relative(item.path).startsWith(unit + "/") || inventory.relative(item.path).startsWith(unit + ".")))) throw fail("Only delete an existing empty .gitkeep while adding source in the selected owner beneath it. Other hidden-file changes are not allowed.", change.path);
@@ -179,7 +187,7 @@ export function checkUnitFiles(options, unit, changes, inventory = inspectUnits(
179
187
  }
180
188
  if (allowNew && unit === "configuration") continue; // The candidate must register every setup/extension file.
181
189
  if (change.path === "ds.config.json" && boundaryCompanion(unit)) continue; // Validated against only this owner below.
182
- if (relative === inventory.publicEntry) continue;
190
+ if (relative === inventory.publicEntry || relative === inventory.reviewEntry) continue;
183
191
  if (unit === "tokens" && relative === inventory.stylesEntry) continue; // Controlled foundation import bookkeeping.
184
192
  if (allowNew && change.originalContent === null && (relative.startsWith(unit + "/") || relative.startsWith(unit + "."))) continue;
185
193
  const owner = inventory.owner(relative);
@@ -232,13 +240,14 @@ export function checkUnitCandidate(afterOptions, unit, changes, before) {
232
240
  }
233
241
  if (JSON.stringify(unrelated(current)) !== JSON.stringify(unrelated(proposed))) throw fail("Foundation companion patches may change only canonical imports for token files created/deleted in this patch; preserve all other imports and comments.", foundationChange.path);
234
242
  }
235
- const exportChange = changes.find(change => before.relative(change.path) === before.publicEntry);
236
- if (exportChange) {
237
- if (!["components", "patterns", "controllers"].includes(unit.split("/")[0])) throw fail("Only a component/pattern/controller unit may update its public re-exports.", exportChange.path);
243
+ for (const entry of [before.publicEntry, before.reviewEntry]) {
244
+ const exportChange = changes.find(change => before.relative(change.path) === entry);
245
+ if (!exportChange) continue;
246
+ if (!(entry === before.reviewEntry ? ["review"] : ["components", "patterns", "controllers"]).includes(unit.split("/")[0])) throw fail("Only its own layer may update re-exports: product owners use the public entry, review owners use the development review entry.", exportChange.path);
238
247
  const current = exportChange.originalContent ?? "", proposed = exportChange.content;
239
248
  if (proposed === null) throw fail("The shared public entry cannot be deleted.", exportChange.path);
240
249
  const resolve = importResolver(afterOptions);
241
- const file = path.join(afterOptions.dsRoot, afterOptions.config.publicEntry);
250
+ const file = path.join(afterOptions.dsRoot, entry);
242
251
  const original = ts.createSourceFile(file, current, ts.ScriptTarget.Latest, true);
243
252
  const candidate = ts.createSourceFile(file, proposed, ts.ScriptTarget.Latest, true);
244
253
  const emptyExport = statement => ts.isExportDeclaration(statement) && !statement.moduleSpecifier && statement.exportClause && ts.isNamedExports(statement.exportClause) && !statement.exportClause.elements.length;
@@ -1,60 +1,190 @@
1
+ // src/verification/browser.mjs
1
2
  import { readFileSync } from "node:fs";
2
3
 
3
- /** Development-only Playwright helper. No browser or framework dependency is installed. */
4
- export function verificationRequest() {
4
+ // preview-client/paint-audit.mjs
5
+ function auditPaintBindings({ container = document.getElementById("root"), bindings = [], auditResources = false } = {}) {
6
+ const diagnostics = [], verified = [], limits = [];
7
+ if (!container || !Array.isArray(bindings) || bindings.length > 100) return { ok: false, diagnostics: ["Invalid bounded paint-audit descriptor."], verified };
8
+ const document2 = container.ownerDocument, view = document2.defaultView;
9
+ const properties = ["color", "fill", "stroke", "stop-color", "background-color", "border-top-color", "border-right-color", "border-bottom-color", "border-left-color"];
10
+ function reference(value, node, descriptor, seen = /* @__PURE__ */ new Set()) {
11
+ value = value.trim();
12
+ if (["none", "currentColor"].includes(value)) return true;
13
+ const variable = /^var\(\s*(--[\w-]+)\s*\)$/.exec(value);
14
+ if (variable) {
15
+ const name = variable[1];
16
+ if (!descriptor.tokens.includes(name) || seen.has(name)) return false;
17
+ const resolved = view.getComputedStyle(node).getPropertyValue(name).trim();
18
+ return !!resolved && view.CSS.supports("color", resolved);
19
+ }
20
+ const resource = /^url\(\s*(["']?)#([^\s"'()#\\\p{Cc}]+)\1\s*\)$/u.exec(value);
21
+ if (resource && view.CSS.supports("fill", value)) {
22
+ const candidates = [...document2.querySelectorAll("[id]")].filter((item) => item.id === resource[2]);
23
+ return candidates.length === 1 && ["linearGradient", "radialGradient", "pattern"].includes(candidates[0].localName) && node.contains(candidates[0]);
24
+ }
25
+ return false;
26
+ }
27
+ for (const descriptor of bindings) {
28
+ if (!descriptor || typeof descriptor.scope !== "string" || !Array.isArray(descriptor.tokens) || typeof descriptor.prefix !== "string" || typeof descriptor.suffix !== "string") {
29
+ diagnostics.push("Invalid paint binding metadata; rebuild previews.");
30
+ continue;
31
+ }
32
+ let roots;
33
+ try {
34
+ roots = [...container.querySelectorAll(descriptor.scope)];
35
+ } catch {
36
+ diagnostics.push(`${descriptor.owner}: invalid binding scope.`);
37
+ continue;
38
+ }
39
+ if (roots.length > 20) limits.push(`${descriptor.owner}: only the first 20 instances were audited; use a focused scenario for complete evidence.`);
40
+ for (const root of roots.slice(0, 20)) {
41
+ const nodes = [root, ...root.querySelectorAll("*")];
42
+ if (nodes.length > 1e3) {
43
+ limits.push(`${descriptor.owner}: instance exceeds 1000 elements; paint effects are not established.`);
44
+ continue;
45
+ }
46
+ const names = [...root.style].filter((name) => name.startsWith(descriptor.prefix) && name.endsWith(descriptor.suffix));
47
+ if (names.length > 100) limits.push(`${descriptor.owner}: only the first 100 binding keys were audited.`);
48
+ for (const name of names.slice(0, 100)) {
49
+ const value = root.style.getPropertyValue(name).trim(), key = name.slice(descriptor.prefix.length, descriptor.suffix ? -descriptor.suffix.length : void 0);
50
+ if (!/^[A-Za-z0-9_-]+$/.test(key) || !reference(value, root, descriptor)) {
51
+ diagnostics.push(`${descriptor.owner}: invalid or inaccessible paint ${name}: ${value}. Use an accessible token, instance-local SVG paint server, none or currentColor.`);
52
+ continue;
53
+ }
54
+ const previous = root.getAttribute("style");
55
+ const nodeStyles = nodes.map((node) => node.getAttribute("style"));
56
+ try {
57
+ for (const node of nodes) node.style.setProperty("transition", "none", "important");
58
+ const before = nodes.map((node) => properties.map((property) => view.getComputedStyle(node).getPropertyValue(property)));
59
+ root.style.setProperty(name, "rgb(17, 103, 211)", "important");
60
+ let changed = nodes.some((node, index) => properties.some((property, i) => view.getComputedStyle(node).getPropertyValue(property) !== before[index][i]));
61
+ if (!changed) {
62
+ root.style.setProperty(name, "rgb(211, 67, 23)", "important");
63
+ changed = nodes.some((node, index) => properties.some((property, i) => view.getComputedStyle(node).getPropertyValue(property) !== before[index][i]));
64
+ }
65
+ if (!changed) limits.push(`${descriptor.owner}: ${name} has no observed paint effect in its current instance. A written variable alone is not rendering evidence; check the relevant selected/visible consumer.`);
66
+ else verified.push({ owner: descriptor.owner, binding: descriptor.name, variable: name });
67
+ } finally {
68
+ nodes.forEach((node, index) => {
69
+ if (nodeStyles[index] === null) node.removeAttribute("style");
70
+ else node.setAttribute("style", nodeStyles[index]);
71
+ });
72
+ if (previous === null) root.removeAttribute("style");
73
+ else root.setAttribute("style", previous);
74
+ }
75
+ }
76
+ for (const node of nodes) for (const property of ["fill", "stroke", "stop-color"]) {
77
+ const value = node.getAttribute(property);
78
+ if (!value) continue;
79
+ const variable = /^var\(\s*(--[\w-]+)\s*\)$/.exec(value);
80
+ if (variable && variable[1].startsWith(descriptor.prefix) && variable[1].endsWith(descriptor.suffix) && !names.includes(variable[1])) diagnostics.push(`${descriptor.owner}: ${variable[1]} is consumed without a value in this instance.`);
81
+ if (value.startsWith("url(") && !reference(value, root, descriptor)) diagnostics.push(`${descriptor.owner}: missing, duplicated or out-of-instance local paint resource ${value}.`);
82
+ }
83
+ }
84
+ }
85
+ const resourceNodes = auditResources ? [...container.querySelectorAll("[fill], [stroke], [style]")] : [];
86
+ if (resourceNodes.length > 1e3) limits.push("Only the first 1000 potential SVG paint consumers were audited.");
87
+ for (const node of resourceNodes.slice(0, 1e3)) for (const property of ["fill", "stroke"]) {
88
+ const value = node.getAttribute(property) ?? node.style.getPropertyValue(property);
89
+ if (!value?.startsWith("url(")) continue;
90
+ const id = /^url\(\s*(["']?)#([^\s"'()#\\\p{Cc}]+)\1\s*\)$/u.exec(value)?.[2];
91
+ const resources = id ? [...document2.querySelectorAll("[id]")].filter((item) => item.id === id) : [];
92
+ const svg = node.ownerSVGElement ?? (node.localName === "svg" ? node : null);
93
+ if (resources.length !== 1 || !svg?.contains(resources[0]) || !["linearGradient", "radialGradient", "pattern"].includes(resources[0]?.localName)) diagnostics.push(`Invalid, missing, duplicated or nonlocal SVG paint resource ${value}. Keep IDs isolated per rendered instance.`);
94
+ }
95
+ return { ok: !diagnostics.length, complete: !limits.length, verified, diagnostics: diagnostics.slice(0, 40), limits: limits.slice(0, 20), scope: "observed App instances and current input; repeat after updates" };
96
+ }
97
+
98
+ // src/verification/browser.mjs
99
+ async function auditBehaviorPaints(frame) {
100
+ const bindings = await frame.locator("#root").getAttribute("data-ads-paint-bindings");
101
+ const auditResources = await frame.locator("#root").getAttribute("data-ads-paint-audit");
102
+ return frame.evaluate(auditPaintBindings, { bindings: JSON.parse(bindings ?? "[]"), auditResources: auditResources === "true" });
103
+ }
104
+ function verificationRequest() {
5
105
  if (!process.env.ADS_VERIFY_REQUEST) throw new Error("Run this check through ads_verify so the request and selected profile are explicit.");
6
106
  return JSON.parse(readFileSync(process.env.ADS_VERIFY_REQUEST, "utf8"));
7
107
  }
8
-
9
- export async function openBehaviorScenario(page, checkId, { timeout = 30000 } = {}) {
108
+ async function openBehaviorScenario(page, checkId, { timeout = 3e4 } = {}) {
10
109
  const request = verificationRequest();
11
- const check = request.checks.find(check => check.id === checkId);
110
+ const check = request.checks.find((check2) => check2.id === checkId);
12
111
  const url = check && request.scenarios[check.scenario];
13
112
  if (!url) throw new Error("This check has no managed browser scenario. StrictMode profiles must supply a project development harness; production preview bundles do not replay React development effects.");
14
113
  return openPreviewScenario(page, url, check.scenario, { timeout });
15
114
  }
16
-
17
- /** Shared readiness for registered verification and on-demand captures. */
18
- export async function openPreviewScenario(page, url, scenario, { timeout = 30000 } = {}) {
115
+ async function openPreviewScenario(page, url, scenario, { timeout = 3e4 } = {}) {
19
116
  const failures = [], requests = [], errors = [];
20
117
  let boundary;
21
118
  const record = (request, detail) => {
22
119
  if (requests.length >= 8) return;
23
120
  let resourceUri;
24
- try { const payload=request.postDataJSON(); if(payload?.method==="resources/read"&&typeof payload.params?.uri==="string"&&payload.params.uri.startsWith("ads://"))resourceUri=payload.params.uri.slice(0,500); } catch { /* Bodies are never retained. */ }
25
- requests.push({method:request.method(),url:request.url().slice(0,300),...detail,...(resourceUri?{resourceUri}:{})});
121
+ try {
122
+ const payload = request.postDataJSON();
123
+ if (payload?.method === "resources/read" && typeof payload.params?.uri === "string" && payload.params.uri.startsWith("ads://")) resourceUri = payload.params.uri.slice(0, 500);
124
+ } catch {
125
+ }
126
+ requests.push({ method: request.method(), url: request.url().slice(0, 300), ...detail, ...resourceUri ? { resourceUri } : {} });
26
127
  };
27
- const failed = request => {
128
+ const failed = (request) => {
28
129
  if (failures.length < 5) failures.push(`${request.method()} ${request.url().slice(0, 300)}: ${request.failure()?.errorText ?? "request failed"}`);
29
- record(request,{error:request.failure()?.errorText??"request failed"});
130
+ record(request, { error: request.failure()?.errorText ?? "request failed" });
131
+ };
132
+ const response = (response2) => {
133
+ if (response2.status() >= 400) record(response2.request(), { status: response2.status() });
134
+ };
135
+ const pageError = (error) => {
136
+ if (errors.length < 3) errors.push(error.message.slice(0, 500));
30
137
  };
31
- const response = response => { if(response.status()>=400)record(response.request(),{status:response.status()}); };
32
- const pageError = error => { if(errors.length<3)errors.push(error.message.slice(0,500)); };
33
- const diagnostic = (outcome, detail={}) => console.warn(JSON.stringify({adsPreviewDiagnostic:{kind:"preview-startup",scenario,outcome,viewport:page.viewportSize(),requests,errors,...detail}}));
138
+ const diagnostic = (outcome, detail = {}) => console.warn(JSON.stringify({ adsPreviewDiagnostic: { kind: "preview-startup", scenario, outcome, viewport: page.viewportSize(), requests, errors, ...detail } }));
34
139
  page.on("requestfailed", failed);
35
- page.on("response", response);page.on("pageerror", pageError);
140
+ page.on("response", response);
141
+ page.on("pageerror", pageError);
36
142
  try {
37
143
  await page.goto(url, { waitUntil: "load", timeout });
38
144
  const end = Date.now() + timeout;
39
145
  while (Date.now() < end) {
40
- for (const frame of page.frames().filter(frame => frame !== page.mainFrame())) {
41
- const state = await frame.evaluate(() => {const data=document.documentElement.dataset;return {state:data.adsPreviewState,id:data.adsScenario,revision:data.adsPreviewRevision,category:data.adsPreviewErrorCategory,code:data.adsPreviewErrorCode,resourceUri:data.adsPreviewResourceUri,message:document.getElementById("ads-preview-status")?.textContent};}).catch(() => ({}));
146
+ for (const frame of page.frames().filter((frame2) => frame2 !== page.mainFrame())) {
147
+ const state = await frame.evaluate(() => {
148
+ const data = document.documentElement.dataset;
149
+ return { state: data.adsPreviewState, id: data.adsScenario, revision: data.adsPreviewRevision, category: data.adsPreviewErrorCategory, code: data.adsPreviewErrorCode, resourceUri: data.adsPreviewResourceUri, message: document.getElementById("ads-preview-status")?.textContent };
150
+ }).catch(() => ({}));
42
151
  if (state.id === scenario && state.state === "ready") {
43
152
  const retries = await page.evaluate(() => Number(document.documentElement.dataset.adsResourceRetries ?? 0)).catch(() => 0);
44
153
  if (retries > 0) console.warn(JSON.stringify({ adsPreviewDiagnostic: { kind: "resource-read-recovery", scenario, retries, failedRequests: failures } }));
45
- diagnostic("ready",{previewRevision:state.revision??null});
154
+ diagnostic("ready", { previewRevision: state.revision ?? null });
46
155
  return frame;
47
156
  }
48
- if (state.id === scenario && state.state === "error") {boundary={category:state.category??"runner",phase:"preview-startup",code:state.code??"ADS_PREVIEW_STARTUP",...(state.resourceUri?{resourceUri:state.resourceUri}:{})};throw new Error(state.message || "The MCP App failed before verification.");}
157
+ if (state.id === scenario && state.state === "error") {
158
+ boundary = { category: state.category ?? "runner", phase: "preview-startup", code: state.code ?? "ADS_PREVIEW_STARTUP", ...state.resourceUri ? { resourceUri: state.resourceUri } : {} };
159
+ throw new Error(state.message || "The MCP App failed before verification.");
160
+ }
161
+ }
162
+ const error = await page.evaluate(() => {
163
+ const node = document.querySelector('#status[data-error="true"]');
164
+ return node ? { message: node.textContent, category: node.dataset.errorCategory, code: node.dataset.errorCode } : null;
165
+ }).catch(() => null);
166
+ if (error) {
167
+ boundary = { category: error.category ?? (requests.length ? "transport" : "runner"), phase: "host-startup", code: error.code ?? (requests.length ? "ADS_BROWSER_REQUEST" : "ADS_HOST_STARTUP") };
168
+ throw new Error(error.message);
49
169
  }
50
- const error = await page.evaluate(() => {const node=document.querySelector('#status[data-error="true"]');return node?{message:node.textContent,category:node.dataset.errorCategory,code:node.dataset.errorCode}:null;}).catch(() => null);
51
- if (error) {boundary={category:error.category??(requests.length?"transport":"runner"),phase:"host-startup",code:error.code??(requests.length?"ADS_BROWSER_REQUEST":"ADS_HOST_STARTUP")};throw new Error(error.message);}
52
170
  await page.waitForTimeout(50);
53
171
  }
54
172
  throw new Error("Timed out waiting for the selected MCP App scenario to become current and ready. Inspect ads_status and ads://project/build.");
55
173
  } catch (error) {
56
- boundary??={category:requests.length?"transport":"runner",phase:"preview-startup",code:requests.length?"ADS_BROWSER_REQUEST":"ADS_PREVIEW_NOT_READY",certainty:requests.length?"observed-boundary":"unclassified"};
57
- diagnostic("failed",{failure:boundary});
58
- throw Object.assign(new Error(`Scenario ${scenario}: ${error.message}${failures.length ? `\nFailed browser requests:\n${failures.join("\n")}` : ""}`, { cause: error }),{failure:boundary});
59
- } finally { page.off("requestfailed", failed);page.off("response",response);page.off("pageerror",pageError); }
174
+ boundary ??= { category: requests.length ? "transport" : "runner", phase: "preview-startup", code: requests.length ? "ADS_BROWSER_REQUEST" : "ADS_PREVIEW_NOT_READY", certainty: requests.length ? "observed-boundary" : "unclassified" };
175
+ diagnostic("failed", { failure: boundary });
176
+ throw Object.assign(new Error(`Scenario ${scenario}: ${error.message}${failures.length ? `
177
+ Failed browser requests:
178
+ ${failures.join("\n")}` : ""}`, { cause: error }), { failure: boundary });
179
+ } finally {
180
+ page.off("requestfailed", failed);
181
+ page.off("response", response);
182
+ page.off("pageerror", pageError);
183
+ }
60
184
  }
185
+ export {
186
+ auditBehaviorPaints,
187
+ openBehaviorScenario,
188
+ openPreviewScenario,
189
+ verificationRequest
190
+ };
@@ -1,5 +1,7 @@
1
1
  const { readFileSync, writeFileSync, renameSync, rmSync } = require("node:fs");
2
2
  const path = require("node:path");
3
+ const { stripVTControlCharacters } = require("node:util");
4
+ const errorText=error=>stripVTControlCharacters(String(error?.message||error?.stack||error?.value||"")).trim().slice(0,1000);
3
5
 
4
6
  /** Case identity is an explicit project test tag: @ads:<case-id>. */
5
7
  module.exports = class AdsBehaviorReporter {
@@ -29,13 +31,21 @@ module.exports = class AdsBehaviorReporter {
29
31
  return {file,line:value.line,column:value.column};
30
32
  }
31
33
  onStepEnd(test, _result, step) {
32
- if (!step.error || this.steps.has(test.id)) return;
33
- this.steps.set(test.id,{title:String(step.title).slice(0,300),category:step.category,location:this.location(step.error.location??step.location),durationMs:step.duration});
34
+ if (!step.error) return;
35
+ const steps=this.steps.get(test.id)??[];
36
+ steps.push({errorText:errorText(step.error),step:{title:String(step.title).slice(0,300),category:step.category,location:this.location(step.error.location??step.location),durationMs:step.duration}});
37
+ this.steps.set(test.id,steps.slice(-20));
34
38
  }
35
39
  onTestEnd(test, result) {
36
40
  this.active.delete(test.id);
37
41
  const diagnostics = this.diagnostics.get(test.id);
38
- const step=this.steps.get(test.id),startup=diagnostics?.findLast(item=>item.kind==="preview-startup"),failed=result.status!=="passed"||test.expectedStatus!=="passed";
42
+ const errors=(result.errors??[]).slice(0,5).map(errorText);
43
+ const matching=(this.steps.get(test.id)??[]).filter(item=>item.errorText&&errors.some(error=>error===item.errorText||error.length>20&&item.errorText.length>20&&(error.includes(item.errorText)||item.errorText.includes(error))));
44
+ // Nested API steps may end before their enclosing expect. Match the actual
45
+ // unhandled case error, then retain its assertion boundary independent of
46
+ // event order. Earlier caught errors are not the failure's source.
47
+ const step=(matching.findLast(item=>item.step.category==="expect")??matching[0])?.step;
48
+ const startup=diagnostics?.findLast(item=>item.kind==="preview-startup"),failed=result.status!=="passed"||test.expectedStatus!=="passed";
39
49
  const failure=failed?(startup?.failure??{category:step?.category==="expect"?"assertion":"runner",phase:step?.category==="expect"?"test-assertion":"test-execution",code:result.status==="timedOut"?"ADS_CASE_TIMEOUT":"ADS_CASE_FAILURE",certainty:step?.category==="expect"?"observed-boundary":"unclassified"}):undefined;
40
50
  const artifacts=(result.attachments??[]).filter(item=>item.path).slice(0,5).map(item=>({name:String(item.name).slice(0,100),mimeType:String(item.contentType).slice(0,100),path:path.relative(path.dirname(process.env.ADS_VERIFY_REPORT),item.path).replaceAll("\\","/")}));
41
51
  for (const entry of this.entries(test)) this.cases.push({ ...entry, status: !failed ? "passed" : ["skipped", "interrupted"].includes(result.status) ? "skipped" : "failed", durationMs: result.duration, workerIndex: result.workerIndex, retry: result.retry, completedAt: new Date().toISOString(), source:step?.location??this.location(result.errors?.[0]?.location??test.location), ...(step?{failingStep:step}:{}),...(failure?{failure}:{}),...(startup?.viewport?{viewport:startup.viewport,viewportSource:"preview-startup"}:{}),...(artifacts.length?{artifacts}:{}), ...(diagnostics?.length ? { diagnostics } : {}), message: result.errors?.map(error => error.message || error.stack || error.value || "The browser runner reported an error without details.").join("\n").slice(0, 1000) });
package/docs/ADS.md CHANGED
@@ -95,7 +95,8 @@ guarded public re-exports. They may use the configured framework and declared
95
95
  dependencies, plain TypeScript and browser APIs; CSS/Linaria and visual DOM belong
96
96
  to consumers. They require no artificial visual scenario.
97
97
 
98
- Optional colocated `behavior.json` (flat: `<Owner>.behavior.json`) registers
98
+ Optional `<Owner>.ads.contract.ts` registers behavior with actual symbol references.
99
+ Legacy colocated `behavior.json` (flat: `<Owner>.behavior.json`) also registers
99
100
  subjects, guarantees and owner-local checks against project-configured runner
100
101
  profiles. `exercises` records public behavioral APIs demonstrated by a scenario;
101
102
  `covers` retains its rendered UI contract. ADS validates references and reachable
@@ -114,7 +115,7 @@ are generated development evidence, outside source ownership and product bundles
114
115
  See the portable [capture workflow](../skills/ads-designer/references/captures.md).
115
116
  Token entries under the configured token directory declare
116
117
  `export const scenario = { title: fixture.title, covers: ["TokenSamples"], samples: fixture.samples };`.
117
- `TokenSamples` is any project-owned public review pattern, not a required name.
118
+ `TokenSamples` is any project-owned development-only review renderer, not a required name.
118
119
  Its colocated fixture declares literal `samples: [{ token, property, probe }]`.
119
120
  Each token must exist and have sample coverage; duplicate bindings, missing probes,
120
121
  empty covers and value-only inspectors fail. The default entry passes the declared
@@ -184,10 +185,29 @@ instead of assigning shared tokens in JavaScript. Full application harnesses
184
185
  belong in application test support. Run ADS/project checks before application,
185
186
  preview builds; review meaningful states in both themes and sizes.
186
187
 
188
+ ## Development review layer
189
+
190
+ `review/` contains demo framing, demonstration controls and token renderers.
191
+ It has a separate `reviewEntry` (default `review/index.ts`), imported through
192
+ `${publicAlias}/review` (`@ds/review` by default). Product patterns are production
193
+ application UI. The public production barrel and product catalogue exclude review
194
+ exports; production code cannot depend on their implementation, types or styles.
195
+ Review rendering stays inside the App and retains the normal token/style and
196
+ public product-contract gates. See the portable
197
+ [review guide](../skills/ads-designer/references/review.md) for scenarios, guarded
198
+ owners, build aliases and migration. `ads_status.project.review` discovers the
199
+ connected project's actual entry. Older tooling must be upgraded before migration.
200
+
187
201
  ## Shared token contract
188
202
 
189
203
  ### Owner-local tokens
190
204
 
205
+ New owner declarations use `tokens: { namespace, stylesheet }` in
206
+ `<Owner>.ads.contract.ts`. The legacy JSON layout below remains supported.
207
+ Use one authoring form per section; competing registrations fail. See the
208
+ [static contract reference](CONTRACTS.md) for paint bindings, declarative APIs,
209
+ marked development files and guarded migration.
210
+
191
211
  Shared semantics remain in the configured token foundation. Constants specific
192
212
  to one component or pattern may live beside it:
193
213
 
@@ -198,7 +218,7 @@ components/Notice/
198
218
  tokens.css # :root { --notice-width: 356px; }
199
219
  Scenario.tsx
200
220
  fixture.ts
201
- Tokens.scenario.tsx # kind: "tokens", public review pattern + samples
221
+ Tokens.scenario.tsx # kind: "tokens", development-only review renderer + samples
202
222
  Tokens.fixture.ts
203
223
  ```
204
224
 
@@ -216,7 +236,7 @@ Measured runtime variables retain their existing exact `styleWrites` boundary.
216
236
  `ads_tokens` returns each token's owner, definitions and local namespace.
217
237
 
218
238
  A colocated scenario sets `kind:"tokens"` and declares real fixture `samples`
219
- consumed by a public review pattern. Its ID still derives from its source path.
239
+ consumed by a development-only review renderer. Its ID still derives from its source path.
220
240
  Discovery attaches its owner's registered definition sheet as `stylesheets` on
221
241
  the generated scenario record; `previewEntriesPlugin` imports it directly into
222
242
  that preview's module graph. CSS-only package side effects can then tree-shake
@@ -294,7 +314,7 @@ layout, component-specific focus rules and hover/disabled skins are rejected.
294
314
  Shared token definitions belong in value sheets, not `base.css`. No project token
295
315
  names are embedded in this policy. A zero-specificity `:where()` wrapper does not
296
316
  change ownership. Put component design and animation in Linaria owners, and
297
- scenario layout variations in typed public review-pattern props. Browser review
317
+ scenario layout variations in typed review-helper props. Browser review
298
318
  still checks the effects and accessibility of permitted defaults.
299
319
 
300
320
 
@@ -394,6 +414,8 @@ The checks enforce the architecture's ownership boundaries:
394
414
  |----------|------------------|
395
415
  | Tokens and styles | Fixed token vocabulary, required definitions, declared value types and references, raw visual values, shared-token assignments, foundation import order, and component/pattern namespaces |
396
416
  | Public API and dependencies | Consumers use public ADS imports; ADS implementations and showcase do not import application source; runtime code does not import showcase fixtures |
417
+ | Component and pattern layers | Component runtime cannot depend on patterns/composition, including through resolved aliases, re-exports and helpers; patterns consume public component symbols, not implementation helpers/types |
418
+ | Component styling contracts | Patterns cannot target proven private component classes/data markers or read known private component variables; public hooks and registered non-state customization variables remain available; ancestor context for a pattern's own styles is allowed |
397
419
  | Interaction policy | Primitive state variables stay inside components; non-state customization hooks require explicit registration |
398
420
  | Renderer exceptions | Inline styles and imperative writes require used, narrow configured boundaries; stale entries fail; public props omit inherited style and reject open-ended visual strings |
399
421
  | Preview ownership | Scenarios define no CSS, custom classes, or standalone native controls; native slot children require public runtime cloning evidence; all declared tokens and public renderable UI have reachable registered scenarios; token references must exist; legacy catalogue pages are checked when present |
@@ -418,6 +440,18 @@ tests. Passing static checks establishes only the policies and source formats
418
440
  they cover. Browser review and behavior tests still verify accessibility,
419
441
  interaction, responsive layout, themes, and motion.
420
442
 
443
+ A component provides one cohesive UI capability; a pattern arranges capabilities
444
+ into a reusable structure or workflow. Component count is not a rule. Components
445
+ may compose components/controllers; layout patterns can arrange content slots;
446
+ component scenarios may use development-only review helpers. Classification is design
447
+ guidance, while `component-dependency`, `component-contract` and `component-style`
448
+ are blocking source-ownership rules. Type-only higher-layer references are not
449
+ runtime edges. Relative component imports inside the ADS are valid when the
450
+ selected symbols are public; a self-import through the root barrel is not required.
451
+ Opaque/computed dependencies and ambiguous/generated style hooks require review.
452
+ See the skill-local [layer guide](../skills/ads-designer/references/layers.md), also
453
+ available through MCP at `ads://guides/layers`.
454
+
421
455
  Component-specific usage rules belong to that component in the receiving
422
456
  project. Import applicable rules with the item and register them through
423
457
  project configuration or maintained tooling integration. Core tooling must
@@ -448,7 +482,7 @@ Root [bootstrap-ds/](../bootstrap-ds/README.md) is the canonical empty scaffold.
448
482
  Its identical copy under `skills/ads-designer/assets/bootstrap-ds/` makes the
449
483
  skill self-contained. Run `npm run sync:skill-starter` after edits and
450
484
  `npm run check:skill-starter` to verify the copy. Both contain empty product component/composition directories and a minimal project-owned
451
- token review pattern. The scaffold pattern can be renamed or replaced; the tool
485
+ token review renderer. The scaffold pattern can be renamed or replaced; the tool
452
486
  does not require that name or its styles and ships no product UI catalogue.
453
487
 
454
488
  Tool releases should version contract changes, rules, skills, bootstrap support,
@@ -67,6 +67,32 @@ state mechanics. Patterns own reusable layout and composition. Application
67
67
  containers provide domain data, routing, application effects, and callbacks through public
68
68
  APIs.
69
69
 
70
+ A component provides one cohesive UI capability. A pattern arranges capabilities
71
+ into a reusable structure or workflow; a layout part may arrange content slots.
72
+ Components may compose other components/controllers, and patterns may compose
73
+ patterns. Import count, element count and implementation size do not decide the
74
+ layer. Scenarios may compose public UI and development review helpers to demonstrate their
75
+ subject without becoming production patterns.
76
+
77
+ Patterns are production application UI. Development-only framing, demo controls
78
+ and token renderers belong in project-owned `review/`, with a separate
79
+ `reviewEntry` (default `review/index.ts`) and `${publicAlias}/review` import alias.
80
+ They are absent from the product barrel and catalogue. Guards reject production
81
+ dependencies on review code, types and styles; review source keeps normal styling,
82
+ token and public product-contract checks. Scenarios exercise the real production
83
+ implementation through fixtures. The [portable review guide](../skills/ads-designer/references/review.md)
84
+ defines guarded owners, build isolation and migration steps. Review rendering
85
+ stays inside the App iframe; it adds no project execution to the host shell.
86
+
87
+ Core guards reject component runtime dependencies on patterns/composition through
88
+ resolved imports, re-exports and helpers. Patterns consume public component
89
+ contracts rather than private implementation symbols or rendered style hooks;
90
+ documented non-state customization variables remain available. Source tracing
91
+ distinguishes styling a private part from reading ancestor context for the
92
+ pattern's own arrangement. Application and development dependencies retain their
93
+ separate boundaries. The portable skill-local [layer guide](../skills/ads-designer/references/layers.md)
94
+ explains repairs and analysis limits; semantic classification still needs review.
95
+
70
96
  The Data pillar describes design values; business data is a separate input to
71
97
  the UX contract. Application adapters own live business-data loading,
72
98
  authentication, stores, routing, caching and refresh policy. Components and
@@ -233,6 +259,13 @@ external bases still require declared CSP origins. This belongs to document
233
259
  loading and changes no project link or scenario handler. External hosts control
234
260
  their own document-loading and CSP behavior.
235
261
 
262
+ The proxy and App sandbox permit native forms (`allow-forms`), including the
263
+ legacy nested widget. This lets browser constraint validation and `submit`
264
+ handlers run for clicks, Enter and `requestSubmit()`. Client-side handlers retain
265
+ their own `preventDefault()` behavior. The App stays opaque; CSP
266
+ `form-action 'none'` blocks native action requests without suppressing submit
267
+ handlers. Hosts outside ADS choose their own sandbox and CSP permissions.
268
+
236
269
  The project compiler publishes a versioned dependency graph: shared runtime and
237
270
  foundation assets, plus independently addressable scenario modules. Inside the
238
271
  App, a resource loader obtains the selected dependency closure through MCP,
@@ -302,12 +335,12 @@ real public implementation. It mounts independently, without opening a showcase
302
335
  page or initializing the application's router, store or services first.
303
336
 
304
337
  Scenarios and their fixtures live beside the tokens, component or pattern they exercise.
305
- Demo framing belongs to public review/layout patterns; component props describe
338
+ Demo framing belongs to development-only review helpers; component props describe
306
339
  intentional product behavior and visual variants.
307
340
  Token scenarios choose real token names and sample inputs; a project-owned
308
- review pattern applies the foundation variables. The tool contains no project
341
+ review renderer applies the foundation variables. The tool contains no project
309
342
  token list, sample templates or assumed visual identity. Even an initial token
310
- scenario uses a public project-owned review pattern to apply its declared samples;
343
+ scenario uses a project-owned development review renderer to apply its declared samples;
311
344
  a definitions-only inspector does not establish preview coverage.
312
345
  Larger scenarios live under `composition/`. They use the same public screen
313
346
  patterns as production: application adapters supply live data and effects;
@@ -538,7 +571,7 @@ For concrete instructions, use:
538
571
 
539
572
  Token preview completeness has two checks. Static guards require explicit fixture
540
573
  bindings (`token`, CSS `property`, and project-selected `probe`) consumed by a public
541
- review pattern. The App then verifies rendered samples in the actual browser by
574
+ review renderer. The App then verifies rendered samples in the actual browser by
542
575
  probing those bindings and restoring them. The tool supplies no token/property
543
576
  classification or visual sample palette. A source build alone is not a rendered
544
577
  verification, and the browser check does not judge design quality.
@@ -551,3 +584,39 @@ sheet directly, so tree-shaking an unused runtime owner cannot remove their valu
551
584
  Namespaces, aliases, ownership and actual colocated sample coverage are checked.
552
585
  The shared foundation remains the cross-owner vocabulary. See the
553
586
  [owner-token contract](ADS.md#owner-local-tokens).
587
+
588
+ ## Static contracts and instance data
589
+
590
+ Owner-local tooling declarations use optional `<Owner>.ads.contract.ts` modules.
591
+ ADS resolves their data and implementation symbols statically without executing
592
+ project imports. Tokens and behavior share this contract with constrained paint
593
+ inputs, instance binding families and declarative parent/consumer relationships.
594
+ Legacy JSON sections remain supported; duplicate policies fail. The
595
+ [contract reference](CONTRACTS.md) specifies the authoring and analysis limits.
596
+
597
+ Any `.ads.` source is development-only. Runtime implementation/types/styles cannot
598
+ import it through aliases or barrels. Only the selected scenario/fixture/review
599
+ closure enters a preview; contracts and specs are not widget modules. Derived
600
+ App audit data adds no ADS dependency to production components. New starter
601
+ scenarios and fixtures use the marked names; existing IDs remain stable.
602
+
603
+ Instance bindings are distinct from static tokens and arbitrary inline styles.
604
+ Source checks trace config origin, key validation, exact map construction and
605
+ attachment to an owner root. Shared/private namespaces and consumer permissions
606
+ remain enforced. Browser audits inspect current scoped values, paint effects and
607
+ local SVG resources; registered checks must cover updates/removal, isolation and
608
+ relevant themes. Registration, source-demonstrated consumption and current browser
609
+ verification are separate evidence. Unknown paths establish no blanket permission.
610
+
611
+ Declarative child APIs belong in `exercises`, while `covers` retains rendered UI.
612
+ Resolved child identity/prop consumption must reach the declared consumer through
613
+ a rendered parent. Metadata alone cannot turn a null-returning stub into verified
614
+ UI. Invalid scenario fields preserve independent valid coverage; unreadable
615
+ prerequisites report that coverage is not established.
616
+
617
+ Owner tokens are private across CSS and supported TypeScript value flows, including
618
+ constants, re-exports, helper returns and paint/property sinks. Normal component
619
+ composition remains valid. Namespaces are a source boundary, not CSS isolation.
620
+ Ownership does not mandate a visual scale: projects may register explicit origin
621
+ policies using `inspectTokenOrigins`, without ADS hardcoding spacing meanings.
622
+ Migration prepares ordinary owner drafts; candidate gates and rollback still apply.