@reqvire-org/ads 0.3.18 → 0.3.20

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 (78) hide show
  1. package/dist/behaviors.mjs +5 -5
  2. package/dist/bootstrap-ds/ADS.md +2 -2
  3. package/dist/bootstrap-ds/README.md +1 -1
  4. package/dist/bootstrap-ds/design-system/index.ts +0 -1
  5. package/dist/bootstrap-ds/design-system/review/index.ts +1 -0
  6. package/dist/bootstrap-ds/design-system/tokens/Foundation.scenario.tsx +1 -1
  7. package/dist/bootstrap-ds/ds.config.json +1 -1
  8. package/dist/bootstrap-ds/tsconfig.json +18 -3
  9. package/dist/bootstrap-ds/vite.config.ts +1 -1
  10. package/dist/boundaries.mjs +4 -2
  11. package/dist/check.mjs +20 -13
  12. package/dist/config.mjs +13 -2
  13. package/dist/init.mjs +17 -14
  14. package/dist/layers.mjs +288 -0
  15. package/dist/lint-config.mjs +1 -1
  16. package/dist/mcp/context.mjs +2 -1
  17. package/dist/mcp/edit-preparation.mjs +3 -3
  18. package/dist/mcp/mutations.mjs +1 -1
  19. package/dist/mcp/prompts.mjs +1 -1
  20. package/dist/mcp/server.mjs +6 -2
  21. package/dist/mcp/widget.bundle.js +1 -1
  22. package/dist/oxlint.mjs +3 -1
  23. package/dist/preview-resources.mjs +11 -1
  24. package/dist/public-api.mjs +13 -6
  25. package/dist/scenario-trace.mjs +6 -6
  26. package/dist/scenarios-vite.mjs +2 -1
  27. package/dist/scenarios.mjs +10 -7
  28. package/dist/showcase/client.bundle.js +1 -1
  29. package/dist/showcase/sandbox.bundle.js +1 -1
  30. package/dist/skills/ads-break-ui/SKILL.md +21 -6
  31. package/dist/skills/ads-break-ui/references/guarded-editing.md +1 -1
  32. package/dist/skills/ads-break-ui/references/layers.md +123 -0
  33. package/dist/skills/ads-break-ui/references/review.md +96 -0
  34. package/dist/skills/ads-break-ui/references/scenarios.md +24 -3
  35. package/dist/skills/ads-designer/SKILL.md +22 -7
  36. package/dist/skills/ads-designer/assets/bootstrap-ds/ADS.md +2 -2
  37. package/dist/skills/ads-designer/assets/bootstrap-ds/README.md +1 -1
  38. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/index.ts +0 -1
  39. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/review/index.ts +1 -0
  40. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/Foundation.scenario.tsx +1 -1
  41. package/dist/skills/ads-designer/assets/bootstrap-ds/ds.config.json +1 -1
  42. package/dist/skills/ads-designer/assets/bootstrap-ds/tsconfig.json +18 -3
  43. package/dist/skills/ads-designer/assets/bootstrap-ds/vite.config.ts +1 -1
  44. package/dist/skills/ads-designer/references/guarded-editing.md +1 -1
  45. package/dist/skills/ads-designer/references/layers.md +123 -0
  46. package/dist/skills/ads-designer/references/review.md +96 -0
  47. package/dist/skills/ads-designer/references/scenarios.md +24 -3
  48. package/dist/skills/ads-import/SKILL.md +21 -6
  49. package/dist/skills/ads-import/references/guarded-editing.md +1 -1
  50. package/dist/skills/ads-import/references/layers.md +123 -0
  51. package/dist/skills/ads-import/references/review.md +96 -0
  52. package/dist/skills/ads-import/references/scenarios.md +24 -3
  53. package/dist/skills/ads-migrate/SKILL.md +21 -6
  54. package/dist/skills/ads-migrate/references/guarded-editing.md +1 -1
  55. package/dist/skills/ads-migrate/references/layers.md +123 -0
  56. package/dist/skills/ads-migrate/references/review.md +96 -0
  57. package/dist/skills/ads-migrate/references/scenarios.md +24 -3
  58. package/dist/skills/ads-motion/SKILL.md +22 -7
  59. package/dist/skills/ads-motion/references/audit.md +1 -1
  60. package/dist/skills/ads-motion/references/guarded-editing.md +1 -1
  61. package/dist/skills/ads-motion/references/layers.md +123 -0
  62. package/dist/skills/ads-motion/references/motion-review.md +35 -0
  63. package/dist/skills/ads-motion/references/review.md +96 -35
  64. package/dist/skills/ads-motion/references/scenarios.md +24 -3
  65. package/dist/units.mjs +17 -14
  66. package/docs/ADS.md +32 -5
  67. package/docs/ARCHITECTURE.md +37 -4
  68. package/docs/BEHAVIORS.md +7 -0
  69. package/docs/CHANGELOG.md +23 -0
  70. package/docs/GUIDE.md +3 -2
  71. package/docs/MCP.md +27 -6
  72. package/package.json +1 -1
  73. package/dist/bootstrap-ds/design-system/patterns/token-preview/Scenario.tsx +0 -4
  74. package/dist/bootstrap-ds/design-system/patterns/token-preview/fixture.ts +0 -8
  75. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/patterns/token-preview/Scenario.tsx +0 -4
  76. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/patterns/token-preview/fixture.ts +0 -8
  77. /package/dist/bootstrap-ds/design-system/{patterns → review}/token-preview/TokenPreview.tsx +0 -0
  78. /package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/{patterns → review}/token-preview/TokenPreview.tsx +0 -0
