design-constraint-validator 2.0.1 → 2.2.0
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 +89 -23
- package/cli/commands/build.d.ts.map +1 -1
- package/cli/commands/build.js +32 -24
- package/cli/commands/build.ts +26 -17
- package/cli/commands/graph.d.ts.map +1 -1
- package/cli/commands/graph.js +35 -18
- package/cli/commands/graph.ts +30 -17
- package/cli/commands/patch-apply.d.ts.map +1 -1
- package/cli/commands/patch-apply.js +4 -1
- package/cli/commands/patch-apply.ts +4 -1
- package/cli/commands/set.d.ts.map +1 -1
- package/cli/commands/set.js +18 -19
- package/cli/commands/set.ts +19 -19
- package/cli/commands/utils.d.ts +1 -0
- package/cli/commands/utils.d.ts.map +1 -1
- package/cli/commands/utils.js +20 -1
- package/cli/commands/utils.ts +23 -1
- package/cli/commands/validate.d.ts.map +1 -1
- package/cli/commands/validate.js +45 -23
- package/cli/commands/validate.ts +47 -26
- package/cli/commands/why.d.ts.map +1 -1
- package/cli/commands/why.js +22 -10
- package/cli/commands/why.ts +20 -9
- package/cli/config-schema.d.ts +171 -166
- package/cli/config-schema.d.ts.map +1 -1
- package/cli/config-schema.js +29 -7
- package/cli/config-schema.ts +31 -7
- package/cli/config.d.ts.map +1 -1
- package/cli/config.js +8 -2
- package/cli/config.ts +8 -2
- package/cli/constraint-registry.d.ts +16 -0
- package/cli/constraint-registry.d.ts.map +1 -1
- package/cli/constraint-registry.js +115 -44
- package/cli/constraint-registry.ts +118 -47
- package/cli/cross-axis-loader.d.ts +62 -0
- package/cli/cross-axis-loader.d.ts.map +1 -1
- package/cli/cross-axis-loader.js +186 -31
- package/cli/cross-axis-loader.ts +199 -24
- package/cli/dcv.js +31 -25
- package/cli/dcv.ts +31 -21
- package/cli/json-output.d.ts +3 -1
- package/cli/json-output.d.ts.map +1 -1
- package/cli/json-output.js +11 -4
- package/cli/json-output.ts +13 -4
- package/cli/types.d.ts +21 -9
- package/cli/types.d.ts.map +1 -1
- package/cli/types.ts +25 -10
- package/cli/validate-api.d.ts +40 -0
- package/cli/validate-api.d.ts.map +1 -0
- package/cli/validate-api.js +90 -0
- package/cli/validate-api.ts +131 -0
- package/core/breakpoints.d.ts +8 -2
- package/core/breakpoints.d.ts.map +1 -1
- package/core/breakpoints.js +24 -3
- package/core/breakpoints.ts +22 -3
- package/core/color.js +4 -4
- package/core/color.ts +4 -4
- package/core/constraints/cross-axis.d.ts.map +1 -1
- package/core/constraints/cross-axis.js +37 -9
- package/core/constraints/cross-axis.ts +37 -9
- package/core/constraints/monotonic-lightness.d.ts.map +1 -1
- package/core/constraints/monotonic-lightness.js +9 -5
- package/core/constraints/monotonic-lightness.ts +9 -4
- package/core/constraints/monotonic.d.ts.map +1 -1
- package/core/constraints/monotonic.js +32 -8
- package/core/constraints/monotonic.ts +29 -8
- package/core/constraints/threshold.d.ts.map +1 -1
- package/core/constraints/threshold.js +24 -4
- package/core/constraints/threshold.ts +23 -4
- package/core/constraints/wcag.d.ts.map +1 -1
- package/core/constraints/wcag.js +7 -1
- package/core/constraints/wcag.ts +7 -1
- package/core/dtcg.d.ts +38 -0
- package/core/dtcg.d.ts.map +1 -0
- package/core/dtcg.js +88 -0
- package/core/dtcg.ts +102 -0
- package/core/engine.d.ts +6 -0
- package/core/engine.d.ts.map +1 -1
- package/core/engine.ts +7 -0
- package/core/flatten.d.ts +5 -3
- package/core/flatten.d.ts.map +1 -1
- package/core/flatten.js +32 -10
- package/core/flatten.ts +48 -16
- package/core/image-export.d.ts.map +1 -1
- package/core/image-export.js +10 -7
- package/core/image-export.ts +9 -6
- package/core/index.d.ts +2 -0
- package/core/index.d.ts.map +1 -1
- package/core/index.js +4 -0
- package/core/index.ts +6 -0
- package/core/poset.d.ts +6 -1
- package/core/poset.d.ts.map +1 -1
- package/core/poset.js +7 -2
- package/core/poset.ts +7 -2
- package/core/why.d.ts +1 -1
- package/core/why.d.ts.map +1 -1
- package/core/why.ts +1 -1
- package/mcp/contracts.d.ts +1561 -0
- package/mcp/contracts.d.ts.map +1 -0
- package/mcp/contracts.js +74 -0
- package/mcp/contracts.ts +105 -0
- package/mcp/index.d.ts +11 -0
- package/mcp/index.d.ts.map +1 -0
- package/mcp/index.js +35 -0
- package/mcp/index.ts +97 -0
- package/mcp/insights.d.ts +94 -0
- package/mcp/insights.d.ts.map +1 -0
- package/mcp/insights.js +445 -0
- package/mcp/insights.ts +541 -0
- package/mcp/tools.d.ts +63 -0
- package/mcp/tools.d.ts.map +1 -0
- package/mcp/tools.js +299 -0
- package/mcp/tools.ts +431 -0
- package/package.json +36 -26
- package/server.json +21 -0
- package/cli/constraints-loader.d.ts.map +0 -1
- package/cli/engine-helpers.d.ts.map +0 -1
- package/core/cross-axis-config.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
[](https://www.npmjs.com/package/design-constraint-validator)
|
|
7
7
|
[](https://github.com/CseperkePapp/design-constraint-validator/actions/workflows/ci.yml)
|
|
8
8
|
[](https://github.com/CseperkePapp/design-constraint-validator/actions/workflows/sbom.yml)
|
|
9
|
+
[](SECURITY.md)
|
|
9
10
|
[](LICENSE)
|
|
10
11
|
[](#)
|
|
11
12
|
|
|
@@ -25,38 +26,70 @@ This is **not** a schema linter; it's a **reasoning validator** for values and r
|
|
|
25
26
|
# Local (recommended)
|
|
26
27
|
npm i -D design-constraint-validator
|
|
27
28
|
|
|
28
|
-
# One-off run
|
|
29
|
-
npx
|
|
29
|
+
# One-off run, no install (the bin name `dcv` belongs to an unrelated package)
|
|
30
|
+
npx design-constraint-validator --help
|
|
30
31
|
```
|
|
31
32
|
|
|
33
|
+
After a local install, the shorter `dcv` bin is available (e.g. `npx dcv --help`).
|
|
34
|
+
|
|
32
35
|
**Requirements:** Node.js ≥ 18.x (ESM)
|
|
33
36
|
|
|
34
37
|
---
|
|
35
38
|
|
|
36
39
|
## Quick Start
|
|
37
40
|
|
|
41
|
+
DCV validates **your** tokens against **your** constraints. From an empty directory:
|
|
42
|
+
|
|
38
43
|
```bash
|
|
39
|
-
#
|
|
40
|
-
|
|
44
|
+
# 1. Your design tokens (DTCG-style "$value")
|
|
45
|
+
cat > tokens.json <<'JSON'
|
|
46
|
+
{
|
|
47
|
+
"color": {
|
|
48
|
+
"text": { "$value": "#888888" },
|
|
49
|
+
"bg": { "$value": "#999999" }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
JSON
|
|
53
|
+
|
|
54
|
+
# 2. Your constraints — auto-discovered as dcv.config.json in the cwd
|
|
55
|
+
cat > dcv.config.json <<'JSON'
|
|
56
|
+
{
|
|
57
|
+
"constraints": {
|
|
58
|
+
"enableBuiltInWcagDefaults": false,
|
|
59
|
+
"enableBuiltInThreshold": false,
|
|
60
|
+
"wcag": [
|
|
61
|
+
{ "foreground": "color.text", "background": "color.bg", "ratio": 4.5, "description": "Body text on background" }
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
JSON
|
|
41
66
|
|
|
42
|
-
#
|
|
43
|
-
npx
|
|
67
|
+
# 3. Validate (positional path or --tokens; exits non-zero on violations)
|
|
68
|
+
npx design-constraint-validator validate tokens.json --summary table
|
|
44
69
|
|
|
45
|
-
#
|
|
46
|
-
npx
|
|
70
|
+
# Explain one token (the tokenId is required)
|
|
71
|
+
npx design-constraint-validator why color.text --tokens tokens.json --format table
|
|
72
|
+
|
|
73
|
+
# Export the dependency graph
|
|
74
|
+
npx design-constraint-validator graph --tokens tokens.json --format mermaid > graph.mmd
|
|
47
75
|
```
|
|
48
76
|
|
|
49
|
-
|
|
77
|
+
> These one-offs use the full package name because the bare `dcv` bin name on npm
|
|
78
|
+
> belongs to an unrelated package. After `npm i -D design-constraint-validator`,
|
|
79
|
+
> use the shorter `npx dcv …`.
|
|
50
80
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
81
|
+
**Example output** (`validate`):
|
|
82
|
+
|
|
83
|
+
```text
|
|
84
|
+
validate: 1 error(s), 0 warning(s)
|
|
85
|
+
ERROR wcag-contrast color.text|color.bg @ Body text on background — Contrast 1.24:1 < 4.5:1
|
|
86
|
+
scope rules warnings errors
|
|
87
|
+
------ ----- -------- ------
|
|
88
|
+
global 1 0 1
|
|
58
89
|
```
|
|
59
90
|
|
|
91
|
+
Exit code is `1` when violations are found, `0` when clean (use `--fail-on off` to always exit `0`). The built-in WCAG/threshold defaults target the bundled example token ids, so disable them (as above) when validating your own token names.
|
|
92
|
+
|
|
60
93
|
---
|
|
61
94
|
|
|
62
95
|
## Programmatic API
|
|
@@ -64,14 +97,15 @@ Exit code: 1 (violations found)
|
|
|
64
97
|
```ts
|
|
65
98
|
import { validate } from 'design-constraint-validator';
|
|
66
99
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
100
|
+
// Synchronous. Point at files, or pass `tokens` / `constraints` inline.
|
|
101
|
+
const result = validate({
|
|
102
|
+
tokensPath: './tokens.json',
|
|
103
|
+
configPath: './dcv.config.json', // omit to auto-discover dcv.config.json in the cwd
|
|
70
104
|
});
|
|
71
105
|
|
|
72
106
|
if (!result.ok) {
|
|
73
107
|
for (const v of result.violations) {
|
|
74
|
-
console.log(`[${v.
|
|
108
|
+
console.log(`[${v.ruleId}] ${v.message}`);
|
|
75
109
|
}
|
|
76
110
|
process.exitCode = 1;
|
|
77
111
|
}
|
|
@@ -81,6 +115,36 @@ See **[API Reference](docs/API.md)** for complete programmatic usage.
|
|
|
81
115
|
|
|
82
116
|
---
|
|
83
117
|
|
|
118
|
+
## Use from AI agents (MCP)
|
|
119
|
+
|
|
120
|
+
DCV ships a second binary, `dcv-mcp`, that exposes the validator over MCP stdio for agent clients. Add it to a Claude Desktop or generic MCP client config like this:
|
|
121
|
+
|
|
122
|
+
```json
|
|
123
|
+
{
|
|
124
|
+
"mcpServers": {
|
|
125
|
+
"dcv": {
|
|
126
|
+
"command": "npx",
|
|
127
|
+
"args": ["-y", "--package", "design-constraint-validator", "dcv-mcp"]
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The server exposes six read-only, JSON-returning tools:
|
|
134
|
+
|
|
135
|
+
- `validate` - validate inline `tokens` or a `tokensPath` against inline `constraints` or a config file.
|
|
136
|
+
- `why` - explain provenance, aliases, dependencies, dependents, and alias chain for one token id.
|
|
137
|
+
- `graph` - return token dependency `nodes` and `edges`.
|
|
138
|
+
- `list-constraints` - enumerate the active constraints (WCAG pairs, thresholds, order/lightness scales, cross-axis) for the given input.
|
|
139
|
+
- `explain` - turn a violation into plain-English text plus machine-readable facts.
|
|
140
|
+
- `suggest-fix` - compute a verified satisfying value for a violation (WCAG color, threshold/monotonic boundary) without writing anything.
|
|
141
|
+
|
|
142
|
+
The three derivation tools (`list-constraints`, `explain`, `suggest-fix`) stay read-only — they return suggestions; applying them is up to you (`dcv set` / `dcv patch`). See **[AI Guide](docs/AI-GUIDE.md)** for the full agent loop.
|
|
143
|
+
|
|
144
|
+
Tool failures are returned as structured JSON: `{ "ok": false, "error": { "code": "...", "message": "..." } }`.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
84
148
|
## Documentation
|
|
85
149
|
|
|
86
150
|
### For Everyone
|
|
@@ -103,6 +167,7 @@ See **[API Reference](docs/API.md)** for complete programmatic usage.
|
|
|
103
167
|
- **[Prior Art / Method](docs/prior-art/)** - Design rationale (Decision Themes, receipts)
|
|
104
168
|
- **[AI Guide](docs/AI-GUIDE.md)** - Using DCV with ChatGPT/Claude/Copilot
|
|
105
169
|
- **[Contributing](CONTRIBUTING.md)** - Contribution guidelines
|
|
170
|
+
- **[Security](SECURITY.md)** - Supply chain security measures
|
|
106
171
|
|
|
107
172
|
---
|
|
108
173
|
|
|
@@ -140,7 +205,7 @@ Adapters normalize common ecosystems:
|
|
|
140
205
|
|
|
141
206
|
- **Style Dictionary** - See [examples/style-dictionary/](examples/style-dictionary/)
|
|
142
207
|
- **Tokens Studio JSON** - See [examples/tokens-studio/](examples/tokens-studio/)
|
|
143
|
-
- **DTCG** (Design Tokens Community Group) - See [examples/dtcg/](examples/dtcg/)
|
|
208
|
+
- **DTCG** (Design Tokens Community Group) — reads the **2025.10 stable spec** (structured sRGB colors, structured dimensions, `{alias}` references, `$extensions` passthrough; non-sRGB spaces warn rather than mis-calculate; composite types out of scope). See [examples/dtcg/](examples/dtcg/)
|
|
144
209
|
|
|
145
210
|
Full adapter documentation: **[Adapters](docs/Adapters.md)**
|
|
146
211
|
|
|
@@ -180,7 +245,8 @@ These documents keep the method openly implementable and prevent patent lock-up.
|
|
|
180
245
|
DCV generates CycloneDX-compliant SBOMs for supply chain transparency:
|
|
181
246
|
|
|
182
247
|
- **CI Builds:** SBOM artifacts on every CI run (90-day retention)
|
|
183
|
-
- **
|
|
248
|
+
- **Version tags:** SBOM artifacts for release tags
|
|
249
|
+
- **GitHub Releases:** SBOM files (JSON + XML) attached when a GitHub Release is created
|
|
184
250
|
- **Manual:** Run `npx @cyclonedx/cyclonedx-npm` in project root
|
|
185
251
|
|
|
186
252
|
**Download:**
|
|
@@ -194,7 +260,7 @@ DCV generates CycloneDX-compliant SBOMs for supply chain transparency:
|
|
|
194
260
|
- Plugin API for **custom constraints**
|
|
195
261
|
- **VS Code** diagnostics (inline explain)
|
|
196
262
|
- **Cross-axis packs** (typography × weight × contrast)
|
|
197
|
-
- **
|
|
263
|
+
- **Signed / attestable receipts** — `dcv validate --receipt` already emits environment + input content hashes today; cryptographic **signing** is the roadmap part
|
|
198
264
|
- UI graph explorer (node inspector, violations focus)
|
|
199
265
|
|
|
200
266
|
---
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["build.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["build.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGhD,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG;IAAE,CAAC,CAAC,EAAE,MAAM,GAAG,GAAG,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA+D9F"}
|
package/cli/commands/build.js
CHANGED
|
@@ -1,21 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { dirname, resolve } from 'node:path';
|
|
2
2
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
3
3
|
import { flattenTokens } from '../../core/flatten.js';
|
|
4
|
+
import { mergeTokens } from '../../core/breakpoints.js';
|
|
4
5
|
import { valuesToCss } from '../../adapters/css.js';
|
|
5
6
|
import { emitJSON } from '../../adapters/json.js';
|
|
6
7
|
import { emitJS } from '../../adapters/js.js';
|
|
8
|
+
import { loadThemeTokens } from './utils.js';
|
|
7
9
|
export async function buildCommand(options) {
|
|
8
10
|
const { loadTokensWithBreakpoint } = await import('../../core/breakpoints.js');
|
|
9
|
-
|
|
10
|
-
const { flat } = flattenTokens(tokens);
|
|
11
|
-
let allValues = Object.fromEntries(Object.values(flat).map(t => [t.id, t.value]));
|
|
11
|
+
let tokens = loadTokensWithBreakpoint(undefined, options.tokens);
|
|
12
12
|
if (options.theme) {
|
|
13
|
-
|
|
14
|
-
if (existsSync(themePath)) {
|
|
15
|
-
const themeTokens = JSON.parse(readFileSync(themePath, 'utf8'));
|
|
16
|
-
Object.assign(allValues, themeTokens);
|
|
17
|
-
}
|
|
13
|
+
tokens = mergeTokens(tokens, loadThemeTokens(options.theme));
|
|
18
14
|
}
|
|
15
|
+
const { flat } = flattenTokens(tokens);
|
|
16
|
+
const allValues = Object.fromEntries(Object.values(flat).map(t => [t.id, t.value]));
|
|
19
17
|
const format = options.format || 'css';
|
|
20
18
|
const defaultOutput = `dist/tokens.${format}`;
|
|
21
19
|
let manifest;
|
|
@@ -34,21 +32,31 @@ export async function buildCommand(options) {
|
|
|
34
32
|
process.exit(1);
|
|
35
33
|
}
|
|
36
34
|
}
|
|
37
|
-
|
|
35
|
+
// Read both the kebab key (CLI, camel-case-expansion is off) and the camelCase
|
|
36
|
+
// key (programmatic callers, e.g. tests). Reading only one form silently no-ops
|
|
37
|
+
// for the other caller — that was the TASK-024 bug class.
|
|
38
|
+
const allFormats = options['all-formats'] ?? options.allFormats;
|
|
39
|
+
const dryRun = options['dry-run'] ?? options.dryRun;
|
|
38
40
|
if (allFormats) {
|
|
41
|
+
const css = valuesToCss(allValues, { manifest });
|
|
42
|
+
const json = emitJSON(allValues, manifest);
|
|
43
|
+
const js = emitJS(allValues, manifest);
|
|
44
|
+
if (dryRun) {
|
|
45
|
+
// Dry run: print every format, write nothing.
|
|
46
|
+
console.log(css);
|
|
47
|
+
console.log(json);
|
|
48
|
+
console.log(js);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
39
51
|
const dir = 'dist';
|
|
40
52
|
if (!existsSync(dir))
|
|
41
53
|
mkdirSync(dir, { recursive: true });
|
|
42
|
-
const css = valuesToCss(allValues, { manifest });
|
|
43
54
|
writeFileSync('dist/tokens.css', css, 'utf8');
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
writeFileSync('dist/tokens.json', emitJSON(allValues, manifest), 'utf8');
|
|
47
|
-
writeFileSync('dist/tokens.js', emitJS(allValues, manifest), 'utf8');
|
|
55
|
+
writeFileSync('dist/tokens.json', json, 'utf8');
|
|
56
|
+
writeFileSync('dist/tokens.js', js, 'utf8');
|
|
48
57
|
console.log(`Tokens written (all formats) to dist/ (css/json/js)${manifest ? ' with mapper' : ''}`);
|
|
49
58
|
return;
|
|
50
59
|
}
|
|
51
|
-
const dryRun = options.dryRun ?? options['dry-run'];
|
|
52
60
|
if (format === 'css') {
|
|
53
61
|
const css = valuesToCss(allValues, { manifest });
|
|
54
62
|
if (dryRun) {
|
|
@@ -63,26 +71,26 @@ export async function buildCommand(options) {
|
|
|
63
71
|
console.log(`CSS tokens written to ${outPath}${manifest ? ' (manifest mapper applied)' : ''}`);
|
|
64
72
|
}
|
|
65
73
|
else if (format === 'json') {
|
|
66
|
-
const outPath = options.output || defaultOutput;
|
|
67
|
-
const dir = dirname(outPath);
|
|
68
|
-
if (!existsSync(dir))
|
|
69
|
-
mkdirSync(dir, { recursive: true });
|
|
70
74
|
if (dryRun) {
|
|
71
75
|
console.log(emitJSON(allValues, manifest));
|
|
72
76
|
return;
|
|
73
77
|
}
|
|
74
|
-
writeFileSync(outPath, emitJSON(allValues, manifest), 'utf8');
|
|
75
|
-
console.log(`JSON tokens written to ${outPath}${manifest ? ' (manifest mapper applied)' : ''}`);
|
|
76
|
-
}
|
|
77
|
-
else if (format === 'js') {
|
|
78
78
|
const outPath = options.output || defaultOutput;
|
|
79
79
|
const dir = dirname(outPath);
|
|
80
80
|
if (!existsSync(dir))
|
|
81
81
|
mkdirSync(dir, { recursive: true });
|
|
82
|
+
writeFileSync(outPath, emitJSON(allValues, manifest), 'utf8');
|
|
83
|
+
console.log(`JSON tokens written to ${outPath}${manifest ? ' (manifest mapper applied)' : ''}`);
|
|
84
|
+
}
|
|
85
|
+
else if (format === 'js') {
|
|
82
86
|
if (dryRun) {
|
|
83
87
|
console.log(emitJS(allValues, manifest));
|
|
84
88
|
return;
|
|
85
89
|
}
|
|
90
|
+
const outPath = options.output || defaultOutput;
|
|
91
|
+
const dir = dirname(outPath);
|
|
92
|
+
if (!existsSync(dir))
|
|
93
|
+
mkdirSync(dir, { recursive: true });
|
|
86
94
|
writeFileSync(outPath, emitJS(allValues, manifest), 'utf8');
|
|
87
95
|
console.log(`JS tokens written to ${outPath}${manifest ? ' (manifest mapper applied)' : ''}`);
|
|
88
96
|
}
|
package/cli/commands/build.ts
CHANGED
|
@@ -1,23 +1,21 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { dirname, resolve } from 'node:path';
|
|
2
2
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
3
3
|
import { flattenTokens, type FlatToken } from '../../core/flatten.js';
|
|
4
|
+
import { mergeTokens } from '../../core/breakpoints.js';
|
|
4
5
|
import { valuesToCss, type ManifestRow } from '../../adapters/css.js';
|
|
5
6
|
import { emitJSON } from '../../adapters/json.js';
|
|
6
7
|
import { emitJS } from '../../adapters/js.js';
|
|
7
8
|
import type { BuildOptions } from '../types.js';
|
|
9
|
+
import { loadThemeTokens } from './utils.js';
|
|
8
10
|
|
|
9
11
|
export async function buildCommand(options: BuildOptions & { [k: string]: any }): Promise<void> {
|
|
10
12
|
const { loadTokensWithBreakpoint } = await import('../../core/breakpoints.js');
|
|
11
|
-
|
|
12
|
-
const { flat } = flattenTokens(tokens);
|
|
13
|
-
let allValues = Object.fromEntries(Object.values(flat).map(t => [t.id, (t as FlatToken).value]));
|
|
13
|
+
let tokens = loadTokensWithBreakpoint(undefined, options.tokens);
|
|
14
14
|
if (options.theme) {
|
|
15
|
-
|
|
16
|
-
if (existsSync(themePath)) {
|
|
17
|
-
const themeTokens = JSON.parse(readFileSync(themePath, 'utf8'));
|
|
18
|
-
Object.assign(allValues, themeTokens);
|
|
19
|
-
}
|
|
15
|
+
tokens = mergeTokens(tokens, loadThemeTokens(options.theme));
|
|
20
16
|
}
|
|
17
|
+
const { flat } = flattenTokens(tokens);
|
|
18
|
+
const allValues = Object.fromEntries(Object.values(flat).map(t => [t.id, (t as FlatToken).value]));
|
|
21
19
|
const format = options.format || 'css';
|
|
22
20
|
const defaultOutput = `dist/tokens.${format}`;
|
|
23
21
|
let manifest: ManifestRow[] | undefined;
|
|
@@ -33,18 +31,29 @@ export async function buildCommand(options: BuildOptions & { [k: string]: any })
|
|
|
33
31
|
process.exit(1);
|
|
34
32
|
}
|
|
35
33
|
}
|
|
36
|
-
|
|
34
|
+
// Read both the kebab key (CLI, camel-case-expansion is off) and the camelCase
|
|
35
|
+
// key (programmatic callers, e.g. tests). Reading only one form silently no-ops
|
|
36
|
+
// for the other caller — that was the TASK-024 bug class.
|
|
37
|
+
const allFormats = options['all-formats'] ?? options.allFormats;
|
|
38
|
+
const dryRun = options['dry-run'] ?? options.dryRun;
|
|
37
39
|
if (allFormats) {
|
|
38
|
-
const dir = 'dist'; if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
39
40
|
const css = valuesToCss(allValues, { manifest });
|
|
41
|
+
const json = emitJSON(allValues, manifest);
|
|
42
|
+
const js = emitJS(allValues, manifest);
|
|
43
|
+
if (dryRun) {
|
|
44
|
+
// Dry run: print every format, write nothing.
|
|
45
|
+
console.log(css);
|
|
46
|
+
console.log(json);
|
|
47
|
+
console.log(js);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const dir = 'dist'; if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
40
51
|
writeFileSync('dist/tokens.css', css, 'utf8');
|
|
41
|
-
|
|
42
|
-
writeFileSync('dist/tokens.
|
|
43
|
-
writeFileSync('dist/tokens.js', emitJS(allValues, manifest), 'utf8');
|
|
52
|
+
writeFileSync('dist/tokens.json', json, 'utf8');
|
|
53
|
+
writeFileSync('dist/tokens.js', js, 'utf8');
|
|
44
54
|
console.log(`Tokens written (all formats) to dist/ (css/json/js)${manifest ? ' with mapper' : ''}`);
|
|
45
55
|
return;
|
|
46
56
|
}
|
|
47
|
-
const dryRun = options.dryRun ?? options['dry-run'];
|
|
48
57
|
if (format === 'css') {
|
|
49
58
|
const css = valuesToCss(allValues, { manifest });
|
|
50
59
|
if (dryRun) { console.log(css); return; }
|
|
@@ -52,13 +61,13 @@ export async function buildCommand(options: BuildOptions & { [k: string]: any })
|
|
|
52
61
|
writeFileSync(outPath, css, 'utf8');
|
|
53
62
|
console.log(`CSS tokens written to ${outPath}${manifest ? ' (manifest mapper applied)' : ''}`);
|
|
54
63
|
} else if (format === 'json') {
|
|
64
|
+
if (dryRun) { console.log(emitJSON(allValues, manifest)); return; }
|
|
55
65
|
const outPath = options.output || defaultOutput; const dir = dirname(outPath); if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
56
|
-
if (dryRun) { console.log(emitJSON(allValues, manifest)); return; }
|
|
57
66
|
writeFileSync(outPath, emitJSON(allValues, manifest), 'utf8');
|
|
58
67
|
console.log(`JSON tokens written to ${outPath}${manifest ? ' (manifest mapper applied)' : ''}`);
|
|
59
68
|
} else if (format === 'js') {
|
|
69
|
+
if (dryRun) { console.log(emitJS(allValues, manifest)); return; }
|
|
60
70
|
const outPath = options.output || defaultOutput; const dir = dirname(outPath); if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
61
|
-
if (dryRun) { console.log(emitJS(allValues, manifest)); return; }
|
|
62
71
|
writeFileSync(outPath, emitJS(allValues, manifest), 'utf8');
|
|
63
72
|
console.log(`JS tokens written to ${outPath}${manifest ? ' (manifest mapper applied)' : ''}`);
|
|
64
73
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["graph.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["graph.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AA8BhD,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAiKvE"}
|
package/cli/commands/graph.js
CHANGED
|
@@ -16,11 +16,14 @@ function generateDependencyGraph(edges, format) {
|
|
|
16
16
|
return dot + '}\n';
|
|
17
17
|
}
|
|
18
18
|
case 'mermaid': {
|
|
19
|
+
// Injective id encoding so distinct token ids (e.g. a.b vs a_b) never
|
|
20
|
+
// collapse to the same Mermaid node (TASK-027).
|
|
21
|
+
const safe = (s) => s.replace(/[^a-zA-Z0-9]/g, (c) => `_${c.charCodeAt(0)}_`);
|
|
19
22
|
let mermaid = 'graph LR\n';
|
|
20
23
|
const mermaidNodes = new Set();
|
|
21
24
|
edges.forEach(([from, to]) => {
|
|
22
|
-
const fromId = from
|
|
23
|
-
const toId = to
|
|
25
|
+
const fromId = safe(from);
|
|
26
|
+
const toId = safe(to);
|
|
24
27
|
if (!mermaidNodes.has(fromId)) {
|
|
25
28
|
mermaid += ` ${fromId}["${from}"]\n`;
|
|
26
29
|
mermaidNodes.add(fromId);
|
|
@@ -44,16 +47,21 @@ export async function graphCommand(options) {
|
|
|
44
47
|
if (options.hasse) {
|
|
45
48
|
const name = options.hasse;
|
|
46
49
|
const bundle = options.bundle;
|
|
50
|
+
const constraintsDir = options['constraints-dir'] ?? 'themes';
|
|
47
51
|
const fmt = (options.format === 'json' ? 'mermaid' : options.format);
|
|
48
|
-
const imageFrom = options
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
const
|
|
52
|
+
const imageFrom = options['image-from'] || 'mermaid';
|
|
53
|
+
// The CLI runs with camel-case-expansion off, so kebab flags arrive only under
|
|
54
|
+
// their kebab keys. Read those (camelCase reads here silently no-op'd before).
|
|
55
|
+
const filterPrefix = options['filter-prefix'];
|
|
56
|
+
const excludePrefix = options['exclude-prefix'];
|
|
57
|
+
const filterPrefixes = filterPrefix ? filterPrefix.split(',').map(s => s.trim()).filter(Boolean) : [];
|
|
58
|
+
const excludePrefixes = excludePrefix ? excludePrefix.split(',').map(s => s.trim()).filter(Boolean) : [];
|
|
59
|
+
const onlyViolations = options['only-violations'] || false;
|
|
60
|
+
const highlightViolations = options['highlight-violations'] || false;
|
|
61
|
+
const violationColor = options['violation-color'] || '#ff2d55';
|
|
62
|
+
const labelViolations = options['label-violations'] || false;
|
|
63
|
+
const labelTruncate = Math.max(0, options['label-truncate'] || 0);
|
|
64
|
+
const minSeverity = options['min-severity'] || 'warn';
|
|
57
65
|
const focus = options.focus;
|
|
58
66
|
const radius = Math.max(0, options.radius || 1);
|
|
59
67
|
for (const breakpoint of plan) {
|
|
@@ -78,7 +86,7 @@ export async function graphCommand(options) {
|
|
|
78
86
|
const outDir = 'dist/graphs';
|
|
79
87
|
const baseFile = `${outDir}/${name}${suffix}-hasse.${ext}`;
|
|
80
88
|
try {
|
|
81
|
-
const src =
|
|
89
|
+
const src = `${constraintsDir}/${name}.order.json`;
|
|
82
90
|
if (!existsSync(src)) {
|
|
83
91
|
console.error(`❌ Order constraint file not found: ${src}`);
|
|
84
92
|
process.exit(1);
|
|
@@ -100,7 +108,7 @@ export async function graphCommand(options) {
|
|
|
100
108
|
let edgeLabels;
|
|
101
109
|
if (onlyViolations || highlightViolations || labelViolations) {
|
|
102
110
|
const { loadTokensWithBreakpoint } = await import('../../core/breakpoints.js');
|
|
103
|
-
const tokens = loadTokensWithBreakpoint(breakpoint);
|
|
111
|
+
const tokens = loadTokensWithBreakpoint(breakpoint, options.tokens);
|
|
104
112
|
const { flattenTokens } = await import('../../core/flatten.js');
|
|
105
113
|
const { Engine } = await import('../../core/engine.js');
|
|
106
114
|
const { MonotonicPlugin, parseSize } = await import('../../core/constraints/monotonic.js');
|
|
@@ -122,12 +130,21 @@ export async function graphCommand(options) {
|
|
|
122
130
|
const numericOrders = order;
|
|
123
131
|
issues = MonotonicPlugin(numericOrders, parseSize, 'monotonic').evaluate(engine, allIdsInHasse);
|
|
124
132
|
}
|
|
125
|
-
// Attach threshold (and any other runtime constraints) respecting config flags
|
|
126
|
-
|
|
127
|
-
|
|
133
|
+
// Attach threshold (and any other runtime constraints) respecting config flags.
|
|
134
|
+
// Honor the global --config and --constraints-dir, matching `validate`.
|
|
135
|
+
const cfgRes = loadConfig(options.config);
|
|
136
|
+
if (!cfgRes.ok) {
|
|
137
|
+
// An explicitly requested config that fails is a hard error (parity with
|
|
138
|
+
// validate); otherwise proceed with order-file violations only.
|
|
139
|
+
if (options.config) {
|
|
140
|
+
console.error(cfgRes.error);
|
|
141
|
+
process.exit(2);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
128
145
|
const config = cfgRes.value;
|
|
129
146
|
const knownIds = new Set(Object.keys(flat));
|
|
130
|
-
setupConstraints(engine, { config, bp: breakpoint }, { knownIds });
|
|
147
|
+
setupConstraints(engine, { config, bp: breakpoint, constraintsDir }, { knownIds });
|
|
131
148
|
const runtimeIssues = engine.evaluate(allIdsInHasse);
|
|
132
149
|
issues.push(...runtimeIssues);
|
|
133
150
|
}
|
|
@@ -211,7 +228,7 @@ export async function graphCommand(options) {
|
|
|
211
228
|
}
|
|
212
229
|
for (const breakpoint of plan) {
|
|
213
230
|
const { loadTokensWithBreakpoint } = await import('../../core/breakpoints.js');
|
|
214
|
-
const tokens = loadTokensWithBreakpoint(breakpoint);
|
|
231
|
+
const tokens = loadTokensWithBreakpoint(breakpoint, options.tokens);
|
|
215
232
|
const { edges } = flattenTokens(tokens);
|
|
216
233
|
let filteredEdges = edges;
|
|
217
234
|
if (options.filter) {
|
package/cli/commands/graph.ts
CHANGED
|
@@ -15,8 +15,11 @@ function generateDependencyGraph(edges: Array<[string, string]>, format: string)
|
|
|
15
15
|
dot += '\n'; edges.forEach(([f,t]) => { dot += ` "${f}" -> "${t}";\n`; });
|
|
16
16
|
return dot + '}\n'; }
|
|
17
17
|
case 'mermaid': {
|
|
18
|
+
// Injective id encoding so distinct token ids (e.g. a.b vs a_b) never
|
|
19
|
+
// collapse to the same Mermaid node (TASK-027).
|
|
20
|
+
const safe = (s: string) => s.replace(/[^a-zA-Z0-9]/g, (c) => `_${c.charCodeAt(0)}_`);
|
|
18
21
|
let mermaid = 'graph LR\n'; const mermaidNodes = new Set<string>();
|
|
19
|
-
edges.forEach(([from,to]) => { const fromId = from
|
|
22
|
+
edges.forEach(([from,to]) => { const fromId = safe(from); const toId = safe(to);
|
|
20
23
|
if (!mermaidNodes.has(fromId)) { mermaid += ` ${fromId}["${from}"]\n`; mermaidNodes.add(fromId); }
|
|
21
24
|
if (!mermaidNodes.has(toId)) { mermaid += ` ${toId}["${to}"]\n`; mermaidNodes.add(toId); }
|
|
22
25
|
mermaid += ` ${fromId} --> ${toId}\n`; });
|
|
@@ -33,16 +36,21 @@ export async function graphCommand(options: GraphOptions): Promise<void> {
|
|
|
33
36
|
if (options.hasse) {
|
|
34
37
|
const name = options.hasse;
|
|
35
38
|
const bundle = (options as any).bundle;
|
|
39
|
+
const constraintsDir = options['constraints-dir'] ?? 'themes';
|
|
36
40
|
const fmt = (options.format === 'json' ? 'mermaid' : options.format) as 'mermaid' | 'dot' | 'svg' | 'png';
|
|
37
|
-
const imageFrom = options
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
const
|
|
41
|
+
const imageFrom = options['image-from'] || 'mermaid';
|
|
42
|
+
// The CLI runs with camel-case-expansion off, so kebab flags arrive only under
|
|
43
|
+
// their kebab keys. Read those (camelCase reads here silently no-op'd before).
|
|
44
|
+
const filterPrefix = options['filter-prefix'];
|
|
45
|
+
const excludePrefix = options['exclude-prefix'];
|
|
46
|
+
const filterPrefixes = filterPrefix ? filterPrefix.split(',').map(s=>s.trim()).filter(Boolean) : [];
|
|
47
|
+
const excludePrefixes = excludePrefix ? excludePrefix.split(',').map(s=>s.trim()).filter(Boolean) : [];
|
|
48
|
+
const onlyViolations = options['only-violations'] || false;
|
|
49
|
+
const highlightViolations = options['highlight-violations'] || false;
|
|
50
|
+
const violationColor = options['violation-color'] || '#ff2d55';
|
|
51
|
+
const labelViolations = options['label-violations'] || false;
|
|
52
|
+
const labelTruncate = Math.max(0, options['label-truncate'] || 0);
|
|
53
|
+
const minSeverity = options['min-severity'] || 'warn';
|
|
46
54
|
const focus = options.focus; const radius = Math.max(0, options.radius || 1);
|
|
47
55
|
for (const breakpoint of plan) {
|
|
48
56
|
const suffixParts: string[] = [];
|
|
@@ -57,7 +65,7 @@ export async function graphCommand(options: GraphOptions): Promise<void> {
|
|
|
57
65
|
const ext = baseFmt === 'mermaid' ? 'mmd' : 'dot';
|
|
58
66
|
const outDir = 'dist/graphs'; const baseFile = `${outDir}/${name}${suffix}-hasse.${ext}`;
|
|
59
67
|
try {
|
|
60
|
-
const src =
|
|
68
|
+
const src = `${constraintsDir}/${name}.order.json`;
|
|
61
69
|
if (!existsSync(src)) { console.error(`❌ Order constraint file not found: ${src}`); process.exit(1); }
|
|
62
70
|
const { order } = JSON.parse(readFileSync(src, 'utf8'));
|
|
63
71
|
const { buildPoset, transitiveReduction, toMermaidHasseStyled, toDotHasseStyled, filterByPrefix, filterExcludePrefix, khopSubgraph, pickSeedsByPattern } = await import('../../core/poset.js');
|
|
@@ -69,7 +77,7 @@ export async function graphCommand(options: GraphOptions): Promise<void> {
|
|
|
69
77
|
let highlight: { nodes: Set<string>; edges: Set<string>; color?: string } | undefined; let edgeLabels: Map<string,string> | undefined;
|
|
70
78
|
if (onlyViolations || highlightViolations || labelViolations) {
|
|
71
79
|
const { loadTokensWithBreakpoint } = await import('../../core/breakpoints.js');
|
|
72
|
-
const tokens = loadTokensWithBreakpoint(breakpoint);
|
|
80
|
+
const tokens = loadTokensWithBreakpoint(breakpoint, options.tokens);
|
|
73
81
|
const { flattenTokens } = await import('../../core/flatten.js');
|
|
74
82
|
const { Engine } = await import('../../core/engine.js');
|
|
75
83
|
const { MonotonicPlugin, parseSize } = await import('../../core/constraints/monotonic.js');
|
|
@@ -93,12 +101,17 @@ export async function graphCommand(options: GraphOptions): Promise<void> {
|
|
|
93
101
|
issues = MonotonicPlugin(numericOrders, parseSize, 'monotonic').evaluate(engine, allIdsInHasse);
|
|
94
102
|
}
|
|
95
103
|
|
|
96
|
-
// Attach threshold (and any other runtime constraints) respecting config flags
|
|
97
|
-
|
|
98
|
-
|
|
104
|
+
// Attach threshold (and any other runtime constraints) respecting config flags.
|
|
105
|
+
// Honor the global --config and --constraints-dir, matching `validate`.
|
|
106
|
+
const cfgRes = loadConfig(options.config);
|
|
107
|
+
if (!cfgRes.ok) {
|
|
108
|
+
// An explicitly requested config that fails is a hard error (parity with
|
|
109
|
+
// validate); otherwise proceed with order-file violations only.
|
|
110
|
+
if (options.config) { console.error(cfgRes.error); process.exit(2); }
|
|
111
|
+
} else {
|
|
99
112
|
const config = cfgRes.value;
|
|
100
113
|
const knownIds = new Set(Object.keys(flat as Record<string, FlatToken>));
|
|
101
|
-
setupConstraints(engine, { config, bp: breakpoint }, { knownIds });
|
|
114
|
+
setupConstraints(engine, { config, bp: breakpoint, constraintsDir }, { knownIds });
|
|
102
115
|
const runtimeIssues = engine.evaluate(allIdsInHasse);
|
|
103
116
|
issues.push(...runtimeIssues);
|
|
104
117
|
}
|
|
@@ -170,7 +183,7 @@ export async function graphCommand(options: GraphOptions): Promise<void> {
|
|
|
170
183
|
return; }
|
|
171
184
|
for (const breakpoint of plan) {
|
|
172
185
|
const { loadTokensWithBreakpoint } = await import('../../core/breakpoints.js');
|
|
173
|
-
const tokens = loadTokensWithBreakpoint(breakpoint); const { edges } = flattenTokens(tokens);
|
|
186
|
+
const tokens = loadTokensWithBreakpoint(breakpoint, options.tokens); const { edges } = flattenTokens(tokens);
|
|
174
187
|
let filteredEdges = edges;
|
|
175
188
|
if (options.filter) { const filterRegex = new RegExp(options.filter); filteredEdges = edges.filter(([from,to]) => filterRegex.test(from) || filterRegex.test(to)); }
|
|
176
189
|
const format = options.format || 'json'; const graph = generateDependencyGraph(filteredEdges, format);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"patch-apply.d.ts","sourceRoot":"","sources":["patch-apply.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAkCrD,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"patch-apply.d.ts","sourceRoot":"","sources":["patch-apply.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAkCrD,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CA+C9E"}
|
|
@@ -60,7 +60,10 @@ export async function patchApplyCommand(opts) {
|
|
|
60
60
|
for (const c of patchDoc.changes) {
|
|
61
61
|
applyChange(tokens, c.id, c.to, c.type);
|
|
62
62
|
}
|
|
63
|
-
|
|
63
|
+
// Read both forms: kebab from the CLI (camel-case-expansion off) and camelCase
|
|
64
|
+
// from programmatic callers. Reading only opts.dryRun was dead for the CLI, so
|
|
65
|
+
// `--dry-run --output` silently wrote the file (TASK-024).
|
|
66
|
+
if (opts['dry-run'] ?? opts.dryRun) {
|
|
64
67
|
outputResult(tokens, 'json');
|
|
65
68
|
return;
|
|
66
69
|
}
|
|
@@ -66,7 +66,10 @@ export async function patchApplyCommand(opts: PatchApplyOptions): Promise<void>
|
|
|
66
66
|
applyChange(tokens, c.id, c.to, c.type);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
|
|
69
|
+
// Read both forms: kebab from the CLI (camel-case-expansion off) and camelCase
|
|
70
|
+
// from programmatic callers. Reading only opts.dryRun was dead for the CLI, so
|
|
71
|
+
// `--dry-run --output` silently wrote the file (TASK-024).
|
|
72
|
+
if (opts['dry-run'] ?? opts.dryRun) {
|
|
70
73
|
outputResult(tokens, 'json');
|
|
71
74
|
return;
|
|
72
75
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"set.d.ts","sourceRoot":"","sources":["set.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"set.d.ts","sourceRoot":"","sources":["set.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAiB,UAAU,EAAe,MAAM,aAAa,CAAC;AAmE1E,wBAAsB,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAuKnE"}
|