@reqvire-org/ads 0.3.18 → 0.3.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/README.md +2 -0
  2. package/dist/behaviors.mjs +19 -9
  3. package/dist/bootstrap-ds/ADS.md +2 -2
  4. package/dist/bootstrap-ds/README.md +2 -2
  5. package/dist/bootstrap-ds/design-system/index.ts +0 -1
  6. package/dist/bootstrap-ds/design-system/review/index.ts +1 -0
  7. package/dist/bootstrap-ds/design-system/tokens/{Foundation.scenario.tsx → Foundation.ads.scenario.tsx} +2 -2
  8. package/dist/bootstrap-ds/ds.config.json +1 -1
  9. package/dist/bootstrap-ds/tsconfig.build.json +9 -2
  10. package/dist/bootstrap-ds/tsconfig.json +18 -3
  11. package/dist/bootstrap-ds/vite.config.ts +1 -1
  12. package/dist/boundaries.mjs +6 -2
  13. package/dist/check.mjs +44 -17
  14. package/dist/config.mjs +33 -5
  15. package/dist/contract-migration.mjs +110 -0
  16. package/dist/contracts.d.mts +22 -0
  17. package/dist/evidence.mjs +1 -1
  18. package/dist/index.mjs +1 -0
  19. package/dist/init.mjs +18 -15
  20. package/dist/layers.mjs +288 -0
  21. package/dist/lint-config.mjs +1 -1
  22. package/dist/mcp/catalog.mjs +9 -2
  23. package/dist/mcp/context.mjs +2 -1
  24. package/dist/mcp/edit-preparation.mjs +9 -6
  25. package/dist/mcp/mutations.mjs +1 -1
  26. package/dist/mcp/preview-widget.bundle.js +41 -39
  27. package/dist/mcp/prompts.mjs +1 -1
  28. package/dist/mcp/server.mjs +9 -3
  29. package/dist/mcp/widget.bundle.js +1 -1
  30. package/dist/oxlint.mjs +3 -1
  31. package/dist/paint-contracts.mjs +124 -0
  32. package/dist/preview-resources.mjs +12 -2
  33. package/dist/public-api.mjs +13 -6
  34. package/dist/public-props.mjs +2 -2
  35. package/dist/runtime-contracts.mjs +225 -0
  36. package/dist/scenario-trace.mjs +37 -9
  37. package/dist/scenarios-vite.mjs +3 -2
  38. package/dist/scenarios.mjs +89 -30
  39. package/dist/scoped-tokens.mjs +9 -4
  40. package/dist/showcase/client.bundle.js +1 -1
  41. package/dist/showcase/sandbox.bundle.js +1 -1
  42. package/dist/skills/ads-break-ui/SKILL.md +25 -6
  43. package/dist/skills/ads-break-ui/references/contracts.md +250 -0
  44. package/dist/skills/ads-break-ui/references/guarded-editing.md +1 -1
  45. package/dist/skills/ads-break-ui/references/layers.md +123 -0
  46. package/dist/skills/ads-break-ui/references/review.md +96 -0
  47. package/dist/skills/ads-break-ui/references/scenarios.md +24 -3
  48. package/dist/skills/ads-designer/SKILL.md +26 -7
  49. package/dist/skills/ads-designer/assets/bootstrap-ds/ADS.md +2 -2
  50. package/dist/skills/ads-designer/assets/bootstrap-ds/README.md +2 -2
  51. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/index.ts +0 -1
  52. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/review/index.ts +1 -0
  53. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/{Foundation.scenario.tsx → Foundation.ads.scenario.tsx} +2 -2
  54. package/dist/skills/ads-designer/assets/bootstrap-ds/ds.config.json +1 -1
  55. package/dist/skills/ads-designer/assets/bootstrap-ds/tsconfig.build.json +9 -2
  56. package/dist/skills/ads-designer/assets/bootstrap-ds/tsconfig.json +18 -3
  57. package/dist/skills/ads-designer/assets/bootstrap-ds/vite.config.ts +1 -1
  58. package/dist/skills/ads-designer/references/contracts.md +250 -0
  59. package/dist/skills/ads-designer/references/guarded-editing.md +1 -1
  60. package/dist/skills/ads-designer/references/layers.md +123 -0
  61. package/dist/skills/ads-designer/references/review.md +96 -0
  62. package/dist/skills/ads-designer/references/scenarios.md +24 -3
  63. package/dist/skills/ads-import/SKILL.md +25 -6
  64. package/dist/skills/ads-import/references/contracts.md +250 -0
  65. package/dist/skills/ads-import/references/guarded-editing.md +1 -1
  66. package/dist/skills/ads-import/references/layers.md +123 -0
  67. package/dist/skills/ads-import/references/review.md +96 -0
  68. package/dist/skills/ads-import/references/scenarios.md +24 -3
  69. package/dist/skills/ads-migrate/SKILL.md +25 -6
  70. package/dist/skills/ads-migrate/references/contracts.md +250 -0
  71. package/dist/skills/ads-migrate/references/guarded-editing.md +1 -1
  72. package/dist/skills/ads-migrate/references/layers.md +123 -0
  73. package/dist/skills/ads-migrate/references/review.md +96 -0
  74. package/dist/skills/ads-migrate/references/scenarios.md +24 -3
  75. package/dist/skills/ads-motion/SKILL.md +26 -7
  76. package/dist/skills/ads-motion/references/audit.md +1 -1
  77. package/dist/skills/ads-motion/references/contracts.md +250 -0
  78. package/dist/skills/ads-motion/references/guarded-editing.md +1 -1
  79. package/dist/skills/ads-motion/references/layers.md +123 -0
  80. package/dist/skills/ads-motion/references/motion-review.md +35 -0
  81. package/dist/skills/ads-motion/references/review.md +96 -35
  82. package/dist/skills/ads-motion/references/scenarios.md +24 -3
  83. package/dist/token-origins.mjs +24 -0
  84. package/dist/tokens.mjs +4 -3
  85. package/dist/unit-contracts.mjs +203 -0
  86. package/dist/units.mjs +26 -17
  87. package/dist/verification/browser.mjs +156 -26
  88. package/dist/verification/playwright-reporter.cjs +13 -3
  89. package/docs/ADS.md +40 -6
  90. package/docs/ARCHITECTURE.md +73 -4
  91. package/docs/BEHAVIORS.md +15 -0
  92. package/docs/CHANGELOG.md +42 -0
  93. package/docs/CONTRACTS.md +250 -0
  94. package/docs/GUIDE.md +29 -3
  95. package/docs/MCP.md +42 -6
  96. package/package.json +4 -1
  97. package/dist/bootstrap-ds/design-system/patterns/token-preview/Scenario.tsx +0 -4
  98. package/dist/bootstrap-ds/design-system/patterns/token-preview/fixture.ts +0 -8
  99. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/patterns/token-preview/Scenario.tsx +0 -4
  100. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/patterns/token-preview/fixture.ts +0 -8
  101. /package/dist/bootstrap-ds/design-system/{patterns → review}/token-preview/TokenPreview.tsx +0 -0
  102. /package/dist/bootstrap-ds/design-system/tokens/{Foundation.fixture.ts → Foundation.ads.fixture.ts} +0 -0
  103. /package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/{patterns → review}/token-preview/TokenPreview.tsx +0 -0
  104. /package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/{Foundation.fixture.ts → Foundation.ads.fixture.ts} +0 -0