@@ -3,10 +3,10 @@ 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
12
 
@@ -31,7 +31,7 @@ export function inspectBehaviors(options, suppliedProgram, suppliedRegistry) {
31
31
  if (!files.length) return { contracts: [], diagnostics: [] };
32
32
  const program = suppliedProgram ?? ts.createProgram(sourceFiles(dsRoot).filter(file => /\.[cm]?[jt]sx?$/.test(file)), { ...projectCompilerOptions(options), allowJs: true });
33
33
  const checker = program.getTypeChecker();
34
- const apis = new Map(publicApi(program, options).map(item => [item.name, item]));
34
+ const apis = new Map(scenarioApi(program, options).map(item => [item.name, item]));
35
35
  const scenarios = (suppliedRegistry ?? inspectScenarios(options, program)).scenarios;
36
36
  const diagnostics = [], contracts = [], owners = new Set();
37
37
  const relative = file => path.relative(root, file).replaceAll("\\", "/");
@@ -45,7 +45,7 @@ export function inspectBehaviors(options, suppliedProgram, suppliedRegistry) {
45
45
  value = parsed.data;
46
46
  } catch (error) { report(error.message); continue; }
47
47
  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; }
48
+ if (!["components", "patterns", "controllers", "review"].includes(unit?.split("/")[0])) { report("Behavior contracts belong to a component, pattern, controller or development review unit."); continue; }
49
49
  if (owners.has(unit)) { report(`Only one behavior contract may describe ${unit}.`); continue; }
50
50
  owners.add(unit);
51
51
  function owned(target, field) {
@@ -61,7 +61,7 @@ export function inspectBehaviors(options, suppliedProgram, suppliedRegistry) {
61
61
  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
62
  } else {
63
63
  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.`);
64
+ if (source && isDevelopmentFile(source, options) && !isReviewFile(source, options)) report(`Subject ${name} must identify runtime implementation, not a fixture, scenario or check.`);
65
65
  if (source && entry.symbol) {
66
66
  const ast = program.getSourceFile(source);
67
67
  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}.`);
@@ -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
 
@@ -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";
1
+ import { TokenPreview } from "@ds/review";
2
2
  import { fixture } from "./Foundation.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
  ]
@@ -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",
@@ -5,14 +5,16 @@ import { isInside, isStateVariable, isTestFile, isDevelopmentFile } from "./conf
5
5
  import { isSharedToken } from "./token-policy.mjs";
6
6
  import { scopedTokenOwner } from "./scoped-tokens.mjs";
7
7
 
