@reqvire-org/ads 0.3.20 → 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 (55) hide show
  1. package/README.md +2 -0
  2. package/dist/behaviors.mjs +14 -4
  3. package/dist/bootstrap-ds/README.md +1 -1
  4. package/dist/bootstrap-ds/design-system/tokens/{Foundation.scenario.tsx → Foundation.ads.scenario.tsx} +1 -1
  5. package/dist/bootstrap-ds/tsconfig.build.json +9 -2
  6. package/dist/boundaries.mjs +2 -0
  7. package/dist/check.mjs +25 -5
  8. package/dist/config.mjs +21 -4
  9. package/dist/contract-migration.mjs +110 -0
  10. package/dist/contracts.d.mts +22 -0
  11. package/dist/evidence.mjs +1 -1
  12. package/dist/index.mjs +1 -0
  13. package/dist/init.mjs +1 -1
  14. package/dist/mcp/catalog.mjs +9 -2
  15. package/dist/mcp/context.mjs +1 -1
  16. package/dist/mcp/edit-preparation.mjs +6 -3
  17. package/dist/mcp/preview-widget.bundle.js +41 -39
  18. package/dist/mcp/server.mjs +3 -1
  19. package/dist/paint-contracts.mjs +124 -0
  20. package/dist/preview-resources.mjs +1 -1
  21. package/dist/public-props.mjs +2 -2
  22. package/dist/runtime-contracts.mjs +225 -0
  23. package/dist/scenario-trace.mjs +32 -4
  24. package/dist/scenarios-vite.mjs +1 -1
  25. package/dist/scenarios.mjs +82 -26
  26. package/dist/scoped-tokens.mjs +9 -4
  27. package/dist/skills/ads-break-ui/SKILL.md +4 -0
  28. package/dist/skills/ads-break-ui/references/contracts.md +250 -0
  29. package/dist/skills/ads-designer/SKILL.md +4 -0
  30. package/dist/skills/ads-designer/assets/bootstrap-ds/README.md +1 -1
  31. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/{Foundation.scenario.tsx → Foundation.ads.scenario.tsx} +1 -1
  32. package/dist/skills/ads-designer/assets/bootstrap-ds/tsconfig.build.json +9 -2
  33. package/dist/skills/ads-designer/references/contracts.md +250 -0
  34. package/dist/skills/ads-import/SKILL.md +4 -0
  35. package/dist/skills/ads-import/references/contracts.md +250 -0
  36. package/dist/skills/ads-migrate/SKILL.md +4 -0
  37. package/dist/skills/ads-migrate/references/contracts.md +250 -0
  38. package/dist/skills/ads-motion/SKILL.md +4 -0
  39. package/dist/skills/ads-motion/references/contracts.md +250 -0
  40. package/dist/token-origins.mjs +24 -0
  41. package/dist/tokens.mjs +4 -3
  42. package/dist/unit-contracts.mjs +203 -0
  43. package/dist/units.mjs +9 -3
  44. package/dist/verification/browser.mjs +156 -26
  45. package/dist/verification/playwright-reporter.cjs +13 -3
  46. package/docs/ADS.md +8 -1
  47. package/docs/ARCHITECTURE.md +36 -0
  48. package/docs/BEHAVIORS.md +8 -0
  49. package/docs/CHANGELOG.md +19 -0
  50. package/docs/CONTRACTS.md +250 -0
  51. package/docs/GUIDE.md +26 -1
  52. package/docs/MCP.md +15 -0
  53. package/package.json +4 -1
  54. /package/dist/bootstrap-ds/design-system/tokens/{Foundation.fixture.ts → Foundation.ads.fixture.ts} +0 -0
  55. /package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/{Foundation.fixture.ts → Foundation.ads.fixture.ts} +0 -0