package/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
 
@@ -3,12 +3,13 @@ import path from "node:path";
3
3
  import { isBuiltin } from "node:module";
4
4
  import ts from "typescript";
5
5
  import { z } from "zod";
6
- import { isBehaviorFile, isDevelopmentFile, isInside, sourceFiles } from "./config.mjs";
6
+ import { isBehaviorFile, isDevelopmentFile, isInside, sourceFiles, isReviewFile } from "./config.mjs";
7
7
  import { importResolver, projectCompilerOptions } from "./boundaries.mjs";
8
8
  import { inspectUnits } from "./units.mjs";
9
- import { publicApi } from "./public-api.mjs";
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,25 +28,27 @@ 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
- const apis = new Map(publicApi(program, options).map(item => [item.name, item]));
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; }
47
50
  const unit = inventory.owner(inventory.relative(relative(file)));
48
- if (!["components", "patterns", "controllers"].includes(unit?.split("/")[0])) { report("Behavior contracts belong to a component, pattern or controller unit."); continue; }
51
+ if (!["components", "patterns", "controllers", "review"].includes(unit?.split("/")[0])) { report("Behavior contracts belong to a component, pattern, controller or development review unit."); continue; }
49
52
  if (owners.has(unit)) { report(`Only one behavior contract may describe ${unit}.`); continue; }
50
53
  owners.add(unit);
