@reqvire-org/ads 0.3.18 → 0.3.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/README.md +2 -0
  2. package/dist/behaviors.mjs +19 -9
  3. package/dist/bootstrap-ds/ADS.md +2 -2
  4. package/dist/bootstrap-ds/README.md +2 -2
  5. package/dist/bootstrap-ds/design-system/index.ts +0 -1
  6. package/dist/bootstrap-ds/design-system/review/index.ts +1 -0
  7. package/dist/bootstrap-ds/design-system/tokens/{Foundation.scenario.tsx → Foundation.ads.scenario.tsx} +2 -2
  8. package/dist/bootstrap-ds/ds.config.json +1 -1
  9. package/dist/bootstrap-ds/tsconfig.build.json +9 -2
  10. package/dist/bootstrap-ds/tsconfig.json +18 -3
  11. package/dist/bootstrap-ds/vite.config.ts +1 -1
  12. package/dist/boundaries.mjs +6 -2
  13. package/dist/check.mjs +44 -17
  14. package/dist/config.mjs +33 -5
  15. package/dist/contract-migration.mjs +110 -0
  16. package/dist/contracts.d.mts +22 -0
  17. package/dist/evidence.mjs +1 -1
  18. package/dist/index.mjs +1 -0
  19. package/dist/init.mjs +18 -15
  20. package/dist/layers.mjs +288 -0
  21. package/dist/lint-config.mjs +1 -1
  22. package/dist/mcp/catalog.mjs +9 -2
  23. package/dist/mcp/context.mjs +2 -1
  24. package/dist/mcp/edit-preparation.mjs +9 -6
  25. package/dist/mcp/mutations.mjs +1 -1
  26. package/dist/mcp/preview-widget.bundle.js +41 -39
  27. package/dist/mcp/prompts.mjs +1 -1
  28. package/dist/mcp/server.mjs +9 -3
  29. package/dist/mcp/widget.bundle.js +1 -1
  30. package/dist/oxlint.mjs +3 -1
  31. package/dist/paint-contracts.mjs +124 -0
  32. package/dist/preview-resources.mjs +12 -2
  33. package/dist/public-api.mjs +13 -6
  34. package/dist/public-props.mjs +2 -2
  35. package/dist/runtime-contracts.mjs +225 -0
  36. package/dist/scenario-trace.mjs +37 -9
  37. package/dist/scenarios-vite.mjs +3 -2
  38. package/dist/scenarios.mjs +89 -30
  39. package/dist/scoped-tokens.mjs +9 -4
  40. package/dist/showcase/client.bundle.js +1 -1
  41. package/dist/showcase/sandbox.bundle.js +1 -1
  42. package/dist/skills/ads-break-ui/SKILL.md +25 -6
  43. package/dist/skills/ads-break-ui/references/contracts.md +250 -0
  44. package/dist/skills/ads-break-ui/references/guarded-editing.md +1 -1
  45. package/dist/skills/ads-break-ui/references/layers.md +123 -0
  46. package/dist/skills/ads-break-ui/references/review.md +96 -0
  47. package/dist/skills/ads-break-ui/references/scenarios.md +24 -3
  48. package/dist/skills/ads-designer/SKILL.md +26 -7
  49. package/dist/skills/ads-designer/assets/bootstrap-ds/ADS.md +2 -2
  50. package/dist/skills/ads-designer/assets/bootstrap-ds/README.md +2 -2
  51. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/index.ts +0 -1
  52. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/review/index.ts +1 -0
  53. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/{Foundation.scenario.tsx → Foundation.ads.scenario.tsx} +2 -2
  54. package/dist/skills/ads-designer/assets/bootstrap-ds/ds.config.json +1 -1
  55. package/dist/skills/ads-designer/assets/bootstrap-ds/tsconfig.build.json +9 -2
  56. package/dist/skills/ads-designer/assets/bootstrap-ds/tsconfig.json +18 -3
  57. package/dist/skills/ads-designer/assets/bootstrap-ds/vite.config.ts +1 -1
  58. package/dist/skills/ads-designer/references/contracts.md +250 -0
  59. package/dist/skills/ads-designer/references/guarded-editing.md +1 -1
  60. package/dist/skills/ads-designer/references/layers.md +123 -0
  61. package/dist/skills/ads-designer/references/review.md +96 -0
  62. package/dist/skills/ads-designer/references/scenarios.md +24 -3
  63. package/dist/skills/ads-import/SKILL.md +25 -6
  64. package/dist/skills/ads-import/references/contracts.md +250 -0
  65. package/dist/skills/ads-import/references/guarded-editing.md +1 -1
  66. package/dist/skills/ads-import/references/layers.md +123 -0
  67. package/dist/skills/ads-import/references/review.md +96 -0
  68. package/dist/skills/ads-import/references/scenarios.md +24 -3
  69. package/dist/skills/ads-migrate/SKILL.md +25 -6
  70. package/dist/skills/ads-migrate/references/contracts.md +250 -0
  71. package/dist/skills/ads-migrate/references/guarded-editing.md +1 -1
  72. package/dist/skills/ads-migrate/references/layers.md +123 -0
  73. package/dist/skills/ads-migrate/references/review.md +96 -0
  74. package/dist/skills/ads-migrate/references/scenarios.md +24 -3
  75. package/dist/skills/ads-motion/SKILL.md +26 -7
  76. package/dist/skills/ads-motion/references/audit.md +1 -1
  77. package/dist/skills/ads-motion/references/contracts.md +250 -0
  78. package/dist/skills/ads-motion/references/guarded-editing.md +1 -1
  79. package/dist/skills/ads-motion/references/layers.md +123 -0
  80. package/dist/skills/ads-motion/references/motion-review.md +35 -0
  81. package/dist/skills/ads-motion/references/review.md +96 -35
  82. package/dist/skills/ads-motion/references/scenarios.md +24 -3
  83. package/dist/token-origins.mjs +24 -0
  84. package/dist/tokens.mjs +4 -3
  85. package/dist/unit-contracts.mjs +203 -0
  86. package/dist/units.mjs +26 -17
  87. package/dist/verification/browser.mjs +156 -26
  88. package/dist/verification/playwright-reporter.cjs +13 -3
  89. package/docs/ADS.md +40 -6
  90. package/docs/ARCHITECTURE.md +73 -4
  91. package/docs/BEHAVIORS.md +15 -0
  92. package/docs/CHANGELOG.md +42 -0
  93. package/docs/CONTRACTS.md +250 -0
  94. package/docs/GUIDE.md +29 -3
  95. package/docs/MCP.md +42 -6
  96. package/package.json +4 -1
  97. package/dist/bootstrap-ds/design-system/patterns/token-preview/Scenario.tsx +0 -4
  98. package/dist/bootstrap-ds/design-system/patterns/token-preview/fixture.ts +0 -8
  99. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/patterns/token-preview/Scenario.tsx +0 -4
  100. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/patterns/token-preview/fixture.ts +0 -8
  101. /package/dist/bootstrap-ds/design-system/{patterns → review}/token-preview/TokenPreview.tsx +0 -0
  102. /package/dist/bootstrap-ds/design-system/tokens/{Foundation.fixture.ts → Foundation.ads.fixture.ts} +0 -0
  103. /package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/{patterns → review}/token-preview/TokenPreview.tsx +0 -0
  104. /package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/{Foundation.fixture.ts → Foundation.ads.fixture.ts} +0 -0