8
- export function projectCompilerOptions({ root }) {
8
+ export function projectCompilerOptions({ root, config, dsRoot }) {
9
9
  const file = ts.findConfigFile(root, ts.sys.fileExists, "tsconfig.json");
10
10
  let compiler = {};
11
11
  if (file && isInside(root, file)) {
12
12
  const parsed = ts.readConfigFile(file, ts.sys.readFile);
13
13
  if (!parsed.error) compiler = ts.parseJsonConfigFileContent(parsed.config, ts.sys, path.dirname(file)).options;
14
14
  }
15
- return compiler;
15
+ return config && dsRoot ? { ...compiler, paths: { ...compiler.paths,
16
+ [config.publicAlias]: [path.join(dsRoot, config.publicEntry)],
17
+ [`${config.publicAlias}/review`]: [path.join(dsRoot, config.reviewEntry)] } } : compiler;
16
18
  }
17
19
 
18
20
  export function importResolver(options) {
package/dist/check.mjs CHANGED
@@ -3,7 +3,7 @@ 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";
@@ -25,8 +25,9 @@ import { createScenarioSlots } from "./scenario-slots.mjs";
25
25
  import { createScenarioSelectors } from "./scenario-selectors.mjs";
26
26
  import { architectureEvidence, failure } from "./evidence.mjs";
27
27
  import { propertyRegistration } from "./property-registrations.mjs";
28
+ import { createLayerBoundaries, layerSourceFiles } from "./layers.mjs";
28
29
 
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.";
30
+ 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
31
  const inlineStyleGuidance = `Own visual styles in component/pattern Linaria. ${scenarioLayoutGuidance} Reserve inlineStyleFiles for documented measured geometry, not demo layout.`;
31
32
 
32
33
  const showcaseControls = new Set(["a", "button", "input", "select", "textarea", "option", "optgroup", "table", "thead", "tbody", "tfoot", "tr", "td", "th", "dialog", "details", "summary", "progress", "meter"]);
@@ -52,16 +53,19 @@ export function checkProject(directory, { trackingRoot } = {}) {
52
53
  const rootHtml = path.join(root, "index.html");
53
54
  const files = [...new Set([...sourceFiles(sourceRoot), ...sourceFiles(dsRoot), ...(existsSync(rootHtml) ? [rootHtml] : [])])];
54
55
  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));
56
+ const scriptFiles = layerSourceFiles([...new Set([...files, ...boundaryFiles])].filter((file) => /\.[cm]?[jt]sx?$/.test(file)), options);
56
57
  const compiler = projectCompilerOptions(options);
57
58
  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
59
  const checker = program.getTypeChecker();
59
60
  const atomicCompositionReview = createAtomicCompositionReview(program, options);
60
61
  const scenarioTrace = createScenarioTrace(program, options);
61
62
  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 });
63
+ function report(file, rule, message, line = 1, column = 1, detail = {}) {
64
+ diagnostics.push({ file: path.relative(root, file).replaceAll("\\", "/"), rule, message, line, column, ...detail });
64
65
  }
66
+ const layers = createLayerBoundaries(program, options, report), layerStyles = [];
67
+ layers.inspectDependencies();
68
+ for (const file of layers.runtimeHelpers()) if (!files.includes(file)) files.push(file);
65
69
  inspectStyleBoundaries(options, program, report);
66
70
  inspectShowcasePages(options, program, resolveImport, report);
67
71
  inspectShowcaseCoverage(options, program, report);
@@ -82,7 +86,7 @@ export function checkProject(directory, { trackingRoot } = {}) {
82
86
  let defined=false;css.walkDecls(registration.name,()=>{defined=true;});
83
87
  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
88
  }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.");
89
+ 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
90
  const violation=checkComponentVariable(registration.name,file,options);if(violation)throw new Error(violation.message);
87
91
  }
88
92
  for(const descriptor of rule.nodes??[])if(descriptor.type==="decl")for(const ref of references(descriptor.value)){
@@ -106,9 +110,11 @@ export function checkProject(directory, { trackingRoot } = {}) {
106
110
  let css;
107
111
  try { css = postcss.parse(text, { from: file }); }
108
112
  catch (error) { report(file, "css-syntax", error.reason ?? error.message, offset.line + (error.line ?? 1) - 1); return; }
113
+ layers.collectComponentStyles(file, css);
114
+ layerStyles.push({ file, css, offset });
109
115
  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);
116
+ 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.");
117
+ 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
118
  css.walkAtRules("import", (rule) => {
113
119
  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
120
  const first = valueParser(rule.params).nodes.find((node) => node.type !== "space" && node.type !== "comment");
@@ -126,7 +132,7 @@ export function checkProject(directory, { trackingRoot } = {}) {
126
132
  }
127
133
  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
134
  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);
135
+ 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
136
  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
137
  });
