design-constraint-validator 2.1.0 → 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 +17 -6
- 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 +33 -16
- package/cli/commands/graph.ts +28 -15
- 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 +5 -17
- package/cli/commands/validate.ts +10 -18
- 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 +144 -178
- package/cli/config-schema.d.ts.map +1 -1
- package/cli/config-schema.js +25 -5
- package/cli/config-schema.ts +27 -5
- package/cli/constraint-registry.d.ts.map +1 -1
- package/cli/constraint-registry.js +53 -15
- package/cli/constraint-registry.ts +53 -18
- 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 +23 -1
- package/cli/dcv.ts +23 -1
- package/cli/types.d.ts +19 -9
- package/cli/types.d.ts.map +1 -1
- package/cli/types.ts +23 -10
- package/cli/validate-api.d.ts.map +1 -1
- package/cli/validate-api.js +6 -1
- package/cli/validate-api.ts +6 -1
- 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.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.js +1 -1
- package/core/constraints/wcag.ts +1 -1
- package/core/flatten.d.ts.map +1 -1
- package/core/flatten.js +8 -0
- package/core/flatten.ts +9 -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/mcp/contracts.d.ts +1456 -13
- package/mcp/contracts.d.ts.map +1 -1
- package/mcp/contracts.js +45 -1
- package/mcp/contracts.ts +55 -1
- package/mcp/index.d.ts +6 -4
- package/mcp/index.d.ts.map +1 -1
- package/mcp/index.js +6 -3
- package/mcp/index.ts +28 -1
- 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 +14 -3
- package/mcp/tools.d.ts.map +1 -1
- package/mcp/tools.js +133 -6
- package/mcp/tools.ts +188 -11
- package/package.json +1 -6
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -26,10 +26,12 @@ This is **not** a schema linter; it's a **reasoning validator** for values and r
|
|
|
26
26
|
# Local (recommended)
|
|
27
27
|
npm i -D design-constraint-validator
|
|
28
28
|
|
|
29
|
-
# One-off run
|
|
30
|
-
npx
|
|
29
|
+
# One-off run, no install (the bin name `dcv` belongs to an unrelated package)
|
|
30
|
+
npx design-constraint-validator --help
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
+
After a local install, the shorter `dcv` bin is available (e.g. `npx dcv --help`).
|
|
34
|
+
|
|
33
35
|
**Requirements:** Node.js ≥ 18.x (ESM)
|
|
34
36
|
|
|
35
37
|
---
|
|
@@ -63,15 +65,19 @@ cat > dcv.config.json <<'JSON'
|
|
|
63
65
|
JSON
|
|
64
66
|
|
|
65
67
|
# 3. Validate (positional path or --tokens; exits non-zero on violations)
|
|
66
|
-
npx
|
|
68
|
+
npx design-constraint-validator validate tokens.json --summary table
|
|
67
69
|
|
|
68
70
|
# Explain one token (the tokenId is required)
|
|
69
|
-
npx
|
|
71
|
+
npx design-constraint-validator why color.text --tokens tokens.json --format table
|
|
70
72
|
|
|
71
73
|
# Export the dependency graph
|
|
72
|
-
npx
|
|
74
|
+
npx design-constraint-validator graph --tokens tokens.json --format mermaid > graph.mmd
|
|
73
75
|
```
|
|
74
76
|
|
|
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 …`.
|
|
80
|
+
|
|
75
81
|
**Example output** (`validate`):
|
|
76
82
|
|
|
77
83
|
```text
|
|
@@ -124,11 +130,16 @@ DCV ships a second binary, `dcv-mcp`, that exposes the validator over MCP stdio
|
|
|
124
130
|
}
|
|
125
131
|
```
|
|
126
132
|
|
|
127
|
-
The server exposes
|
|
133
|
+
The server exposes six read-only, JSON-returning tools:
|
|
128
134
|
|
|
129
135
|
- `validate` - validate inline `tokens` or a `tokensPath` against inline `constraints` or a config file.
|
|
130
136
|
- `why` - explain provenance, aliases, dependencies, dependents, and alias chain for one token id.
|
|
131
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.
|
|
132
143
|
|
|
133
144
|
Tool failures are returned as structured JSON: `{ "ok": false, "error": { "code": "...", "message": "..." } }`.
|
|
134
145
|
|
|
@@ -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);
|
|
@@ -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
|
}
|
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');
|
|
@@ -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
|
}
|
|
@@ -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"}
|
package/cli/commands/set.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { join } from 'node:path';
|
|
2
|
-
import { readFileSync, existsSync } from 'node:fs';
|
|
3
1
|
import { loadConfig } from '../config.js';
|
|
4
2
|
import { Engine } from '../../core/engine.js';
|
|
5
3
|
import { flattenTokens } from '../../core/flatten.js';
|
|
6
|
-
import {
|
|
4
|
+
import { mergeTokens } from '../../core/breakpoints.js';
|
|
5
|
+
import { loadThemeTokens, loadTokens, outputResult } from './utils.js';
|
|
7
6
|
import { setupConstraints } from '../constraint-registry.js';
|
|
8
7
|
// Lightweight suggestion helpers (kept local – why command uses core formatter instead)
|
|
9
8
|
function levenshtein(a, b) {
|
|
@@ -81,7 +80,10 @@ export async function setCommand(options) {
|
|
|
81
80
|
process.exit(2);
|
|
82
81
|
}
|
|
83
82
|
const config = cfgRes.value;
|
|
84
|
-
|
|
83
|
+
let tokens = loadTokens(tokensPath);
|
|
84
|
+
if (options.theme) {
|
|
85
|
+
tokens = mergeTokens(tokens, loadThemeTokens(options.theme));
|
|
86
|
+
}
|
|
85
87
|
// Create engine with flattened tokens
|
|
86
88
|
const { flat, edges } = flattenTokens(tokens);
|
|
87
89
|
const init = {};
|
|
@@ -103,18 +105,6 @@ export async function setCommand(options) {
|
|
|
103
105
|
process.exit(1);
|
|
104
106
|
}
|
|
105
107
|
}
|
|
106
|
-
if (options.theme) {
|
|
107
|
-
const themePath = join('tokens/themes', `${options.theme}.json`);
|
|
108
|
-
if (existsSync(themePath)) {
|
|
109
|
-
const themeTokens = JSON.parse(readFileSync(themePath, 'utf8'));
|
|
110
|
-
for (const [id, value] of Object.entries(themeTokens)) {
|
|
111
|
-
engine.commit(id, value);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
else {
|
|
115
|
-
console.warn(`Theme file not found: ${themePath}`);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
108
|
let finalResult = {};
|
|
119
109
|
function setDeep(obj, parts, v) {
|
|
120
110
|
let cur = obj;
|
|
@@ -210,7 +200,7 @@ export async function setCommand(options) {
|
|
|
210
200
|
for (const e of entries)
|
|
211
201
|
console.log(' ', e);
|
|
212
202
|
}
|
|
213
|
-
const dryRun =
|
|
203
|
+
const dryRun = !!(options['dry-run'] ?? options.dryRun);
|
|
214
204
|
for (const { id, value, unset } of entries) {
|
|
215
205
|
if (unset) {
|
|
216
206
|
if (!options.quiet)
|
|
@@ -227,7 +217,8 @@ export async function setCommand(options) {
|
|
|
227
217
|
}
|
|
228
218
|
}
|
|
229
219
|
const format = options.format || 'json';
|
|
230
|
-
|
|
220
|
+
// Dry-run prints the patch to stdout instead of writing --output (no file writes).
|
|
221
|
+
outputResult(finalResult, format, dryRun ? undefined : options.output);
|
|
231
222
|
if (options.write && !dryRun) {
|
|
232
223
|
const path = 'tokens/overrides/local.json';
|
|
233
224
|
let local = {};
|
|
@@ -264,9 +255,17 @@ export async function setCommand(options) {
|
|
|
264
255
|
}
|
|
265
256
|
Object.assign(finalResult, result.patch);
|
|
266
257
|
}
|
|
258
|
+
const dryRun = !!(options['dry-run'] ?? options.dryRun);
|
|
267
259
|
const format = options.format || 'json';
|
|
268
|
-
|
|
260
|
+
// Dry-run prints the patch to stdout instead of writing --output (no file writes).
|
|
261
|
+
outputResult(finalResult, format, dryRun ? undefined : options.output);
|
|
269
262
|
if (options.write || (options.unset && options.unset.length)) {
|
|
263
|
+
// --dry-run must not touch the filesystem (it previously persisted the
|
|
264
|
+
// override file on the positional path; the batch path already guarded it).
|
|
265
|
+
if (dryRun) {
|
|
266
|
+
console.log('Dry-run: changes not written');
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
270
269
|
const fs = await import('node:fs');
|
|
271
270
|
const path = 'tokens/overrides/local.json';
|
|
272
271
|
let local = {};
|
package/cli/commands/set.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { join } from 'node:path';
|
|
2
|
-
import { readFileSync, existsSync } from 'node:fs';
|
|
3
1
|
import { loadConfig } from '../config.js';
|
|
4
2
|
import { Engine } from '../../core/engine.js';
|
|
5
3
|
import { flattenTokens, type FlatToken } from '../../core/flatten.js';
|
|
4
|
+
import { mergeTokens } from '../../core/breakpoints.js';
|
|
6
5
|
import type { OverridesTree, SetOptions, ValuesPatch } from '../types.js';
|
|
7
|
-
import { loadTokens, outputResult } from './utils.js';
|
|
6
|
+
import { loadThemeTokens, loadTokens, outputResult } from './utils.js';
|
|
8
7
|
import { setupConstraints } from '../constraint-registry.js';
|
|
9
8
|
|
|
10
9
|
// Lightweight suggestion helpers (kept local – why command uses core formatter instead)
|
|
@@ -76,7 +75,11 @@ export async function setCommand(options: SetOptions): Promise<void> {
|
|
|
76
75
|
const cfgRes = loadConfig(options.config);
|
|
77
76
|
if (!cfgRes.ok) { console.error(cfgRes.error); process.exit(2); }
|
|
78
77
|
const config = cfgRes.value;
|
|
79
|
-
|
|
78
|
+
let tokens = loadTokens(tokensPath);
|
|
79
|
+
|
|
80
|
+
if (options.theme) {
|
|
81
|
+
tokens = mergeTokens(tokens, loadThemeTokens(options.theme));
|
|
82
|
+
}
|
|
80
83
|
|
|
81
84
|
// Create engine with flattened tokens
|
|
82
85
|
const { flat, edges } = flattenTokens(tokens);
|
|
@@ -105,18 +108,6 @@ export async function setCommand(options: SetOptions): Promise<void> {
|
|
|
105
108
|
}
|
|
106
109
|
}
|
|
107
110
|
|
|
108
|
-
if (options.theme) {
|
|
109
|
-
const themePath = join('tokens/themes', `${options.theme}.json`);
|
|
110
|
-
if (existsSync(themePath)) {
|
|
111
|
-
const themeTokens = JSON.parse(readFileSync(themePath, 'utf8'));
|
|
112
|
-
for (const [id, value] of Object.entries(themeTokens)) {
|
|
113
|
-
engine.commit(id, value as string | number);
|
|
114
|
-
}
|
|
115
|
-
} else {
|
|
116
|
-
console.warn(`Theme file not found: ${themePath}`);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
120
111
|
let finalResult: ValuesPatch = {};
|
|
121
112
|
|
|
122
113
|
function setDeep(obj: OverridesTree, parts: string[], v: unknown) {
|
|
@@ -183,7 +174,7 @@ export async function setCommand(options: SetOptions): Promise<void> {
|
|
|
183
174
|
for (const ent of entries) ensureKnownOrSuggest(ent.id);
|
|
184
175
|
const debug = process.argv.includes('--debug-set') || process.env.DCV_DEBUG_SET === '1';
|
|
185
176
|
if (debug) { console.log('[set:batch] parsed entries:'); for (const e of entries) console.log(' ', e); }
|
|
186
|
-
const dryRun =
|
|
177
|
+
const dryRun = !!(options['dry-run'] ?? options.dryRun);
|
|
187
178
|
for (const { id, value, unset } of entries) {
|
|
188
179
|
if (unset) { if (!options.quiet) console.log(`preview: unset ${id}`); }
|
|
189
180
|
else {
|
|
@@ -193,7 +184,8 @@ export async function setCommand(options: SetOptions): Promise<void> {
|
|
|
193
184
|
}
|
|
194
185
|
}
|
|
195
186
|
const format = options.format || 'json';
|
|
196
|
-
|
|
187
|
+
// Dry-run prints the patch to stdout instead of writing --output (no file writes).
|
|
188
|
+
outputResult(finalResult, format, dryRun ? undefined : options.output);
|
|
197
189
|
if (options.write && !dryRun) {
|
|
198
190
|
const path = 'tokens/overrides/local.json';
|
|
199
191
|
let local: OverridesTree = {} as OverridesTree;
|
|
@@ -218,9 +210,17 @@ export async function setCommand(options: SetOptions): Promise<void> {
|
|
|
218
210
|
}
|
|
219
211
|
Object.assign(finalResult, result.patch);
|
|
220
212
|
}
|
|
213
|
+
const dryRun = !!(options['dry-run'] ?? options.dryRun);
|
|
221
214
|
const format = options.format || 'json';
|
|
222
|
-
|
|
215
|
+
// Dry-run prints the patch to stdout instead of writing --output (no file writes).
|
|
216
|
+
outputResult(finalResult, format, dryRun ? undefined : options.output);
|
|
223
217
|
if (options.write || (options.unset && options.unset.length)) {
|
|
218
|
+
// --dry-run must not touch the filesystem (it previously persisted the
|
|
219
|
+
// override file on the positional path; the batch path already guarded it).
|
|
220
|
+
if (dryRun) {
|
|
221
|
+
console.log('Dry-run: changes not written');
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
224
|
const fs = await import('node:fs');
|
|
225
225
|
const path = 'tokens/overrides/local.json';
|
|
226
226
|
let local: OverridesTree = {} as OverridesTree;
|
package/cli/commands/utils.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { TokenNode } from '../../core/flatten.js';
|
|
2
2
|
export declare function loadTokens(tokensPath: string): TokenNode;
|
|
3
|
+
export declare function loadThemeTokens(theme: string): TokenNode;
|
|
3
4
|
export declare function outputResult(data: unknown, format: string, outputPath?: string): void;
|
|
4
5
|
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["utils.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAc,MAAM,uBAAuB,CAAC;AAInE,wBAAgB,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,CAUxD;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CA8BrF"}
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["utils.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAc,MAAM,uBAAuB,CAAC;AAInE,wBAAgB,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,CAUxD;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,CAoBxD;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CA8BrF"}
|