@reqvire-org/ads 0.3.3 → 0.3.5

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 (52) hide show
  1. package/README.md +12 -4
  2. package/dist/behaviors.mjs +157 -0
  3. package/dist/bootstrap-ds/ADS.md +7 -0
  4. package/dist/bootstrap-ds/design-system/controllers/.gitkeep +0 -0
  5. package/dist/check.mjs +7 -2
  6. package/dist/cli.mjs +17 -0
  7. package/dist/config.mjs +19 -3
  8. package/dist/css-values.mjs +26 -0
  9. package/dist/index.mjs +2 -0
  10. package/dist/init.mjs +1 -1
  11. package/dist/mcp/build-status.mjs +10 -3
  12. package/dist/mcp/catalog.mjs +11 -8
  13. package/dist/mcp/context.mjs +2 -2
  14. package/dist/mcp/mutations.mjs +10 -0
  15. package/dist/mcp/preview-owner-guard.mjs +29 -0
  16. package/dist/mcp/preview-watch.mjs +28 -4
  17. package/dist/mcp/preview-widget.bundle.js +2 -2
  18. package/dist/mcp/prompts.mjs +2 -2
  19. package/dist/mcp/server.mjs +56 -19
  20. package/dist/mcp/showcase-host.mjs +9 -5
  21. package/dist/public-api.mjs +39 -0
  22. package/dist/scenarios-compiler.mjs +17 -2
  23. package/dist/scenarios.mjs +27 -39
  24. package/dist/skills/README.md +3 -3
  25. package/dist/skills/ads-break-ui/SKILL.md +22 -0
  26. package/dist/skills/ads-designer/SKILL.md +23 -1
  27. package/dist/skills/ads-designer/assets/bootstrap-ds/ADS.md +7 -0
  28. package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/controllers/.gitkeep +0 -0
  29. package/dist/skills/ads-designer/references/cli.md +1 -1
  30. package/dist/skills/ads-designer/references/design-craft.md +1 -1
  31. package/dist/skills/ads-import/SKILL.md +22 -0
  32. package/dist/skills/ads-import/references/cli.md +1 -1
  33. package/dist/skills/{ads-assimilate → ads-migrate}/SKILL.md +28 -6
  34. package/dist/skills/ads-migrate/agents/openai.yaml +4 -0
  35. package/dist/skills/{ads-assimilate → ads-migrate}/references/cli.md +1 -1
  36. package/dist/skills/{ads-assimilate → ads-migrate}/references/native-implementation.md +11 -0
  37. package/dist/skills/ads-motion/SKILL.md +22 -0
  38. package/dist/tokens.mjs +3 -2
  39. package/dist/units.mjs +20 -9
  40. package/dist/verification/browser.mjs +27 -0
  41. package/dist/verification/playwright-reporter.cjs +20 -0
  42. package/dist/verification.mjs +214 -0
  43. package/docs/ADS.md +27 -2
  44. package/docs/ARCHITECTURE.md +39 -1
  45. package/docs/BEHAVIORS.md +230 -0
  46. package/docs/CHANGELOG.md +36 -0
  47. package/docs/GUIDE.md +16 -1
  48. package/docs/MCP.md +46 -7
  49. package/package.json +4 -2
  50. package/dist/skills/ads-assimilate/agents/openai.yaml +0 -4
  51. /package/dist/skills/{ads-assimilate → ads-migrate}/references/lint-and-docs.md +0 -0
  52. /package/dist/skills/{ads-assimilate → ads-migrate}/references/testing.md +0 -0
@@ -1,6 +1,7 @@
1
1
  import path from "node:path";
2
2
  import { existsSync } from "node:fs";
3
3
  import ts from "typescript";
4
+ import { publicApi } from "./public-api.mjs";
4
5
  import { uiOperation } from "./ui-framework.mjs";
5
6
  import { isDevelopmentFile, isInside, isScenarioFile, sourceFiles } from "./config.mjs";
6
7
  import { projectCompilerOptions } from "./boundaries.mjs";
@@ -27,46 +28,17 @@ function literal(node, checker, seen = new Set()) {
27
28
  }
28
29
 
29
30
  export function publicUi(program, options) {
30
- const checker = program.getTypeChecker();
31
- const entry = program.getSourceFile(path.join(options.dsRoot, options.config.publicEntry));
32
- const module = entry && checker.getSymbolAtLocation(entry);
33
- const items = [];
34
- for (const exported of module ? checker.getExportsOfModule(module) : []) {
35
- let symbol = exported;
36
- if (symbol.flags & ts.SymbolFlags.Alias) symbol = checker.getAliasedSymbol(symbol);
37
- const declaration = symbol.valueDeclaration ?? symbol.declarations?.[0];
38
- if (!declaration || !(symbol.flags & ts.SymbolFlags.Value)) continue;
39
- const file = declaration.getSourceFile().fileName;
40
- if (isDevelopmentFile(file, options)) continue;
41
- const kind = isInside(path.join(options.dsRoot, "components"), file) ? "components" : isInside(path.join(options.dsRoot, "patterns"), file) ? "patterns" : null;
42
- if (!kind) continue;
43
- let renderable = false;
44
- function visit(node) {
45
- if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node)) renderable = true;
46
- if (ts.isCallExpression(node) && ["createElement", "forwardRef", "memo"].includes(uiOperation(node.expression, checker))) renderable = true;
47
- if (ts.isTaggedTemplateExpression(node)) {
48
- let tag = node.tag;
49
- if (ts.isCallExpression(tag)) tag = tag.expression;
50
- let base = tag;
51
- while (ts.isPropertyAccessExpression(base)) base = base.expression;
52
- const symbol = checker.getSymbolAtLocation(base);
53
- if (symbol?.declarations?.some(item => ts.isImportSpecifier(item) && (item.propertyName ?? item.name).text === "styled" && item.parent.parent.parent.moduleSpecifier.text.startsWith("@linaria/") || ts.isNamespaceImport(item) && item.parent.parent.moduleSpecifier.text.startsWith("@linaria/") && /^\.styled(?:\.|$)/.test(tag.getText().slice(base.getText().length)))) renderable = true;
54
- }
55
- ts.forEachChild(node, visit);
56
- }
57
- visit(declaration);
58
- items.push({ name: exported.name, kind, file, declaration, renderable });
59
- }
60
- return items;
31
+ return publicApi(program, options).filter(item => item.kind !== "controllers");
61
32
  }