@@ -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,16 +152,16 @@ 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"]) {
156
- const content = readFileSync(path.join(starter, "design-system/tokens", name), "utf8").replaceAll('"@ds"', JSON.stringify(config.publicAlias));
155
+ for (const name of ["Foundation.ads.scenario.tsx", "Foundation.ads.fixture.ts"]) {
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
  }
159
- for (const name of ["TokenPreview.tsx", "Scenario.tsx", "fixture.ts"]) {
160
- let content = readFileSync(path.join(starter, "design-system/patterns/token-preview", name), "utf8");
159
+ for (const name of ["TokenPreview.tsx"]) {
160
+ let content = readFileSync(path.join(starter, "design-system/review/token-preview", name), "utf8");
161
161
  if (selectedFramework === "preact") content = content.replace('from "react"', 'from "preact/hooks"');
162
- addMissing(path.join(dsRoot, "patterns/token-preview", name), content.replaceAll('"@ds"', JSON.stringify(config.publicAlias)));
162
+ addMissing(path.join(dsRoot, "review/token-preview", name), content.replaceAll('"@ds"', JSON.stringify(config.publicAlias)));
163
163
  }
164
- const reviewFile = path.relative(root, path.join(dsRoot, "patterns/token-preview/TokenPreview.tsx")).replaceAll("\\", "/");
164
+ const reviewFile = path.relative(root, path.join(dsRoot, "review/token-preview/TokenPreview.tsx")).replaceAll("\\", "/");
165
165
  if (!config.boundaries.styleWrites.some(item => item.file === reviewFile)) config.boundaries.styleWrites.push({ file: reviewFile, properties: ["--ux-token-preview-value"] });
166
166
  }
167
167
  tokenFiles.sort((a, b) => foundationFiles.indexOf(path.basename(a)) - foundationFiles.indexOf(path.basename(b)) || a.localeCompare(b));