@@ -0,0 +1,203 @@
1
+ import { existsSync, realpathSync, statSync } from "node:fs";
2
+ import path from "node:path";
3
+ import ts from "typescript";
4
+ import { z } from "zod";
5
+ import { isContractFile, isDevelopmentFile, isInside, isReviewFile, sourceFiles } from "./config.mjs";
6
+ import { projectCompilerOptions } from "./boundaries.mjs";
7
+ import { inspectUnits } from "./units.mjs";
8
+ import { publicApi, reviewApi } from "./public-api.mjs";
9
+
10
+ const identities = new WeakSet();
11
+ const ref = z.custom(value => value && typeof value === "object" && identities.has(value), "Use a resolved implementation symbol, not its name or an object describing it.");
12
+ const id = z.string().regex(/^[A-Za-z0-9][\w.-]{0,99}$/);
13
+ const checks = z.array(id).min(1).max(100);
14
+ const field = z.string().regex(/^[A-Za-z_$][\w$]*(?:\.(?:[A-Za-z_$][\w$]*|\*))*$/).max(200);
15
+ const paint = z.object({ api: ref, property: field, kind: z.literal("paint-reference"), bindings: z.array(id).max(20).optional() }).strict();
16
+ const binding = z.object({ input: z.object({ api: ref, property: field }).strict(), producer: ref, valueFrom: field,
17
+ variable: z.string().regex(/^--[a-z][\w-]*\$\{key\}[\w-]*$/), scope: z.object({ api: ref, selector: z.string().min(1).max(200) }).strict(), checks }).strict();
18
+ const part = z.object({ api: ref, parent: ref, consumer: ref, checks }).strict();
19
+ const schema = z.object({ schemaVersion: z.literal(1),
20
+ tokens: z.object({ namespace: z.string(), stylesheet: z.string().min(1).max(256) }).strict().optional(),
21
+ paintInputs: z.array(paint).max(100).optional(), bindings: z.record(id, binding).optional(),
22
+ declarativeParts: z.array(part).max(100).optional(), behavior: z.record(z.string(), z.unknown()).optional(),
23
+ }).strict();
24
+
25
+ const slash = file => file.replaceAll("\\", "/");
26
+ export function implementationIdentity(declaration, root) {
27
+ return `${slash(path.relative(root, declaration.getSourceFile().fileName))}:${declaration.pos}`;
28
+ }
29
+
30
+ export function implementationNode(program, reference, root) {
31
+ const source = program.getSourceFile(path.resolve(root, reference.source));
32
+ let found;
33
+ function visit(node) {
34
+ if (node.pos === reference.position && (ts.isFunctionDeclaration(node) || ts.isVariableDeclaration(node) || ts.isClassDeclaration(node))) found = node;
35
+ else if (!found) ts.forEachChild(node, visit);
36
+ }
37
+ if (source) visit(source);
38
+ return found;
39
+ }
40
+
41
+ /** Pure AST evaluation. Functions are opaque identities; project code never runs. */
42
+ export function staticContractValue(expression, checker, options, locations = new Map()) {
43
+ let visits = 0;
44
+ function read(node, key = "", seen = new Set()) {
45
+ if (++visits > 12000 || seen.size > 60) throw new Error("Static contract expansion exceeded its bounded limit.");
46
+ while (node && (ts.isAsExpression(node) || ts.isSatisfiesExpression(node) || ts.isParenthesizedExpression(node))) node = node.expression;
47
+ if (!node || seen.has(node)) throw new Error(`Unresolved or cyclic contract value at ${key || "default export"}.`);
48
+ locations.set(key, node);
49
+ seen = new Set([...seen, node]);
50
+ if (ts.isStringLiteralLike(node) || ts.isNumericLiteral(node)) return ts.isNumericLiteral(node) ? Number(node.text) : node.text;
51
+ if (node.kind === ts.SyntaxKind.TrueKeyword) return true;
52
+ if (node.kind === ts.SyntaxKind.FalseKeyword) return false;
53
+ if (node.kind === ts.SyntaxKind.NullKeyword) return null;
54
+ if (ts.isPrefixUnaryExpression(node) && node.operator === ts.SyntaxKind.MinusToken && ts.isNumericLiteral(node.operand)) return -Number(node.operand.text);
55
+ if (ts.isArrayLiteralExpression(node)) {
56
+ const result = [];
57
+ for (const item of node.elements) {
58
+ if (ts.isSpreadElement(item)) { const values = read(item.expression, key, seen); if (!Array.isArray(values)) throw new Error("Contract array spreads require a static array."); result.push(...values); }
59
+ else result.push(read(item, `${key}.${result.length}`, seen));
60
+ }
61
+ return result;
62
+ }
63
+ if (ts.isObjectLiteralExpression(node)) {
64
+ const result = Object.create(null);
65
+ for (const item of node.properties) {
66
+ if (ts.isSpreadAssignment(item)) {
67
+ const value = read(item.expression, key, seen);
68
+ if (!value || Array.isArray(value) || typeof value !== "object" || identities.has(value)) throw new Error("Contract object spreads require a plain static object.");
69
+ for (const [name, valueItem] of Object.entries(value)) { if (Object.hasOwn(result, name)) throw new Error(`Duplicate contract field ${key}.${name}.`); result[name] = valueItem; }
70
+ continue;
71
+ }
72
+ if (!ts.isPropertyAssignment(item) && !ts.isShorthandPropertyAssignment(item)) throw new Error("Contracts contain data fields, not getters, methods or executable expressions.");
73
+ if (!ts.isIdentifier(item.name) && !ts.isStringLiteralLike(item.name)) throw new Error("Contract property keys must be literal names.");
74
+ const name = item.name.text;
75
+ if (["__proto__", "prototype", "constructor"].includes(name) || Object.hasOwn(result, name)) throw new Error(`Invalid or duplicate contract field ${key}.${name}.`);
76
+ const input = ts.isShorthandPropertyAssignment(item) ? checker.getShorthandAssignmentValueSymbol(item)?.valueDeclaration : null;
77
+ result[name] = read(input?.initializer ?? (ts.isPropertyAssignment(item) ? item.initializer : item.name), key ? `${key}.${name}` : name, seen);
78
+ }
79
+ return result;
80
+ }
81
+ if (ts.isIdentifier(node) || ts.isPropertyAccessExpression(node)) {
82
+ let symbol = checker.getSymbolAtLocation(ts.isPropertyAccessExpression(node) ? node.name : node);
83
+ if (symbol?.flags & ts.SymbolFlags.Alias) symbol = checker.getAliasedSymbol(symbol);
84
+ const declaration = symbol?.valueDeclaration;
85
+ if (!declaration) throw new Error(`Cannot resolve contract symbol ${node.getText()}.`);
86
+ const runtime = !isDevelopmentFile(declaration.getSourceFile().fileName, options)||isReviewFile(declaration.getSourceFile().fileName,options);
87
+ const initializer = ts.isVariableDeclaration(declaration) ? declaration.initializer : ts.isPropertyAssignment(declaration) ? declaration.initializer : null;
88
+ const callable = ts.isFunctionDeclaration(declaration) || ts.isClassDeclaration(declaration) || initializer && (ts.isArrowFunction(initializer) || ts.isFunctionExpression(initializer) || ts.isCallExpression(initializer));
89
+ if (runtime && callable) {
90
+ const value = { source: slash(path.relative(options.root, declaration.getSourceFile().fileName)), symbol: symbol.name, position: declaration.pos, identity: implementationIdentity(declaration, options.root) };
91
+ identities.add(value); return value;
92
+ }
93
+ if (initializer && (!ts.isVariableDeclaration(declaration) || declaration.parent.flags & ts.NodeFlags.Const)) return read(initializer, key, seen);
94
+ }
95
+ throw new Error(`Unsupported static contract expression at ${key || "default export"}: ${ts.SyntaxKind[node.kind]}. Calls, getters and environment-dependent code are not evaluated.`);
96
+ }
97
+ return read(expression);
98
+ }
99
+
100
+ export function inspectUnitContracts(options) {
101
+ if (options.unitContracts) return options.unitContracts;
102
+ const files = sourceFiles(options.dsRoot).filter(isContractFile);
103
+ const result = { contracts: [], diagnostics: [], program: null };
104
+ options.unitContracts = result;
105
+ if (!files.length) return result;
106
+ const inventory = inspectUnits(options);
107
+ const program = ts.createProgram(sourceFiles(options.dsRoot).filter(file => /\.[cm]?[jt]sx?$/.test(file)), { ...projectCompilerOptions(options), allowJs: true });
108
+ result.program = program;
109
+ const checker = program.getTypeChecker();
110
+ const publicItems = [...publicApi(program, options), ...reviewApi(program, options)];
111
+ const publicByIdentity = new Map(publicItems.map(api => [implementationIdentity(api.declaration, options.root), api]));
112
+ const seen = new Set();
113
+ for (const file of files) {
114
+ const relative = slash(path.relative(options.root, file));
115
+ const unit = inventory.owner(inventory.relative(relative));
116
+ const source = program.getSourceFile(file), locations = new Map();
117
+ function report(message, fieldName = "") {
118
+ const node = locations.get(fieldName) ?? source;
119
+ const position = node?.getSourceFile().getLineAndCharacterOfPosition(node.getStart());
120
+ result.diagnostics.push({ file: relative, rule: "unit-contract", message, line: (position?.line ?? 0) + 1, column: (position?.character ?? 0) + 1, ...(fieldName ? { entry: fieldName } : {}) });
121
+ }
122
+ try {
123
+ if (!source || statSync(file).size > 128 * 1024 || realpathSync(file) !== file) throw new Error("Contracts must be real owner-local files of at most 128 KiB.");
124
+ if (!/^(?:components|patterns|controllers|review)\//.test(unit ?? "")) throw new Error("Contracts belong to a component, pattern, controller or review owner.");
125
+ if (seen.has(unit)) throw new Error(`Only one TypeScript contract may describe ${unit}.`);
126
+ seen.add(unit);
127
+ for (const statement of source.statements) {
128
+ if (ts.isImportDeclaration(statement) || ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement) || ts.isExportAssignment(statement) || ts.isEmptyStatement(statement)) continue;
129
+ if (ts.isVariableStatement(statement) && statement.declarationList.flags & ts.NodeFlags.Const) continue;
130
+ throw new Error("Contract modules contain imports, types, static constants and one default data export; executable statements are forbidden.");
131
+ }
132
+ const exports = source.statements.filter(ts.isExportAssignment);
133
+ if (exports.length !== 1 || exports[0].isExportEquals) throw new Error("Export one default contract data object.");
134
+ const value = staticContractValue(exports[0].expression, checker, options, locations);
135
+ for(const statement of source.statements.filter(ts.isVariableStatement))for(const declaration of statement.declarationList.declarations)staticContractValue(declaration.initializer,checker,options);
136
+ const parsed = schema.safeParse(value);
137
+ if (!parsed.success) { for (const issue of parsed.error.issues.slice(0, 12)) report(`${issue.path.join(".")}: ${issue.message}`, issue.path.join(".")); continue; }
138
+ const data = parsed.data;
139
+ const apiName = (reference, fieldName) => {
140
+ const owner = inventory.owner(inventory.relative(reference.source));
141
+ const api = publicByIdentity.get(reference.identity);
142
+ if (owner !== unit || !api) { report(`${fieldName} must reference a real public API owned by ${unit}.`, fieldName); return null; }
143
+ return api.name;
144
+ };
145
+ function implementation(reference, fieldName) {
146
+ const absolute = path.resolve(options.root, reference.source);
147
+ if (!isInside(options.dsRoot, absolute) || inventory.owner(inventory.relative(reference.source)) !== unit || isDevelopmentFile(absolute, options)&&!isReviewFile(absolute,options)) report(`${fieldName} must reference runtime or review implementation inside ${unit}, not a contract, fixture or spec.`, fieldName);
148
+ }
149
+ if (data.tokens) {
150
+ if(!/^(?:components|patterns)\//.test(unit))report("Owner-local design token namespaces belong to components or patterns. Controllers remain headless; review uses the shared vocabulary and its own presentation state.","tokens");
151
+ const absolute = path.resolve(path.dirname(file), data.tokens.stylesheet);
152
+ if (!isInside(options.dsRoot, absolute) || inventory.owner(inventory.relative(slash(path.relative(options.root, absolute)))) !== unit || !absolute.endsWith(".css") || !existsSync(absolute)) report("Token stylesheet must be a real CSS file in this owner.", "tokens.stylesheet");
153
+ data.tokens.file = slash(path.relative(options.root, absolute));
154
+ }
155
+ for (const [index, input] of (data.paintInputs ?? []).entries()) {
156
+ if(data.paintInputs.slice(0,index).some(prior=>prior.api.identity===input.api.identity&&prior.property===input.property))report("Duplicate paint-input registration is ambiguous.",`paintInputs.${index}`);
157
+ input.apiName = apiName(input.api, `paintInputs.${index}.api`);
158
+ for (const name of input.bindings ?? []) if (!Object.hasOwn(data.bindings ?? {}, name)) report(`Unknown binding ${name}.`, `paintInputs.${index}.bindings`);
159
+ }
160
+ for (const [name, entry] of Object.entries(data.bindings ?? {})) {
161
+ entry.apiName = apiName(entry.input.api, `bindings.${name}.input.api`);
162
+ entry.scope.apiName = apiName(entry.scope.api, `bindings.${name}.scope.api`);
163
+ if(!publicByIdentity.get(entry.scope.api.identity)?.renderable)report("Binding scopes require a real public rendering API.",`bindings.${name}.scope.api`);
164
+ implementation(entry.producer, `bindings.${name}.producer`);
165
+ if (entry.apiName !== entry.scope.apiName) report("The binding input and attachment scope must belong to the same public API.", `bindings.${name}`);
166
+ }
167
+ for (const [index, entry] of (data.declarativeParts ?? []).entries()) {
168
+ if(data.declarativeParts.slice(0,index).some(prior=>prior.api.identity===entry.api.identity))report("A declarative API needs one unambiguous parent/consumer relationship.",`declarativeParts.${index}`);
169
+ entry.apiName = apiName(entry.api, `declarativeParts.${index}.api`);
170
+ entry.parentName = apiName(entry.parent, `declarativeParts.${index}.parent`);
171
+ if(!publicByIdentity.get(entry.parent.identity)?.renderable)report("Declarative parents require a real public rendering API.",`declarativeParts.${index}.parent`);
172
+ implementation(entry.consumer, `declarativeParts.${index}.consumer`);
173
+ }
174
+ let behavior;
175
+ if (data.behavior) {
176
+ if(Object.hasOwn(data.behavior,"schemaVersion"))report("Behavior inherits the owner contract's schemaVersion; remove the competing nested version.","behavior.schemaVersion");
177
+ behavior = { ...data.behavior, schemaVersion: 1 };
178
+ behavior.subjects = Object.fromEntries(Object.entries(behavior.subjects ?? {}).map(([name, subject]) => {
179
+ if (subject?.api && identities.has(subject.api)) return [name, { api: apiName(subject.api, `behavior.subjects.${name}.api`) ?? "" }];
180
+ if (subject?.implementation && identities.has(subject.implementation)) {
181
+ implementation(subject.implementation, `behavior.subjects.${name}.implementation`);
182
+ return [name, { source: `./${slash(path.relative(path.dirname(file), path.resolve(options.root, subject.implementation.source)))}`, symbol: subject.implementation.symbol }];
183
+ }
184
+ report("Behavior subjects must use an actual public api or owner-local implementation symbol.", `behavior.subjects.${name}`);
185
+ return [name, subject];
186
+ }));
187
+ }
188
+ for (const entry of [...Object.values(data.bindings ?? {}), ...(data.declarativeParts ?? [])]) for (const check of entry.checks) if (!behavior?.checks?.[check]) report(`Required check ${check} is not registered in this contract's behavior checks.`);
189
+ result.contracts.push({ ...data, ...(behavior ? { behavior } : {}), unit, file: relative });
190
+ } catch (error) { report(error.message); }
191
+ }
192
+ return result;
193
+ }
194
+
195
+ /** An ownership lookup, not source/browser certification of the declaration. */
196
+ export function runtimeBindingOwner(options, name) {
197
+ for (const contract of inspectUnitContracts(options).contracts) for (const [id, binding] of Object.entries(contract.bindings ?? {})) {
198
+ const [prefix, suffix] = binding.variable.split("${key}");
199
+ const key = name.slice(prefix.length, suffix ? -suffix.length : undefined);
200
+ if (name.startsWith(prefix) && name.endsWith(suffix) && /^[A-Za-z0-9_-]+$/.test(key)) return { ...binding, id, unit: contract.unit };
201
+ }
202
+ return null;
203
+ }
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";
@@ -98,13 +98,17 @@ 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
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
  }