51
54
  function owned(target, field) {
@@ -61,7 +64,7 @@ export function inspectBehaviors(options, suppliedProgram, suppliedRegistry) {
61
64
  if (!api || inventory.owner(inventory.relative(relative(api.file))) !== unit) report(`Subject ${name} must resolve to a public API owned by ${unit}: ${entry.api}`);
62
65
  } else {
63
66
  const source = owned(entry.source, `Subject ${name}`);
64
- if (source && isDevelopmentFile(source, options)) report(`Subject ${name} must identify runtime implementation, not a fixture, scenario or check.`);
67
+ if (source && isDevelopmentFile(source, options) && !isReviewFile(source, options)) report(`Subject ${name} must identify runtime implementation, not a fixture, scenario or check.`);
65
68
  if (source && entry.symbol) {
66
69
  const ast = program.getSourceFile(source);
67
70
  if (!ast || !checker.getSymbolsInScope(ast, ts.SymbolFlags.Value).some(symbol => symbol.name === entry.symbol && symbol.declarations?.some(declaration => declaration.getSourceFile() === ast))) report(`Subject ${name}: symbol ${entry.symbol} is not declared in ${entry.source}.`);
@@ -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 };
@@ -72,7 +72,7 @@ layout, component-specific focus rules and hover/disabled skins are rejected.
72
72
  Shared token definitions belong in value sheets, not `base.css`. No project token
73
73
  names are embedded in this policy. A zero-specificity `:where()` wrapper does not
74
74
  change ownership. Put component design and animation in Linaria owners, and
75
- scenario layout variations in typed public review-pattern props. Browser review
75
+ scenario layout variations in typed review-helper props. Browser review
76
76
  still checks the effects and accessibility of permitted defaults.
77
77
 
78
78
  Own primitive markup, focus, hover, selected, disabled, loading, and motion in
@@ -151,7 +151,7 @@ Flat owners may use `Button.scenario.tsx` and `Button.fixture.ts` instead.
151
151
  `export const scenario = { title: "Button states", covers: ["Button"] };`.
152
152
  Token entries under the configured token directory declare
153
153
  `export const scenario = { title: fixture.title, covers: ["TokenSamples"], samples: fixture.samples };`.
154
- `TokenSamples` is any project-owned public review pattern, not a required name.
154
+ `TokenSamples` is any project-owned development-only review renderer, not a required name.
155
155
  Its colocated fixture declares literal `samples: [{ token, property, probe }]`.
156
156
  Each token must exist and have sample coverage; duplicate bindings, missing probes,
157
157
  empty covers and value-only inspectors fail. The default entry passes the declared
@@ -1,7 +1,7 @@
1
1
  # Empty native design system
2
2
 
3
3
  This private React/TypeScript workspace contains semantic CSS tokens, a project-owned
4
- token review pattern, a public barrel, Linaria extraction and a preview compiler
4
+ token review renderer, a public barrel, Linaria extraction and a preview compiler
5
5
  adapter. It has no product components, icons, fonts or example screens. The minimal showcase
6
6
  shell and MCP client come from ADS tooling.
7
7
 
@@ -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
@@ -2,4 +2,3 @@
2
2
  import "./styles.css";
3
3
 
4
4
  // Add public component and pattern exports here as they are implemented.
5
- export { TokenPreview } from "./patterns/token-preview/TokenPreview";
@@ -0,0 +1 @@
1
+ export { TokenPreview } from "./token-preview/TokenPreview";
@@ -1,4 +1,4 @@
1
- import { TokenPreview } from "@ds";
2
- import { fixture } from "./Foundation.fixture";
1
+ import { TokenPreview } from "@ds/review";
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} />; }
@@ -13,7 +13,7 @@
13
13
  "boundaries": {
14
14
  "styleWrites": [
15
15
  {
16
- "file": "design-system/patterns/token-preview/TokenPreview.tsx",
16
+ "file": "design-system/review/token-preview/TokenPreview.tsx",
17
17
  "properties": [
18
18
  "--ux-token-preview-value"
19
19
  ]
@@ -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
  }
@@ -1,7 +1,11 @@
1
1
  {
2
2
  "compilerOptions": {
3
3
  "target": "ES2022",
4
- "lib": ["ES2022", "DOM", "DOM.Iterable"],
4
+ "lib": [
5
+ "ES2022",
6
+ "DOM",
7
+ "DOM.Iterable"
8
+ ],
5
9
  "module": "ESNext",
6
10
  "moduleResolution": "bundler",
7
11
  "skipLibCheck": true,
@@ -14,7 +18,18 @@
14
18
  "noUnusedParameters": true,
15
19
  "noFallthroughCasesInSwitch": true,
16
20
  "baseUrl": ".",
17
- "paths": { "@ds": ["design-system/index.ts"] }
21
+ "paths": {
22
+ "@ds/review": [
23
+ "design-system/review/index.ts"
24
+ ],
25
+ "@ds": [
26
+ "design-system/index.ts"
27
+ ]
28
+ }
18
29
  },
19
- "include": ["src", "design-system", "vite.config.ts"]
30
+ "include": [
31
+ "src",
32
+ "design-system",
33
+ "vite.config.ts"
34
+ ]
20
35
  }
@@ -7,7 +7,7 @@ import { linariaTagResolver } from "@reqvire-org/ads/linaria";
7
7
  export default defineConfig({
8
8
  plugins: [wyw({ tagResolver: linariaTagResolver }), react()],
9
9
  resolve: {
10
- alias: { "@ds": path.resolve(import.meta.dirname, "design-system/index.ts") },
10
+ alias: { "@ds/review": path.resolve(import.meta.dirname, "design-system/review/index.ts"), "@ds": path.resolve(import.meta.dirname, "design-system/index.ts") },
11
11
  },
12
12
  build: {
13
13
  outDir: "dist",
@@ -4,15 +4,18 @@ 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
- export function projectCompilerOptions({ root }) {
9
+ export function projectCompilerOptions({ root, config, dsRoot }) {
9
10
  const file = ts.findConfigFile(root, ts.sys.fileExists, "tsconfig.json");
10
11
  let compiler = {};
11
12
  if (file && isInside(root, file)) {
12
13
  const parsed = ts.readConfigFile(file, ts.sys.readFile);
13
14
  if (!parsed.error) compiler = ts.parseJsonConfigFileContent(parsed.config, ts.sys, path.dirname(file)).options;
14
15
  }
15
- return compiler;
16
+ return config && dsRoot ? { ...compiler, paths: { ...compiler.paths,
17
+ [config.publicAlias]: [path.join(dsRoot, config.publicEntry)],
18
+ [`${config.publicAlias}/review`]: [path.join(dsRoot, config.reviewEntry)] } } : compiler;
16
19
  }
17
20
 
18
21
  export function importResolver(options) {
@@ -62,6 +65,7 @@ export function checkComponentVariable(name, filename, options) {
62
65
  const showcase = development;
63
66
  const owned = scopedTokenOwner(options, name);
64
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.` };
65
69
  if (isSharedToken(name)) return tokens ? null : { rule: "token-ownership", message: `Define ${name} in ADS tokens, not component, pattern, showcase, or app styles.` };
66
70
  if (name.startsWith("--ux-")) return patterns || showcase ? null : { rule: "variable-ownership", message: "--ux-* variables belong to ADS patterns and showcase composition." };
67
71
  if (name.startsWith("--showcase-")) return showcase ? null : { rule: "variable-ownership", message: "--showcase-* variables belong to the ADS showcase." };
package/dist/check.mjs CHANGED
@@ -3,12 +3,16 @@ import path from "node:path";
3
3
  import ts from "typescript";
4
4
  import postcss from "postcss";
5
5
  import valueParser from "postcss-value-parser";
6
- import { importViolation, isInside, loadConfig, sourceFiles, isDevelopmentFile, isTestFile } from "./config.mjs";
6
+ import { importViolation, isInside, loadConfig, sourceFiles, isDevelopmentFile, isTestFile, isReviewFile } from "./config.mjs";
7
7
  import { inspectTokens, references } from "./tokens.mjs";
8
8
  import { foundationFiles, foundationImportTarget, isSharedToken } from "./token-policy.mjs";
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";
@@ -25,8 +29,9 @@ import { createScenarioSlots } from "./scenario-slots.mjs";
25
29
  import { createScenarioSelectors } from "./scenario-selectors.mjs";
26
30
  import { architectureEvidence, failure } from "./evidence.mjs";
27
31
  import { propertyRegistration } from "./property-registrations.mjs";
32
+ import { createLayerBoundaries, layerSourceFiles } from "./layers.mjs";
28
33
 
29
- const scenarioLayoutGuidance = "Put demo-only sizing and framing in a public review/layout pattern. Keep required behavior in the runtime owner; add component props for deliberate product API needs.";
34
+ const scenarioLayoutGuidance = "Put demo-only sizing and framing in the development-only review/ layer, imported through the separate review entry. Patterns are production UI. Keep required behavior in the runtime owner; add component props for deliberate product API needs.";
30
35
  const inlineStyleGuidance = `Own visual styles in component/pattern Linaria. ${scenarioLayoutGuidance} Reserve inlineStyleFiles for documented measured geometry, not demo layout.`;
31
36
 
32
37
  const showcaseControls = new Set(["a", "button", "input", "select", "textarea", "option", "optgroup", "table", "thead", "tbody", "tfoot", "tr", "td", "th", "dialog", "details", "summary", "progress", "meter"]);
@@ -42,7 +47,7 @@ export function checkProject(directory, { trackingRoot } = {}) {
42
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.` });
43
48
  }
44
49
  if (diagnostics.length) return architectureEvidence({ ok: false, diagnostics });
45
- const tokenInventory = inspectTokens(root);
50
+ const tokenInventory = inspectTokens(root, options);
46
51
  const declaredTokens = new Set(tokenInventory.tokens.map((token) => token.name));
47
52
  diagnostics.push(...tokenInventory.diagnostics);
48
53
  diagnostics.push(...inspectAssets(options));
@@ -52,16 +57,25 @@ export function checkProject(directory, { trackingRoot } = {}) {
52
57
  const rootHtml = path.join(root, "index.html");
53
58
  const files = [...new Set([...sourceFiles(sourceRoot), ...sourceFiles(dsRoot), ...(existsSync(rootHtml) ? [rootHtml] : [])])];
54
59
  const boundaryFiles = [...config.inlineStyleFiles, ...config.boundaries.styleWrites.map(entry => entry.file)].map(file => path.resolve(root, file)).filter(file => existsSync(file) && statSync(file).isFile());
55
- const scriptFiles = [...new Set([...files, ...boundaryFiles])].filter((file) => /\.[cm]?[jt]sx?$/.test(file));
60
+ const scriptFiles = layerSourceFiles([...new Set([...files, ...boundaryFiles])].filter((file) => /\.[cm]?[jt]sx?$/.test(file)), options);
56
61
  const compiler = projectCompilerOptions(options);
57
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 });
58
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.`});
59
69
  const atomicCompositionReview = createAtomicCompositionReview(program, options);
60
70
  const scenarioTrace = createScenarioTrace(program, options);
61
71
  let scenarioSlot, scenarioSelector;
62
- function report(file, rule, message, line = 1, column = 1) {
63
- diagnostics.push({ file: path.relative(root, file).replaceAll("\\", "/"), rule, message, line, column });
72
+ function report(file, rule, message, line = 1, column = 1, detail = {}) {
73
+ diagnostics.push({ file: path.relative(root, file).replaceAll("\\", "/"), rule, message, line, column, ...detail });
64
74
  }
75
+ const layers = createLayerBoundaries(program, options, report), layerStyles = [];
76
+ layers.inspectDependencies();
77
+ inspectTokenValueFlows(program, options, tokenInventory, scoped, report);
78
+ for (const file of layers.runtimeHelpers()) if (!files.includes(file)) files.push(file);
65
79
  inspectStyleBoundaries(options, program, report);
66
80
  inspectShowcasePages(options, program, resolveImport, report);
67
81
  inspectShowcaseCoverage(options, program, report);
@@ -82,7 +96,7 @@ export function checkProject(directory, { trackingRoot } = {}) {
82
96
  let defined=false;css.walkDecls(registration.name,()=>{defined=true;});
83
97
  if(!isSharedToken(registration.name)||!declaredTokens.has(registration.name)||!defined||["base.css","fonts.css"].includes(path.basename(file)))throw new Error("Shared @property registrations belong beside their real token definitions in foundation value sheets, not base.css or fonts.css.");
84
98
  }else{
85
- if(!owner||!/^(?:components|patterns)\//.test(owner))throw new Error("@property belongs to its runtime component/pattern or real token value sheet, never application, scenario, controller or shared reset styling.");
99
+ if(!owner||!/^(?:components|patterns|review)\//.test(owner))throw new Error("@property belongs to its component/pattern/review implementation or real token value sheet, never application, scenario, controller or shared reset styling.");
86
100
  const violation=checkComponentVariable(registration.name,file,options);if(violation)throw new Error(violation.message);
87
101
  }
88
102
  for(const descriptor of rule.nodes??[])if(descriptor.type==="decl")for(const ref of references(descriptor.value)){
@@ -106,9 +120,11 @@ export function checkProject(directory, { trackingRoot } = {}) {
106
120
  let css;
107
121
  try { css = postcss.parse(text, { from: file }); }
108
122
  catch (error) { report(file, "css-syntax", error.reason ?? error.message, offset.line + (error.line ?? 1) - 1); return; }
123
+ layers.collectComponentStyles(file, css);
124
+ layerStyles.push({ file, css, offset });
109
125
  checkRegistrations(file,css,offset);
110
- if (!foundation && (isDevelopmentFile(file, options) || !isInside(path.join(dsRoot, "components"), file) && !isInside(path.join(dsRoot, "patterns"), file) && file !== foundationEntry)) report(file, "style-ownership", "Visual CSS belongs to the local ADS's runtime components or patterns.");
111
- if (isDevelopmentFile(file, options) && css.nodes.some((node) => node.type !== "comment" && !(node.type === "atrule" && node.name === "import"))) report(file, "showcase-style", `Scenarios and fixtures define no styles. ${scenarioLayoutGuidance}`, offset.line);
126
+ if (!foundation && !isReviewFile(file, options) && (isDevelopmentFile(file, options) || !isInside(path.join(dsRoot, "components"), file) && !isInside(path.join(dsRoot, "patterns"), file) && file !== foundationEntry)) report(file, "style-ownership", "Visual CSS belongs to the local ADS's runtime components/patterns or development review implementations.");
127
+ if (isDevelopmentFile(file, options) && !isReviewFile(file, options) && css.nodes.some((node) => node.type !== "comment" && !(node.type === "atrule" && node.name === "import"))) report(file, "showcase-style", `Scenarios and fixtures define no styles. ${scenarioLayoutGuidance}`, offset.line);
112
128
  css.walkAtRules("import", (rule) => {
113
129
  if (foundation) report(file, "foundation-import", "Import foundation sheets through the configured styles entry. Token sheets cannot import an unchecked styling layer.", rule.source.start.line);
114
130
  const first = valueParser(rule.params).nodes.find((node) => node.type !== "space" && node.type !== "comment");
@@ -126,7 +142,7 @@ export function checkProject(directory, { trackingRoot } = {}) {
126
142
  }
127
143
  if (isInside(path.join(dsRoot, "showcase"), file) && /:(?:hover|active|focus(?:-visible|-within)?|checked|disabled|enabled)\b|\[(?:aria-(?:selected|pressed|checked|disabled|expanded)|data-state)\b/.test(rule.selector)) report(file, "showcase-state", "Interactive state styling belongs to the real ADS component or pattern, not showcase or mock styles.", offset.line + rule.source.start.line - 1);
128
144
  if (/\.ads-[\w-]+/.test(rule.selector) && !isInside(path.join(dsRoot, "components"), file)) report(file, "style-ownership", "ADS component internals are owned by components, not app or showcase CSS.", offset.line + rule.source.start.line - 1);
129
- if (/\.ux-[\w-]+/.test(rule.selector) && !isInside(path.join(dsRoot, "patterns"), file)) report(file, "style-ownership", "Pattern internals are owned by patterns, not app or showcase CSS.", offset.line + rule.source.start.line - 1);
145
+ if (/\.ux-[\w-]+/.test(rule.selector) && !isInside(path.join(dsRoot, "patterns"), file) && !isReviewFile(file, options)) report(file, "style-ownership", "Pattern internals are owned by patterns, not app or scenario CSS.", offset.line + rule.source.start.line - 1);
130
146
  if (/\.showcase-[\w-]+/.test(rule.selector) && !isInside(path.join(dsRoot, "showcase"), file)) report(file, "style-ownership", "Showcase internals are owned by showcase scaffolding.", offset.line + rule.source.start.line - 1);
131
147
  });
132
148
  if (foundation) css.walkAtRules(rule => {
@@ -148,6 +164,11 @@ export function checkProject(directory, { trackingRoot } = {}) {
148
164
  const variable = checkComponentVariable(decl.prop, file, options);
149
165
  if (variable) report(file, variable.rule, variable.message, offset.line + decl.source.start.line - 1, decl.source.start.column);
150
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
+ }
151
172
  const owned = scopedTokenOwner(options, ref.name);
152
173
  if (owned) {
153
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);
@@ -173,6 +194,9 @@ export function checkProject(directory, { trackingRoot } = {}) {
173
194
  });
174
195
  }
175
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.`);
176
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.
177
201
  if (file.endsWith(".css") && isInside(path.join(dsRoot, config.tokenDir), file) && !isDevelopmentFile(file, options)) { checkCss(file, readFileSync(file, "utf8"), undefined, true); continue; }
178
202
  if (file.endsWith(".html")) { inspectHtml(readFileSync(file, "utf8"), (rule, message, line) => report(file, rule, message, line)); continue; }
@@ -190,7 +214,7 @@ export function checkProject(directory, { trackingRoot } = {}) {
190
214
  report(file, "source-syntax", ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"), position.line + 1, position.character + 1);
191
215
  }
192
216
  const inlineAllowed = config.inlineStyleFiles.some((allowed) => path.resolve(root, allowed) === file);
193
- const showcase = isDevelopmentFile(file, options) && !isTestFile(file);
217
+ const showcase = isDevelopmentFile(file, options) && !isTestFile(file) && !isReviewFile(file, options);
194
218
  function nodeReport(node, rule, message, detail) {
195
219
  const position = source.getLineAndCharacterOfPosition(node.getStart(source));
196
220
  report(file, rule, message, position.line + 1, position.character + 1);
@@ -206,7 +230,7 @@ export function checkProject(directory, { trackingRoot } = {}) {
206
230
  const selector = native ? (scenarioSelector ??= createScenarioSelectors(program, options))(node) : { ok: false, reasons: ["A selector cannot establish custom widget behavior or keyboard semantics."] };
207
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" }) });
208
232
  }
209
- 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);
210
234
  function isLinariaTag(tag) {
211
235
  if (ts.isCallExpression(tag)) tag = tag.expression;
212
236
  let symbolNode = tag;
@@ -234,8 +258,8 @@ export function checkProject(directory, { trackingRoot } = {}) {
234
258
  function inspectClass(value) {
235
259
  if (ts.isStringLiteralLike(value)) {
236
260
  if (/(?:^|\s)ads-[\w-]+/.test(value.text) && !isInside(path.join(dsRoot, "components"), file)) nodeReport(value, "style-ownership", "Primitive ads-* classes must be emitted by components, not consumers.");
237
- if (/(?:^|\s)ux-[\w-]+/.test(value.text) && !isInside(path.join(dsRoot, "patterns"), file)) nodeReport(value, "style-ownership", "Pattern ux-* classes must be emitted by patterns, not consumers.");
238
- if (/(?:^|\s)showcase-[\w-]+/.test(value.text) && !isInside(path.join(dsRoot, "showcase"), file)) nodeReport(value, "style-ownership", "Showcase hooks must be emitted by the showcase, not runtime implementations.");
261
+ if (/(?:^|\s)ux-[\w-]+/.test(value.text) && !isInside(path.join(dsRoot, "patterns"), file) && !isReviewFile(file, options)) nodeReport(value, "style-ownership", "Pattern ux-* classes must be emitted by patterns, not consumers.");
262
+ if (/(?:^|\s)showcase-[\w-]+/.test(value.text) && !isInside(path.join(dsRoot, "showcase"), file) && !isReviewFile(file, options)) nodeReport(value, "style-ownership", "Showcase hooks must be emitted by development review code, not runtime implementations.");
239
263
  }
240
264
  ts.forEachChild(value, inspectClass);
241
265
  }
@@ -289,13 +313,14 @@ export function checkProject(directory, { trackingRoot } = {}) {
289
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.");
290
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.");
291
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.");
292
- 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);
293
318
  if (ts.isJsxAttribute(node) && ts.isIdentifier(node.name) && node.name.text === "style" && node.initializer) {
294
319
  inspectProperty(node.initializer);
295
320
  }
296
321
  const mutation = styleWriteViolation(node, file, options);
297
322
  if (mutation) nodeReport(node, "style-mutation", mutation);
298
- 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 public review pattern.");
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.");
299
324
  if (ts.isJsxAttribute(node) && ts.isIdentifier(node.name) && node.name.text === "className" && node.initializer) {
300
325
  inspectClass(node.initializer);
301
326
  }
@@ -310,6 +335,8 @@ export function checkProject(directory, { trackingRoot } = {}) {
310
335
  visit(source);
311
336
  }
312
337
  // Bounded clients should see actionable warnings before source-analysis limits.
338
+ for (const { file, css, offset } of layerStyles) layers.inspectStyles(file, css, offset);
313
339
  advisories.sort((first, second) => Number(first.severity === "info") - Number(second.severity === "info"));
314
- 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 } : {}) });
315
342
  }
package/dist/config.mjs CHANGED
@@ -11,6 +11,7 @@ export const defaultConfig = {
11
11
  dsDir: "design-system",
12
12
  publicAlias: "@ds",
13
13
  publicEntry: "index.ts",
14
+ reviewEntry: "review/index.ts",
14
15
  stylesEntry: "styles.css",
15
16
  tokenDir: "tokens",
16
17
  inlineStyleFiles: [],
@@ -54,7 +55,8 @@ export function loadConfig(directory = process.cwd()) {
54
55
  if (!Array.isArray(config.inlineStyleFiles)) throw new Error("inlineStyleFiles must be an array of relative paths.");
55
56
  const sourceRoot = projectPath(root, config.sourceDir, "sourceDir");
56
57
  const dsRoot = projectPath(root, config.dsDir, "dsDir");
57
- for (const field of ["publicEntry", "stylesEntry", "tokenDir"]) projectPath(dsRoot, config[field], field);
58
+ for (const field of ["publicEntry", "reviewEntry", "stylesEntry", "tokenDir"]) projectPath(dsRoot, config[field], field);
59
+ if (!isInside(path.join(dsRoot, "review"), path.resolve(dsRoot, config.reviewEntry)) || path.resolve(dsRoot, config.reviewEntry) === path.join(dsRoot, "review") || !/\.[cm]?[jt]sx?$/.test(config.reviewEntry)) throw new Error("reviewEntry must name a script entry inside the ADS review/ development layer.");
58
60
  for (const file of config.inlineStyleFiles) projectPath(root, file, "inlineStyleFiles");
59
61
  if (!Array.isArray(config.boundaries.componentVariables) || config.boundaries.componentVariables.some((name) => typeof name !== "string" || !/^--ds-[\w-]+$/.test(name) || isStateVariable(name))) throw new Error("boundaries.componentVariables must name non-state --ds-* customization variables.");
60
62
  if (!Array.isArray(config.boundaries.styleWrites)) throw new Error("boundaries.styleWrites must be an array.");
@@ -161,15 +163,37 @@ export function isTokenManifestFile(file) {
161
163
  return /(?:^|[\\/])tokens\.json$|\.tokens\.json$/.test(file);
162
164
  }
163
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
+
164
183
  export function isDevelopmentFile(file, { dsRoot }) {
165
- if (!isInside(dsRoot, file)) return isTestFile(file);
184
+ if (!isInside(dsRoot, file)) return isTestFile(file) || adsFileRole(file) !== null;
166
185
  const relative = path.relative(dsRoot, file).replaceAll("\\", "/");
167
- return isTestFile(file) || isScenarioFile(file) || isFixtureFile(file) || isBehaviorFile(file) || isTokenManifestFile(file)
168
- || /^(?:showcase|composition)\//.test(relative)
186
+ return adsFileRole(file) !== null || isTestFile(file) || isScenarioFile(file) || isFixtureFile(file) || isBehaviorFile(file) || isTokenManifestFile(file)
187
+ || /^(?:showcase|composition|review)\//.test(relative)
169
188
  || /(?:^|\/)(?:scenario|fixtures)(?:\/|$)/.test(relative)
170
189
  || /\.scenario(?:-adapter)?\.[cm]?[jt]sx?$/.test(relative);
171
190
  }
172
191
 
192
+ /** Review implementations may own demo presentation; scenarios/fixtures may not. */
193
+ export function isReviewFile(file, { dsRoot }) {
194
+ return isInside(path.join(dsRoot, "review"), file) && adsFileRole(file) === null && !isTestFile(file) && !isScenarioFile(file) && !isFixtureFile(file) && !isBehaviorFile(file) && !isTokenManifestFile(file);
195
+ }
196
+
173
197
  const ignored = new Set(["node_modules", "dist", "dist-showcase", "dist-kit", ".git", ".vite", ".ds", "coverage"]);
174
198
 
175
199
  export function sourceFiles(root) {
@@ -187,7 +211,8 @@ export function sourceFiles(root) {
187
211
  export function importViolation(specifier, filename, options, resolve) {
188
212
  const { root, dsRoot, config } = options;
189
213
  // ADS implementation code needs its private imports; the showcase is a consumer.
190
- 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.
215
+ if (specifier === `${config.publicAlias}/review`) return !isDevelopmentFile(filename, options) && !isTestFile(filename);
191
216
  if (specifier.startsWith(`${config.publicAlias}/`)) return true;
192
217
  if (specifier === config.publicAlias) return false;
193
218
  const imported = resolve ? resolve(specifier, filename)
@@ -195,6 +220,9 @@ export function importViolation(specifier, filename, options, resolve) {
195
220
  : specifier.startsWith(`${config.dsDir}/`) ? [path.resolve(root, specifier)] : [];
196
221
  return imported.some((file) => {
197
222
  if (!isInside(dsRoot, file)) return false;
223
+ // Review implementations can reuse local foundation helpers. Resolved public
224
+ // UI/controller contracts are enforced by the layer inspector, as for patterns.
225
+ if (isReviewFile(filename, options)) return false;
198
226
  // Owner-local white-box tests may exercise private helpers without making
199
227
  // those helpers public API. Scenarios remain public-API consumers.
200
228
  if (isTestFile(filename) && isInside(dsRoot, filename) && isInside(path.dirname(filename), file)) return false;