132
138
  if (foundation) css.walkAtRules(rule => {
@@ -190,7 +196,7 @@ export function checkProject(directory, { trackingRoot } = {}) {
190
196
  report(file, "source-syntax", ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n"), position.line + 1, position.character + 1);
191
197
  }
192
198
  const inlineAllowed = config.inlineStyleFiles.some((allowed) => path.resolve(root, allowed) === file);
193
- const showcase = isDevelopmentFile(file, options) && !isTestFile(file);
199
+ const showcase = isDevelopmentFile(file, options) && !isTestFile(file) && !isReviewFile(file, options);
194
200
  function nodeReport(node, rule, message, detail) {
195
201
  const position = source.getLineAndCharacterOfPosition(node.getStart(source));
196
202
  report(file, rule, message, position.line + 1, position.character + 1);
@@ -234,8 +240,8 @@ export function checkProject(directory, { trackingRoot } = {}) {
234
240
  function inspectClass(value) {
235
241
  if (ts.isStringLiteralLike(value)) {
236
242
  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.");
243
+ 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.");
244
+ 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
245
  }
240
246
  ts.forEachChild(value, inspectClass);
241
247
  }
@@ -295,7 +301,7 @@ export function checkProject(directory, { trackingRoot } = {}) {
295
301
  }
296
302
  const mutation = styleWriteViolation(node, file, options);
297
303
  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.");
304
+ if (!inlineAllowed && isUiCssProperties(node, checker)) nodeReport(node, "css-properties", "Keep CSSProperties out of the public API. Use deliberate component contracts or a documented measured-geometry renderer boundary; demo layout belongs in a development-only review/ helper.");
299
305
  if (ts.isJsxAttribute(node) && ts.isIdentifier(node.name) && node.name.text === "className" && node.initializer) {
300
306
  inspectClass(node.initializer);
301
307
  }
@@ -310,6 +316,7 @@ export function checkProject(directory, { trackingRoot } = {}) {
310
316
  visit(source);
311
317
  }
312
318
  // Bounded clients should see actionable warnings before source-analysis limits.
319
+ for (const { file, css, offset } of layerStyles) layers.inspectStyles(file, css, offset);
313
320
  advisories.sort((first, second) => Number(first.severity === "info") - Number(second.severity === "info"));
314
321
  return architectureEvidence({ ok: diagnostics.length === 0, diagnostics, ...(advisories.length ? { advisories } : {}) });
315
322
  }
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.");
@@ -165,11 +167,16 @@ export function isDevelopmentFile(file, { dsRoot }) {
165
167
  if (!isInside(dsRoot, file)) return isTestFile(file);
166
168
  const relative = path.relative(dsRoot, file).replaceAll("\\", "/");
167
169
  return isTestFile(file) || isScenarioFile(file) || isFixtureFile(file) || isBehaviorFile(file) || isTokenManifestFile(file)
168
- || /^(?:showcase|composition)\//.test(relative)
170
+ || /^(?:showcase|composition|review)\//.test(relative)
169
171
  || /(?:^|\/)(?:scenario|fixtures)(?:\/|$)/.test(relative)
170
172
  || /\.scenario(?:-adapter)?\.[cm]?[jt]sx?$/.test(relative);
171
173
  }
172
174
 
175
+ /** Review implementations may own demo presentation; scenarios/fixtures may not. */
176
+ export function isReviewFile(file, { dsRoot }) {
177
+ return isInside(path.join(dsRoot, "review"), file) && !isTestFile(file) && !isScenarioFile(file) && !isFixtureFile(file) && !isBehaviorFile(file) && !isTokenManifestFile(file);
178
+ }
179
+
173
180
  const ignored = new Set(["node_modules", "dist", "dist-showcase", "dist-kit", ".git", ".vite", ".ds", "coverage"]);
174
181
 
175
182
  export function sourceFiles(root) {
@@ -188,6 +195,7 @@ export function importViolation(specifier, filename, options, resolve) {
188
195
  const { root, dsRoot, config } = options;
189
196
  // ADS implementation code needs its private imports; the showcase is a consumer.
190
197
  if (isInside(dsRoot, filename) && !isDevelopmentFile(filename, options)) return false;
198
+ if (specifier === `${config.publicAlias}/review`) return !isDevelopmentFile(filename, options) && !isTestFile(filename);
191
199
  if (specifier.startsWith(`${config.publicAlias}/`)) return true;
192
200
  if (specifier === config.publicAlias) return false;
193
201
  const imported = resolve ? resolve(specifier, filename)
@@ -195,6 +203,9 @@ export function importViolation(specifier, filename, options, resolve) {
195
203
  : specifier.startsWith(`${config.dsDir}/`) ? [path.resolve(root, specifier)] : [];
196
204
  return imported.some((file) => {
197
205
  if (!isInside(dsRoot, file)) return false;
206
+ // Review implementations can reuse local foundation helpers. Resolved public
207
+ // UI/controller contracts are enforced by the layer inspector, as for patterns.
208
+ if (isReviewFile(filename, options)) return false;
198
209
  // Owner-local white-box tests may exercise private helpers without making
199
210
  // those helpers public API. Scenarios remain public-API consumers.
200
211
  if (isTestFile(filename) && isInside(dsRoot, filename) && isInside(path.dirname(filename), file)) return false;
package/dist/init.mjs CHANGED
@@ -153,15 +153,15 @@ export function initProject(directory = process.cwd(), { toolPackage, packageMan
153
153
  });
154
154
  if (!existingTokens.length) {
155
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));
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")));