62
33
 
63
34
  /** Discover metadata statically. The MCP process never imports scenario modules. */
64
35
  export function inspectScenarios(options, suppliedProgram) {
65
36
  const { root, dsRoot } = options;
66
37
  const files = sourceFiles(dsRoot).filter(isScenarioFile);
67
- const program = suppliedProgram ?? ts.createProgram([path.join(dsRoot, options.config.publicEntry), ...files], projectCompilerOptions(options));
38
+ const program = suppliedProgram ?? ts.createProgram([path.join(dsRoot, options.config.publicEntry), ...files], { ...projectCompilerOptions(options), allowJs: true });
68
39
  const known = new Map(publicUi(program, options).map((item) => [item.name, item]));
69
40
  const checker = program.getTypeChecker();
41
+ const behaviorApis = new Map(publicApi(program, options).filter(item => !item.renderable && item.behavioral).map(item => [item.name, item]));
70
42
  const tokenNames = new Set(inspectTokens(root).tokens.map(token => token.name));
71
43
  const scenarios = [];
72
44
  const diagnostics = [];
@@ -105,21 +77,22 @@ export function inspectScenarios(options, suppliedProgram) {
105
77
  }
106
78
  const fixtureBase = path.basename(file).startsWith("Scenario.") ? path.join(path.dirname(file), "fixture") : file.replace(/\.scenario\.[^.]+$/, ".fixture");
107
79
  if (![".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", ".mjs", ".cjs", ".json"].some(extension => existsSync(fixtureBase + extension))) report(file, "Keep this scenario's inputs beside it: fixture.ts (directory entry) or <Owner>.fixture.ts (flat entry). An empty fixture is valid for a component without inputs.");
108
- if (!metadata || Object.keys(metadata).some((key) => !["title", "covers", "tokens", "samples"].includes(key)) || typeof metadata.title !== "string" || !metadata.title.trim() || !Array.isArray(metadata.covers) || !metadata.covers.length || metadata.covers.some((name) => typeof name !== "string" || !known.get(name)?.renderable)
80
+ if (!metadata || Object.keys(metadata).some((key) => !["title", "covers", "exercises", "tokens", "samples"].includes(key)) || typeof metadata.title !== "string" || !metadata.title.trim() || !Array.isArray(metadata.covers) || !metadata.covers.length || metadata.covers.some((name) => typeof name !== "string" || !known.get(name)?.renderable)
81
+ || Object.hasOwn(metadata, "exercises") && (!Array.isArray(metadata.exercises) || metadata.exercises.some(name => typeof name !== "string" || !behaviorApis.has(name)))
109
82
  || Object.hasOwn(metadata, "tokens") && (!Array.isArray(metadata.tokens) || metadata.tokens.some(name => typeof name !== "string" || !name.startsWith("--")))
110
83
  || kind === "tokens" && (Object.hasOwn(metadata, "tokens") || !validTokenSamples(metadata.samples) || !metadata.covers.some(name => known.get(name)?.kind === "patterns"))
111
84
  || Object.hasOwn(metadata, "samples") && !validTokenSamples(metadata.samples)) {
112
- report(file, "Export static metadata with nonempty covers. Token scenarios must render a project-owned public review pattern and declare samples: fixture.samples, a literal list of { token, property, probe } CSS samples. A plain value list or covers: [] is incomplete."); continue;
85
+ report(file, "Export static metadata with nonempty covers naming rendered public UI. Optional exercises names public behavioral APIs, not rendered UI. Token scenarios must render a project-owned public review pattern and declare samples: fixture.samples, a literal list of { token, property, probe } CSS samples. A plain value list or covers: [] is incomplete."); continue;
113
86
  }
114
87
  if (!["composition", "tokens"].includes(kind) && metadata.covers.some((name) => known.get(name).kind !== kind)) report(file, "Component scenarios cover primitives; pattern scenarios cover patterns. Use composition/ for larger interfaces.");
115
88
  const tokens = [...new Set([...(metadata.tokens ?? []), ...(metadata.samples ?? []).map(sample => sample.token)])];
116
89
  for (const name of tokens) if (!tokenNames.has(name)) diagnostics.push({ file: path.relative(root, file).replaceAll("\\", "/"), rule: "token-scenario-reference", line: 1, column: 1, message: `Scenario references undeclared token ${name}. Use a real foundation token.` });
117
- scenarios.push({ id, kind, title: metadata.title, covers: [...new Set(metadata.covers)], tokens, ...(metadata.samples ? { samples: metadata.samples } : {}), file: path.relative(root, file).replaceAll("\\", "/"), artifact: path.relative(root, scenarioArtifact(root, id)).replaceAll("\\", "/") });
90
+ scenarios.push({ id, kind, title: metadata.title, covers: [...new Set(metadata.covers)], ...(metadata.exercises ? { exercises: [...new Set(metadata.exercises)] } : {}), tokens, ...(metadata.samples ? { samples: metadata.samples } : {}), file: path.relative(root, file).replaceAll("\\", "/"), artifact: path.relative(root, scenarioArtifact(root, id)).replaceAll("\\", "/") });
118
91
  }
119
92
  return { scenarios: scenarios.sort((a, b) => a.id.localeCompare(b.id)), diagnostics };
120
93
  }
121
94
 