@@ -147,6 +151,8 @@ export function inspectUnits(options) {
147
151
  if (!units.get(id).files.includes(file)) units.get(id).files.push(file);
148
152
  }
149
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];
150
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." });
151
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." });
152
158
  if (/^(?:components|patterns|controllers|composition|review)\//.test(unit.id)) {
@@ -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
@@ -201,6 +202,12 @@ connected project's actual entry. Older tooling must be upgraded before migratio
201
202
 
202
203
  ### Owner-local tokens
203
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
+
204
211
  Shared semantics remain in the configured token foundation. Constants specific
205
212
  to one component or pattern may live beside it:
206
213
 
@@ -584,3 +584,39 @@ sheet directly, so tree-shaking an unused runtime owner cannot remove their valu
584
584
  Namespaces, aliases, ownership and actual colocated sample coverage are checked.
585
585
  The shared foundation remains the cross-owner vocabulary. See the
586
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.
package/docs/BEHAVIORS.md CHANGED
@@ -25,6 +25,14 @@ These gates inspect supported direct imports, markup and style boundaries.
25
25
  External package internals and delegated DOM behavior still require source,
26
26
  build and browser review; declaring a dependency does not prove it is headless.
27
27
 
28
+ New owner registrations can live in `<Owner>.ads.contract.ts` with actual API and
29
+ implementation references; legacy JSON remains supported. The
30
+ [contract reference](CONTRACTS.md) covers static authoring, `.ads.behavior.spec.ts`,
31
+ declarative `exercises`, required browser checks and guarded migration.
32
+ A contract declaration alone is not passing evidence. Use
33
+ `auditBehaviorPaints(frame)` after relevant binding updates to repeat the current
34
+ App's generic paint audit, in addition to assertions for the named guarantee.
35
+
28
36
  ## Development boundary
29
37
 
30
38
  Production includes real components, controllers, tokens and compiled styles.
package/docs/CHANGELOG.md CHANGED
@@ -2,6 +2,25 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.3.21
6
+
7
+ - Add statically read TypeScript owner contracts and canonical `.ads.` development
8
+ roles, retaining legacy token/behavior registration and stable scenario IDs.
9
+ - Validate constrained paint inputs, source-proven instance binding producers and
10
+ declarative child consumers. Audit current paint bindings/local SVG resources
11
+ inside Apps; registered browser checks remain required for guarantees.
12
+ - Close supported cross-owner TypeScript token-value flows and preserve independent
13
+ render evidence with positioned scenario field/entry diagnostics.
14
+ - Prepare contract/file-role migrations as normal guarded owner drafts; expose
15
+ compact contract/API metadata and portable guidance in all five skills.
16
+ - Expose read-only token-origin analysis for explicit project scale policies.
17
+ - Keep starter production declarations limited to the public runtime dependency
18
+ closure, excluding development contracts, scenarios, review and checks.
19
+ - Reuse contract TypeScript programs during discovery. Match failed Playwright
20
+ steps to the unhandled case error so nested API event order and earlier caught
21
+ failures cannot mislabel an assertion as a runner failure.
22
+
23
+
5
24
  ## 0.3.20
6
25
 
7
26
  - Preserve the motion-review checklist separately from the development review-layer