@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.
- package/README.md +2 -0
- package/dist/behaviors.mjs +19 -9
- package/dist/bootstrap-ds/ADS.md +2 -2
- package/dist/bootstrap-ds/README.md +2 -2
- package/dist/bootstrap-ds/design-system/index.ts +0 -1
- package/dist/bootstrap-ds/design-system/review/index.ts +1 -0
- package/dist/bootstrap-ds/design-system/tokens/{Foundation.scenario.tsx → Foundation.ads.scenario.tsx} +2 -2
- package/dist/bootstrap-ds/ds.config.json +1 -1
- package/dist/bootstrap-ds/tsconfig.build.json +9 -2
- package/dist/bootstrap-ds/tsconfig.json +18 -3
- package/dist/bootstrap-ds/vite.config.ts +1 -1
- package/dist/boundaries.mjs +6 -2
- package/dist/check.mjs +44 -17
- package/dist/config.mjs +33 -5
- package/dist/contract-migration.mjs +110 -0
- package/dist/contracts.d.mts +22 -0
- package/dist/evidence.mjs +1 -1
- package/dist/index.mjs +1 -0
- package/dist/init.mjs +18 -15
- package/dist/layers.mjs +288 -0
- package/dist/lint-config.mjs +1 -1
- package/dist/mcp/catalog.mjs +9 -2
- package/dist/mcp/context.mjs +2 -1
- package/dist/mcp/edit-preparation.mjs +9 -6
- package/dist/mcp/mutations.mjs +1 -1
- package/dist/mcp/preview-widget.bundle.js +41 -39
- package/dist/mcp/prompts.mjs +1 -1
- package/dist/mcp/server.mjs +9 -3
- package/dist/mcp/widget.bundle.js +1 -1
- package/dist/oxlint.mjs +3 -1
- package/dist/paint-contracts.mjs +124 -0
- package/dist/preview-resources.mjs +12 -2
- package/dist/public-api.mjs +13 -6
- package/dist/public-props.mjs +2 -2
- package/dist/runtime-contracts.mjs +225 -0
- package/dist/scenario-trace.mjs +37 -9
- package/dist/scenarios-vite.mjs +3 -2
- package/dist/scenarios.mjs +89 -30
- package/dist/scoped-tokens.mjs +9 -4
- package/dist/showcase/client.bundle.js +1 -1
- package/dist/showcase/sandbox.bundle.js +1 -1
- package/dist/skills/ads-break-ui/SKILL.md +25 -6
- package/dist/skills/ads-break-ui/references/contracts.md +250 -0
- package/dist/skills/ads-break-ui/references/guarded-editing.md +1 -1
- package/dist/skills/ads-break-ui/references/layers.md +123 -0
- package/dist/skills/ads-break-ui/references/review.md +96 -0
- package/dist/skills/ads-break-ui/references/scenarios.md +24 -3
- package/dist/skills/ads-designer/SKILL.md +26 -7
- package/dist/skills/ads-designer/assets/bootstrap-ds/ADS.md +2 -2
- package/dist/skills/ads-designer/assets/bootstrap-ds/README.md +2 -2
- package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/index.ts +0 -1
- package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/review/index.ts +1 -0
- package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/{Foundation.scenario.tsx → Foundation.ads.scenario.tsx} +2 -2
- package/dist/skills/ads-designer/assets/bootstrap-ds/ds.config.json +1 -1
- package/dist/skills/ads-designer/assets/bootstrap-ds/tsconfig.build.json +9 -2
- package/dist/skills/ads-designer/assets/bootstrap-ds/tsconfig.json +18 -3
- package/dist/skills/ads-designer/assets/bootstrap-ds/vite.config.ts +1 -1
- package/dist/skills/ads-designer/references/contracts.md +250 -0
- package/dist/skills/ads-designer/references/guarded-editing.md +1 -1
- package/dist/skills/ads-designer/references/layers.md +123 -0
- package/dist/skills/ads-designer/references/review.md +96 -0
- package/dist/skills/ads-designer/references/scenarios.md +24 -3
- package/dist/skills/ads-import/SKILL.md +25 -6
- package/dist/skills/ads-import/references/contracts.md +250 -0
- package/dist/skills/ads-import/references/guarded-editing.md +1 -1
- package/dist/skills/ads-import/references/layers.md +123 -0
- package/dist/skills/ads-import/references/review.md +96 -0
- package/dist/skills/ads-import/references/scenarios.md +24 -3
- package/dist/skills/ads-migrate/SKILL.md +25 -6
- package/dist/skills/ads-migrate/references/contracts.md +250 -0
- package/dist/skills/ads-migrate/references/guarded-editing.md +1 -1
- package/dist/skills/ads-migrate/references/layers.md +123 -0
- package/dist/skills/ads-migrate/references/review.md +96 -0
- package/dist/skills/ads-migrate/references/scenarios.md +24 -3
- package/dist/skills/ads-motion/SKILL.md +26 -7
- package/dist/skills/ads-motion/references/audit.md +1 -1
- package/dist/skills/ads-motion/references/contracts.md +250 -0
- package/dist/skills/ads-motion/references/guarded-editing.md +1 -1
- package/dist/skills/ads-motion/references/layers.md +123 -0
- package/dist/skills/ads-motion/references/motion-review.md +35 -0
- package/dist/skills/ads-motion/references/review.md +96 -35
- package/dist/skills/ads-motion/references/scenarios.md +24 -3
- package/dist/token-origins.mjs +24 -0
- package/dist/tokens.mjs +4 -3
- package/dist/unit-contracts.mjs +203 -0
- package/dist/units.mjs +26 -17
- package/dist/verification/browser.mjs +156 -26
- package/dist/verification/playwright-reporter.cjs +13 -3
- package/docs/ADS.md +40 -6
- package/docs/ARCHITECTURE.md +73 -4
- package/docs/BEHAVIORS.md +15 -0
- package/docs/CHANGELOG.md +42 -0
- package/docs/CONTRACTS.md +250 -0
- package/docs/GUIDE.md +29 -3
- package/docs/MCP.md +42 -6
- package/package.json +4 -1
- package/dist/bootstrap-ds/design-system/patterns/token-preview/Scenario.tsx +0 -4
- package/dist/bootstrap-ds/design-system/patterns/token-preview/fixture.ts +0 -8
- package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/patterns/token-preview/Scenario.tsx +0 -4
- package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/patterns/token-preview/fixture.ts +0 -8
- /package/dist/bootstrap-ds/design-system/{patterns → review}/token-preview/TokenPreview.tsx +0 -0
- /package/dist/bootstrap-ds/design-system/tokens/{Foundation.fixture.ts → Foundation.ads.fixture.ts} +0 -0
- /package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/{patterns → review}/token-preview/TokenPreview.tsx +0 -0
- /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
|
|
236
|
-
|
|
237
|
-
if (!
|
|
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,
|
|
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
|
-
|
|
4
|
-
|
|
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(
|
|
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 {
|
|
25
|
-
|
|
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
|
|
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);
|
|
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(
|
|
41
|
-
const state = await frame.evaluate(() => {
|
|
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") {
|
|
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 ?
|
|
59
|
-
|
|
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
|
|
33
|
-
this.steps.
|
|
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
|
|
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
|
|
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
|
|
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",
|
|
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
|
|
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
|
|
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
|
|
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,
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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.
|