@@ -172,16 +172,19 @@ export function initProject(directory = process.cwd(), { toolPackage, packageMan
172
172
  };
173
173
  addMissing(stylesFile, tokenFiles.map((file) => `@import ${JSON.stringify(relativeImport(stylesFile, file))};`).join("\n") + "\n");
174
174
  const entryFile = path.join(dsRoot, config.publicEntry);
175
- const reviewExport = !existingTokens.length ? `export { TokenPreview } from ${JSON.stringify(relativeImport(entryFile, path.join(dsRoot, "patterns/token-preview/TokenPreview")))};\n` : "";
176
- addMissing(entryFile, `// Export your project's public components and types here.\nimport ${JSON.stringify(relativeImport(entryFile, stylesFile))};\n${reviewExport}`);
177
- if (reviewExport && existsSync(entryFile) && !readFileSync(entryFile, "utf8").includes("patterns/token-preview/TokenPreview")) {
178
- let current = readFileSync(entryFile, "utf8");
179
- if (/\bTokenPreview\b/.test(current)) throw new Error("TokenPreview is already owned by this project. Add a token review scenario using its public API before initializing the foundation.");
180
- const ast = ts.createSourceFile(entryFile, current, ts.ScriptTarget.Latest, true);
181
- for (const statement of [...ast.statements].reverse()) if (ts.isExportDeclaration(statement) && !statement.moduleSpecifier && statement.exportClause && ts.isNamedExports(statement.exportClause) && !statement.exportClause.elements.length) {
182
- current = current.slice(0, statement.getStart(ast)) + current.slice(statement.end);
175
+ addMissing(entryFile, `// Export your project's public components and types here.\nimport ${JSON.stringify(relativeImport(entryFile, stylesFile))};\n`);
176
+ if (!existingTokens.length) {
177
+ const reviewEntry = path.join(dsRoot, config.reviewEntry);
178
+ const reviewExport = `export { TokenPreview } from ${JSON.stringify(relativeImport(reviewEntry, path.join(dsRoot, "review/token-preview/TokenPreview")))};\n`;
179
+ addMissing(reviewEntry, reviewExport);
180
+ if (existsSync(reviewEntry) && !readFileSync(reviewEntry, "utf8").includes("token-preview/TokenPreview")) throw new Error("The project owns an existing review entry. Add the new token renderer through that development API before initializing the foundation.");
181
+ if (existsSync(entryFile)) {
182
+ let current = readFileSync(entryFile, "utf8");
183
+ const ast = ts.createSourceFile(entryFile, current, ts.ScriptTarget.Latest, true);
184
+ for (const statement of [...ast.statements].reverse()) if (ts.isExportDeclaration(statement) && !statement.moduleSpecifier && statement.exportClause && ts.isNamedExports(statement.exportClause) && !statement.exportClause.elements.length) current = current.slice(0, statement.getStart(ast)) + current.slice(statement.end);
185
+ if (!ast.statements.some(statement => ts.isImportDeclaration(statement) || statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword))) current += `\nimport ${JSON.stringify(relativeImport(entryFile, stylesFile))};\n`;
186
+ if (current !== readFileSync(entryFile, "utf8")) updates.set(entryFile, current);
183
187
  }
184
- updates.set(entryFile, current + "\n" + reviewExport);
185
188
  }
186
189
  for (const section of ["components", "patterns", "controllers", "composition"]) addMissing(path.join(dsRoot, section, ".gitkeep"), "");
187
190
  addMissing(path.join(dsRoot, "assets/brand/README.md"), readFileSync(path.join(starter, "design-system/assets/brand/README.md")));
@@ -0,0 +1,288 @@
1
+ import path from "node:path";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import ts from "typescript";
4
+ import selectorParser from "postcss-selector-parser";
5
+ import valueParser from "postcss-value-parser";
6
+ import { isDevelopmentFile, isInside, isStateVariable, isTestFile, isReviewFile } from "./config.mjs";
7
+ import { importResolver, styleAccessProperty } from "./boundaries.mjs";
8
+
9
+ const guidance = "See ads://guides/layers. A component provides one cohesive UI capability; a pattern arranges capabilities into a reusable structure or workflow. Component count does not decide the layer.";
10
+
11
+ /** Include statically addressed local helpers outside the configured source roots.
12
+ * Dependency packages remain external; no project module is imported/executed. */
13
+ export function layerSourceFiles(initial, options) {
14
+ const files = new Set(initial), resolve = importResolver(options);
15
+ for (const file of files) {
16
+ const source = ts.createSourceFile(file, readFileSync(file, "utf8"), ts.ScriptTarget.Latest, true);
17
+ function include(literal) {
18
+ if (!literal || !ts.isStringLiteralLike(literal)) return;
19
+ for (const dependency of resolve(literal.text, file)) {
20
+ if (isInside(options.root, dependency) && !dependency.split(path.sep).includes("node_modules") && /\.[cm]?[jt]sx?$/.test(dependency) && existsSync(dependency)) files.add(dependency);
21
+ }
22
+ }
23
+ function visit(node) {
24
+ if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) include(node.moduleSpecifier);
25
+ if (ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference)) include(node.moduleReference.expression);
26
+ if (ts.isImportTypeNode(node) && ts.isLiteralTypeNode(node.argument)) include(node.argument.literal);
27
+ if (ts.isCallExpression(node) && (node.expression.kind === ts.SyntaxKind.ImportKeyword || ts.isIdentifier(node.expression) && node.expression.text === "require")) include(node.arguments[0]);
28
+ ts.forEachChild(node, visit);
29
+ }
30
+ visit(source);
31
+ }
32
+ return [...files];
33
+ }
34
+
35
+ /** Source-only dependency and private-style evidence; never evaluates project code. */
36
+ export function createLayerBoundaries(program, options, report) {
37
+ const { root, dsRoot, sourceRoot, config } = options;
38
+ const checker = program.getTypeChecker(), resolve = importResolver(options);
39
+ const sources = program.getSourceFiles().filter(source => isInside(root, source.fileName) && !source.isDeclarationFile && !source.fileName.split(path.sep).includes("node_modules"));
40
+ const runtime = file => !isTestFile(file) && !isDevelopmentFile(file, options);
41
+ const layer = file => ["components", "patterns", "controllers", "composition", "review"].find(name => isInside(path.join(dsRoot, name), file)) ?? (isInside(sourceRoot, file) && !isInside(dsRoot, file) ? "application" : "shared");
42
+ const unalias = symbol => symbol?.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol;
43
+ const entry = program.getSourceFile(path.join(dsRoot, config.publicEntry));
44
+ const module = entry && checker.getSymbolAtLocation(entry);
45
+ const publicSymbols = new Set((module ? checker.getExportsOfModule(module) : []).map(unalias));
46
+ const publicFiles = new Set([...publicSymbols].filter(symbol => symbol?.flags & ts.SymbolFlags.Value).flatMap(symbol => symbol.declarations?.map(node => node.getSourceFile().fileName) ?? []));
47
+ const edges = new Map(), emitted = new Set(), classOwners = new Map(), attributeOwners = new Map(), componentVariables = new Set(), patternVariables = new Set(), publicHooks = new Set();
48
+
49
+ function emit(node, rule, message) {
50
+ const source = node.getSourceFile(), position = source.getLineAndCharacterOfPosition(node.getStart(source));
51
+ const key = `${source.fileName}:${node.pos}:${rule}:${message}`;
52
+ if (emitted.has(key)) return;
53
+ emitted.add(key);
54
+ report(source.fileName, rule, `${message} ${guidance}`, position.line + 1, position.character + 1, { guidanceUri: "ads://guides/layers" });
55
+ }
56
+ function target(specifier, source) {
57
+ return resolve(specifier, source.fileName).find(file => program.getSourceFile(file));
58
+ }
59
+ function exportsOf(file) {
60
+ const source = file && program.getSourceFile(file), symbol = source && checker.getSymbolAtLocation(source);
61
+ return symbol ? checker.getExportsOfModule(symbol) : [];
62
+ }
63
+ function origins(symbol) {
64
+ symbol = unalias(symbol);
65
+ return [...new Set(symbol?.declarations?.map(node => node.getSourceFile().fileName) ?? [])].filter(file => isInside(root, file) && !file.split(path.sep).includes("node_modules")).map(file => ({ file, symbol }));
66
+ }
67
+ function bindings(pattern) {
68
+ return ts.isObjectBindingPattern(pattern) ? pattern.elements.filter(node => !node.dotDotDotToken).map(node => (node.propertyName ?? node.name).text).filter(Boolean) : null;
69
+ }
70
+ function members(identifier, source) {
71
+ const symbol = checker.getSymbolAtLocation(identifier), names = new Set();
72
+ let opaque = false;
73
+ function visit(node) {
74
+ if (ts.isIdentifier(node) && node !== identifier && checker.getSymbolAtLocation(node) === symbol) {
75
+ const parent = node.parent;
76
+ if (ts.isPropertyAccessExpression(parent) && parent.expression === node) names.add(parent.name.text);
77
+ else if (ts.isElementAccessExpression(parent) && parent.expression === node && ts.isStringLiteralLike(parent.argumentExpression)) names.add(parent.argumentExpression.text);
78
+ else if (ts.isVariableDeclaration(parent) && parent.initializer === node && bindings(parent.name)) for (const name of bindings(parent.name)) names.add(name);
79
+ else opaque = true;
80
+ }
81
+ ts.forEachChild(node, visit);
82
+ }
83
+ visit(source);
84
+ return opaque ? null : [...names];
85
+ }
86
+ function selectedImport(call, source) {
87
+ let expression = call;
88
+ while (ts.isAwaitExpression(expression.parent) || ts.isParenthesizedExpression(expression.parent)) expression = expression.parent;
89
+ const parent = expression.parent;
90
+ if (ts.isPropertyAccessExpression(parent) && parent.expression === expression && parent.name.text !== "then") return [parent.name.text];
91
+ if (ts.isElementAccessExpression(parent) && parent.expression === expression && ts.isStringLiteralLike(parent.argumentExpression)) return [parent.argumentExpression.text];
92
+ if (ts.isVariableDeclaration(parent)) return bindings(parent.name) ?? (ts.isIdentifier(parent.name) ? members(parent.name, source) : null);
93
+ if (ts.isPropertyAccessExpression(parent) && parent.name.text === "then" && ts.isCallExpression(parent.parent)) {
94
+ const callback = parent.parent.arguments[0];
95
+ if (callback && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) && callback.parameters[0]) {
96
+ const parameter = callback.parameters[0].name;
97
+ return bindings(parameter) ?? (ts.isIdentifier(parameter) ? members(parameter, source) : null);
98
+ }
99
+ }
100
+ return null;
101
+ }
102
+ function add(source, node, dependency, typeOnly = false) {
103
+ if (!dependency?.file || !isInside(root, dependency.file) || dependency.file.split(path.sep).includes("node_modules")) return;
104
+ const list = edges.get(source.fileName) ?? [];
105
+ list.push({ ...dependency, node, typeOnly });
106
+ edges.set(source.fileName, list);
107
+ }
108
+ function addSymbol(source, node, symbol, typeOnly = false) {
109
+ for (const origin of origins(symbol)) add(source, node, origin, typeOnly);
110
+ }
111
+ function addModule(source, node, file, names, typeOnly = false) {
112
+ if (!file) return;
113
+ const values = exportsOf(file).filter(symbol => names === null || names.includes(symbol.name));
114
+ for (const symbol of values) if (typeOnly || unalias(symbol)?.flags & ts.SymbolFlags.Value) addSymbol(source, node, symbol, typeOnly);
115
+ if (!values.length || layer(file) === "composition" || isDevelopmentFile(file, options)) add(source, node, { file }, typeOnly);
116
+ // Explicit side effects in a forwarding barrel still run for a selected export.
117
+ if (!typeOnly) for (const statement of program.getSourceFile(file)?.statements ?? []) {
118
+ if (ts.isImportDeclaration(statement) && !statement.importClause && ts.isStringLiteralLike(statement.moduleSpecifier)) {
119
+ const sideEffect = target(statement.moduleSpecifier.text, statement.getSourceFile());
120
+ if (sideEffect) add(source, node, { file: sideEffect });
121
+ }
122
+ }
123
+ }
124
+ for (const source of sources) {
125
+ function visit(node) {
126
+ if (ts.isImportDeclaration(node) && ts.isStringLiteralLike(node.moduleSpecifier)) {
127
+ const file = target(node.moduleSpecifier.text, source), clause = node.importClause;
128
+ if (!clause) add(source, node, { file: file ?? resolve(node.moduleSpecifier.text, source.fileName)[0] });
129
+ else {
130
+ if (clause.name) addSymbol(source, node, checker.getSymbolAtLocation(clause.name), !!clause.isTypeOnly);
131
+ const named = clause.namedBindings;
132
+ if (named && ts.isNamedImports(named)) for (const item of named.elements) addSymbol(source, item, checker.getSymbolAtLocation(item.name), !!(clause.isTypeOnly || item.isTypeOnly));
133
+ if (named && ts.isNamespaceImport(named)) addModule(source, node, file, members(named.name, source), !!clause.isTypeOnly);
134
+ }
135
+ }
136
+ if (ts.isExportDeclaration(node)) {
137
+ if (node.exportClause && ts.isNamedExports(node.exportClause)) for (const item of node.exportClause.elements) addSymbol(source, item, checker.getSymbolAtLocation(item.name), !!(node.isTypeOnly || item.isTypeOnly));
138
+ else if (node.moduleSpecifier && ts.isStringLiteralLike(node.moduleSpecifier)) addModule(source, node, target(node.moduleSpecifier.text, source), null, !!node.isTypeOnly);
139
+ }
140
+ if (ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference) && node.moduleReference.expression && ts.isStringLiteralLike(node.moduleReference.expression)) {
141
+ addModule(source, node, target(node.moduleReference.expression.text, source), members(node.name, source), !!node.isTypeOnly);
142
+ }
143
+ if (ts.isImportTypeNode(node) && ts.isLiteralTypeNode(node.argument) && ts.isStringLiteralLike(node.argument.literal)) addModule(source, node, target(node.argument.literal.text, source), null, true);
144
+ const requireSymbol = ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "require" ? checker.getSymbolAtLocation(node.expression) : null;
145
+ if (ts.isCallExpression(node) && (node.expression.kind === ts.SyntaxKind.ImportKeyword || ts.isIdentifier(node.expression) && node.expression.text === "require" && (!requireSymbol || requireSymbol.declarations?.every(declaration => declaration.getSourceFile().isDeclarationFile))) && ts.isStringLiteralLike(node.arguments[0])) {
146
+ addModule(source, node, target(node.arguments[0].text, source), selectedImport(node, source));
147
+ }
148
+ ts.forEachChild(node, visit);
149
+ }
150
+ visit(source);
151
+ }
152
+
153
+ function runtimeHelpers() {
154
+ const found = new Set(sources.filter(source => isReviewFile(source.fileName, options) || runtime(source.fileName) && ["components", "patterns", "controllers"].includes(layer(source.fileName))).map(source => source.fileName));
155
+ for (const file of found) for (const edge of edges.get(file) ?? []) if (!edge.typeOnly && runtime(edge.file) && program.getSourceFile(edge.file)) found.add(edge.file);
156
+ return [...found];
157
+ }
158
+
159
+ function inspectDependencies() {
160
+ for (const source of sources.filter(source => runtime(source.fileName) || isReviewFile(source.fileName, options))) {
161
+ const ownLayer = layer(source.fileName);
162
+ // Check resolved declaration ownership too: a barrel cannot launder fixtures/app code.
163
+ if (isInside(dsRoot, source.fileName) || ownLayer === "application") for (const edge of edges.get(source.fileName) ?? []) {
164
+ if (runtime(source.fileName) && layer(edge.file) === "review") emit(edge.node, "review-import", "Production code and its public entry cannot depend on review implementations or their types, including through aliases and re-exports. Use the development review entry only from scenarios, fixtures and tests.");
165
+ if (edge.typeOnly) continue;
166
+ if (runtime(source.fileName) && !runtime(edge.file)) emit(edge.node, "showcase-import", "Runtime code cannot depend on review helpers, scenarios, fixtures or checks, including through re-exports. Keep demonstrations beside their owner and integration harnesses in tests.");
167
+ if (isInside(dsRoot, source.fileName) && layer(edge.file) === "application") emit(edge.node, "no-app-import", "ADS runtime cannot import application stores, routing or services, including through forwarding modules. Receive data and navigation callbacks through public APIs.");
168
+ }
169
+ if (!["components", "patterns", "review"].includes(ownLayer)) continue;
170
+ for (const initial of edges.get(source.fileName) ?? []) {
171
+ if (initial.typeOnly && ownLayer === "components") continue;
172
+ const queue = [{ edge: initial, trail: [source.fileName] }], seen = new Set();
173
+ while (queue.length) {
174
+ const { edge, trail } = queue.shift();
175
+ const key = `${edge.file}:${edge.symbol?.name ?? ""}`;
176
+ if (seen.has(key)) continue;
177
+ seen.add(key);
178
+ const dependencyLayer = layer(edge.file), chain = [...trail, edge.file];
179
+ const via = chain.map(file => path.relative(root, file).replaceAll("\\", "/")).join(" → ");
180
+ if (ownLayer === "components" && ["patterns", "composition"].includes(dependencyLayer)) {
181
+ emit(initial.node, "component-dependency", `Component runtime depends on ${dependencyLayer}: ${via}. Move the arrangement to a pattern or extract a lower-level component/controller; do not change a valid API just to satisfy classification.`);
182
+ break;
183
+ }
184
+ if (["patterns", "review"].includes(ownLayer) && dependencyLayer === "components") {
185
+ if (edge.symbol ? !publicSymbols.has(edge.symbol) : !publicFiles.has(edge.file)) emit(initial.node, "component-contract", `Pattern accesses private component ${edge.symbol?.name ?? "module"}: ${via}. Export an intentional public contract or keep the helper/state/style inside its component; importing a module directly is allowed when the selected symbol is public.`);
186
+ continue; // Public component implementations may use their own private files.
187
+ }
188
+ if (ownLayer === "review" && ["patterns", "controllers"].includes(dependencyLayer)) {
189
+ if (edge.symbol ? !publicSymbols.has(edge.symbol) : !publicFiles.has(edge.file)) emit(initial.node, "review-contract", `Review accesses private ${dependencyLayer} ${edge.symbol?.name ?? "module"}: ${via}. Demonstrate the production API instead of depending on private implementation details.`);
190
+ continue;
191
+ }
192
+ if (dependencyLayer === "application" || !runtime(edge.file)) continue;
193
+ if (["patterns", "review"].includes(ownLayer) && dependencyLayer !== "shared") continue;
194
+ for (const next of edges.get(edge.file) ?? []) if (!next.typeOnly) queue.push({ edge: next, trail: chain });
195
+ }
196
+ }
197
+ }
198
+ }
199
+
200
+ function strings(node, seen = new Set()) {
201
+ if (!node || seen.has(node)) return [];
202
+ seen.add(node);
203
+ if (ts.isStringLiteralLike(node)) return [node.text];
204
+ if (ts.isIdentifier(node) || ts.isPropertyAccessExpression(node)) {
205
+ const symbol = unalias(checker.getSymbolAtLocation(ts.isPropertyAccessExpression(node) ? node.name : node));
206
+ return (symbol?.declarations ?? []).flatMap(declaration => ts.isVariableDeclaration(declaration) || ts.isPropertyAssignment(declaration) ? strings(declaration.initializer, seen) : []);
207
+ }
208
+ if (ts.isTaggedTemplateExpression(node)) return [];
209
+ const found = [];
210
+ ts.forEachChild(node, child => { found.push(...strings(child, seen)); });
211
+ return found;
212
+ }
213
+ function own(map, key, file) {
214
+ if (!map.has(key)) map.set(key, new Set());
215
+ map.get(key).add(file);
216
+ }
217
+ for (const symbol of publicSymbols) for (const node of symbol?.declarations ?? []) if (ts.isVariableDeclaration(node)) for (const value of strings(node.initializer)) publicHooks.add(value);
218
+ for (const source of sources.filter(source => runtime(source.fileName))) {
219
+ function visit(node) {
220
+ let name, value;
221
+ if (["components", "patterns"].includes(layer(source.fileName))) {
222
+ const write = styleAccessProperty(node);
223
+ if (typeof write === "string" && write.startsWith("--")) (layer(source.fileName) === "components" ? componentVariables : patternVariables).add(write);
224
+ }
225
+ if (ts.isJsxAttribute(node)) { name = node.name.getText(); value = node.initializer; }
226
+ if (ts.isPropertyAssignment(node) && (ts.isIdentifier(node.name) || ts.isStringLiteralLike(node.name))) { name = node.name.text; value = node.initializer; }
227
+ if (name === "className" || name === "class") for (const text of strings(value)) for (const token of text.split(/\s+/).filter(Boolean)) own(classOwners, token, source.fileName);
228
+ if (name?.startsWith("data-")) for (const text of strings(value)) own(attributeOwners, `${name}=${text}`, source.fileName);
229
+ ts.forEachChild(node, visit);
230
+ }
231
+ visit(source);
232
+ }
233
+ function privateHook(map, key, kinds = ["components"]) {
234
+ const owners = map.get(key);
235
+ return !publicHooks.has(key) && owners?.size && (map !== attributeOwners || owners.size === 1) && [...owners].every(file => kinds.includes(layer(file)));
236
+ }
237
+ // Called first for all component styles, then for patterns by checkProject.
238
+ function collectComponentStyles(file, css) {
239
+ if (runtime(file) && ["components", "patterns"].includes(layer(file))) css.walkDecls(declaration => {
240
+ if (declaration.prop.startsWith("--")) (layer(file) === "components" ? componentVariables : patternVariables).add(declaration.prop);
241
+ });
242
+ }
243
+ function inspectStyles(file, css, offset = { line: 1 }) {
244
+ if (!runtime(file) && !isReviewFile(file, options)) return;
245
+ const ownLayer = layer(file);
246
+ css.walkAtRules("import", rule => {
247
+ const first = valueParser(rule.params).nodes.find(node => node.type !== "space" && node.type !== "comment");
248
+ const specifier = first?.type === "string" ? first.value : first?.type === "function" && first.value === "url" ? first.nodes[0]?.value : null;
249
+ if (!specifier || /^(?:[a-z]+:|\/\/)/i.test(specifier)) return;
250
+ for (const dependency of resolve(specifier, file)) {
251
+ const dependencyLayer = layer(dependency);
252
+ const boundary = ownLayer === "components" && ["patterns", "composition"].includes(dependencyLayer) ? "component-dependency"
253
+ : ["patterns", "review"].includes(ownLayer) && dependencyLayer === "components" ? "component-contract" : null;
254
+ if (boundary) report(file, boundary, `Stylesheet import crosses the ${ownLayer} → ${dependencyLayer} boundary: ${specifier}. Consume public component contracts instead of importing private styles. ${guidance}`, offset.line + (rule.source?.start.line ?? 1) - 1, rule.source?.start.column ?? 1, { guidanceUri: "ads://guides/layers" });
255
+ }
256
+ });
257
+ if (!["patterns", "review"].includes(ownLayer)) return;
258
+ const failures = new Set();
259
+ const styleReport = (node, message) => {
260
+ const line = offset.line + (node.source?.start.line ?? 1) - 1;
261
+ const key = `${line}:${message}`;
262
+ if (failures.has(key)) return;
263
+ failures.add(key);
264
+ report(file, "component-style", `${message} Use component props, public class helpers or registered non-state customization variables; patterns own arrangement and spacing. ${guidance}`, line, node.source?.start.column ?? 1, { guidanceUri: "ads://guides/layers" });
265
+ };
266
+ css.walkRules(rule => {
267
+ try {
268
+ selectorParser(selectors => {
269
+ function subject(selector) {
270
+ const nodes = selector.nodes ?? [], lastCombinator = nodes.findLastIndex(node => node.type === "combinator");
271
+ for (const node of nodes.slice(lastCombinator + 1)) {
272
+ if (node.type === "class" && privateHook(classOwners, node.value, ownLayer === "review" ? ["components", "patterns"] : undefined)) styleReport(rule, `${ownLayer === "review" ? "Review selector targets private product" : "Pattern selector targets private component"} class .${node.value}.`);
273
+ if (node.type === "attribute" && node.operator === "=" && privateHook(attributeOwners, `${node.attribute}=${node.value}`, ownLayer === "review" ? ["components", "patterns"] : undefined) && !publicHooks.has(node.value)) styleReport(rule, `${ownLayer === "review" ? "Review selector targets private product" : "Pattern selector targets private component"} marker [${node.attribute}="${node.value}"].`);
274
+ if (node.type === "pseudo" && [":is", ":where"].includes(node.value)) for (const branch of node.nodes ?? []) subject(branch);
275
+ }
276
+ }
277
+ for (const selector of selectors.nodes) subject(selector);
278
+ }).processSync(rule.selector);
279
+ } catch { /* CSS syntax diagnostics are owned by the existing parser/build. */ }
280
+ });
281
+ css.walkDecls(declaration => valueParser(declaration.value).walk(node => {
282
+ if (node.type !== "function" || node.value !== "var") return;
283
+ const name = node.nodes.find(child => child.type === "word")?.value;
284
+ if ((componentVariables.has(name) || ownLayer === "review" && patternVariables.has(name) && !publicHooks.has(name)) && (isStateVariable(name) || !config.boundaries.componentVariables.includes(name))) styleReport(declaration, `${ownLayer === "review" ? "Review reads private product" : "Pattern reads private component"} variable ${name}.`);
285
+ }));
286
+ }
287
+ return { inspectDependencies, runtimeHelpers, collectComponentStyles, inspectStyles };
288
+ }
@@ -13,7 +13,7 @@ export function createLintConfig({ root, config }) {
13
13
  if (!existsSync(specifier)) throw new Error(`Project plugin not found: ${plugin.path}`);
14
14
  return { name: plugin.name, specifier };
15
15
  });
