@reqvire-org/ads 0.3.20 → 0.3.23

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 +253 -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 +253 -0
  34. package/dist/skills/ads-import/SKILL.md +4 -0
  35. package/dist/skills/ads-import/references/contracts.md +253 -0
  36. package/dist/skills/ads-migrate/SKILL.md +4 -0
  37. package/dist/skills/ads-migrate/references/contracts.md +253 -0
  38. package/dist/skills/ads-motion/SKILL.md +4 -0
  39. package/dist/skills/ads-motion/references/contracts.md +253 -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 +34 -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 +28 -0
  50. package/docs/CONTRACTS.md +253 -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
package/README.md CHANGED
@@ -31,6 +31,8 @@ uses Preact. See [implementation support](docs/GUIDE.md#implementation-support).
31
31
  proposed candidates, exposed as MCP image resources.
32
32
  - **Verify behavior** through project-owned guarantees and registered tests,
33
33
  with progress, timings and evidence tied to current source.
34
+ - **Keep contracts explicit** for typed paint inputs, scoped data bindings and
35
+ declarative APIs, without adding ADS to production components.
34
36
  - **Guide agents** with prompts and portable skills for building, importing,
35
37
  migrating, motion and UI stress testing.
36
38
 
@@ -9,6 +9,7 @@ import { inspectUnits } from "./units.mjs";
9
9
  import { scenarioApi } from "./public-api.mjs";
10
10
  import { inspectScenarios } from "./scenarios.mjs";
11
11
  import { uiOperation } from "./ui-framework.mjs";
12
+ import { inspectUnitContracts } from "./unit-contracts.mjs";
12
13
 
13
14
  const id = z.string().regex(/^[a-zA-Z0-9][\w.-]{0,99}$/);
14
15
  const reference = z.string().min(1).max(500);
@@ -27,20 +28,22 @@ export const behaviorSchema = z.object({
27
28
  export function inspectBehaviors(options, suppliedProgram, suppliedRegistry) {
28
29
  const { root, dsRoot, config } = options;
29
30
  const inventory = inspectUnits(options);
30
- const files = sourceFiles(dsRoot).filter(isBehaviorFile);
31
+ const declarations = inspectUnitContracts(options);
32
+ const typed = declarations.contracts.filter(entry => entry.behavior);
33
+ const files = [...sourceFiles(dsRoot).filter(isBehaviorFile), ...typed.map(entry => path.resolve(root, entry.file))];
31
34
  if (!files.length) return { contracts: [], diagnostics: [] };
32
- const program = suppliedProgram ?? ts.createProgram(sourceFiles(dsRoot).filter(file => /\.[cm]?[jt]sx?$/.test(file)), { ...projectCompilerOptions(options), allowJs: true });
35
+ const program = suppliedProgram ?? declarations.program ?? ts.createProgram(sourceFiles(dsRoot).filter(file => /\.[cm]?[jt]sx?$/.test(file)), { ...projectCompilerOptions(options), allowJs: true });
33
36
  const checker = program.getTypeChecker();
34
37
  const apis = new Map(scenarioApi(program, options).map(item => [item.name, item]));
35
38
  const scenarios = (suppliedRegistry ?? inspectScenarios(options, program)).scenarios;
36
- const diagnostics = [], contracts = [], owners = new Set();
39
+ const diagnostics = [...declarations.diagnostics], contracts = [], owners = new Set();
37
40
  const relative = file => path.relative(root, file).replaceAll("\\", "/");
38
41
  for (const file of files) {
39
42
  const report = message => diagnostics.push({ file: relative(file), rule: "behavior-contract", line: 1, column: 1, message });
40
43
  let value;
41
44
  try {
42
45
  if (statSync(file).size > 128 * 1024 || realpathSync(file) !== file) throw new Error("Behavior contracts must be regular files of at most 128 KiB.");
43
- const parsed = behaviorSchema.safeParse(JSON.parse(readFileSync(file, "utf8")));
46
+ const parsed = behaviorSchema.safeParse(typed.find(entry => path.resolve(root, entry.file) === file)?.behavior ?? JSON.parse(readFileSync(file, "utf8")));
44
47
  if (!parsed.success) throw new Error(parsed.error.issues.slice(0, 5).map(issue => `${issue.path.join(".")}: ${issue.message}`).join("; "));
45
48
  value = parsed.data;
46
49
  } catch (error) { report(error.message); continue; }
@@ -85,6 +88,13 @@ export function inspectBehaviors(options, suppliedProgram, suppliedRegistry) {
85
88
  if (cases.has(identity)) report(`Duplicate execution identity for check ${name}. Use one check ID and reference it from several guarantees.`);
86
89
  cases.add(identity);
87
90
  }
91
+ const declaration=typed.find(entry=>entry.unit===unit);
92
+ for(const entry of [...Object.values(declaration?.bindings??{}),...(declaration?.declarativeParts??[])])for(const name of entry.checks) {
93
+ const check=value.checks[name],scenario=check&&scenarios.find(scenario=>scenario.id===check.scenario);
94
+ const parent=entry.parentName??entry.scope.apiName;
95
+ if(!check||config.mcp.behaviorProfiles[check.profile]?.mode!=="browser"||!scenario?.covers.includes(parent)||entry.parentName&&!scenario.exercises?.includes(entry.apiName))report(`Required contract check ${name} must use a browser profile and a scenario covering ${parent}${entry.parentName?` and exercising ${entry.apiName}`:""}. Source registration does not establish rendered behavior.`);
96
+ if(!value.guarantees.some(guarantee=>guarantee.checks.includes(name)))report(`Required contract check ${name} must back a named guarantee, not remain an unclaimed execution.`);
97
+ }
88
98
  contracts.push({ ...value, unit, file: relative(file), demonstrations: Object.fromEntries(Object.entries(value.subjects).filter(([, item]) => item.api).map(([name, item]) => [name, scenarios.filter(scenario => scenario.exercises?.includes(item.api)).map(scenario => scenario.id)])) });
89
99
  }
90
100
  return { contracts, diagnostics };
@@ -56,7 +56,7 @@ Its MCP client discovers entries, invokes preview tools and follows notification
56
56
  There are no required TokensPage/ComponentsPage/PatternsPage/CompositionPage
57
57
  files, and no separate project browser app to implement or rebuild.
58
58
 
59
- The existing `tokens/Foundation.scenario.tsx` and colocated fixture visually demonstrate all
59
+ The existing `tokens/Foundation.ads.scenario.tsx` and colocated fixture visually demonstrate all
60
60
  starter token values. The fixture owns `{ token, property, probe }` bindings;
61
61
  the public `TokenPreview` pattern owns their presentation and applies the real CSS
62
62
  variables. Add/remove samples alongside token definitions. `ads check` rejects
@@ -1,4 +1,4 @@
1
1
  import { TokenPreview } from "@ds/review";
2
- import { fixture } from "./Foundation.fixture";
2
+ import { fixture } from "./Foundation.ads.fixture";
3
3
  export const scenario = { title: fixture.title, covers: ["TokenPreview"], samples: fixture.samples };
4
4
  export default function Scenario() { return <TokenPreview {...fixture} />; }
@@ -7,6 +7,13 @@
7
7
  "rootDir": "design-system",
8
8
  "outDir": "dist"
9
9
  },
10
- "include": ["design-system/**/*.ts", "design-system/**/*.tsx"],
11
- "exclude": ["design-system/showcase"]
10
+ "include": [
11
+ "design-system/index.ts"
12
+ ],
13
+ "exclude": [
14
+ "design-system/showcase",
15
+ "design-system/review",
16
+ "design-system/composition",
17
+ "**/*.ads.*"
18
+ ]
12
19
  }
@@ -4,6 +4,7 @@ import { importedUiName } from "./ui-framework.mjs";
4
4
  import { isInside, isStateVariable, isTestFile, isDevelopmentFile } from "./config.mjs";
5
5
  import { isSharedToken } from "./token-policy.mjs";
6
6
  import { scopedTokenOwner } from "./scoped-tokens.mjs";
7
+ import { runtimeBindingOwner } from "./unit-contracts.mjs";
7
8
 
8
9
  export function projectCompilerOptions({ root, config, dsRoot }) {
9
10
  const file = ts.findConfigFile(root, ts.sys.fileExists, "tsconfig.json");
@@ -64,6 +65,7 @@ export function checkComponentVariable(name, filename, options) {
64
65
  const showcase = development;
65
66
  const owned = scopedTokenOwner(options, name);
66
67
  if (owned) return path.resolve(options.root, owned.file) === filename ? null : { rule: "token-ownership", message: `Define ${name} only in ${owned.file}, owned by ${owned.unit}. Consume it with var(); runtime geometry remains a separate styleWrites boundary.` };
68
+ if (runtimeBindingOwner(options, name)) return { rule: "binding-ownership", message: `${name} belongs to a declared runtime producer. Do not redefine instance bindings in CSS or arbitrary style objects.` };
67
69
  if (isSharedToken(name)) return tokens ? null : { rule: "token-ownership", message: `Define ${name} in ADS tokens, not component, pattern, showcase, or app styles.` };
68
70
  if (name.startsWith("--ux-")) return patterns || showcase ? null : { rule: "variable-ownership", message: "--ux-* variables belong to ADS patterns and showcase composition." };
69
71
  if (name.startsWith("--showcase-")) return showcase ? null : { rule: "variable-ownership", message: "--showcase-* variables belong to the ADS showcase." };
package/dist/check.mjs CHANGED
@@ -9,6 +9,10 @@ import { foundationFiles, foundationImportTarget, isSharedToken } from "./token-
9
9
  import { inspectAssets } from "./assets.mjs";
10
10
  import { appImportViolation, showcaseImportViolation, checkComponentVariable, importResolver, projectCompilerOptions, isUiCssProperties, styleWriteViolation } from "./boundaries.mjs";
11
11
  import { inspectPublicProps } from "./public-props.mjs";
12
+ import { adsFileRole } from "./config.mjs";
13
+ import { inspectPaintContracts, inspectTokenValueFlows } from "./paint-contracts.mjs";
14
+ import { runtimeBindingOwner } from "./unit-contracts.mjs";
15
+ import { inspectRuntimeContracts } from "./runtime-contracts.mjs";
12
16
  import { inspectHtml, isMountCss } from "./html.mjs";
13
17
  import { inspectShowcasePages } from "./showcase.mjs";
14
18
  import { inspectShowcaseCoverage } from "./showcase-coverage.mjs";
@@ -43,7 +47,7 @@ export function checkProject(directory, { trackingRoot } = {}) {
43
47
  if (!existsSync(file) || (field === "dsDir" ? !statSync(file).isDirectory() : !statSync(file).isFile())) diagnostics.push({ rule: "project-structure", file: path.relative(root, file), line: 1, column: 1, message: `Missing or invalid ${field}. Bootstrap an empty ADS or configure its actual path.` });
44
48
  }
45
49
  if (diagnostics.length) return architectureEvidence({ ok: false, diagnostics });
46
- const tokenInventory = inspectTokens(root);
50
+ const tokenInventory = inspectTokens(root, options);
47
51
  const declaredTokens = new Set(tokenInventory.tokens.map((token) => token.name));
48
52
  diagnostics.push(...tokenInventory.diagnostics);
49
53
  diagnostics.push(...inspectAssets(options));
@@ -57,6 +61,11 @@ export function checkProject(directory, { trackingRoot } = {}) {
57
61
  const compiler = projectCompilerOptions(options);
58
62
  const program = ts.createProgram(scriptFiles, { ...compiler, paths: { ...compiler.paths, [config.publicAlias]: [path.join(dsRoot, config.publicEntry)] }, allowJs: true, noResolve: true, noLib: true, types: [], target: ts.ScriptTarget.Latest, module: ts.ModuleKind.ESNext, jsx: ts.JsxEmit.Preserve });
59
63
  const checker = program.getTypeChecker();
64
+ const paintContracts = inspectPaintContracts(options);
65
+ diagnostics.push(...paintContracts.diagnostics);
66
+ const runtimeContracts = inspectRuntimeContracts(options, program);
67
+ diagnostics.push(...runtimeContracts.diagnostics);
68
+ for (const binding of runtimeContracts.bindings) if (scoped.definitions.some(entry=>entry.namespace.startsWith(binding.prefix)||binding.prefix.startsWith(entry.namespace))) diagnostics.push({file:binding.file,rule:"binding-collision",line:1,column:1,message:`Runtime family ${binding.variable} overlaps an owner-local design-token namespace. Keep instance bindings separate from token definitions.`});
60
69
  const atomicCompositionReview = createAtomicCompositionReview(program, options);
61
70
  const scenarioTrace = createScenarioTrace(program, options);
62
71
  let scenarioSlot, scenarioSelector;
@@ -65,6 +74,7 @@ export function checkProject(directory, { trackingRoot } = {}) {
65
74
  }
66
75
  const layers = createLayerBoundaries(program, options, report), layerStyles = [];
67
76
  layers.inspectDependencies();
77
+ inspectTokenValueFlows(program, options, tokenInventory, scoped, report);
68
78
  for (const file of layers.runtimeHelpers()) if (!files.includes(file)) files.push(file);
69
79
  inspectStyleBoundaries(options, program, report);
70
80
  inspectShowcasePages(options, program, resolveImport, report);
@@ -154,6 +164,11 @@ export function checkProject(directory, { trackingRoot } = {}) {
154
164
  const variable = checkComponentVariable(decl.prop, file, options);
155
165
  if (variable) report(file, variable.rule, variable.message, offset.line + decl.source.start.line - 1, decl.source.start.column);
156
166
  for (const ref of references(decl.value)) {
167
+ const binding = runtimeBindingOwner(options, ref.name);
168
+ if (binding) {
169
+ if (scoped.inventory.owner(scoped.inventory.relative(path.relative(root, file))) !== binding.unit) report(file, "binding-ownership", `${ref.name} is an instance binding owned by ${binding.unit}, not a shared design token.`, offset.line + decl.source.start.line - 1, decl.source.start.column);
170
+ continue;
171
+ }
157
172
  const owned = scopedTokenOwner(options, ref.name);
158
173
  if (owned) {
159
174
  if (scoped.inventory.owner(scoped.inventory.relative(path.relative(root, file))) !== owned.unit || !declaredTokens.has(ref.name)) report(file, "token-reference", `${ref.name} belongs to ${owned.unit}. Use shared tokens across owners; owner-local tokens must be defined and consumed by their owner.`, offset.line + decl.source.start.line - 1, decl.source.start.column);
@@ -179,6 +194,9 @@ export function checkProject(directory, { trackingRoot } = {}) {
179
194
  });
180
195
  }
181
196
  for (const file of files) {
197
+ const role = adsFileRole(file);
198
+ const roleExtension=role==="contract"?/\.[cm]?ts$/.test(file):["fixture","fixtures"].includes(role)?/\.(?:[cm]?[jt]sx?|json)$/.test(file):/\.[cm]?[jt]sx?$/.test(file);
199
+ if (role && (!["contract", "scenario", "fixture", "fixtures", "helpers", "test", "spec", "behavior.spec"].includes(role)||!roleExtension)) report(file, "development-role", `Unknown .ads. role or unsupported extension: ${role}. Use a TypeScript contract, a script scenario/fixture/helpers/test/behavior.spec, or JSON fixture; marking development code does not grant styling or mutation permissions.`);
182
200
  if (scopedFiles.has(file)) { try{checkRegistrations(file,postcss.parse(readFileSync(file,"utf8"),{from:file}));}catch{/* Token inspection reports malformed sheets. */} continue; } // Closed definition-only contract checked by inspectTokens.
183
201
  if (file.endsWith(".css") && isInside(path.join(dsRoot, config.tokenDir), file) && !isDevelopmentFile(file, options)) { checkCss(file, readFileSync(file, "utf8"), undefined, true); continue; }
184
202
  if (file.endsWith(".html")) { inspectHtml(readFileSync(file, "utf8"), (rule, message, line) => report(file, rule, message, line)); continue; }
@@ -212,7 +230,7 @@ export function checkProject(directory, { trackingRoot } = {}) {
212
230
  const selector = native ? (scenarioSelector ??= createScenarioSelectors(program, options))(node) : { ok: false, reasons: ["A selector cannot establish custom widget behavior or keyboard semantics."] };
213
231
  if (!selector.ok) nodeReport(node, "showcase-control", `Scenario controls must compose public ADS runtime UI. Native semantics require source evidence of owned cloning props or an attached runtime Linaria selector addressing that native tag or a verified runtime-owned part in that position. A prop name, broad selector or scenario-authored marker grants no permission. ${selector.reasons.join(" ")} Preserve valid APIs when tracing is inconclusive; report the limitation instead of changing the API to satisfy coverage. ${scenarioLayoutGuidance} See ads://guides/scenarios.`, { failure: failure("architecture", "source-ownership", "ownership-not-proven", { certainty: "inconclusive" }) });
214
232
  }
215
- if (!showcase && (isInside(path.join(dsRoot, "components"), file) || isInside(path.join(dsRoot, "patterns"), file))) inspectPublicProps(source, checker, nodeReport);
233
+ if (!showcase && (isInside(path.join(dsRoot, "components"), file) || isInside(path.join(dsRoot, "patterns"), file))) inspectPublicProps(source, checker, nodeReport, paintContracts.allowedProperties);
216
234
  function isLinariaTag(tag) {
217
235
  if (ts.isCallExpression(tag)) tag = tag.expression;
218
236
  let symbolNode = tag;
@@ -295,13 +313,14 @@ export function checkProject(directory, { trackingRoot } = {}) {
295
313
  if (specifier && resolveImport(specifier, file).some((target) => (specifier.startsWith(".") || existsSync(target)) && target.endsWith(".css") && isInside(root, target) && !isInside(dsRoot, target) && target !== path.join(sourceRoot, "app-mount.css") && !isInside(path.join(root, "node_modules"), target))) nodeReport(node, "style-ownership", "Imported project CSS belongs to ADS components, patterns, or the foundation. Only structural app-mount.css is allowed outside the ADS.");
296
314
  if (specifier && appImportViolation(specifier, file, options, resolveImport)) nodeReport(node, "no-app-import", "ADS implementations and showcase fixtures must not import app code. Use public ADS APIs with local fixture data; keep application integration harnesses in application tests.");
297
315
  if (specifier && showcaseImportViolation(specifier, file, options, resolveImport)) nodeReport(node, "showcase-import", "Showcase code and styles are development fixtures; runtime code must not import them.");
298
- if (ts.isJsxAttribute(node) && ts.isIdentifier(node.name) && node.name.text === "style" && (!inlineAllowed || showcase)) nodeReport(node, "no-inline-style", showcase ? `Scenarios define no inline styles. ${scenarioLayoutGuidance}` : inlineStyleGuidance);
316
+ const boundStyle = runtimeContracts.allowedStyles.has(`${file}:${node.pos}`);
317
+ if (ts.isJsxAttribute(node) && ts.isIdentifier(node.name) && node.name.text === "style" && (!inlineAllowed || showcase) && !boundStyle) nodeReport(node, "no-inline-style", showcase ? `Scenarios define no inline styles. ${scenarioLayoutGuidance}` : inlineStyleGuidance);
299
318
  if (ts.isJsxAttribute(node) && ts.isIdentifier(node.name) && node.name.text === "style" && node.initializer) {
300
319
  inspectProperty(node.initializer);
301
320
  }
302
321
  const mutation = styleWriteViolation(node, file, options);
303
322
  if (mutation) nodeReport(node, "style-mutation", mutation);
304
- if (!inlineAllowed && isUiCssProperties(node, checker)) nodeReport(node, "css-properties", "Keep CSSProperties out of the public API. Use deliberate component contracts or a documented measured-geometry renderer boundary; demo layout belongs in a development-only review/ helper.");
323
+ if (!inlineAllowed && isUiCssProperties(node, checker) && !runtimeContracts.allowedStyleTypes.has(`${file}:${node.pos}`)) nodeReport(node, "css-properties", "Keep CSSProperties out of the public API. Use deliberate component contracts or a documented measured-geometry renderer boundary; demo layout belongs in a development-only review/ helper.");
305
324
  if (ts.isJsxAttribute(node) && ts.isIdentifier(node.name) && node.name.text === "className" && node.initializer) {
306
325
  inspectClass(node.initializer);
307
326
  }
@@ -318,5 +337,6 @@ export function checkProject(directory, { trackingRoot } = {}) {
318
337
  // Bounded clients should see actionable warnings before source-analysis limits.
319
338
  for (const { file, css, offset } of layerStyles) layers.inspectStyles(file, css, offset);
320
339
  advisories.sort((first, second) => Number(first.severity === "info") - Number(second.severity === "info"));
321
- return architectureEvidence({ ok: diagnostics.length === 0, diagnostics, ...(advisories.length ? { advisories } : {}) });
340
+ const uniqueDiagnostics=[...new Map(diagnostics.map(diagnostic=>[JSON.stringify([diagnostic.file,diagnostic.rule,diagnostic.line,diagnostic.column,diagnostic.message]),diagnostic])).values()];
341
+ return architectureEvidence({ ok: uniqueDiagnostics.length === 0, diagnostics:uniqueDiagnostics, ...(advisories.length ? { advisories } : {}) });
322
342
  }
package/dist/config.mjs CHANGED
@@ -163,10 +163,27 @@ export function isTokenManifestFile(file) {
163
163
  return /(?:^|[\\/])tokens\.json$|\.tokens\.json$/.test(file);
164
164
  }
165
165
 
166
+ /** A visible development boundary, independent of whether its role is supported. */
167
+ export function adsFileRole(file) {
168
+ const match = path.basename(file).match(/\.ads\.(.+)\.[^.]+$/);
169
+ return match ? match[1] : null;
170
+ }
171
+
172
+ export function isContractFile(file) {
173
+ return /\.ads\.contract\.[cm]?ts$/.test(file);
174
+ }
175
+
176
+ /** Filenames may change without changing the identity used by clients/checks. */
177
+ export function scenarioIdentity(file, dsRoot) {
178
+ let id = path.relative(dsRoot, file).replaceAll("\\", "/").replace(/\.ads(?=\.)/, "").replace(/(?:\/Scenario|\.scenario)\.[^.]+$/, "");
179
+ if (adsFileRole(file) === "scenario" && path.posix.basename(id) === path.posix.basename(path.posix.dirname(id))) id = path.posix.dirname(id);
180
+ return id;
181
+ }
182
+
166
183
  export function isDevelopmentFile(file, { dsRoot }) {
167
- if (!isInside(dsRoot, file)) return isTestFile(file);
184
+ if (!isInside(dsRoot, file)) return isTestFile(file) || adsFileRole(file) !== null;
168
185
  const relative = path.relative(dsRoot, file).replaceAll("\\", "/");
169
- return isTestFile(file) || isScenarioFile(file) || isFixtureFile(file) || isBehaviorFile(file) || isTokenManifestFile(file)
186
+ return adsFileRole(file) !== null || isTestFile(file) || isScenarioFile(file) || isFixtureFile(file) || isBehaviorFile(file) || isTokenManifestFile(file)
170
187
  || /^(?:showcase|composition|review)\//.test(relative)
171
188
  || /(?:^|\/)(?:scenario|fixtures)(?:\/|$)/.test(relative)
172
189
  || /\.scenario(?:-adapter)?\.[cm]?[jt]sx?$/.test(relative);
@@ -174,7 +191,7 @@ export function isDevelopmentFile(file, { dsRoot }) {
174
191
 
175
192
  /** Review implementations may own demo presentation; scenarios/fixtures may not. */
176
193
  export function isReviewFile(file, { dsRoot }) {
177
- return isInside(path.join(dsRoot, "review"), file) && !isTestFile(file) && !isScenarioFile(file) && !isFixtureFile(file) && !isBehaviorFile(file) && !isTokenManifestFile(file);
194
+ return isInside(path.join(dsRoot, "review"), file) && adsFileRole(file) === null && !isTestFile(file) && !isScenarioFile(file) && !isFixtureFile(file) && !isBehaviorFile(file) && !isTokenManifestFile(file);
178
195
  }
179
196
 
180
197
  const ignored = new Set(["node_modules", "dist", "dist-showcase", "dist-kit", ".git", ".vite", ".ds", "coverage"]);
@@ -194,7 +211,7 @@ export function sourceFiles(root) {
194
211
  export function importViolation(specifier, filename, options, resolve) {
195
212
  const { root, dsRoot, config } = options;
196
213
  // ADS implementation code needs its private imports; the showcase is a consumer.
197
- if (isInside(dsRoot, filename) && !isDevelopmentFile(filename, options)) return false;
214
+ if (isInside(dsRoot, filename) && (!isDevelopmentFile(filename, options) || isContractFile(filename))) return false; // Contract symbol ownership is checked without executing imports.
198
215
  if (specifier === `${config.publicAlias}/review`) return !isDevelopmentFile(filename, options) && !isTestFile(filename);
199
216
  if (specifier.startsWith(`${config.publicAlias}/`)) return true;
200
217
  if (specifier === config.publicAlias) return false;
@@ -0,0 +1,110 @@
1
+ import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import ts from "typescript";
4
+ import { adsFileRole, isBehaviorFile, isScenarioFile, loadConfig, scenarioIdentity } from "./config.mjs";
5
+ import { inspectUnitContracts } from "./unit-contracts.mjs";
6
+ import { publicApi } from "./public-api.mjs";
7
+ import { projectCompilerOptions } from "./boundaries.mjs";
8
+
9
+ const slash=value=>value.replaceAll("\\","/");
10
+
11
+ /** Mechanical edits in an ordinary owner draft. Never writes live source. */
12
+ export function migrateContractDraft(options, prepared) {
13
+ const base=prepared.directory, name=path.posix.basename(prepared.unit), renames=new Map(), notes=[];
14
+ const relative=file=>slash(path.relative(base,file));
15
+ const resolve=file=>path.join(base,file);
16
+ const files=prepared.files.filter(file=>!file.endsWith("ds.config.json")&&file.startsWith(options.config.dsDir+"/"));
17
+ const owned=files.filter(file=>!file.endsWith("/"+options.config.publicEntry)&&!file.endsWith("/"+options.config.reviewEntry));
18
+ for(const file of owned) {
19
+ if(adsFileRole(file)||!/\.[cm]?[jt]sx?$/.test(file))continue;
20
+ let next;
21
+ if(isScenarioFile(file))next=path.basename(file).startsWith("Scenario.")?path.join(path.dirname(file),`${name}.ads.scenario${path.extname(file)}`):file.replace(/\.scenario\./,".ads.scenario.");
22
+ else if(/(?:^|\/)fixture\.[cm]?[jt]sx?$/.test(file))next=path.join(path.dirname(file),`${name}.ads.fixture${path.extname(file)}`);
23
+ else if(/\.(?:fixture|fixtures|test|spec)\.[cm]?[jt]sx?$/.test(file))next=file.replace(/\.(behavior\.spec|fixture|fixtures|test|spec)(\.[^.]+)$/,".ads.$1$2");
24
+ else if(/\.scenario-adapter\.[cm]?[jt]sx?$/.test(file))next=file.replace(/\.scenario-adapter\./,".ads.helpers.");
25
+ if(next&&next!==file) {
26
+ next=slash(next);
27
+ if(isScenarioFile(file)&&scenarioIdentity(path.join(options.root,file),options.dsRoot)===prepared.unit&&scenarioIdentity(path.join(options.root,next),options.dsRoot)!==prepared.unit){notes.push(`Kept ${file}: a default-directory rename would change this flat owner's identity. Review a deliberate owner migration separately.`);continue;}
28
+ if([...renames.values()].includes(next)&&isScenarioFile(file))next=next.replace(".ads.scenario.",".named.ads.scenario.");
29
+ if(existsSync(resolve(next)))throw new Error(`Migration would overwrite ${next}; resolve the naming conflict in this draft.`);
30
+ renames.set(file,next);
31
+ }
32
+ }
33
+ for(const file of owned.filter(isScenarioFile)) {
34
+ const fixtureBase=path.basename(file).startsWith("Scenario.")?path.join(path.dirname(file),"fixture"):file.replace(/\.scenario\.[^.]+$/,".fixture");
35
+ const fixture=owned.find(candidate=>candidate.startsWith(fixtureBase+".")&&/\.(?:[cm]?[jt]sx?|json)$/.test(candidate));
36
+ if(!fixture)continue;
37
+ const scenario=renames.get(file);
38
+ if(scenario)renames.set(fixture,scenario.replace(/\.scenario\.[^.]+$/,".fixture")+path.extname(fixture));
39
+ else renames.delete(fixture);
40
+ }
41
+ if(new Set(renames.values()).size!==renames.size)throw new Error("Migration has colliding development filenames; resolve the owner draft deliberately.");
42
+ const jsonFiles=owned.filter(file=>isBehaviorFile(file)||/(?:^|\/)tokens\.json$|\.tokens\.json$/.test(file));
43
+ for(const [from,to]of renames)if(isScenarioFile(from)&&scenarioIdentity(path.join(options.root,from),options.dsRoot)!==scenarioIdentity(path.join(options.root,to),options.dsRoot)) {
44
+ const text=readFileSync(resolve(from),"utf8"),source=ts.createSourceFile(from,text,ts.ScriptTarget.Latest,true,ts.ScriptKind.TSX);
45
+ const declaration=source.statements.filter(ts.isVariableStatement).flatMap(statement=>statement.declarationList.declarations).find(declaration=>ts.isIdentifier(declaration.name)&&declaration.name.text==="scenario");
46
+ let expression=declaration?.initializer;while(expression&&(ts.isAsExpression(expression)||ts.isSatisfiesExpression(expression)||ts.isParenthesizedExpression(expression)))expression=expression.expression;
47
+ if(expression&&ts.isObjectLiteralExpression(expression)) {
48
+ if(!expression.properties.some(property=>property.name?.getText()==="id")){const at=expression.getStart()+1;writeFileSync(resolve(from),text.slice(0,at)+`id:${JSON.stringify(scenarioIdentity(path.join(options.root,from),options.dsRoot))},`+text.slice(at));}
49
+ }else {renames.delete(from);notes.push(`Kept ${from}: its indirect metadata needs an explicit stable id before this rename.`);}
50
+ }
51
+ const runtime=owned.find(file=>!adsFileRole(file)&&!isScenarioFile(file)&&new RegExp(`(?:^|/)${name.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}\\.[cm]?[jt]sx?$`).test(file));
52
+ const directory=runtime?path.dirname(runtime):path.dirname(owned[0]??path.join(options.config.dsDir,prepared.unit,"entry.ts"));
53
+ const contractFile=slash(path.join(directory,`${name}.ads.contract.ts`));
54
+ const contract={schemaVersion:1},imports=[],references=new Map();
55
+ const compiler=ts.createProgram(files.filter(file=>/\.[cm]?[jt]sx?$/.test(file)).map(file=>path.resolve(options.root,file)),{...projectCompilerOptions(options),allowJs:true});
56
+ const checker=compiler.getTypeChecker(),apis=publicApi(compiler,options);
57
+ function importReference(file,symbol) {
58
+ const source=compiler.getSourceFile(path.resolve(options.root,file));
59
+ const module=source&&checker.getSymbolAtLocation(source);
60
+ const exported=module&&checker.getExportsOfModule(module).find(item=>item.name===symbol);
61
+ if(!exported)throw new Error(`${file}#${symbol} is not an importable internal export; keep the legacy section until its owner deliberately exposes a local contract reference.`);
62
+ const key=`${file}#${symbol}`;
63
+ if(!references.has(key)) {
64
+ const alias=`subject${references.size}`;references.set(key,alias);
65
+ let specifier=slash(path.relative(path.dirname(contractFile),file)).replace(/\.[cm]?[jt]sx?$/,"");if(!specifier.startsWith("."))specifier="./"+specifier;
66
+ imports.push(`import { ${symbol} as ${alias} } from ${JSON.stringify(specifier)};`);
67
+ }
68
+ return `__ads_reference_${references.get(key)}__`;
69
+ }
70
+ const remove=[];
71
+ if(!existsSync(resolve(contractFile)))for(const file of jsonFiles) {
72
+ const value=JSON.parse(readFileSync(resolve(file),"utf8"));
73
+ if(isBehaviorFile(file)) {
74
+ try {
75
+ const subjects={};
76
+ for(const [id,subject]of Object.entries(value.subjects??{})) {
77
+ if(subject.api) {const api=apis.find(api=>api.name===subject.api);if(!api)throw new Error(`Unknown API ${subject.api}`);subjects[id]={api:importReference(slash(path.relative(options.root,api.file)),api.declaration.name?.text??subject.api)};}
78
+ else if(subject.symbol)subjects[id]={implementation:importReference(slash(path.normalize(path.join(path.dirname(file),subject.source))),subject.symbol)};
79
+ else throw new Error(`Subject ${id} names a whole file. Select an actual implementation symbol before migrating this behavior section.`);
80
+ }
81
+ const {schemaVersion:_version,...behavior}=value;contract.behavior={...behavior,subjects};remove.push(file);
82
+ }catch(error){notes.push(error.message);}
83
+ }else {contract.tokens={namespace:value.namespace,stylesheet:"./"+slash(path.relative(path.dirname(contractFile),file.replace(/\.json$/,".css")))};remove.push(file);}
84
+ }
85
+ for(const file of files) {
86
+ if(!/\.[cm]?[jt]sx?$|\.json$|\.md$/.test(file)||remove.includes(file))continue;
87
+ const original=readFileSync(resolve(file),"utf8");let text=original;
88
+ for(const [before,after]of renames) {
89
+ let from=slash(path.relative(path.dirname(file),before)),to=slash(path.relative(path.dirname(renames.get(file)??file),after));
90
+ if(!from.startsWith("."))from="./"+from;if(!to.startsWith("."))to="./"+to;
91
+ for(const [left,right]of [[before,after],[from,to],[from.replace(/\.[cm]?[jt]sx?$/,""),to.replace(/\.[cm]?[jt]sx?$/,"")]]) {
92
+ for(const quote of ['"',"'","`"] )text=text.replaceAll(quote+left+quote,quote+right+quote);
93
+ if(file.endsWith(".md"))text=text.replaceAll(`](${left})`,`](${right})`);
94
+ }
95
+ }
96
+ if(text!==original)writeFileSync(resolve(file),text);
97
+ }
98
+ if(remove.length) {
99
+ if(contract.behavior)for(const check of Object.values(contract.behavior.checks)) {
100
+ const old=slash(path.normalize(path.join(directory,check.file))),next=renames.get(old);
101
+ if(next)check.file="./"+slash(path.relative(directory,next));
102
+ }
103
+ let text=JSON.stringify(contract,null,2);
104
+ for(const alias of references.values())text=text.replaceAll(JSON.stringify(`__ads_reference_${alias}__`),alias);
105
+ writeFileSync(resolve(contractFile),`import type { UnitContract } from "@reqvire-org/ads/contracts";\n${imports.filter(line=>[...references.values()].some(alias=>line.includes(` as ${alias} `)&&new RegExp(`\\b${alias}\\b`).test(text))).join("\n")}\nexport default ${text} satisfies UnitContract;\n`);
106
+ for(const file of remove) {const fsPath=resolve(file);renameSync(fsPath,path.join(base,"..",`migrated-${path.basename(file)}`));}
107
+ }else if(inspectUnitContracts(loadConfig(options.root)).contracts.some(contract=>contract.unit===prepared.unit))notes.push("Existing TypeScript contract retained. Only legacy file roles/references were renamed.");
108
+ for(const [from,to]of renames)renameSync(resolve(from),resolve(to));
109
+ return {...prepared,migration:{renamed:[...renames].map(([from,to])=>({from,to})),converted:remove,contractFile:remove.length?relative(resolve(contractFile)):null,notes},next:"Review the migrated draft and its references, then use action: diff and ads_apply_patch dryRun: true. Live source is unchanged; unresolved sections retain their legacy registration. Migrate one passing owner at a time."};
110
+ }
@@ -0,0 +1,22 @@
1
+ /** Development-only declaration types. ADS reads the contract AST without execution. */
2
+ export type Implementation = ((...args: never[]) => unknown) | (abstract new (...args: never[]) => unknown);
3
+ export interface BehaviorCheck { file: string; case: string; scenario?: string; profile: string; }
4
+ export interface UnitContract {
5
+ schemaVersion: 1;
6
+ tokens?: { namespace: string; stylesheet: string };
7
+ paintInputs?: readonly { api: Implementation; property: string; kind: "paint-reference"; bindings?: readonly string[] }[];
8
+ bindings?: Readonly<Record<string, {
9
+ input: { api: Implementation; property: string };
10
+ producer: Implementation;
11
+ valueFrom: string;
12
+ variable: string;
13
+ scope: { api: Implementation; selector: string };
14
+ checks: readonly string[];
15
+ }>>;
16
+ declarativeParts?: readonly { api: Implementation; parent: Implementation; consumer: Implementation; checks: readonly string[] }[];
17
+ behavior?: {
18
+ subjects: Readonly<Record<string, { api: Implementation } | { implementation: Implementation }>>;
19
+ guarantees: readonly { id: string; description: string; subjects: readonly string[]; checks: readonly string[] }[];
20
+ checks: Readonly<Record<string, BehaviorCheck>>;
21
+ };
22
+ }
package/dist/evidence.mjs CHANGED
@@ -12,7 +12,7 @@ export function acceptance(scope, passed, established, unverified) {
12
12
 
13
13
  export function architectureEvidence(result) {
14
14
  return { ...result,
15
- diagnostics: result.diagnostics.map(diagnostic => ({ ...diagnostic, failure: diagnostic.failure ?? failure("architecture", "source-analysis", diagnostic.rule) })),
15
+ diagnostics: result.diagnostics.map(diagnostic => ({ ...diagnostic, ...(/^(?:unit-contract|paint-.+|binding-.+|declarative-.+|development-role)$/.test(diagnostic.rule)&&!diagnostic.guidanceUri?{guidanceUri:"ads://guides/contracts"}:{}), failure: diagnostic.failure ?? failure("architecture", "source-analysis", diagnostic.rule) })),
16
16
  acceptance: acceptance("core-architecture", result.ok, result.ok ? ["core-source-guards"] : [], ["project-checks", "compiled-preview", "browser-behavior", "visual-parity", "accessibility-audit"]),
17
17
  };
18
18
  }
package/dist/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  export { initProject } from "./init.mjs";
2
2
  export { checkProject } from "./check.mjs";
3
3
  export { inspectTokens, tokenContract } from "./tokens.mjs";
4
+ export { inspectTokenOrigins } from "./token-origins.mjs";
4
5
  export { generateLintConfig } from "./lint-config.mjs";
5
6
  export { generateBrandAssets } from "./brand-assets.mjs";
6
7
  // Node-only helpers for registered project lint extensions. They reuse the
package/dist/init.mjs CHANGED
@@ -152,7 +152,7 @@ export function initProject(directory = process.cwd(), { toolPackage, packageMan
152
152
  return file;
153
153
  });
154
154
  if (!existingTokens.length) {
155
- for (const name of ["Foundation.scenario.tsx", "Foundation.fixture.ts"]) {
155
+ for (const name of ["Foundation.ads.scenario.tsx", "Foundation.ads.fixture.ts"]) {
156
156
  const content = readFileSync(path.join(starter, "design-system/tokens", name), "utf8").replaceAll('"@ds/review"', JSON.stringify(`${config.publicAlias}/review`)).replaceAll('"@ds"', JSON.stringify(config.publicAlias));
157
157
  addMissing(path.join(tokenRoot, name), content);
158
158
  }
@@ -5,6 +5,7 @@ import { isInside, sourceFiles } from "../config.mjs";
5
5
  import { projectCompilerOptions } from "../boundaries.mjs";
6
6
  import { publicApi } from "../public-api.mjs";
7
7
  import { projectDocumentFiles } from "../project-documents.mjs";
8
+ import { inspectUnitContracts } from "../unit-contracts.mjs";
8
9
 
9
10
  export const PROJECT_URI = "ads://project";
10
11
  export const sourceUri = (file) => `${PROJECT_URI}/files/${file.split("/").map(encodeURIComponent).join("/")}`;
@@ -32,7 +33,7 @@ export function inspectCatalog(options, scenarios = []) {
32
33
  const { root, dsRoot, config } = options;
33
34
  const entry = path.join(dsRoot, config.publicEntry);
34
35
  if (!existsSync(entry)) return [];
35
- const program = ts.createProgram([entry], { ...projectCompilerOptions(options), allowJs: true });
36
+ const program = inspectUnitContracts(options).program ?? ts.createProgram([entry], { ...projectCompilerOptions(options), allowJs: true });
36
37
  const checker = program.getTypeChecker();
37
38
  const source = program.getSourceFile(entry);
38
39
  const module = source && checker.getSymbolAtLocation(source);
@@ -40,6 +41,7 @@ export function inspectCatalog(options, scenarios = []) {
40
41
  const documents = projectDocuments(options);
41
42
  const items = [];
42
43
  const api = new Map(publicApi(program, options).map(item => [item.name, item]));
44
+ const contracts = inspectUnitContracts(options).contracts;
43
45
  for (const exported of checker.getExportsOfModule(module)) {
44
46
  let symbol = exported;
45
47
  if (symbol.flags & ts.SymbolFlags.Alias) symbol = checker.getAliasedSymbol(symbol);
@@ -54,7 +56,12 @@ export function inspectCatalog(options, scenarios = []) {
54
56
  const relative = path.relative(root, file).replaceAll("\\", "/");
55
57
  const line = declaration.getSourceFile().getLineAndCharacterOfPosition(declaration.getStart()).line + 1;
56
58
  const guides = documents.filter((doc) => doc.file.endsWith(".md") && path.dirname(doc.file) === path.dirname(file)).map((doc) => doc.uri);
57
- items.push({ name: exported.name, kind, apiKind: ui ? "ui" : "behavior", importFrom: config.publicAlias, file: relative, line, sourceUri: sourceUri(relative), guides,
59
+ const contract=contracts.find(contract=>[...(contract.paintInputs??[]),...(contract.declarativeParts??[])].some(input=>input.apiName===exported.name)||Object.values(contract.bindings??{}).some(binding=>binding.apiName===exported.name));
60
+ const declarative=contract?.declarativeParts?.find(part=>part.apiName===exported.name);
61
+ const bindings=Object.entries(contract?.bindings??{}).filter(([,binding])=>binding.apiName===exported.name);
62
+ items.push({ name: exported.name, kind, apiKind: declarative ? "declarative" : ui ? "ui" : "behavior", importFrom: config.publicAlias, file: relative, line, sourceUri: sourceUri(relative), guides,
63
+ ...(contract?{contractUri:sourceUri(contract.file),paintInputCount:contract.paintInputs?.filter(input=>input.apiName===exported.name).length??0,paintInputs:contract.paintInputs?.filter(input=>input.apiName===exported.name).slice(0,8).map(input=>({property:input.property,kind:input.kind,bindings:input.bindings??[]})),...(declarative?{consumer:{parent:declarative.parentName,sourceUri:sourceUri(declarative.consumer.source),symbol:declarative.consumer.symbol,checks:declarative.checks.slice(0,8),checkCount:declarative.checks.length},evidence:"registered relationship; source and browser verification are separate"}:{})}:{}),
64
+ ...(bindings.length?{bindingCount:bindings.length,bindings:bindings.slice(0,4).map(([id,binding])=>({id,variable:binding.variable,scope:binding.scope.selector,producer:{sourceUri:sourceUri(binding.producer.source),symbol:binding.producer.symbol},checks:binding.checks.slice(0,8),checkCount:binding.checks.length})),bindingEvidence:"registered; verify source diagnostics and current browser checks"}:{}),
58
65
  previews: scenarios.filter(item => item.covers.includes(exported.name) || item.exercises?.includes(exported.name)).map(item => item.id) });
59
66
  }
60
67
  return items.sort((a, b) => a.name.localeCompare(b.name));
@@ -57,7 +57,7 @@ Use this server for UI components/patterns, tokens/themes/branding, layout, inte
57
57
  3. Select one workflow with ads_skills, then ads_read_skill. ${route.skills.map((skill) => `${skill.task}: ${skill.name}`).join("; ")}. Native skill activation is host-controlled. Relative skill references resolve from its SKILL.md URI. Read support files only when relevant.
58
58
  4. ${route.preview} Token samples: ads_scenarios(kind: tokens), then ads_show_scenario or ads_show_tokens(category: id). Token source inspection: ads_tokens with a focused query. Registered previews: ${previews.map((preview) => `${preview.id}: ${preview.tool ?? "ads_show_scenario"}`).join(", ") || "none"}. Showcase URL: ${context.showcase.url ?? "not configured; use the project's showcase command"}.
59
59
  Inspection connects a public export to its owning source, available guides, token values and discovered preview IDs. Bounded tools keep discovery focused; explicit resource reads supply detail. Source metadata does not prove layout, interaction or motion. A scenario renders the real public implementation and its fixture context in either review surface.
60
- 5. Check ads_status.mutations. When enabled, discover one owner with ads_units, read relevant source ranges with ads_read_file and submit only a Git unified diff through ads_apply_patch({unit, patch, dryRun}). Include that owner's scenarios, fixtures and required public re-exports, never peer units. tokens is the foundation unit; its manifest lists companionFiles for required canonical foundation imports when token files are created/deleted in the same patch. Preserve unrelated imports/comments and public-entry statements; a real selected-owner re-export can remove its redundant export {};. Dry-run to inspect validation; apply only a passing patch. Fix rejected patches in the owning unit; use ads_apply_transaction for coupled owners instead of bypassing rejection with direct writes. ads_prepare_patch creates a local owner draft/diff; ads_prepare_token_samples reconciles explicit project bindings without guessing visual meaning. Intentional project setup, registered guard/compiler code and exceptions use configuration; shared globals use foundation, guides documentation and assets their discovered assets/* unit. Keep mandatory ADS gates. Binary/large Git diffs may use patchFile under .ds/patches/ to avoid model-context bytes. In read-only mode, source edits use the client editor/filesystem tools. Reuse the local public API (${context.publicEntry}). Edit presentation in its ADS owner; keep application data/services/routing in application code and development fixtures beside their scenario; larger scenarios live in composition/. Demo framing and token renderers live in development-only review/, imported through ${context.review.importFrom}; never export them from the production barrel or import them into application runtime. Read ads://guides/review for guarded owners, aliases and migration. Linaria extracts owned styling. Inspect relevant tokens with ads_tokens and source/owning guides before edits.
60
+ 5. Check ads_status.mutations. When enabled, discover one owner with ads_units, read relevant source ranges with ads_read_file and submit only a Git unified diff through ads_apply_patch({unit, patch, dryRun}). Include that owner's scenarios, fixtures and required public re-exports, never peer units. tokens is the foundation unit; its manifest lists companionFiles for required canonical foundation imports when token files are created/deleted in the same patch. Preserve unrelated imports/comments and public-entry statements; a real selected-owner re-export can remove its redundant export {};. Dry-run to inspect validation; apply only a passing patch. Fix rejected patches in the owning unit; use ads_apply_transaction for coupled owners instead of bypassing rejection with direct writes. ads_prepare_patch creates a local owner draft/diff; ads_prepare_token_samples reconciles explicit project bindings without guessing visual meaning. Intentional project setup, registered guard/compiler code and exceptions use configuration; shared globals use foundation, guides documentation and assets their discovered assets/* unit. Keep mandatory ADS gates. Binary/large Git diffs may use patchFile under .ds/patches/ to avoid model-context bytes. In read-only mode, source edits use the client editor/filesystem tools. Reuse the local public API (${context.publicEntry}). Edit presentation in its ADS owner; keep application data/services/routing in application code and development fixtures beside their scenario; larger scenarios live in composition/. Demo framing and token renderers live in development-only review/, imported through ${context.review.importFrom}; never export them from the production barrel or import them into application runtime. Read ads://guides/review for review owners/aliases and ads://guides/contracts when editing static owner declarations, .ads. development roles, constrained paints/bindings or declarative APIs. Contract registration, source evidence and browser verification remain separate. Use ads_prepare_patch action: migrate for a reviewable owner draft, then normal diff and guarded application. Linaria extracts owned styling. Inspect relevant tokens with ads_tokens and source/owning guides before edits.
61
61
  6. ${route.validation} Registered commands: ${context.commands.map((command) => command.args?.join(" ") ?? `choose the project package manager for ${command.script}`).join("; ") || "none discovered; inspect the project's build setup"}. Run them from ${context.projectRoot}.
62
62
  Execution replies from ads_verify and guarded edits contain counts, acceptance and resultId, not full reports. Read only needed outcomes/diagnostics/captures with ads_results, following nextCursor with identical filters. A truncated row has recordIndex for bounded text ranges. For one failure, use ads_investigate with its unit/check/runId; read request/error/artifact sections only when needed. Only the latest verification and mutation results, and latest investigation per check, are available. Notifications do not establish freshness.
63
63
 
@@ -5,6 +5,7 @@ import path from "node:path";
5
5
  import { isInside } from "../config.mjs";
6
6
  import { checkUnitFiles, inspectUnits, validateUnitId } from "../units.mjs";
7
7
  import { textDiff } from "./text-diff.mjs";
8
+ import { migrateContractDraft } from "../contract-migration.mjs";
8
9
 
9
10
  const hash = value => createHash("sha256").update(value).digest("hex");
10
11
  const binary = bytes => bytes?.includes(0) || bytes && !Buffer.from(bytes.toString("utf8")).equals(bytes);
@@ -26,8 +27,9 @@ export function storePreparedPatch(options, patch) {
26
27
  export function prepareUnitPatch(options, {unit, action = "create", draft}) {
27
28
  validateUnitId(unit);
28
29
  const inventory = inspectUnits(options);
29
- if (action === "create") {
30
+ if (action === "create" || action === "migrate") {
30
31
  const owner = inventory.units.find(entry => entry.id === unit);
32
+ if(action==="migrate"&&!owner)throw new Error("Discover an existing owner before preparing its contract migration.");
31
33
  const files = [...new Set([...(owner?.files ?? []), ...(owner?.companionFiles ?? []).map(entry => entry.path),
32
34
  ...(!owner ? ["ds.config.json"] : []),
33
35
  ...(/^(?:components|patterns|controllers|review)\//.test(unit) ? [path.relative(options.root,path.join(options.dsRoot,unit.startsWith("review/")?options.config.reviewEntry:options.config.publicEntry)).replaceAll("\\","/")] : [])])];
@@ -58,7 +60,8 @@ export function prepareUnitPatch(options, {unit, action = "create", draft}) {
58
60
  }
59
61
  mkdirSync(folder,{recursive:true});
60
62
  writeFileSync(base,JSON.stringify({schemaVersion:1,unit,entries}));
61
- return {draft,unit,directory:folder,files, next:"Edit only this draft, then call ads_prepare_patch with action: diff and this draft ID. Live source is unchanged; the resulting Git patch still requires ads_apply_patch and all its gates."};
63
+ const prepared={draft,unit,directory:folder,files, next:"Edit only this draft, then call ads_prepare_patch with action: diff and this draft ID. Live source is unchanged; the resulting Git patch still requires ads_apply_patch and all its gates."};
64
+ return action === "migrate" ? migrateContractDraft(options,prepared) : prepared;
62
65
  }
63
66
  if (!/^[a-f0-9-]{36}$/.test(draft ?? "")) throw new Error("Use the draft ID returned by ads_prepare_patch.");
64
67
  const base = path.join(options.root,".ds/patch-drafts",draft);
@@ -66,7 +69,7 @@ export function prepareUnitPatch(options, {unit, action = "create", draft}) {
66
69
  const metadata = JSON.parse(readFileSync(path.join(base,"manifest.json"),"utf8"));
67
70
  if (metadata.schemaVersion !== 1 || metadata.unit !== unit) throw new Error("Draft and selected unit do not match.");
68
71
  if (action === "discard") {rmSync(base,{recursive:true});return {discarded:true,draft,unit};}
69
- if (action !== "diff") throw new Error("Choose create, diff or discard.");
72
+ if (action !== "diff") throw new Error("Choose create, migrate, diff or discard.");
70
73
  const files = new Set(metadata.entries.map(entry => entry.file));
71
74
  function walk(directory) {
72
75
  if (realpathSync(directory) !== directory) throw new Error("Draft directories cannot resolve through symlinks.");