@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.
- package/README.md +2 -0
- package/dist/behaviors.mjs +19 -9
- package/dist/bootstrap-ds/ADS.md +2 -2
- package/dist/bootstrap-ds/README.md +2 -2
- package/dist/bootstrap-ds/design-system/index.ts +0 -1
- package/dist/bootstrap-ds/design-system/review/index.ts +1 -0
- package/dist/bootstrap-ds/design-system/tokens/{Foundation.scenario.tsx → Foundation.ads.scenario.tsx} +2 -2
- package/dist/bootstrap-ds/ds.config.json +1 -1
- package/dist/bootstrap-ds/tsconfig.build.json +9 -2
- package/dist/bootstrap-ds/tsconfig.json +18 -3
- package/dist/bootstrap-ds/vite.config.ts +1 -1
- package/dist/boundaries.mjs +6 -2
- package/dist/check.mjs +44 -17
- package/dist/config.mjs +33 -5
- package/dist/contract-migration.mjs +110 -0
- package/dist/contracts.d.mts +22 -0
- package/dist/evidence.mjs +1 -1
- package/dist/index.mjs +1 -0
- package/dist/init.mjs +18 -15
- package/dist/layers.mjs +288 -0
- package/dist/lint-config.mjs +1 -1
- package/dist/mcp/catalog.mjs +9 -2
- package/dist/mcp/context.mjs +2 -1
- package/dist/mcp/edit-preparation.mjs +9 -6
- package/dist/mcp/mutations.mjs +1 -1
- package/dist/mcp/preview-widget.bundle.js +41 -39
- package/dist/mcp/prompts.mjs +1 -1
- package/dist/mcp/server.mjs +9 -3
- package/dist/mcp/widget.bundle.js +1 -1
- package/dist/oxlint.mjs +3 -1
- package/dist/paint-contracts.mjs +124 -0
- package/dist/preview-resources.mjs +12 -2
- package/dist/public-api.mjs +13 -6
- package/dist/public-props.mjs +2 -2
- package/dist/runtime-contracts.mjs +225 -0
- package/dist/scenario-trace.mjs +37 -9
- package/dist/scenarios-vite.mjs +3 -2
- package/dist/scenarios.mjs +89 -30
- package/dist/scoped-tokens.mjs +9 -4
- package/dist/showcase/client.bundle.js +1 -1
- package/dist/showcase/sandbox.bundle.js +1 -1
- package/dist/skills/ads-break-ui/SKILL.md +25 -6
- package/dist/skills/ads-break-ui/references/contracts.md +250 -0
- package/dist/skills/ads-break-ui/references/guarded-editing.md +1 -1
- package/dist/skills/ads-break-ui/references/layers.md +123 -0
- package/dist/skills/ads-break-ui/references/review.md +96 -0
- package/dist/skills/ads-break-ui/references/scenarios.md +24 -3
- package/dist/skills/ads-designer/SKILL.md +26 -7
- package/dist/skills/ads-designer/assets/bootstrap-ds/ADS.md +2 -2
- package/dist/skills/ads-designer/assets/bootstrap-ds/README.md +2 -2
- package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/index.ts +0 -1
- package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/review/index.ts +1 -0
- package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/tokens/{Foundation.scenario.tsx → Foundation.ads.scenario.tsx} +2 -2
- package/dist/skills/ads-designer/assets/bootstrap-ds/ds.config.json +1 -1
- package/dist/skills/ads-designer/assets/bootstrap-ds/tsconfig.build.json +9 -2
- package/dist/skills/ads-designer/assets/bootstrap-ds/tsconfig.json +18 -3
- package/dist/skills/ads-designer/assets/bootstrap-ds/vite.config.ts +1 -1
- package/dist/skills/ads-designer/references/contracts.md +250 -0
- package/dist/skills/ads-designer/references/guarded-editing.md +1 -1
- package/dist/skills/ads-designer/references/layers.md +123 -0
- package/dist/skills/ads-designer/references/review.md +96 -0
- package/dist/skills/ads-designer/references/scenarios.md +24 -3
- package/dist/skills/ads-import/SKILL.md +25 -6
- package/dist/skills/ads-import/references/contracts.md +250 -0
- package/dist/skills/ads-import/references/guarded-editing.md +1 -1
- package/dist/skills/ads-import/references/layers.md +123 -0
- package/dist/skills/ads-import/references/review.md +96 -0
- package/dist/skills/ads-import/references/scenarios.md +24 -3
- package/dist/skills/ads-migrate/SKILL.md +25 -6
- package/dist/skills/ads-migrate/references/contracts.md +250 -0
- package/dist/skills/ads-migrate/references/guarded-editing.md +1 -1
- package/dist/skills/ads-migrate/references/layers.md +123 -0
- package/dist/skills/ads-migrate/references/review.md +96 -0
- package/dist/skills/ads-migrate/references/scenarios.md +24 -3
- package/dist/skills/ads-motion/SKILL.md +26 -7
- package/dist/skills/ads-motion/references/audit.md +1 -1
- package/dist/skills/ads-motion/references/contracts.md +250 -0
- package/dist/skills/ads-motion/references/guarded-editing.md +1 -1
- package/dist/skills/ads-motion/references/layers.md +123 -0
- package/dist/skills/ads-motion/references/motion-review.md +35 -0
- package/dist/skills/ads-motion/references/review.md +96 -35
- package/dist/skills/ads-motion/references/scenarios.md +24 -3
- package/dist/token-origins.mjs +24 -0
- package/dist/tokens.mjs +4 -3
- package/dist/unit-contracts.mjs +203 -0
- package/dist/units.mjs +26 -17
- package/dist/verification/browser.mjs +156 -26
- package/dist/verification/playwright-reporter.cjs +13 -3
- package/docs/ADS.md +40 -6
- package/docs/ARCHITECTURE.md +73 -4
- package/docs/BEHAVIORS.md +15 -0
- package/docs/CHANGELOG.md +42 -0
- package/docs/CONTRACTS.md +250 -0
- package/docs/GUIDE.md +29 -3
- package/docs/MCP.md +42 -6
- package/package.json +4 -1
- package/dist/bootstrap-ds/design-system/patterns/token-preview/Scenario.tsx +0 -4
- package/dist/bootstrap-ds/design-system/patterns/token-preview/fixture.ts +0 -8
- package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/patterns/token-preview/Scenario.tsx +0 -4
- package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/patterns/token-preview/fixture.ts +0 -8
- /package/dist/bootstrap-ds/design-system/{patterns → review}/token-preview/TokenPreview.tsx +0 -0
- /package/dist/bootstrap-ds/design-system/tokens/{Foundation.fixture.ts → Foundation.ads.fixture.ts} +0 -0
- /package/dist/skills/ads-designer/assets/bootstrap-ds/design-system/{patterns → review}/token-preview/TokenPreview.tsx +0 -0
- /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
|
|
package/dist/behaviors.mjs
CHANGED
|
@@ -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 {
|
|
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
|
|
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(
|
|
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
|
|
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 };
|
package/dist/bootstrap-ds/ADS.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
@@ -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} />; }
|
|
@@ -7,6 +7,13 @@
|
|
|
7
7
|
"rootDir": "design-system",
|
|
8
8
|
"outDir": "dist"
|
|
9
9
|
},
|
|
10
|
-
"include": [
|
|
11
|
-
|
|
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": [
|
|
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": {
|
|
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": [
|
|
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",
|
package/dist/boundaries.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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;
|