16
- const options = { projectRoot: root, sourceDir: config.sourceDir, dsDir: config.dsDir, publicAlias: config.publicAlias, stylesEntry: config.stylesEntry, inlineStyleFiles: config.inlineStyleFiles,
16
+ const options = { projectRoot: root, sourceDir: config.sourceDir, dsDir: config.dsDir, publicAlias: config.publicAlias, publicEntry: config.publicEntry, reviewEntry: config.reviewEntry, stylesEntry: config.stylesEntry, inlineStyleFiles: config.inlineStyleFiles,
17
17
  configurationFiles: [...configurationFiles({ root, config, sourceRoot: path.resolve(root,config.sourceDir), dsRoot: path.resolve(root,config.dsDir) })].sort() };
18
18
  return {
19
19
  jsPlugins: [{ name: "ads", specifier: fileURLToPath(new URL("./oxlint.mjs", import.meta.url)) }, ...projectPlugins],
@@ -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));
@@ -24,6 +24,7 @@ export function projectContext(options) {
24
24
  const guides = [...projectGuideFiles(root), path.join(config.dsDir, "README.md"), path.join(config.dsDir, "readme.md"), path.join(config.dsDir, "SKILL.md")].filter((file) => existsSync(path.join(root, file))).map((file) => ({ file: file.replaceAll("\\", "/"), uri: sourceUri(file.replaceAll("\\", "/")) }));
25
25
  return { name: pkg.name ?? path.basename(root), projectRoot: root, dsRoot, sourceRoot, framework: config.framework,
26
26
  publicEntry: path.join(dsRoot, config.publicEntry), publicEntryUri: existsSync(path.join(dsRoot, config.publicEntry)) ? sourceUri(path.relative(root, path.join(dsRoot, config.publicEntry)).replaceAll("\\", "/")) : null,
27
+ review: { importFrom: `${config.publicAlias}/review`, entry: path.join(dsRoot, config.reviewEntry), sourceUri: existsSync(path.join(dsRoot, config.reviewEntry)) ? sourceUri(path.relative(root, path.join(dsRoot, config.reviewEntry)).replaceAll("\\", "/")) : null, developmentOnly: true },
27
28
  stylesEntry: path.join(dsRoot, config.stylesEntry), packageManager, commands, guides,
28
29
  showcase: { client: "tool-owned-mcp", url: config.mcp.showcaseUrl, title: config.mcp.showcaseTitle ?? `${pkg.name ?? path.basename(root)} · Design System` },
29
30
  runtime: { node: process.execPath, nodeBinDir: path.dirname(process.execPath), nodeVersion: process.version,
@@ -56,7 +57,7 @@ Use this server for UI components/patterns, tokens/themes/branding, layout, inte
56
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.
57
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"}.
58
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.
59
- 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/. 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.
60
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}.
61
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.
62
63