122
- export function inspectScenarioCoverage(options, program, report) {
95
+ export function inspectScenarioCoverage(options, program, report, advise = () => {}) {
123
96
  const registry = inspectScenarios(options, program);
124
97
  for (const item of registry.diagnostics) report(path.join(options.root, item.file), item.rule, item.message, item.line, item.column);
125
98
  const checker = program.getTypeChecker();
@@ -153,7 +126,7 @@ export function inspectScenarioCoverage(options, program, report) {
153
126
  return symbol.declarations?.length ? symbol : null;
154
127
  }
155
128
  const identities = new Map();
156
- for (const item of ui) {
129
+ for (const item of publicApi(program, options)) {
157
130
  if (!identities.has(item.declaration)) identities.set(item.declaration, new Set());
158
131
  identities.get(item.declaration).add(item.name);
159
132
  }
@@ -165,12 +138,16 @@ export function inspectScenarioCoverage(options, program, report) {
165
138
  for (const scenario of registry.scenarios) {
166
139
  const source = program.getSourceFile(path.join(options.root, scenario.file));
167
140
  const rendered = new Set();
141
+ const exercised = new Set(), referencedApis = new Set();
168
142
  const referencedTokens = new Set();
169
143
  let samplesPassed = false;
170
144
  const visited = new Set();
171
145
  function visit(node, reached = false) {
172
146
  if (!node || visited.has(node)) return;
173
- if (!reached && (ts.isFunctionDeclaration(node) || ts.isVariableDeclaration(node))) return;
147
+ if (!reached && ts.isFunctionDeclaration(node)) return;
148
+ // Calls/construction in a mounted adapter execute even when only a result
149
+ // property is read later. Unused literal fixture fields still do not count.
150
+ if (!reached && ts.isVariableDeclaration(node) && !(node.initializer && (ts.isCallExpression(node.initializer) || ts.isNewExpression(node.initializer)))) return;
174
151
  if (!reached && (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) && !ts.isCallExpression(node.parent) && !ts.isJsxExpression(node.parent)) return;
175
152
  visited.add(node);
176
153
  if (ts.isStringLiteralLike(node) && (isSharedToken(node.text) || scenario.tokens.includes(node.text))) referencedTokens.add(node.text);
@@ -180,18 +157,25 @@ export function inspectScenarioCoverage(options, program, report) {
180
157
  if (expression) {
181
158
  const symbol = canonical(symbolOf(expression));
182
159
  const names = identities.get(symbol?.valueDeclaration ?? symbol?.declarations?.[0]);
183
- for (const name of names ?? []) rendered.add(name);
160
+ for (const name of names ?? []) { rendered.add(name); exercised.add(name); }
184
161
  if (scenario.kind === "tokens" && [...names ?? []].some(name => scenario.covers.includes(name))) {
185
162
  const attributes = ts.isCallExpression(node) ? literal(node.arguments[1], checker) : Object.assign({}, ...node.attributes.properties.map(attribute => ts.isJsxSpreadAttribute(attribute) ? literal(attribute.expression, checker) : ts.isJsxAttribute(attribute) && ts.isIdentifier(attribute.name) && attribute.initializer && ts.isJsxExpression(attribute.initializer) ? { [attribute.name.text]: literal(attribute.initializer.expression, checker) } : {}));
186
163
  if (JSON.stringify(attributes?.samples) === JSON.stringify(scenario.samples)) samplesPassed = true;
187
164
  }
188
165
  }
166
+ if (ts.isCallExpression(node) || ts.isNewExpression(node)) {
167
+ let expression = node.expression;
168
+ if (ts.isPropertyAccessExpression(expression) && ["call", "apply"].includes(expression.name.text)) expression = expression.expression;
169
+ const symbol = canonical(symbolOf(expression));
170
+ for (const name of identities.get(symbol?.valueDeclaration ?? symbol?.declarations?.[0]) ?? []) exercised.add(name);
171
+ }
189
172
  // Follow local adapters reachable from the default entry. Unused example
190
173
  // functions do not satisfy coverage; production implementations are never
191
174
  // traversed to manufacture coverage for their internal dependencies.
192
175
  if (ts.isPropertyAccessExpression(node) || ts.isElementAccessExpression(node) || ts.isIdentifier(node) && !(ts.isPropertyAccessExpression(node.parent) || ts.isElementAccessExpression(node.parent))) {
193
176
  const symbol = canonical(symbolOf(node));
194
177
  const declaration = symbol?.valueDeclaration;
178
+ for (const name of identities.get(declaration ?? symbol?.declarations?.[0]) ?? []) referencedApis.add(name);
195
179
  if (declaration && isDevelopmentFile(declaration.getSourceFile().fileName, options)) visit(declaration, true);
196
180
  }
197
181
  ts.forEachChild(node, child => visit(child, reached && (ts.isExportAssignment(node) || ts.isVariableDeclaration(node))));
@@ -201,6 +185,10 @@ export function inspectScenarioCoverage(options, program, report) {
201
185
  if (!rendered.has(name)) report(source.fileName, "scenario-render", `${name} is declared in covers but has no public render reference in this scenario. Render the real API; an import alone does not count.`);
202
186
  else covered.add(name);
203
187
  }
188
+ for (const name of scenario.exercises ?? []) {
189
+ if (!exercised.has(name) && !referencedApis.has(name)) report(source.fileName, "scenario-exercise", `${name} is declared in exercises but has no reachable behavioral use. Imports and unused adapters do not count.`);
190
+ else if (!exercised.has(name)) advise(source.fileName, "scenario-exercise-review", `${name} is referenced dynamically. Verify its invocation in the registered browser/behavior checks; a reference alone does not prove execution.`);
191
+ }
204
192
  for (const name of referencedTokens) if (!declaredTokens.has(name)) report(source.fileName, "token-scenario-reference", `Reachable scenario input references undeclared token ${name}.`);
205
193
  if (scenario.kind === "tokens" && !samplesPassed) report(source.fileName, "token-scenario-render", "Pass the declared fixture.samples into a rendered public review pattern's samples prop. Printing token names or reading unused fixtures is not sample coverage.");
206
194
  for (const name of scenario.tokens) {
@@ -3,13 +3,13 @@
3
3
  Install the skills you need with `npx skills`:
4
4
 
5
5
  ```sh
6
- npx skills add reqvire-org/ads --skill ads-designer --skill ads-import --skill ads-assimilate --skill ads-break-ui --skill ads-motion
6
+ npx skills add reqvire-org/ads --skill ads-designer --skill ads-import --skill ads-migrate --skill ads-break-ui --skill ads-motion
7
7
  ```
8
8
 
9
9
  For local development, install from this checkout:
10
10
 
11
11
  ```sh
12
- npx skills add ./skills --skill ads-designer --skill ads-import --skill ads-assimilate --skill ads-break-ui --skill ads-motion
12
+ npx skills add ./skills --skill ads-designer --skill ads-import --skill ads-migrate --skill ads-break-ui --skill ads-motion
13
13
  ```
14
14
 
15
15
  Each skill includes its own instructions and references. `ads-designer` also
@@ -36,7 +36,7 @@ the browser, and provides a running URL or exact command for review. See the
36
36
  |-------|-----|---------|
37
37
  | [ads-designer](ads-designer/SKILL.md) | Bootstrap or build UI using the local ADS, with craft/mobile guidance | `$ads-designer build a settings page using this project's ADS` |
38
38
  | [ads-import](ads-import/SKILL.md) | Import selected external ADS source, brand artwork, or compatible source token values | `$ads-import /path/to/source-repo component Toast` |
39
- | [ads-assimilate](ads-assimilate/SKILL.md) | Study an external component library and implement a native counterpart | `$ads-assimilate https://github.com/emilkowalski/sonner` |
39
+ | [ads-migrate](ads-migrate/SKILL.md) | Study an external component library and implement a native counterpart | `$ads-migrate https://github.com/emilkowalski/sonner` |
40
40
  | [ads-break-ui](ads-break-ui/SKILL.md) | Stress-test real components/patterns; optionally fix demonstrated failures | `$ads-break-ui AppShell +fix` |
41
41
  | [ads-motion](ads-motion/SKILL.md) | Implement, review, audit, or profile motion | `$ads-motion review Toast` |
42
42
 
@@ -46,6 +46,23 @@ ADS gates. For binary/large assets, generate `git diff --binary --full-index` in
46
46
  stay out of model context. Binary changes require Git. When editing is disabled,
47
47
  use the client's normal source-editing tools.
48
48
 
49
+
50
+ Controllers are UI behavior owners: private helpers stay with their component or
51
+ pattern; reusable controllers use discovered `controllers/<Name>` units. Use the
52
+ project's configured framework and declared dependencies, or plain TypeScript,
53
+ browser APIs and local helpers. MCP hosts supply no ad hoc runtime APIs. The
54
+ framework setting selects compilation/mounting; controller checks also validate
55
+ imports. For controller/behavior work, read `ads://guides/behaviors` (offline:
56
+ the installed ADS package's `docs/BEHAVIORS.md`). Discover registered guarantees
57
+ with `ads_behaviors`; use `ads_verify` for selected real checks. Optional
58
+ `exercises` links public behavioral APIs to scenarios; keep `covers` for rendered
59
+ UI. Registration and demonstration are not passing evidence. `behavior.json`,
60
+ scenarios, fixtures and checks stay out of production exports/imports. Verify
61
+ current results, including lifecycle setup/cleanup/setup and the actual browser
62
+ profile when promised. React StrictMode needs a development harness; wrapping a
63
+ production widget cannot replay development effects. Keep dynamic behavior and
64
+ capabilities intact rather than rewriting them only for static coverage.
65
+
49
66
  Before trusting a preview after edits, check `ads_status.build` or
50
67
  `ads://project/build`. Fix build diagnostics in owned source; an older view or
51
68
  unreported freshness is not evidence of current code. Notifications may not
@@ -226,6 +243,11 @@ Do not copy an unchecked upstream preflight or turn a component repair into a re
226
243
  Keep behavior and skin in their runtime owner. Put demo-only sizing and framing
227
244
  in a public review/layout pattern; add component props for a deliberate runtime
228
245
  API need. Preserve required capabilities while following ownership rules.
246
+ Use tokens for absolute/font-relative lengths, including `ch`, `ex`, `cap`, `ic`
247
+ and `lh`; calculations and fallback values follow the same rule. Structural
248
+ angle units remain available for orientation, skew and spin. Put tunable visual
249
+ angles in project tokens; static checks cannot infer that intent. Percentages,
250
+ grid fractions and viewport/container relationships retain structural use.
229
251
  Browser review still verifies the effects.
230
252
 
231
253
  Invalid metadata and source failures keep the managed compiler alive for repairs.
@@ -78,6 +78,23 @@ ADS gates. For binary/large assets, generate `git diff --binary --full-index` in
78
78
  stay out of model context. Binary changes require Git. When editing is disabled,
79
79
  use the client's normal source-editing tools.
80
80
 
81
+ Controllers are UI behavior owners: private helpers stay with their component or
82
+ pattern; reusable controllers use discovered `controllers/<Name>` units. Use the
83
+ project's configured framework and declared dependencies, or plain TypeScript,
84
+ browser APIs and local helpers. MCP hosts supply no ad hoc runtime APIs. The
85
+ framework setting selects compilation/mounting; controller checks also validate
86
+ imports. For controller/behavior work, read `ads://guides/behaviors` (offline:
87
+ the installed ADS package's `docs/BEHAVIORS.md`). Discover registered guarantees
88
+ with `ads_behaviors`; use `ads_verify` for selected real checks. Optional
89
+ `exercises` links public behavioral APIs to scenarios; keep `covers` for rendered
90
+ UI. Registration and demonstration are not passing evidence. `behavior.json`,
91
+ scenarios, fixtures and checks stay out of production exports/imports. Verify
92
+ current results, including lifecycle setup/cleanup/setup and the actual browser
93
+ profile when promised. React StrictMode needs a development harness; wrapping a
94
+ production widget cannot replay development effects. Keep dynamic behavior and
95
+ capabilities intact rather than rewriting them only for static coverage.
96
+
97
+
81
98
  Check `ads_status.previewWatch` before launching another preview builder. With a
82
99
  managed watcher, edit owned sources, wait for the rebuild and invoke the preview
83
100
  again; the client already starts and stops the builder through `ads mcp`. Check
@@ -153,7 +170,7 @@ Do not assume it has a particular component, hook, font, or token helper.
153
170
  Record the source revision and required dependencies.
154
171
 
155
172
  Use the ADS import workflow to bring selected existing ADS items into the target,
156
- or assimilation to create a native component from an upstream library. Follow
173
+ or migration to create a native component from an upstream library. Follow
157
174
  those workflows directly if the corresponding skills are unavailable: copy
158
175
  selected source and its dependency closure, or study/reproduce the upstream
159
176
  contract; integrate local ownership, exports, styles, assets, credits, applicable
@@ -364,6 +381,11 @@ Do not copy an unchecked upstream preflight or turn a component repair into a re
364
381
  Keep behavior and skin in their runtime owner. Put demo-only sizing and framing
365
382
  in a public review/layout pattern; add component props for a deliberate runtime
366
383
  API need. Preserve required capabilities while following ownership rules.
384
+ Use tokens for absolute/font-relative lengths, including `ch`, `ex`, `cap`, `ic`
385
+ and `lh`; calculations and fallback values follow the same rule. Structural
386
+ angle units remain available for orientation, skew and spin. Put tunable visual
387
+ angles in project tokens; static checks cannot infer that intent. Percentages,
388
+ grid fractions and viewport/container relationships retain structural use.
367
389
  Browser review still verifies the effects.
368
390
 
369
391
  Invalid metadata and source failures keep the managed compiler alive for repairs.
@@ -241,3 +241,10 @@ Run `npm run build` after adding or changing components. Add meaningful tests
241
241
  for stateful behavior, accessibility, gestures, or async lifecycle where needed.
242
242
  Run `npm run dev` to inspect the real showcase. A screenshot alone does not
243
243
  verify interaction or animation. Generated output and dependencies are not source.
244
+
245
+ Shared behavior may live in `controllers/<Name>/`; private controllers stay with
246
+ their component/pattern. Use the configured framework and declared dependencies.
247
+ Behavior contracts/checks are development-only, and scenario `exercises` links
248
+ public hooks to demonstrations separately from rendered `covers`. Read the
249
+ installed ADS package's `docs/BEHAVIORS.md` or MCP `ads://guides/behaviors` before
250
+ registering guarantees, profiles and verification.
@@ -102,7 +102,7 @@ script but does not install the project preview compiler. Integrate its graph
102
102
  compiler/mount adapter and configure `mcp.previewManifest` plus `mcp.previewWatch`;
103
103
  then use the same foundation and Linaria extraction as the application. Source discovery,
104
104
  selected component/pattern/style/brand imports, explicit theme-value imports, and
105
- assimilation are agent workflows. There is no CLI `import` command. Core checks
105
+ migration are agent workflows. There is no CLI `import` command. Core checks
106
106
  do not copy source, import values, or establish behavior/motion parity; continue
107
107
  project type checking, meaningful tests, and browser comparisons.
108
108
 
@@ -56,7 +56,7 @@ changing them to a generic default.
56
56
 
57
57
  Keep typed props, examples, state behavior, token hooks, and important tradeoffs
58
58
  in the item README. Record concrete upstream influences, authors, revision, and
59
- applicable notices when importing or assimilating source. Do not describe an
59
+ applicable notices when importing or migrating source. Do not describe an
60
60
  adapted implementation as independent solely because it uses native primitives.
61
61
 
62
62
  Run the actual project's checks after changes and inspect the real consumer
@@ -42,6 +42,23 @@ ADS gates. For binary/large assets, generate `git diff --binary --full-index` in
42
42
  stay out of model context. Binary changes require Git. When editing is disabled,
43
43
  use the client's normal source-editing tools.
44
44
 
45
+ Controllers are UI behavior owners: private helpers stay with their component or
46
+ pattern; reusable controllers use discovered `controllers/<Name>` units. Use the
47
+ project's configured framework and declared dependencies, or plain TypeScript,
48
+ browser APIs and local helpers. MCP hosts supply no ad hoc runtime APIs. The
49
+ framework setting selects compilation/mounting; controller checks also validate
50
+ imports. For controller/behavior work, read `ads://guides/behaviors` (offline:
51
+ the installed ADS package's `docs/BEHAVIORS.md`). Discover registered guarantees
52
+ with `ads_behaviors`; use `ads_verify` for selected real checks. Optional
53
+ `exercises` links public behavioral APIs to scenarios; keep `covers` for rendered
54
+ UI. Registration and demonstration are not passing evidence. `behavior.json`,
55
+ scenarios, fixtures and checks stay out of production exports/imports. Verify
56
+ current results, including lifecycle setup/cleanup/setup and the actual browser
57
+ profile when promised. React StrictMode needs a development harness; wrapping a
58
+ production widget cannot replay development effects. Keep dynamic behavior and
59
+ capabilities intact rather than rewriting them only for static coverage.
60
+
61
+
45
62
  Before trusting a preview after edits, check `ads_status.build` or
46
63
  `ads://project/build`. Fix build diagnostics in owned source; an older view or
47
64
  unreported freshness is not evidence of current code. Notifications may not
@@ -331,6 +348,11 @@ Do not copy an unchecked upstream preflight or turn a component repair into a re
331
348
  Keep behavior and skin in their runtime owner. Put demo-only sizing and framing
332
349
  in a public review/layout pattern; add component props for a deliberate runtime
333
350
  API need. Preserve required capabilities while following ownership rules.
351
+ Use tokens for absolute/font-relative lengths, including `ch`, `ex`, `cap`, `ic`
352
+ and `lh`; calculations and fallback values follow the same rule. Structural
353
+ angle units remain available for orientation, skew and spin. Put tunable visual
354
+ angles in project tokens; static checks cannot infer that intent. Percentages,
355
+ grid fractions and viewport/container relationships retain structural use.
334
356
  Browser review still verifies the effects.
335
357
 
336
358
  Invalid metadata and source failures keep the managed compiler alive for repairs.
@@ -102,7 +102,7 @@ script but does not install the project preview compiler. Integrate its graph
102
102
  compiler/mount adapter and configure `mcp.previewManifest` plus `mcp.previewWatch`;
103
103
  then use the same foundation and Linaria extraction as the application. Source discovery,
104
104
  selected component/pattern/style/brand imports, explicit theme-value imports, and
105
- assimilation are agent workflows. There is no CLI `import` command. Core checks
105
+ migration are agent workflows. There is no CLI `import` command. Core checks
106
106
  do not copy source, import values, or establish behavior/motion parity; continue
107
107
  project type checking, meaningful tests, and browser comparisons.
108
108
 
@@ -1,9 +1,9 @@
1
1
  ---
2
- name: ads-assimilate
3
- description: "Implement or refine a native design-system component from a component-library URL, studying the original implementation and verifying behavior, visuals, and motion against the real reference. Use for component assimilation, including local documentation and justified usage lint rules; not for simply installing or wrapping a dependency."
2
+ name: ads-migrate
3
+ description: "Implement or refine a native design-system component from a component-library URL, studying the original implementation and verifying behavior, visuals, and motion against the real reference. Use for component migration, including local documentation and justified usage lint rules; not for simply installing or wrapping a dependency."
4
4
  ---
5
5
 
6
- # ADS Assimilate
6
+ # ADS Migrate
7
7
 
8
8
  When ADS MCP is connected, discover public UI and addressable scenarios with
9
9
  `ads_status`/`ads_catalog`; use `ads_tokens` for definitions and values, and
@@ -43,6 +43,23 @@ ADS gates. For binary/large assets, generate `git diff --binary --full-index` in
43
43
  stay out of model context. Binary changes require Git. When editing is disabled,
44
44
  use the client's normal source-editing tools.
45
45
 
46
+
47
+ Controllers are UI behavior owners: private helpers stay with their component or
48
+ pattern; reusable controllers use discovered `controllers/<Name>` units. Use the
49
+ project's configured framework and declared dependencies, or plain TypeScript,
50
+ browser APIs and local helpers. MCP hosts supply no ad hoc runtime APIs. The
51
+ framework setting selects compilation/mounting; controller checks also validate
52
+ imports. For controller/behavior work, read `ads://guides/behaviors` (offline:
53
+ the installed ADS package's `docs/BEHAVIORS.md`). Discover registered guarantees
54
+ with `ads_behaviors`; use `ads_verify` for selected real checks. Optional
55
+ `exercises` links public behavioral APIs to scenarios; keep `covers` for rendered
56
+ UI. Registration and demonstration are not passing evidence. `behavior.json`,
57
+ scenarios, fixtures and checks stay out of production exports/imports. Verify
58
+ current results, including lifecycle setup/cleanup/setup and the actual browser
59
+ profile when promised. React StrictMode needs a development harness; wrapping a
60
+ production widget cannot replay development effects. Keep dynamic behavior and
61
+ capabilities intact rather than rewriting them only for static coverage.
62
+
46
63
  Before trusting a preview after edits, check `ads_status.build` or
47
64
  `ads://project/build`. Fix build diagnostics in owned source; an older view or
48
65
  unreported freshness is not evidence of current code. Notifications may not
@@ -75,8 +92,8 @@ workflows. Do not look for a built-in or optional UI catalogue in the tool.
75
92
  Invocation accepts a library/repository/documentation URL as its first argument:
76
93
 
77
94
  ```text
78
- $ads-assimilate <library-url> [component-name] [requirements]
79
- $ads-assimilate https://github.com/emilkowalski/sonner toast
95
+ $ads-migrate <library-url> [component-name] [requirements]
96
+ $ads-migrate https://github.com/emilkowalski/sonner toast
80
97
  ```
81
98
 
82
99
  The URL is the required reference, not a request to install the library into the
@@ -147,7 +164,7 @@ the scenario; larger entries belong under `composition/`. Browsing pages only
147
164
  select these entries. The scenario labels, controls and review layouts are public ADS entities with defined
148
165
  APIs. The outer shell belongs to the tool client. Showcase/scenario files define no CSS, custom classes, or widget markup.
149
166
  Change the owning ADS pattern when a frame layout is missing; do not recreate
150
- the assimilated UI there or override its internals.
167
+ the migrated UI there or override its internals.
151
168
 
152
169
  The showcase is a minimal shell plus an MCP client and Apps host, supplied by ADS
153
170
  tooling. It discovers Tokens/Components/Patterns/Composition entries through MCP
@@ -274,6 +291,11 @@ Do not copy an unchecked upstream preflight or turn a component repair into a re
274
291
  Keep behavior and skin in their runtime owner. Put demo-only sizing and framing
275
292
  in a public review/layout pattern; add component props for a deliberate runtime
276
293
  API need. Preserve required capabilities while following ownership rules.
294
+ Use tokens for absolute/font-relative lengths, including `ch`, `ex`, `cap`, `ic`
295
+ and `lh`; calculations and fallback values follow the same rule. Structural
296
+ angle units remain available for orientation, skew and spin. Put tunable visual
297
+ angles in project tokens; static checks cannot infer that intent. Percentages,
298
+ grid fractions and viewport/container relationships retain structural use.
277
299
  Browser review still verifies the effects.
278
300
 
279
301
  Invalid metadata and source failures keep the managed compiler alive for repairs.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "ADS Migrate"
3
+ short_description: "Adapt reference components into a native design system"
4
+ default_prompt: "Use $ads-migrate with a component-library URL to implement and verify a native design-system component."
@@ -102,7 +102,7 @@ script but does not install the project preview compiler. Integrate its graph
102
102
  compiler/mount adapter and configure `mcp.previewManifest` plus `mcp.previewWatch`;
103
103
  then use the same foundation and Linaria extraction as the application. Source discovery,
104
104
  selected component/pattern/style/brand imports, explicit theme-value imports, and
105
- assimilation are agent workflows. There is no CLI `import` command. Core checks
105
+ migration are agent workflows. There is no CLI `import` command. Core checks
106
106
  do not copy source, import values, or establish behavior/motion parity; continue
107
107
  project type checking, meaningful tests, and browser comparisons.
108
108
 
@@ -112,3 +112,14 @@ README and in a References and credits section in the component-local README.
112
112
  Link the library and the actual source/docs/articles studied, identify the
113
113
  version or commit, name the authors, and explain the concrete influences.
114
114
  Document material adaptations and agreed deviations beside the public contract.
115
+
116
+ ## Behavioral evidence
117
+
118
+ Keep private controllers in their owner; extract a shared controller only for a
119
+ real reuse need. Register optional behavior guarantees and real colocated checks;
120
+ see `ads://guides/behaviors` or the installed package's `docs/BEHAVIORS.md`. Link
121
+ public hooks with scenario `exercises`, while `covers` still requires rendered UI.
122
+ Move reviewed scratchpad investigations into maintained owner checks. Disabling
123
+ StrictMode can isolate an upstream lifecycle defect; keep a development StrictMode
124
+ check and verify the native port's setup/cleanup/setup independently. Use first-
125
+ render browser cases for lazy intrinsic sizing, retaining offscreen rendering.
@@ -63,6 +63,23 @@ ADS gates. For binary/large assets, generate `git diff --binary --full-index` in
63
63
  stay out of model context. Binary changes require Git. When editing is disabled,
64
64
  use the client's normal source-editing tools.
65
65
 
66
+
67
+ Controllers are UI behavior owners: private helpers stay with their component or
68
+ pattern; reusable controllers use discovered `controllers/<Name>` units. Use the
69
+ project's configured framework and declared dependencies, or plain TypeScript,
70
+ browser APIs and local helpers. MCP hosts supply no ad hoc runtime APIs. The
71
+ framework setting selects compilation/mounting; controller checks also validate
72
+ imports. For controller/behavior work, read `ads://guides/behaviors` (offline:
73
+ the installed ADS package's `docs/BEHAVIORS.md`). Discover registered guarantees
74
+ with `ads_behaviors`; use `ads_verify` for selected real checks. Optional
75
+ `exercises` links public behavioral APIs to scenarios; keep `covers` for rendered
76
+ UI. Registration and demonstration are not passing evidence. `behavior.json`,
77
+ scenarios, fixtures and checks stay out of production exports/imports. Verify
78
+ current results, including lifecycle setup/cleanup/setup and the actual browser
79
+ profile when promised. React StrictMode needs a development harness; wrapping a
80
+ production widget cannot replay development effects. Keep dynamic behavior and
81
+ capabilities intact rather than rewriting them only for static coverage.
82
+
66
83
  Before trusting a preview after edits, check `ads_status.build` or
67
84
  `ads://project/build`. Fix build diagnostics in owned source; an older view or
68
85
  unreported freshness is not evidence of current code. Notifications may not
@@ -221,6 +238,11 @@ Do not copy an unchecked upstream preflight or turn a component repair into a re
221
238
  Keep behavior and skin in their runtime owner. Put demo-only sizing and framing
222
239
  in a public review/layout pattern; add component props for a deliberate runtime
223
240
  API need. Preserve required capabilities while following ownership rules.
241
+ Use tokens for absolute/font-relative lengths, including `ch`, `ex`, `cap`, `ic`
242
+ and `lh`; calculations and fallback values follow the same rule. Structural
243
+ angle units remain available for orientation, skew and spin. Put tunable visual
244
+ angles in project tokens; static checks cannot infer that intent. Percentages,
245
+ grid fractions and viewport/container relationships retain structural use.
224
246
  Browser review still verifies the effects.
225
247
 
226
248
  Invalid metadata and source failures keep the managed compiler alive for repairs.
package/dist/tokens.mjs CHANGED
@@ -4,6 +4,7 @@ import postcss from "postcss";
4
4
  import valueParser from "postcss-value-parser";
5
5
  import { loadConfig, sourceFiles } from "./config.mjs";
6
6
  import { foundationFiles, isSharedToken, tokenContract } from "./token-policy.mjs";
7
+ import { dimensionValue, numericCssUnit } from "./css-values.mjs";
7
8
  import { checkComponentVariable } from "./boundaries.mjs";
8
9
 
9
10
  export { tokenContract };
@@ -33,8 +34,8 @@ function matchesType(value, type) {
33
34
  if (/^(?:inherit|initial|unset|revert(?:-layer)?)$/.test(value)) return true;
34
35
  switch (type) {
35
36
  case "color": return /^(?:#[\da-f]{3,8}|[a-z][\w-]*|(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch|color|color-mix|light-dark)\([\s\S]+\))$/i.test(value);
36
- case "dimension": return /^[-+]?(?:\d*\.)?\d+(?:px|r?em|ch|ex|cap|lh|rlh|v[wh]|[sld]v[wh]|vmin|vmax|cm|mm|in|pt|pc|%)$/i.test(value) || /^[-+]?0(?:\.0+)?$/.test(value);
37
- case "duration": return /^[-+]?(?:\d*\.)?\d+m?s$/i.test(value);
37
+ case "dimension": return dimensionValue(value);
38
+ case "duration": return ["s", "ms"].includes(numericCssUnit(value));
38
39
  case "number": return /^[-+]?(?:\d*\.)?\d+$/.test(value);
39
40
  case "easing": return /^(?:linear|ease(?:-in|-out|-in-out)?|step-start|step-end|(?:cubic-bezier|steps|linear)\([\s\S]+\))$/.test(value);
40
41
  case "shadow": return value === "none" || /(?:\d(?:px|rem|em)|var\()/.test(value);
package/dist/units.mjs CHANGED
@@ -2,7 +2,7 @@ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import ts from "typescript";
4
4
  import postcss from "postcss";
5
- import { isInside } from "./config.mjs";
5
+ import { isDevelopmentFile, isInside } from "./config.mjs";
6
6
  import { importResolver } from "./boundaries.mjs";
7
7
  import { isProjectDocument, projectGuideFiles } from "./project-documents.mjs";
8
8
  import { foundationFiles, foundationImportTarget } from "./token-policy.mjs";
@@ -16,14 +16,21 @@ export function configurationFiles(options) {
16
16
  const { root, config, sourceRoot, dsRoot } = options;
17
17
  const files = new Set(["ds.config.json", "package.json"]);
18
18
  for (const entry of readdirSync(root, { withFileTypes: true })) {
19
- if (entry.isFile() && /^(?:(?:ts|js)config[^/]*\.jsonc?|(?:vite|vitest|linaria|babel|postcss|biome|oxlint)[^/]*\.(?:jsonc?|[cm]?[jt]s)|\.(?:oxlintrc|linariarc|babelrc)(?:\.json)?)$/.test(entry.name)) files.add(entry.name);
19
+ if (entry.isFile() && /^(?:(?:ts|js)config[^/]*\.jsonc?|(?:vite|vitest|playwright|linaria|babel|postcss|biome|oxlint)[^/]*\.(?:jsonc?|[cm]?[jt]s)|\.(?:oxlintrc|linariarc|babelrc)(?:\.json)?)$/.test(entry.name)) files.add(entry.name);
20
20
  }
21
21
  const resolve = importResolver(options), visited = new Set();
22
22
  function visit(file, entrypoint = false, typeConfig = false) {
23
23
  if (!isInside(root, file) || file === root || /(?:^|[\\/])(?:node_modules|\.git|\.ds|dist)(?:[\\/]|$)/.test(path.relative(root, file)) || visited.has(file) || !existsSync(file)) return;
24
+ // Registering a test command must not reassign its colocated checks or
25
+ // fixtures to configuration. Those remain in the behavior owner's patch.
26
+ if (isInside(dsRoot, file) && isDevelopmentFile(file, options)) return;
24
27
  if (!entrypoint && isInside(sourceRoot, file) && !isInside(dsRoot, file)) return;
25
- if (!entrypoint && isInside(dsRoot, file) && /^(?:components|patterns|composition|tokens)[\\/]/.test(path.relative(dsRoot, file))) {
26
- if (/\.[jt]sx$|\.(?:scenario|fixture)\./.test(file)) return;
28
+ if (!entrypoint && isInside(dsRoot, file) && /^(?:components|patterns|controllers|composition|tokens)[\\/]/.test(path.relative(dsRoot, file))) {
29
+ if (isInside(path.join(dsRoot, "controllers"), file) || isDevelopmentFile(file, options)) return;
30
+ const folder = path.dirname(file), stem = path.basename(file).split(".")[0];
31
+ const anchored = ["index", path.basename(folder)].some(name => [".ts", ".tsx", ".js", ".jsx", ".mjs", ".mts"].some(extension => existsSync(path.join(folder, name + extension))));
32
+ const sidecar = path.basename(file).split(".").length > 2 && [".ts", ".tsx", ".js", ".jsx", ".mjs", ".mts"].some(extension => existsSync(path.join(folder, stem + extension)));
33
+ if (anchored || sidecar || /\.[jt]sx$/.test(file)) return;
27
34
  if (source.test(file) && /(?:from\s*["'](?:react|preact|@linaria\/)|\b(?:require|import)\s*\(\s*["'](?:react|preact|@linaria\/))/.test(readFileSync(file,"utf8"))) return;
28
35
  }
29
36
  visited.add(file);
@@ -65,6 +72,10 @@ export function configurationFiles(options) {
65
72
  if (["npm", "bun"].includes(watch.command) && watch.args[0] === "run") script(watch.args[1]);
66
73
  else for (const argument of [watch.command, ...watch.args]) if (/\.[cm]?[jt]sx?$/.test(argument)) visit(path.resolve(root, argument), true);
67
74
  }
75
+ for (const profile of Object.values(config.mcp.behaviorProfiles)) {
76
+ if (["npm", "bun"].includes(profile.command) && profile.args[0] === "run") script(profile.args[1]);
77
+ else for (const argument of [profile.command, ...profile.args]) if (/\.[cm]?[jt]sx?$/.test(argument)) visit(path.resolve(root, argument), true);
78
+ }
68
79
  return files;
69
80
  }
70
81
 
@@ -90,14 +101,14 @@ export function inspectUnits(options) {
90
101
  const flatFamilies = new Set(files.filter(file => /\.scenario\.[cm]?[jt]sx?$/.test(file)).map(file => path.posix.dirname(file)));
91
102
  const directories = new Set(files.filter(file => /(?:^|\/)Scenario\.[cm]?[jt]sx?$/.test(file)).map(file => slash(path.dirname(file))));
92
103
  for (const file of files) {
93
- if (!/^(?:components|patterns|composition)\//.test(file) || !source.test(file)) continue;
104
+ if (!/^(?:components|patterns|controllers|composition)\//.test(file) || !source.test(file)) continue;
94
105
  const directory = slash(path.dirname(file)), name = path.basename(file).replace(source, "");
95
106
  if (!directory.includes("/") || /\.(?:scenario|fixture|scenario-adapter)$/.test(name)) continue;
96
107
  if (flatFamilies.has(directory)) continue;
97
108
  if (name === "index" || sameName(name) === sameName(path.posix.basename(directory)) || name === "fixture") directories.add(directory);
98
109
  }
99
110
  const directoryUnits = [...directories].sort((a, b) => b.length - a.length);
100
- const flatUnits = [...new Set(files.filter(file => /^(?:components|patterns|composition)\//.test(file) && source.test(file) && !directoryUnits.some(directory => file.startsWith(directory + "/"))).map(file => {
111
+ const flatUnits = [...new Set(files.filter(file => /^(?:components|patterns|controllers|composition)\//.test(file) && source.test(file) && !directoryUnits.some(directory => file.startsWith(directory + "/"))).map(file => {
101
112
  const base = path.basename(file).split(".")[0];
102
113
  return slash(path.join(path.dirname(file), base));
103
114
  }))].sort((a, b) => b.length - a.length);
@@ -113,7 +124,7 @@ export function inspectUnits(options) {
113
124
  if (relative.startsWith(tokenDir + "/")) return "tokens";
114
125
  if (relative.startsWith("assets/")) return relative.split("/").length > 2 ? relative.split("/").slice(0, 2).join("/") : "assets";
115
126
  if (!relative.includes("/")) return /(?:\.(?:md|txt)$|^LICENSE$)/i.test(relative) ? "documentation" : "foundation";
116
- if (!/^(?:components|patterns|composition)\//.test(relative)) return relative.split("/")[0];
127
+ if (!/^(?:components|patterns|controllers|composition)\//.test(relative)) return relative.split("/")[0];
117
128
  for (const directory of directoryUnits) if (relative.startsWith(directory + "/")) return directory;
118
129
  for (const unit of flatUnits) if (relative.startsWith(unit + "/") || relative.startsWith(unit + ".")) return unit;
119
130
  const base = path.basename(relative).split(".")[0];
@@ -142,7 +153,7 @@ export function inspectUnits(options) {
142
153
  function boundaryCompanion(unit) { return !["configuration", "documentation", "assets"].includes(unit.split("/")[0]); }
143
154
 
144
155
  export function validateUnitId(unit) {
145
- if (typeof unit !== "string" || unit.length > 256 || !/^[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*$/.test(unit) || ["components", "patterns", "composition"].includes(unit)) throw fail("Select one ADS unit ID from ads_units or an unoccupied owner path following its colocation convention.");
156
+ if (typeof unit !== "string" || unit.length > 256 || !/^[A-Za-z0-9_-]+(?:\/[A-Za-z0-9_-]+)*$/.test(unit) || ["components", "patterns", "controllers", "composition"].includes(unit)) throw fail("Select one ADS unit ID from ads_units or an unoccupied owner path following its colocation convention.");
146
157
  }
147
158
 
148
159
  /** Reject arbitrary file batches, even when every path is inside the ADS. */
@@ -207,7 +218,7 @@ export function checkUnitCandidate(afterOptions, unit, changes, before) {
207
218
  }
208
219
  const exportChange = changes.find(change => before.relative(change.path) === before.publicEntry);
209
220
  if (exportChange) {
210
- if (!["components", "patterns"].includes(unit.split("/")[0])) throw fail("Only a component/pattern unit may update its public re-exports.", exportChange.path);
221
+ if (!["components", "patterns", "controllers"].includes(unit.split("/")[0])) throw fail("Only a component/pattern/controller unit may update its public re-exports.", exportChange.path);
211
222
  const current = exportChange.originalContent ?? "", proposed = exportChange.content;
212
223
  if (proposed === null) throw fail("The shared public entry cannot be deleted.", exportChange.path);
213
224
  const resolve = importResolver(afterOptions);