design-constraint-validator 2.2.1 → 2.3.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 +8 -0
- package/adapters/css.d.ts +14 -0
- package/adapters/css.d.ts.map +1 -1
- package/adapters/css.js +33 -2
- package/adapters/css.ts +36 -2
- package/adapters/js.d.ts.map +1 -1
- package/adapters/js.js +3 -1
- package/adapters/js.ts +3 -1
- package/adapters/json.d.ts.map +1 -1
- package/adapters/json.js +3 -1
- package/adapters/json.ts +3 -1
- package/cli/commands/build.d.ts.map +1 -1
- package/cli/commands/build.js +8 -6
- package/cli/commands/build.ts +8 -6
- package/cli/commands/graph.d.ts.map +1 -1
- package/cli/commands/graph.js +14 -3
- package/cli/commands/graph.ts +14 -3
- package/cli/commands/patch-apply.d.ts.map +1 -1
- package/cli/commands/patch-apply.js +28 -18
- package/cli/commands/patch-apply.ts +26 -18
- package/cli/commands/validate.d.ts.map +1 -1
- package/cli/commands/validate.js +9 -5
- package/cli/commands/validate.ts +9 -5
- package/cli/commands/why.d.ts.map +1 -1
- package/cli/commands/why.js +16 -4
- package/cli/commands/why.ts +16 -4
- package/cli/dcv.js +28 -8
- package/cli/dcv.ts +28 -8
- package/cli/types.d.ts +4 -0
- package/cli/types.d.ts.map +1 -1
- package/cli/types.ts +4 -0
- package/core/breakpoints.d.ts +1 -0
- package/core/breakpoints.d.ts.map +1 -1
- package/core/breakpoints.js +11 -2
- package/core/breakpoints.ts +12 -1
- package/core/dtcg.d.ts.map +1 -1
- package/core/dtcg.js +7 -1
- package/core/dtcg.ts +6 -1
- package/core/patch.d.ts +7 -0
- package/core/patch.d.ts.map +1 -1
- package/core/patch.js +15 -4
- package/core/patch.ts +16 -4
- package/package.json +1 -1
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
|
|
19
19
|
This is **not** a schema linter; it's a **reasoning validator** for values and relationships.
|
|
20
20
|
|
|
21
|
+

|
|
22
|
+
|
|
23
|
+

|
|
24
|
+
|
|
21
25
|
---
|
|
22
26
|
|
|
23
27
|
## Installation
|
|
@@ -183,6 +187,8 @@ Conventional linters catch **schema** issues ("has a value, has a type").
|
|
|
183
187
|
|
|
184
188
|
This transforms tokens from "bags of numbers" into a **formal design system**.
|
|
185
189
|
|
|
190
|
+

|
|
191
|
+
|
|
186
192
|
---
|
|
187
193
|
|
|
188
194
|
## Comparison: Schema Linters vs DCV
|
|
@@ -194,6 +200,8 @@ This transforms tokens from "bags of numbers" into a **formal design system**.
|
|
|
194
200
|
| **Purpose** | Format compliance | Design system integrity |
|
|
195
201
|
| **Examples** | DTCG schema validator | WCAG checks, monotonic scales |
|
|
196
202
|
|
|
203
|
+

|
|
204
|
+
|
|
197
205
|
> DCV is not affiliated with Anima's `design-tokens-validator` (schema-focused).
|
|
198
206
|
|
|
199
207
|
---
|
package/adapters/css.d.ts
CHANGED
|
@@ -2,6 +2,20 @@ export type TokenId = string;
|
|
|
2
2
|
export type TokenValue = string | number;
|
|
3
3
|
export type VarMapper = (id: TokenId) => string | null;
|
|
4
4
|
export declare const defaultVarMapper: VarMapper;
|
|
5
|
+
/**
|
|
6
|
+
* A CSS custom-property value derived from a token must not break out of its
|
|
7
|
+
* declaration (TASK-035 C). `;`, `{`, `}` and comment markers can't appear in a
|
|
8
|
+
* well-formed token value, so strip them — a malformed value can otherwise
|
|
9
|
+
* inject or corrupt sibling declarations.
|
|
10
|
+
*/
|
|
11
|
+
export declare function sanitizeCssValue(v: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* Detect non-injective var mapping (TASK-035 C): `defaultVarMapper` collapses
|
|
14
|
+
* both `.` and other separators to `-`, so distinct ids like `a.b` and `a-b` map
|
|
15
|
+
* to the same `--a-b` and would silently overwrite each other in CSS/JSON/JS.
|
|
16
|
+
* Throw a clear error naming the colliding ids rather than dropping a token.
|
|
17
|
+
*/
|
|
18
|
+
export declare function assertNoVarCollisions(ids: Iterable<string>, resolve: (id: string) => string | null): void;
|
|
5
19
|
export type ManifestRow = {
|
|
6
20
|
id: string;
|
|
7
21
|
canonicalVar?: string | null;
|
package/adapters/css.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"css.d.ts","sourceRoot":"","sources":["css.ts"],"names":[],"mappings":"AACA,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC;AAC7B,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,CAAC;AAIzC,MAAM,MAAM,SAAS,GAAG,CAAC,EAAE,EAAE,OAAO,KAAK,MAAM,GAAG,IAAI,CAAC;AAEvD,eAAO,MAAM,gBAAgB,EAAE,SAC4C,CAAC;
|
|
1
|
+
{"version":3,"file":"css.d.ts","sourceRoot":"","sources":["css.ts"],"names":[],"mappings":"AACA,MAAM,MAAM,OAAO,GAAG,MAAM,CAAC;AAC7B,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,CAAC;AAIzC,MAAM,MAAM,SAAS,GAAG,CAAC,EAAE,EAAE,OAAO,KAAK,MAAM,GAAG,IAAI,CAAC;AAEvD,eAAO,MAAM,gBAAgB,EAAE,SAC4C,CAAC;AAE5E;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAElD;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,GAAG,IAAI,CAczG;AAGD,MAAM,MAAM,WAAW,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAE9F,MAAM,WAAW,UAAU;IAAG,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,EAAE,CAAA;CAAE;AAEpE;;;GAGG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC,EAAE,WAAW,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAsBxG;AAED,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,WAAW,EAAE,GAAG,SAAS,CAIxE;AAkCD;;GAEG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,EACnC,IAAI,CAAC,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,EAAE,WAAW,EAAE,CAAA;CAAE,GACzF,MAAM,CAKR;AAED;;GAEG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,EAClC,IAAI,CAAC,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,SAAS,CAAC;IAAC,QAAQ,CAAC,EAAE,WAAW,EAAE,CAAA;CAAE,GACzF,MAAM,CAKR;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE;IAAE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,EAAE,OAAO,EAAE,MAAM,QAEzF"}
|
package/adapters/css.js
CHANGED
|
@@ -1,4 +1,33 @@
|
|
|
1
1
|
export const defaultVarMapper = (id) => `--${id.replace(/[^a-z0-9.]/gi, "-").replace(/\.+/g, "-").toLowerCase()}`;
|
|
2
|
+
/**
|
|
3
|
+
* A CSS custom-property value derived from a token must not break out of its
|
|
4
|
+
* declaration (TASK-035 C). `;`, `{`, `}` and comment markers can't appear in a
|
|
5
|
+
* well-formed token value, so strip them — a malformed value can otherwise
|
|
6
|
+
* inject or corrupt sibling declarations.
|
|
7
|
+
*/
|
|
8
|
+
export function sanitizeCssValue(v) {
|
|
9
|
+
return v.replace(/\/\*|\*\//g, "").replace(/[;{}]/g, "").trim();
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Detect non-injective var mapping (TASK-035 C): `defaultVarMapper` collapses
|
|
13
|
+
* both `.` and other separators to `-`, so distinct ids like `a.b` and `a-b` map
|
|
14
|
+
* to the same `--a-b` and would silently overwrite each other in CSS/JSON/JS.
|
|
15
|
+
* Throw a clear error naming the colliding ids rather than dropping a token.
|
|
16
|
+
*/
|
|
17
|
+
export function assertNoVarCollisions(ids, resolve) {
|
|
18
|
+
const seen = new Map();
|
|
19
|
+
for (const id of ids) {
|
|
20
|
+
const v = resolve(id);
|
|
21
|
+
if (!v)
|
|
22
|
+
continue; // intentionally unmapped
|
|
23
|
+
const prev = seen.get(v);
|
|
24
|
+
if (prev !== undefined && prev !== id) {
|
|
25
|
+
throw new Error(`Variable name collision: tokens "${prev}" and "${id}" both map to "${v}". ` +
|
|
26
|
+
`Rename one id or provide a manifest with distinct canonicalVar values.`);
|
|
27
|
+
}
|
|
28
|
+
seen.set(v, id);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
2
31
|
;
|
|
3
32
|
/**
|
|
4
33
|
* Build a mapping of token id -> {canonical, aliases} from an optional manifest.
|
|
@@ -42,11 +71,12 @@ function buildCssBlock(values, opts) {
|
|
|
42
71
|
const decls = [];
|
|
43
72
|
if (opts.manifest) {
|
|
44
73
|
const mapping = buildVarMapping(Object.keys(values), opts.manifest);
|
|
74
|
+
assertNoVarCollisions(Object.keys(values), (id) => mapping.get(id)?.canonical ?? null);
|
|
45
75
|
for (const [id, val] of Object.entries(values)) {
|
|
46
76
|
const m = mapping.get(id);
|
|
47
77
|
if (!m)
|
|
48
78
|
continue;
|
|
49
|
-
const v = String(val)
|
|
79
|
+
const v = sanitizeCssValue(String(val));
|
|
50
80
|
if (!v)
|
|
51
81
|
continue;
|
|
52
82
|
decls.push(`${m.canonical}: ${v};`);
|
|
@@ -57,11 +87,12 @@ function buildCssBlock(values, opts) {
|
|
|
57
87
|
}
|
|
58
88
|
else {
|
|
59
89
|
const mapVar = opts.mapVar ?? defaultVarMapper;
|
|
90
|
+
assertNoVarCollisions(Object.keys(values), mapVar);
|
|
60
91
|
for (const [id, val] of Object.entries(values)) {
|
|
61
92
|
const cssVar = mapVar(id);
|
|
62
93
|
if (!cssVar)
|
|
63
94
|
continue; // skip if intentionally unmapped
|
|
64
|
-
const v = String(val)
|
|
95
|
+
const v = sanitizeCssValue(String(val));
|
|
65
96
|
if (v)
|
|
66
97
|
decls.push(`${cssVar}: ${v};`);
|
|
67
98
|
}
|
package/adapters/css.ts
CHANGED
|
@@ -9,6 +9,38 @@ export type VarMapper = (id: TokenId) => string | null;
|
|
|
9
9
|
export const defaultVarMapper: VarMapper = (id) =>
|
|
10
10
|
`--${id.replace(/[^a-z0-9.]/gi, "-").replace(/\.+/g, "-").toLowerCase()}`;
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* A CSS custom-property value derived from a token must not break out of its
|
|
14
|
+
* declaration (TASK-035 C). `;`, `{`, `}` and comment markers can't appear in a
|
|
15
|
+
* well-formed token value, so strip them — a malformed value can otherwise
|
|
16
|
+
* inject or corrupt sibling declarations.
|
|
17
|
+
*/
|
|
18
|
+
export function sanitizeCssValue(v: string): string {
|
|
19
|
+
return v.replace(/\/\*|\*\//g, "").replace(/[;{}]/g, "").trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Detect non-injective var mapping (TASK-035 C): `defaultVarMapper` collapses
|
|
24
|
+
* both `.` and other separators to `-`, so distinct ids like `a.b` and `a-b` map
|
|
25
|
+
* to the same `--a-b` and would silently overwrite each other in CSS/JSON/JS.
|
|
26
|
+
* Throw a clear error naming the colliding ids rather than dropping a token.
|
|
27
|
+
*/
|
|
28
|
+
export function assertNoVarCollisions(ids: Iterable<string>, resolve: (id: string) => string | null): void {
|
|
29
|
+
const seen = new Map<string, string>();
|
|
30
|
+
for (const id of ids) {
|
|
31
|
+
const v = resolve(id);
|
|
32
|
+
if (!v) continue; // intentionally unmapped
|
|
33
|
+
const prev = seen.get(v);
|
|
34
|
+
if (prev !== undefined && prev !== id) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`Variable name collision: tokens "${prev}" and "${id}" both map to "${v}". ` +
|
|
37
|
+
`Rename one id or provide a manifest with distinct canonicalVar values.`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
seen.set(v, id);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
12
44
|
// Manifest driven mapping allowing canonical + legacy aliases.
|
|
13
45
|
export type ManifestRow = { id: string; canonicalVar?: string | null; legacyVars?: string[] };
|
|
14
46
|
|
|
@@ -56,10 +88,11 @@ function buildCssBlock(
|
|
|
56
88
|
const decls: string[] = [];
|
|
57
89
|
if (opts.manifest) {
|
|
58
90
|
const mapping = buildVarMapping(Object.keys(values), opts.manifest);
|
|
91
|
+
assertNoVarCollisions(Object.keys(values), (id) => mapping.get(id)?.canonical ?? null);
|
|
59
92
|
for (const [id, val] of Object.entries(values)) {
|
|
60
93
|
const m = mapping.get(id);
|
|
61
94
|
if (!m) continue;
|
|
62
|
-
const v = String(val)
|
|
95
|
+
const v = sanitizeCssValue(String(val));
|
|
63
96
|
if (!v) continue;
|
|
64
97
|
decls.push(`${m.canonical}: ${v};`);
|
|
65
98
|
for (const alias of m.aliases) {
|
|
@@ -68,10 +101,11 @@ function buildCssBlock(
|
|
|
68
101
|
}
|
|
69
102
|
} else {
|
|
70
103
|
const mapVar = opts.mapVar ?? defaultVarMapper;
|
|
104
|
+
assertNoVarCollisions(Object.keys(values), mapVar);
|
|
71
105
|
for (const [id, val] of Object.entries(values)) {
|
|
72
106
|
const cssVar = mapVar(id);
|
|
73
107
|
if (!cssVar) continue; // skip if intentionally unmapped
|
|
74
|
-
const v = String(val)
|
|
108
|
+
const v = sanitizeCssValue(String(val));
|
|
75
109
|
if (v) decls.push(`${cssVar}: ${v};`);
|
|
76
110
|
}
|
|
77
111
|
}
|
package/adapters/js.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"js.d.ts","sourceRoot":"","sources":["js.ts"],"names":[],"mappings":"AACA,OAAO,
|
|
1
|
+
{"version":3,"file":"js.d.ts","sourceRoot":"","sources":["js.ts"],"names":[],"mappings":"AACA,OAAO,EAA4D,KAAK,WAAW,EAAE,MAAM,UAAU,CAAC;AAEtG,wBAAgB,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,QAAQ,CAAC,EAAE,WAAW,EAAE,GAAG,MAAM,CAYpF"}
|
package/adapters/js.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// adapters/js.ts
|
|
2
|
-
import { buildVarMapping, defaultVarMapper } from './css.js';
|
|
2
|
+
import { buildVarMapping, defaultVarMapper, assertNoVarCollisions } from './css.js';
|
|
3
3
|
export function emitJS(values, manifest) {
|
|
4
4
|
const ids = Object.keys(values).sort();
|
|
5
5
|
let mapping;
|
|
6
6
|
if (manifest)
|
|
7
7
|
mapping = buildVarMapping(ids, manifest);
|
|
8
|
+
// Distinct ids that collapse to the same key would silently overwrite (TASK-035 C).
|
|
9
|
+
assertNoVarCollisions(ids, (id) => (mapping ? mapping.get(id).canonical : defaultVarMapper(id)));
|
|
8
10
|
const lines = [];
|
|
9
11
|
for (const id of ids) {
|
|
10
12
|
const canonical = mapping ? mapping.get(id).canonical : defaultVarMapper(id);
|
package/adapters/js.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// adapters/js.ts
|
|
2
|
-
import { buildVarMapping, defaultVarMapper, type ManifestRow } from './css.js';
|
|
2
|
+
import { buildVarMapping, defaultVarMapper, assertNoVarCollisions, type ManifestRow } from './css.js';
|
|
3
3
|
|
|
4
4
|
export function emitJS(values: Record<string, any>, manifest?: ManifestRow[]): string {
|
|
5
5
|
const ids = Object.keys(values).sort();
|
|
6
6
|
let mapping: Map<string, { canonical: string; aliases: string[] }> | undefined;
|
|
7
7
|
if (manifest) mapping = buildVarMapping(ids, manifest);
|
|
8
|
+
// Distinct ids that collapse to the same key would silently overwrite (TASK-035 C).
|
|
9
|
+
assertNoVarCollisions(ids, (id) => (mapping ? mapping.get(id)!.canonical : defaultVarMapper(id)));
|
|
8
10
|
const lines: string[] = [];
|
|
9
11
|
for (const id of ids) {
|
|
10
12
|
const canonical = mapping ? mapping.get(id)!.canonical : defaultVarMapper(id);
|
package/adapters/json.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"json.d.ts","sourceRoot":"","sources":["json.ts"],"names":[],"mappings":"AACA,OAAO,
|
|
1
|
+
{"version":3,"file":"json.d.ts","sourceRoot":"","sources":["json.ts"],"names":[],"mappings":"AACA,OAAO,EAA4D,KAAK,WAAW,EAAE,MAAM,UAAU,CAAC;AAEtG,wBAAgB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,QAAQ,CAAC,EAAE,WAAW,EAAE,GAAG,MAAM,CAYtF;AAED,OAAO,KAAK,EAAE,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAEzE;;GAEG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE;IACjC,IAAI,EAAE,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;IACjC,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,EAAE,CAAC;CAC7B,GAAG,MAAM,CAQT;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE;IACjC,KAAK,EAAE,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;IACnC,QAAQ,EAAE,OAAO,EAAE,CAAC;CACrB,GAAG,MAAM,CAKT"}
|
package/adapters/json.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// adapters/json.ts
|
|
2
|
-
import { buildVarMapping, defaultVarMapper } from './css.js';
|
|
2
|
+
import { buildVarMapping, defaultVarMapper, assertNoVarCollisions } from './css.js';
|
|
3
3
|
export function emitJSON(values, manifest) {
|
|
4
4
|
const ids = Object.keys(values).sort();
|
|
5
5
|
let mapping;
|
|
6
6
|
if (manifest)
|
|
7
7
|
mapping = buildVarMapping(ids, manifest);
|
|
8
|
+
// Distinct ids that collapse to the same key would silently overwrite (TASK-035 C).
|
|
9
|
+
assertNoVarCollisions(ids, (id) => (mapping ? mapping.get(id).canonical : defaultVarMapper(id)));
|
|
8
10
|
const out = {};
|
|
9
11
|
for (const id of ids) {
|
|
10
12
|
const canonical = mapping ? mapping.get(id).canonical : defaultVarMapper(id);
|
package/adapters/json.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// adapters/json.ts
|
|
2
|
-
import { buildVarMapping, defaultVarMapper, type ManifestRow } from './css.js';
|
|
2
|
+
import { buildVarMapping, defaultVarMapper, assertNoVarCollisions, type ManifestRow } from './css.js';
|
|
3
3
|
|
|
4
4
|
export function emitJSON(values: Record<string, any>, manifest?: ManifestRow[]): string {
|
|
5
5
|
const ids = Object.keys(values).sort();
|
|
6
6
|
let mapping: Map<string, { canonical: string; aliases: string[] }> | undefined;
|
|
7
7
|
if (manifest) mapping = buildVarMapping(ids, manifest);
|
|
8
|
+
// Distinct ids that collapse to the same key would silently overwrite (TASK-035 C).
|
|
9
|
+
assertNoVarCollisions(ids, (id) => (mapping ? mapping.get(id)!.canonical : defaultVarMapper(id)));
|
|
8
10
|
const out: Record<string, any> = {};
|
|
9
11
|
for (const id of ids) {
|
|
10
12
|
const canonical = mapping ? mapping.get(id)!.canonical : defaultVarMapper(id);
|
|
@@ -1 +1 @@
|
|
|
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,
|
|
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,CAiE9F"}
|
package/cli/commands/build.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { dirname, resolve } from 'node:path';
|
|
1
|
+
import { dirname, resolve, join } from 'node:path';
|
|
2
2
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
3
3
|
import { flattenTokens } from '../../core/flatten.js';
|
|
4
4
|
import { mergeTokens } from '../../core/breakpoints.js';
|
|
@@ -48,13 +48,15 @@ export async function buildCommand(options) {
|
|
|
48
48
|
console.log(js);
|
|
49
49
|
return;
|
|
50
50
|
}
|
|
51
|
-
|
|
51
|
+
// --output is honored as the target DIRECTORY for --all-formats (TASK-035 C:
|
|
52
|
+
// it was previously ignored and dist/ hardcoded). Defaults to dist/.
|
|
53
|
+
const dir = options.output || 'dist';
|
|
52
54
|
if (!existsSync(dir))
|
|
53
55
|
mkdirSync(dir, { recursive: true });
|
|
54
|
-
writeFileSync('
|
|
55
|
-
writeFileSync('
|
|
56
|
-
writeFileSync('
|
|
57
|
-
console.log(`Tokens written (all formats) to
|
|
56
|
+
writeFileSync(join(dir, 'tokens.css'), css, 'utf8');
|
|
57
|
+
writeFileSync(join(dir, 'tokens.json'), json, 'utf8');
|
|
58
|
+
writeFileSync(join(dir, 'tokens.js'), js, 'utf8');
|
|
59
|
+
console.log(`Tokens written (all formats) to ${dir}/ (css/json/js)${manifest ? ' with mapper' : ''}`);
|
|
58
60
|
return;
|
|
59
61
|
}
|
|
60
62
|
if (format === 'css') {
|
package/cli/commands/build.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { dirname, resolve } from 'node:path';
|
|
1
|
+
import { dirname, resolve, join } from 'node:path';
|
|
2
2
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
3
3
|
import { flattenTokens, type FlatToken } from '../../core/flatten.js';
|
|
4
4
|
import { mergeTokens } from '../../core/breakpoints.js';
|
|
@@ -47,11 +47,13 @@ export async function buildCommand(options: BuildOptions & { [k: string]: any })
|
|
|
47
47
|
console.log(js);
|
|
48
48
|
return;
|
|
49
49
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
writeFileSync('
|
|
54
|
-
|
|
50
|
+
// --output is honored as the target DIRECTORY for --all-formats (TASK-035 C:
|
|
51
|
+
// it was previously ignored and dist/ hardcoded). Defaults to dist/.
|
|
52
|
+
const dir = options.output || 'dist'; if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
53
|
+
writeFileSync(join(dir, 'tokens.css'), css, 'utf8');
|
|
54
|
+
writeFileSync(join(dir, 'tokens.json'), json, 'utf8');
|
|
55
|
+
writeFileSync(join(dir, 'tokens.js'), js, 'utf8');
|
|
56
|
+
console.log(`Tokens written (all formats) to ${dir}/ (css/json/js)${manifest ? ' with mapper' : ''}`);
|
|
55
57
|
return;
|
|
56
58
|
}
|
|
57
59
|
if (format === 'css') {
|
|
@@ -1 +1 @@
|
|
|
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,
|
|
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,CA4KvE"}
|
package/cli/commands/graph.js
CHANGED
|
@@ -86,7 +86,11 @@ export async function graphCommand(options) {
|
|
|
86
86
|
const outDir = 'dist/graphs';
|
|
87
87
|
const baseFile = `${outDir}/${name}${suffix}-hasse.${ext}`;
|
|
88
88
|
try {
|
|
89
|
-
|
|
89
|
+
// Mirror discoverConstraints (TASK-034/035 B): under --breakpoint use the
|
|
90
|
+
// per-bp order file when present, else fall back to the global one — so the
|
|
91
|
+
// bp-labeled Hasse output actually reflects the bp poset, not the global.
|
|
92
|
+
const bpSrc = breakpoint ? `${constraintsDir}/${name}.${breakpoint}.order.json` : undefined;
|
|
93
|
+
const src = bpSrc && existsSync(bpSrc) ? bpSrc : `${constraintsDir}/${name}.order.json`;
|
|
90
94
|
if (!existsSync(src)) {
|
|
91
95
|
console.error(`❌ Order constraint file not found: ${src}`);
|
|
92
96
|
process.exit(1);
|
|
@@ -107,8 +111,15 @@ export async function graphCommand(options) {
|
|
|
107
111
|
let highlight;
|
|
108
112
|
let edgeLabels;
|
|
109
113
|
if (onlyViolations || highlightViolations || labelViolations) {
|
|
110
|
-
const { loadTokensWithBreakpoint } = await import('../../core/breakpoints.js');
|
|
111
|
-
|
|
114
|
+
const { loadTokensWithBreakpoint, mergeTokens } = await import('../../core/breakpoints.js');
|
|
115
|
+
let tokens = loadTokensWithBreakpoint(breakpoint, options.tokens);
|
|
116
|
+
// Theme overlay affects VALUES, so violation overlays must reflect it
|
|
117
|
+
// (TASK-025) — merge-then-flatten like build/validate. The dependency
|
|
118
|
+
// graph itself is value-independent and intentionally unthemed.
|
|
119
|
+
if (options.theme) {
|
|
120
|
+
const { loadThemeTokens } = await import('./utils.js');
|
|
121
|
+
tokens = mergeTokens(tokens, loadThemeTokens(options.theme));
|
|
122
|
+
}
|
|
112
123
|
const { flattenTokens } = await import('../../core/flatten.js');
|
|
113
124
|
const { Engine } = await import('../../core/engine.js');
|
|
114
125
|
const { MonotonicPlugin, parseSize } = await import('../../core/constraints/monotonic.js');
|
package/cli/commands/graph.ts
CHANGED
|
@@ -65,7 +65,11 @@ export async function graphCommand(options: GraphOptions): Promise<void> {
|
|
|
65
65
|
const ext = baseFmt === 'mermaid' ? 'mmd' : 'dot';
|
|
66
66
|
const outDir = 'dist/graphs'; const baseFile = `${outDir}/${name}${suffix}-hasse.${ext}`;
|
|
67
67
|
try {
|
|
68
|
-
|
|
68
|
+
// Mirror discoverConstraints (TASK-034/035 B): under --breakpoint use the
|
|
69
|
+
// per-bp order file when present, else fall back to the global one — so the
|
|
70
|
+
// bp-labeled Hasse output actually reflects the bp poset, not the global.
|
|
71
|
+
const bpSrc = breakpoint ? `${constraintsDir}/${name}.${breakpoint}.order.json` : undefined;
|
|
72
|
+
const src = bpSrc && existsSync(bpSrc) ? bpSrc : `${constraintsDir}/${name}.order.json`;
|
|
69
73
|
if (!existsSync(src)) { console.error(`❌ Order constraint file not found: ${src}`); process.exit(1); }
|
|
70
74
|
const { order } = JSON.parse(readFileSync(src, 'utf8'));
|
|
71
75
|
const { buildPoset, transitiveReduction, toMermaidHasseStyled, toDotHasseStyled, filterByPrefix, filterExcludePrefix, khopSubgraph, pickSeedsByPattern } = await import('../../core/poset.js');
|
|
@@ -76,8 +80,15 @@ export async function graphCommand(options: GraphOptions): Promise<void> {
|
|
|
76
80
|
if (focus) { const nodes = new Set<string>([...h.keys(), ...Array.from(h.values()).flatMap(s=>[...s])]); const seeds = pickSeedsByPattern(nodes, focus); h = khopSubgraph(h, seeds, radius); }
|
|
77
81
|
let highlight: { nodes: Set<string>; edges: Set<string>; color?: string } | undefined; let edgeLabels: Map<string,string> | undefined;
|
|
78
82
|
if (onlyViolations || highlightViolations || labelViolations) {
|
|
79
|
-
const { loadTokensWithBreakpoint } = await import('../../core/breakpoints.js');
|
|
80
|
-
|
|
83
|
+
const { loadTokensWithBreakpoint, mergeTokens } = await import('../../core/breakpoints.js');
|
|
84
|
+
let tokens = loadTokensWithBreakpoint(breakpoint, options.tokens);
|
|
85
|
+
// Theme overlay affects VALUES, so violation overlays must reflect it
|
|
86
|
+
// (TASK-025) — merge-then-flatten like build/validate. The dependency
|
|
87
|
+
// graph itself is value-independent and intentionally unthemed.
|
|
88
|
+
if (options.theme) {
|
|
89
|
+
const { loadThemeTokens } = await import('./utils.js');
|
|
90
|
+
tokens = mergeTokens(tokens, loadThemeTokens(options.theme));
|
|
91
|
+
}
|
|
81
92
|
const { flattenTokens } = await import('../../core/flatten.js');
|
|
82
93
|
const { Engine } = await import('../../core/engine.js');
|
|
83
94
|
const { MonotonicPlugin, parseSize } = await import('../../core/constraints/monotonic.js');
|
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"patch-apply.d.ts","sourceRoot":"","sources":["patch-apply.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAqCrD,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoD9E"}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { loadTokens, outputResult } from './utils.js';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
|
-
import {
|
|
4
|
-
import { createHash } from 'node:crypto';
|
|
3
|
+
import { computeBaseTokensHash } from '../../core/patch.js';
|
|
5
4
|
function applyChange(root, id, to, type) {
|
|
6
5
|
const parts = id.split('.');
|
|
7
6
|
let cur = root;
|
|
@@ -10,7 +9,12 @@ function applyChange(root, id, to, type) {
|
|
|
10
9
|
if (i === parts.length - 1) {
|
|
11
10
|
if (type === 'remove') {
|
|
12
11
|
if (cur[p] && typeof cur[p] === 'object') {
|
|
13
|
-
delete cur[p].$value;
|
|
12
|
+
delete cur[p].$value;
|
|
13
|
+
delete cur[p].$type; // drop the now-orphaned type with the value
|
|
14
|
+
// Remove the node entirely if nothing else remains (no dangling
|
|
15
|
+
// type-only node left behind — TASK-035 D).
|
|
16
|
+
if (Object.keys(cur[p]).length === 0)
|
|
17
|
+
delete cur[p];
|
|
14
18
|
}
|
|
15
19
|
}
|
|
16
20
|
else {
|
|
@@ -28,30 +32,36 @@ function applyChange(root, id, to, type) {
|
|
|
28
32
|
}
|
|
29
33
|
export async function patchApplyCommand(opts) {
|
|
30
34
|
const tokens = loadTokens(opts.tokens || 'tokens/tokens.example.json');
|
|
31
|
-
//
|
|
32
|
-
|
|
33
|
-
const flat = flattenTokens(JSON.parse(JSON.stringify(toks))).flat;
|
|
34
|
-
const values = {};
|
|
35
|
-
Object.keys(flat).sort().forEach(id => { values[id] = flat[id]?.value; });
|
|
36
|
-
// Keep deterministic ordering
|
|
37
|
-
const ordered = Object.keys(values).sort().reduce((acc, k) => { acc[k] = values[k]; return acc; }, {});
|
|
38
|
-
return createHash('sha256').update(JSON.stringify(ordered)).digest('hex');
|
|
39
|
-
}
|
|
40
|
-
// Parse patch
|
|
41
|
-
let patchDoc;
|
|
35
|
+
// Parse patch (friendly errors instead of a raw SyntaxError/TypeError — TASK-035 D)
|
|
36
|
+
let raw;
|
|
42
37
|
if (fs.existsSync(opts.patch)) {
|
|
43
|
-
|
|
38
|
+
raw = fs.readFileSync(opts.patch, 'utf8');
|
|
44
39
|
}
|
|
45
40
|
else if (opts.patch.trim().startsWith('{')) {
|
|
46
|
-
|
|
41
|
+
raw = opts.patch;
|
|
47
42
|
}
|
|
48
43
|
else {
|
|
49
44
|
throw new Error(`Patch not found: ${opts.patch}`);
|
|
50
45
|
}
|
|
46
|
+
let patchDoc;
|
|
47
|
+
try {
|
|
48
|
+
patchDoc = JSON.parse(raw);
|
|
49
|
+
}
|
|
50
|
+
catch (e) {
|
|
51
|
+
throw new Error(`Patch is not valid JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
52
|
+
}
|
|
53
|
+
// Shape validation before use, so a malformed doc gives a clear message rather
|
|
54
|
+
// than "changes is not iterable" / a raw TypeError (TASK-035 D).
|
|
55
|
+
if (!patchDoc || typeof patchDoc !== 'object' || Array.isArray(patchDoc)) {
|
|
56
|
+
throw new Error('Patch document must be a JSON object.');
|
|
57
|
+
}
|
|
51
58
|
if (patchDoc.version !== 1)
|
|
52
|
-
throw new Error(
|
|
59
|
+
throw new Error(`Unsupported patch version: ${patchDoc.version}. Expected 1.`);
|
|
60
|
+
if (!Array.isArray(patchDoc.changes)) {
|
|
61
|
+
throw new Error('Patch document is missing a "changes" array.');
|
|
62
|
+
}
|
|
53
63
|
if (patchDoc.baseTokensHash) {
|
|
54
|
-
const currentHash =
|
|
64
|
+
const currentHash = computeBaseTokensHash(tokens);
|
|
55
65
|
if (currentHash !== patchDoc.baseTokensHash) {
|
|
56
66
|
console.warn(`⚠ Base tokens hash mismatch. Patch built against ${patchDoc.baseTokensHash} but current base is ${currentHash}. Proceeding (use --dry-run to inspect first).`);
|
|
57
67
|
}
|
|
@@ -2,8 +2,7 @@ import { loadTokens, outputResult } from './utils.js';
|
|
|
2
2
|
import type { PatchApplyOptions } from '../types.js';
|
|
3
3
|
import type { TokenNode } from '../../core/flatten.js';
|
|
4
4
|
import fs from 'node:fs';
|
|
5
|
-
import {
|
|
6
|
-
import { createHash } from 'node:crypto';
|
|
5
|
+
import { computeBaseTokensHash } from '../../core/patch.js';
|
|
7
6
|
|
|
8
7
|
interface PatchDocumentV1 {
|
|
9
8
|
version: 1;
|
|
@@ -20,7 +19,11 @@ function applyChange(root: any, id: string, to: any, type: 'modify'|'add'|'remov
|
|
|
20
19
|
if (i === parts.length - 1) {
|
|
21
20
|
if (type === 'remove') {
|
|
22
21
|
if (cur[p] && typeof cur[p] === 'object') {
|
|
23
|
-
delete cur[p].$value;
|
|
22
|
+
delete cur[p].$value;
|
|
23
|
+
delete cur[p].$type; // drop the now-orphaned type with the value
|
|
24
|
+
// Remove the node entirely if nothing else remains (no dangling
|
|
25
|
+
// type-only node left behind — TASK-035 D).
|
|
26
|
+
if (Object.keys(cur[p]).length === 0) delete cur[p];
|
|
24
27
|
}
|
|
25
28
|
} else {
|
|
26
29
|
if (!cur[p] || typeof cur[p] !== 'object') cur[p] = {};
|
|
@@ -35,27 +38,32 @@ function applyChange(root: any, id: string, to: any, type: 'modify'|'add'|'remov
|
|
|
35
38
|
|
|
36
39
|
export async function patchApplyCommand(opts: PatchApplyOptions): Promise<void> {
|
|
37
40
|
const tokens: TokenNode = loadTokens(opts.tokens || 'tokens/tokens.example.json');
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
const flat = flattenTokens(JSON.parse(JSON.stringify(toks))).flat as Record<string, any>;
|
|
41
|
-
const values: Record<string, any> = {};
|
|
42
|
-
Object.keys(flat).sort().forEach(id => { values[id] = flat[id]?.value; });
|
|
43
|
-
// Keep deterministic ordering
|
|
44
|
-
const ordered = Object.keys(values).sort().reduce((acc, k) => { acc[k] = values[k]; return acc; }, {} as Record<string, any>);
|
|
45
|
-
return createHash('sha256').update(JSON.stringify(ordered)).digest('hex');
|
|
46
|
-
}
|
|
47
|
-
// Parse patch
|
|
48
|
-
let patchDoc: PatchDocumentV1;
|
|
41
|
+
// Parse patch (friendly errors instead of a raw SyntaxError/TypeError — TASK-035 D)
|
|
42
|
+
let raw: string;
|
|
49
43
|
if (fs.existsSync(opts.patch)) {
|
|
50
|
-
|
|
44
|
+
raw = fs.readFileSync(opts.patch, 'utf8');
|
|
51
45
|
} else if (opts.patch.trim().startsWith('{')) {
|
|
52
|
-
|
|
46
|
+
raw = opts.patch;
|
|
53
47
|
} else {
|
|
54
48
|
throw new Error(`Patch not found: ${opts.patch}`);
|
|
55
49
|
}
|
|
56
|
-
|
|
50
|
+
let patchDoc: PatchDocumentV1;
|
|
51
|
+
try {
|
|
52
|
+
patchDoc = JSON.parse(raw);
|
|
53
|
+
} catch (e) {
|
|
54
|
+
throw new Error(`Patch is not valid JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
55
|
+
}
|
|
56
|
+
// Shape validation before use, so a malformed doc gives a clear message rather
|
|
57
|
+
// than "changes is not iterable" / a raw TypeError (TASK-035 D).
|
|
58
|
+
if (!patchDoc || typeof patchDoc !== 'object' || Array.isArray(patchDoc)) {
|
|
59
|
+
throw new Error('Patch document must be a JSON object.');
|
|
60
|
+
}
|
|
61
|
+
if (patchDoc.version !== 1) throw new Error(`Unsupported patch version: ${(patchDoc as any).version}. Expected 1.`);
|
|
62
|
+
if (!Array.isArray(patchDoc.changes)) {
|
|
63
|
+
throw new Error('Patch document is missing a "changes" array.');
|
|
64
|
+
}
|
|
57
65
|
if (patchDoc.baseTokensHash) {
|
|
58
|
-
const currentHash =
|
|
66
|
+
const currentHash = computeBaseTokensHash(tokens);
|
|
59
67
|
if (currentHash !== patchDoc.baseTokensHash) {
|
|
60
68
|
console.warn(`⚠ Base tokens hash mismatch. Patch built against ${patchDoc.baseTokensHash} but current base is ${currentHash}. Proceeding (use --dry-run to inspect first).`);
|
|
61
69
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["validate.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAOnD,wBAAsB,eAAe,CAAC,QAAQ,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"validate.d.ts","sourceRoot":"","sources":["validate.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAOnD,wBAAsB,eAAe,CAAC,QAAQ,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAgM9E"}
|
package/cli/commands/validate.js
CHANGED
|
@@ -8,8 +8,9 @@ import { setupConstraints, collectReferencedIds } from '../constraint-registry.j
|
|
|
8
8
|
import { printVersionBanner } from '../version-banner.js';
|
|
9
9
|
import { loadThemeTokens } from './utils.js';
|
|
10
10
|
export async function validateCommand(_options) {
|
|
11
|
-
// Show version banner (subtle, dimmed)
|
|
12
|
-
|
|
11
|
+
// Show version banner (subtle, dimmed). Stay quiet whenever stdout must be
|
|
12
|
+
// machine-parseable — JSON output OR a JSON summary (TASK-035 B).
|
|
13
|
+
printVersionBanner({ quiet: _options.format === 'json' || _options.summary === 'json' });
|
|
13
14
|
try {
|
|
14
15
|
const bps = parseBreakpoints(process.argv);
|
|
15
16
|
const crossAxisDebug = process.argv.includes('--cross-axis-debug');
|
|
@@ -109,12 +110,15 @@ export async function validateCommand(_options) {
|
|
|
109
110
|
pushRow(bp ?? 'global', { rules: rulesEvaluated, warnings: warns.length, errors: errs.length });
|
|
110
111
|
const dur = globalThis.performance.now() - tStart;
|
|
111
112
|
perBpTimings.push({ bp: bp ?? 'global', ms: dur });
|
|
112
|
-
// Only print text output if not in JSON mode
|
|
113
|
+
// Only print text output if not in JSON mode. When --summary json is set,
|
|
114
|
+
// route the human lines to STDERR so stdout carries only the parseable JSON
|
|
115
|
+
// summary (TASK-035 B: per-issue lines previously polluted the summary).
|
|
113
116
|
if (outputFormat !== 'json') {
|
|
114
|
-
|
|
117
|
+
const line = summaryFmt === 'json' ? console.error : console.log;
|
|
118
|
+
line(`validate${bp ? ` [bp=${bp}]` : ''}: ${errs.length} error(s), ${warns.length} warning(s)${_options.perf ? ` (${dur.toFixed(2)}ms)` : ''}`);
|
|
115
119
|
for (const it of issues) {
|
|
116
120
|
const tag = it.level === 'error' ? 'ERROR' : 'WARN ';
|
|
117
|
-
|
|
121
|
+
line(`${tag} ${it.rule} ${it.id}${it.where ? ' @ ' + it.where : ''}${bp ? ` [${bp}]` : ''} — ${it.message}`);
|
|
118
122
|
}
|
|
119
123
|
}
|
|
120
124
|
}
|
package/cli/commands/validate.ts
CHANGED
|
@@ -11,8 +11,9 @@ import { printVersionBanner } from '../version-banner.js';
|
|
|
11
11
|
import { loadThemeTokens } from './utils.js';
|
|
12
12
|
|
|
13
13
|
export async function validateCommand(_options: ValidateOptions): Promise<void> {
|
|
14
|
-
// Show version banner (subtle, dimmed)
|
|
15
|
-
|
|
14
|
+
// Show version banner (subtle, dimmed). Stay quiet whenever stdout must be
|
|
15
|
+
// machine-parseable — JSON output OR a JSON summary (TASK-035 B).
|
|
16
|
+
printVersionBanner({ quiet: _options.format === 'json' || _options.summary === 'json' });
|
|
16
17
|
|
|
17
18
|
try {
|
|
18
19
|
const bps = parseBreakpoints(process.argv);
|
|
@@ -115,10 +116,13 @@ export async function validateCommand(_options: ValidateOptions): Promise<void>
|
|
|
115
116
|
const dur = globalThis.performance.now() - tStart;
|
|
116
117
|
perBpTimings.push({ bp: bp ?? 'global', ms: dur });
|
|
117
118
|
|
|
118
|
-
// Only print text output if not in JSON mode
|
|
119
|
+
// Only print text output if not in JSON mode. When --summary json is set,
|
|
120
|
+
// route the human lines to STDERR so stdout carries only the parseable JSON
|
|
121
|
+
// summary (TASK-035 B: per-issue lines previously polluted the summary).
|
|
119
122
|
if (outputFormat !== 'json') {
|
|
120
|
-
|
|
121
|
-
|
|
123
|
+
const line = summaryFmt === 'json' ? console.error : console.log;
|
|
124
|
+
line(`validate${bp ? ` [bp=${bp}]` : ''}: ${errs.length} error(s), ${warns.length} warning(s)${_options.perf ? ` (${dur.toFixed(2)}ms)` : ''}`);
|
|
125
|
+
for (const it of issues) { const tag = it.level === 'error' ? 'ERROR' : 'WARN '; line(`${tag} ${it.rule} ${it.id}${it.where ? ' @ ' + it.where : ''}${bp ? ` [${bp}]` : ''} — ${it.message}`); }
|
|
122
126
|
}
|
|
123
127
|
}
|
|
124
128
|
const totalMs = globalThis.performance.now() - tStartTotal;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"why.d.ts","sourceRoot":"","sources":["why.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"why.d.ts","sourceRoot":"","sources":["why.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAQ9C,wBAAsB,UAAU,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAyKnE"}
|
package/cli/commands/why.js
CHANGED
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { flattenTokens } from '../../core/flatten.js';
|
|
3
3
|
import { explain } from '../../core/why.js';
|
|
4
|
-
import {
|
|
4
|
+
import { loadThemeTokens } from './utils.js';
|
|
5
|
+
import { loadTokensWithBreakpoint, mergeTokens, parseBreakpoints } from '../../core/breakpoints.js';
|
|
5
6
|
import { Engine } from '../../core/engine.js';
|
|
6
7
|
import { loadConfig } from '../config.js';
|
|
7
8
|
import { setupConstraints } from '../constraint-registry.js';
|
|
8
9
|
export async function whyCommand(options) {
|
|
9
|
-
const tokensPath = options.tokens || 'tokens/tokens.json';
|
|
10
|
-
|
|
10
|
+
const tokensPath = options.tokens || 'tokens/tokens.example.json';
|
|
11
|
+
// Theme/breakpoint awareness (TASK-025): mirror validate's merge-then-flatten so
|
|
12
|
+
// `why` resolves the same values a themed/breakpoint validate would. Both loaders
|
|
13
|
+
// fail closed on a missing/malformed file.
|
|
14
|
+
const bp = (options.breakpoint ?? parseBreakpoints(process.argv)[0]);
|
|
15
|
+
let tokens = loadTokensWithBreakpoint(bp, tokensPath);
|
|
16
|
+
let themeFlat;
|
|
17
|
+
if (options.theme) {
|
|
18
|
+
const themeTokens = loadThemeTokens(options.theme);
|
|
19
|
+
tokens = mergeTokens(tokens, themeTokens);
|
|
20
|
+
themeFlat = flattenTokens(themeTokens).flat;
|
|
21
|
+
}
|
|
11
22
|
const { flat, edges } = flattenTokens(tokens);
|
|
12
23
|
const target = options.tokenId;
|
|
13
24
|
if (!flat[target]) {
|
|
@@ -45,6 +56,7 @@ export async function whyCommand(options) {
|
|
|
45
56
|
const overrides = safeLoad('tokens/overrides/local.json');
|
|
46
57
|
const baseReport = explain(target, flat, edges, {
|
|
47
58
|
overrides: overrides?.overrides ?? overrides,
|
|
59
|
+
theme: themeFlat,
|
|
48
60
|
});
|
|
49
61
|
// Best-effort constraint summary: which rules currently implicate this token
|
|
50
62
|
let constraintsSummary;
|
|
@@ -70,7 +82,7 @@ export async function whyCommand(options) {
|
|
|
70
82
|
const knownIds = new Set(Object.keys(init));
|
|
71
83
|
// Discover and attach all constraints via centralized registry.
|
|
72
84
|
// Honor --constraints-dir, matching `validate` (default: themes).
|
|
73
|
-
setupConstraints(engine, { config, constraintsDir: options['constraints-dir'] ?? 'themes' }, { knownIds });
|
|
85
|
+
setupConstraints(engine, { config, constraintsDir: options['constraints-dir'] ?? 'themes', bp }, { knownIds });
|
|
74
86
|
const candidates = new Set([target]);
|
|
75
87
|
const allIssues = engine.evaluate(candidates);
|
|
76
88
|
if (allIssues.length) {
|
package/cli/commands/why.ts
CHANGED
|
@@ -2,15 +2,26 @@ import { readFileSync } from 'node:fs';
|
|
|
2
2
|
import { flattenTokens, type FlatToken } from '../../core/flatten.js';
|
|
3
3
|
import { explain } from '../../core/why.js';
|
|
4
4
|
import type { WhyOptions } from '../types.js';
|
|
5
|
-
import {
|
|
5
|
+
import { loadThemeTokens } from './utils.js';
|
|
6
|
+
import { loadTokensWithBreakpoint, mergeTokens, parseBreakpoints, type Breakpoint } from '../../core/breakpoints.js';
|
|
6
7
|
import { Engine } from '../../core/engine.js';
|
|
7
8
|
import { loadConfig } from '../config.js';
|
|
8
9
|
import type { ConstraintIssue } from '../../core/engine.js';
|
|
9
10
|
import { setupConstraints } from '../constraint-registry.js';
|
|
10
11
|
|
|
11
12
|
export async function whyCommand(options: WhyOptions): Promise<void> {
|
|
12
|
-
const tokensPath = options.tokens || 'tokens/tokens.json';
|
|
13
|
-
|
|
13
|
+
const tokensPath = options.tokens || 'tokens/tokens.example.json';
|
|
14
|
+
// Theme/breakpoint awareness (TASK-025): mirror validate's merge-then-flatten so
|
|
15
|
+
// `why` resolves the same values a themed/breakpoint validate would. Both loaders
|
|
16
|
+
// fail closed on a missing/malformed file.
|
|
17
|
+
const bp = (options.breakpoint ?? parseBreakpoints(process.argv)[0]) as Breakpoint | undefined;
|
|
18
|
+
let tokens = loadTokensWithBreakpoint(bp, tokensPath);
|
|
19
|
+
let themeFlat: Record<string, FlatToken> | undefined;
|
|
20
|
+
if (options.theme) {
|
|
21
|
+
const themeTokens = loadThemeTokens(options.theme);
|
|
22
|
+
tokens = mergeTokens(tokens, themeTokens);
|
|
23
|
+
themeFlat = flattenTokens(themeTokens).flat as Record<string, FlatToken>;
|
|
24
|
+
}
|
|
14
25
|
const { flat, edges } = flattenTokens(tokens);
|
|
15
26
|
const target = options.tokenId;
|
|
16
27
|
|
|
@@ -50,6 +61,7 @@ export async function whyCommand(options: WhyOptions): Promise<void> {
|
|
|
50
61
|
|
|
51
62
|
const baseReport = explain(target, flat, edges, {
|
|
52
63
|
overrides: (overrides as any)?.overrides ?? overrides,
|
|
64
|
+
theme: themeFlat,
|
|
53
65
|
});
|
|
54
66
|
|
|
55
67
|
// Best-effort constraint summary: which rules currently implicate this token
|
|
@@ -87,7 +99,7 @@ export async function whyCommand(options: WhyOptions): Promise<void> {
|
|
|
87
99
|
// Honor --constraints-dir, matching `validate` (default: themes).
|
|
88
100
|
setupConstraints(
|
|
89
101
|
engine,
|
|
90
|
-
{ config, constraintsDir: options['constraints-dir'] ?? 'themes' },
|
|
102
|
+
{ config, constraintsDir: options['constraints-dir'] ?? 'themes', bp },
|
|
91
103
|
{ knownIds },
|
|
92
104
|
);
|
|
93
105
|
|
package/cli/dcv.js
CHANGED
|
@@ -4,6 +4,23 @@ import yargs from 'yargs/yargs';
|
|
|
4
4
|
import { hideBin } from 'yargs/helpers';
|
|
5
5
|
import { setCommand, buildCommand, validateCommand, graphCommand, whyCommand, patchCommand, patchApplyCommand } from './commands/index.js';
|
|
6
6
|
import { getVersionInfo } from './version-banner.js';
|
|
7
|
+
// Uniform handler wrapper (TASK-035 B): without this, a thrown IO/config error
|
|
8
|
+
// in a command (e.g. patch:apply on a missing file) escapes as an unhandled
|
|
9
|
+
// rejection — Node prints a raw stack trace and exits 1. Wrapping gives every
|
|
10
|
+
// command the clean "exit 2 + one-line message" contract that `validate` already
|
|
11
|
+
// has and docs/JSON-OUTPUT.md promises. Commands that call process.exit()
|
|
12
|
+
// themselves are unaffected (they terminate before the catch).
|
|
13
|
+
function run(fn) {
|
|
14
|
+
return async (a) => {
|
|
15
|
+
try {
|
|
16
|
+
await fn(a);
|
|
17
|
+
}
|
|
18
|
+
catch (e) {
|
|
19
|
+
console.error(`dcv: ${e instanceof Error ? e.message : String(e)}`);
|
|
20
|
+
process.exit(2);
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
7
24
|
const cli = yargs(hideBin(process.argv))
|
|
8
25
|
.scriptName('dcv')
|
|
9
26
|
// camel-case-expansion is intentionally OFF, so the CLI delivers flags only
|
|
@@ -27,7 +44,7 @@ cli.command('set [expressions..]', 'Set token values', y => y
|
|
|
27
44
|
.option('output', { type: 'string' })
|
|
28
45
|
.option('theme', { type: 'string' })
|
|
29
46
|
.option('debug-set', { type: 'boolean', hidden: true }) // hidden debug aid (also DCV_DEBUG_SET=1)
|
|
30
|
-
.option('tokens', { type: 'string', default: 'tokens/tokens.example.json' }),
|
|
47
|
+
.option('tokens', { type: 'string', default: 'tokens/tokens.example.json' }), run(setCommand));
|
|
31
48
|
cli.command('build', 'Build token outputs', y => y
|
|
32
49
|
.option('format', { type: 'string', choices: ['css', 'json', 'js'], default: 'css' })
|
|
33
50
|
.option('all-formats', { type: 'boolean', default: false })
|
|
@@ -35,7 +52,7 @@ cli.command('build', 'Build token outputs', y => y
|
|
|
35
52
|
.option('mapper', { type: 'string' })
|
|
36
53
|
.option('theme', { type: 'string' })
|
|
37
54
|
.option('dry-run', { type: 'boolean', default: false })
|
|
38
|
-
.option('tokens', { type: 'string', describe: 'Path to a tokens file (defaults to tokens/tokens.example.json)' }),
|
|
55
|
+
.option('tokens', { type: 'string', describe: 'Path to a tokens file (defaults to tokens/tokens.example.json)' }), run(buildCommand));
|
|
39
56
|
cli.command('validate [tokens-path]', 'Validate constraints', y => y
|
|
40
57
|
.positional('tokens-path', { type: 'string', describe: 'Path to a tokens file (positional alias for --tokens)' })
|
|
41
58
|
.option('constraints-dir', { type: 'string', describe: 'Directory holding order / cross-axis constraint files (default: themes)' })
|
|
@@ -46,12 +63,12 @@ cli.command('validate [tokens-path]', 'Validate constraints', y => y
|
|
|
46
63
|
.option('receipt', { type: 'string', describe: 'Generate validation receipt with audit trail' })
|
|
47
64
|
.option('tokens', { type: 'string', describe: 'Path to a tokens file (defaults to tokens/tokens.example.json)' })
|
|
48
65
|
.option('theme', { type: 'string', describe: 'Apply named theme tokens before validation' })
|
|
49
|
-
.option('breakpoint', { type: 'string' })
|
|
66
|
+
.option('breakpoint', { type: 'string', choices: ['sm', 'md', 'lg'] })
|
|
50
67
|
.option('all-breakpoints', { type: 'boolean' })
|
|
51
68
|
.option('cross-axis-debug', { type: 'boolean', hidden: true }) // hidden debug aid
|
|
52
69
|
.option('perf', { type: 'boolean', describe: 'Print timing info' })
|
|
53
70
|
.option('budget-total-ms', { type: 'number', describe: 'Fail if total validation exceeds this (ms)' })
|
|
54
|
-
.option('budget-per-bp-ms', { type: 'number', describe: 'Fail if any single breakpoint exceeds this (ms)' }),
|
|
71
|
+
.option('budget-per-bp-ms', { type: 'number', describe: 'Fail if any single breakpoint exceeds this (ms)' }), run(validateCommand));
|
|
55
72
|
cli.command('graph', 'Generate dependency / constraint graph', y => y
|
|
56
73
|
.option('format', { type: 'string', choices: ['json', 'mermaid', 'dot', 'svg', 'png'], default: 'json' })
|
|
57
74
|
.option('bundle', { type: 'boolean', describe: 'When used with --hasse export mermaid+dot (+image if svg/png requested)' })
|
|
@@ -71,22 +88,25 @@ cli.command('graph', 'Generate dependency / constraint graph', y => y
|
|
|
71
88
|
.option('image-from', { type: 'string', choices: ['mermaid', 'dot'], describe: 'Source format for svg/png export (default: mermaid)' })
|
|
72
89
|
.option('focus', { type: 'string' })
|
|
73
90
|
.option('radius', { type: 'number', default: 1 })
|
|
74
|
-
.option('
|
|
91
|
+
.option('theme', { type: 'string', describe: 'Apply named theme tokens before computing --hasse violation overlays (dependency graph is unaffected)' })
|
|
92
|
+
.option('tokens', { type: 'string', describe: 'Path to a tokens file (defaults to tokens/tokens.example.json)' }), run(graphCommand));
|
|
75
93
|
cli.command('why <tokenId>', 'Explain token provenance', y => y
|
|
76
94
|
.positional('tokenId', { type: 'string', demandOption: true })
|
|
77
95
|
.option('format', { type: 'string', choices: ['json', 'table'], default: 'json' })
|
|
78
96
|
.option('constraints-dir', { type: 'string', describe: 'Directory holding order / cross-axis constraint files for the constraint summary (default: themes)' })
|
|
79
|
-
.option('
|
|
97
|
+
.option('theme', { type: 'string', describe: 'Apply named theme tokens (tokens/themes/<name>.json) before resolving values' })
|
|
98
|
+
.option('breakpoint', { type: 'string', choices: ['sm', 'md', 'lg'], describe: 'Resolve values for a breakpoint override' })
|
|
99
|
+
.option('tokens', { type: 'string', default: 'tokens/tokens.example.json' }), run(whyCommand));
|
|
80
100
|
cli.command('patch', 'Export patch (diff) from overrides', y => y
|
|
81
101
|
.option('overrides', { type: 'string', describe: 'Path or inline JSON of flat overrides' })
|
|
82
102
|
.option('format', { type: 'string', choices: ['json', 'css', 'js'], default: 'json' })
|
|
83
103
|
.option('output', { type: 'string' })
|
|
84
|
-
.option('tokens', { type: 'string', default: 'tokens/tokens.example.json' }),
|
|
104
|
+
.option('tokens', { type: 'string', default: 'tokens/tokens.example.json' }), run(patchCommand));
|
|
85
105
|
cli.command('patch:apply <patch>', 'Apply patch document to tokens', y => y
|
|
86
106
|
.positional('patch', { type: 'string', demandOption: true })
|
|
87
107
|
.option('tokens', { type: 'string', default: 'tokens/tokens.example.json' })
|
|
88
108
|
.option('output', { type: 'string', describe: 'Write updated tokens to this file' })
|
|
89
|
-
.option('dry-run', { type: 'boolean', default: false }),
|
|
109
|
+
.option('dry-run', { type: 'boolean', default: false }), run(patchApplyCommand));
|
|
90
110
|
// Wire `--version` to the real package version (same source as the banner).
|
|
91
111
|
// Without this, yargs can't locate package.json from the installed bin and
|
|
92
112
|
// prints "unknown" (TASK-021 release smoke-test finding).
|
package/cli/dcv.ts
CHANGED
|
@@ -6,6 +6,23 @@ import { type SetOptions, type BuildOptions, type ValidateOptions, type GraphOpt
|
|
|
6
6
|
import { setCommand, buildCommand, validateCommand, graphCommand, whyCommand, patchCommand, patchApplyCommand } from './commands/index.js';
|
|
7
7
|
import { getVersionInfo } from './version-banner.js';
|
|
8
8
|
|
|
9
|
+
// Uniform handler wrapper (TASK-035 B): without this, a thrown IO/config error
|
|
10
|
+
// in a command (e.g. patch:apply on a missing file) escapes as an unhandled
|
|
11
|
+
// rejection — Node prints a raw stack trace and exits 1. Wrapping gives every
|
|
12
|
+
// command the clean "exit 2 + one-line message" contract that `validate` already
|
|
13
|
+
// has and docs/JSON-OUTPUT.md promises. Commands that call process.exit()
|
|
14
|
+
// themselves are unaffected (they terminate before the catch).
|
|
15
|
+
function run<T>(fn: (a: T) => void | Promise<void>): (a: T) => Promise<void> {
|
|
16
|
+
return async (a: T) => {
|
|
17
|
+
try {
|
|
18
|
+
await fn(a);
|
|
19
|
+
} catch (e) {
|
|
20
|
+
console.error(`dcv: ${e instanceof Error ? e.message : String(e)}`);
|
|
21
|
+
process.exit(2);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
9
26
|
const cli = yargs(hideBin(process.argv))
|
|
10
27
|
.scriptName('dcv')
|
|
11
28
|
// camel-case-expansion is intentionally OFF, so the CLI delivers flags only
|
|
@@ -31,7 +48,7 @@ cli.command<SetOptions>('set [expressions..]', 'Set token values', y => y
|
|
|
31
48
|
.option('theme', { type: 'string' })
|
|
32
49
|
.option('debug-set', { type: 'boolean', hidden: true }) // hidden debug aid (also DCV_DEBUG_SET=1)
|
|
33
50
|
.option('tokens', { type: 'string', default: 'tokens/tokens.example.json' }),
|
|
34
|
-
|
|
51
|
+
run(setCommand)
|
|
35
52
|
);
|
|
36
53
|
|
|
37
54
|
cli.command<BuildOptions>('build', 'Build token outputs', y => y
|
|
@@ -42,7 +59,7 @@ cli.command<BuildOptions>('build', 'Build token outputs', y => y
|
|
|
42
59
|
.option('theme', { type: 'string' })
|
|
43
60
|
.option('dry-run', { type: 'boolean', default: false })
|
|
44
61
|
.option('tokens', { type: 'string', describe: 'Path to a tokens file (defaults to tokens/tokens.example.json)' }),
|
|
45
|
-
|
|
62
|
+
run(buildCommand)
|
|
46
63
|
);
|
|
47
64
|
|
|
48
65
|
cli.command<ValidateOptions>('validate [tokens-path]', 'Validate constraints', y => y
|
|
@@ -55,13 +72,13 @@ cli.command<ValidateOptions>('validate [tokens-path]', 'Validate constraints', y
|
|
|
55
72
|
.option('receipt', { type: 'string', describe: 'Generate validation receipt with audit trail' })
|
|
56
73
|
.option('tokens', { type: 'string', describe: 'Path to a tokens file (defaults to tokens/tokens.example.json)' })
|
|
57
74
|
.option('theme', { type: 'string', describe: 'Apply named theme tokens before validation' })
|
|
58
|
-
.option('breakpoint', { type: 'string' })
|
|
75
|
+
.option('breakpoint', { type: 'string', choices: ['sm','md','lg'] })
|
|
59
76
|
.option('all-breakpoints', { type: 'boolean' })
|
|
60
77
|
.option('cross-axis-debug', { type: 'boolean', hidden: true }) // hidden debug aid
|
|
61
78
|
.option('perf', { type: 'boolean', describe: 'Print timing info' })
|
|
62
79
|
.option('budget-total-ms', { type: 'number', describe: 'Fail if total validation exceeds this (ms)' })
|
|
63
80
|
.option('budget-per-bp-ms', { type: 'number', describe: 'Fail if any single breakpoint exceeds this (ms)' }),
|
|
64
|
-
|
|
81
|
+
run(validateCommand)
|
|
65
82
|
);
|
|
66
83
|
|
|
67
84
|
cli.command<GraphOptions>('graph', 'Generate dependency / constraint graph', y => y
|
|
@@ -83,16 +100,19 @@ cli.command<GraphOptions>('graph', 'Generate dependency / constraint graph', y =
|
|
|
83
100
|
.option('image-from', { type: 'string', choices: ['mermaid','dot'], describe: 'Source format for svg/png export (default: mermaid)' })
|
|
84
101
|
.option('focus', { type: 'string' })
|
|
85
102
|
.option('radius', { type: 'number', default: 1 })
|
|
103
|
+
.option('theme', { type: 'string', describe: 'Apply named theme tokens before computing --hasse violation overlays (dependency graph is unaffected)' })
|
|
86
104
|
.option('tokens', { type: 'string', describe: 'Path to a tokens file (defaults to tokens/tokens.example.json)' }),
|
|
87
|
-
|
|
105
|
+
run(graphCommand)
|
|
88
106
|
);
|
|
89
107
|
|
|
90
108
|
cli.command<WhyOptions>('why <tokenId>', 'Explain token provenance', y => y
|
|
91
109
|
.positional('tokenId', { type: 'string', demandOption: true })
|
|
92
110
|
.option('format', { type: 'string', choices: ['json','table'], default: 'json' })
|
|
93
111
|
.option('constraints-dir', { type: 'string', describe: 'Directory holding order / cross-axis constraint files for the constraint summary (default: themes)' })
|
|
112
|
+
.option('theme', { type: 'string', describe: 'Apply named theme tokens (tokens/themes/<name>.json) before resolving values' })
|
|
113
|
+
.option('breakpoint', { type: 'string', choices: ['sm','md','lg'], describe: 'Resolve values for a breakpoint override' })
|
|
94
114
|
.option('tokens', { type: 'string', default: 'tokens/tokens.example.json' }),
|
|
95
|
-
|
|
115
|
+
run(whyCommand)
|
|
96
116
|
);
|
|
97
117
|
|
|
98
118
|
cli.command<PatchOptions>('patch', 'Export patch (diff) from overrides', y => y
|
|
@@ -100,7 +120,7 @@ cli.command<PatchOptions>('patch', 'Export patch (diff) from overrides', y => y
|
|
|
100
120
|
.option('format', { type: 'string', choices: ['json','css','js'], default: 'json' })
|
|
101
121
|
.option('output', { type: 'string' })
|
|
102
122
|
.option('tokens', { type: 'string', default: 'tokens/tokens.example.json' }),
|
|
103
|
-
|
|
123
|
+
run(patchCommand)
|
|
104
124
|
);
|
|
105
125
|
|
|
106
126
|
cli.command<PatchApplyOptions>('patch:apply <patch>', 'Apply patch document to tokens', y => y
|
|
@@ -108,7 +128,7 @@ cli.command<PatchApplyOptions>('patch:apply <patch>', 'Apply patch document to t
|
|
|
108
128
|
.option('tokens', { type: 'string', default: 'tokens/tokens.example.json' })
|
|
109
129
|
.option('output', { type: 'string', describe: 'Write updated tokens to this file' })
|
|
110
130
|
.option('dry-run', { type: 'boolean', default: false }),
|
|
111
|
-
|
|
131
|
+
run(patchApplyCommand)
|
|
112
132
|
);
|
|
113
133
|
|
|
114
134
|
// Wire `--version` to the real package version (same source as the banner).
|
package/cli/types.d.ts
CHANGED
|
@@ -81,11 +81,15 @@ export interface GraphOptions extends GlobalOptions {
|
|
|
81
81
|
focus?: string;
|
|
82
82
|
radius?: number;
|
|
83
83
|
tokens?: string;
|
|
84
|
+
theme?: string;
|
|
85
|
+
breakpoint?: 'sm' | 'md' | 'lg';
|
|
84
86
|
}
|
|
85
87
|
export interface WhyOptions extends GlobalOptions {
|
|
86
88
|
tokenId: string;
|
|
87
89
|
'constraints-dir'?: string;
|
|
88
90
|
format?: 'json' | 'table';
|
|
91
|
+
theme?: string;
|
|
92
|
+
breakpoint?: 'sm' | 'md' | 'lg';
|
|
89
93
|
}
|
|
90
94
|
export interface PatchOptions extends GlobalOptions {
|
|
91
95
|
overrides?: string;
|
package/cli/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["types.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["types.ts"],"names":[],"mappings":"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"}
|
package/cli/types.ts
CHANGED
|
@@ -77,11 +77,15 @@ export interface GraphOptions extends GlobalOptions {
|
|
|
77
77
|
focus?: string;
|
|
78
78
|
radius?: number;
|
|
79
79
|
tokens?: string;
|
|
80
|
+
theme?: string;
|
|
81
|
+
breakpoint?: 'sm' | 'md' | 'lg';
|
|
80
82
|
}
|
|
81
83
|
export interface WhyOptions extends GlobalOptions {
|
|
82
84
|
tokenId: string;
|
|
83
85
|
'constraints-dir'?: string;
|
|
84
86
|
format?: 'json' | 'table';
|
|
87
|
+
theme?: string;
|
|
88
|
+
breakpoint?: 'sm' | 'md' | 'lg';
|
|
85
89
|
}
|
|
86
90
|
export interface PatchOptions extends GlobalOptions {
|
|
87
91
|
overrides?: string; // path to flat overrides json or inline json
|
package/core/breakpoints.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { TokenNode } from "./flatten.js";
|
|
2
2
|
export type Breakpoint = "sm" | "md" | "lg";
|
|
3
|
+
export declare const BREAKPOINTS: readonly Breakpoint[];
|
|
3
4
|
export declare function parseBreakpoints(argv: string[]): Breakpoint[];
|
|
4
5
|
export declare function loadJsonSafe<T = unknown>(path: string): T | null;
|
|
5
6
|
export declare function loadOrders(axis: string, bp?: Breakpoint): [string, "<=" | ">=", string][];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"breakpoints.d.ts","sourceRoot":"","sources":["breakpoints.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9C,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAE5C,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,EAAE,
|
|
1
|
+
{"version":3,"file":"breakpoints.d.ts","sourceRoot":"","sources":["breakpoints.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAE9C,MAAM,MAAM,UAAU,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAE5C,eAAO,MAAM,WAAW,EAAE,SAAS,UAAU,EAAuB,CAAC;AAErE,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,UAAU,EAAE,CAe7D;AAED,wBAAgB,YAAY,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,GAAG,IAAI,CAOhE;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,UAAU,GAAG,CAAC,MAAM,EAAE,IAAI,GAAG,IAAI,EAAE,MAAM,CAAC,EAAE,CAKzF;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,GAAG,SAAS,CAQtE;AAED,gFAAgF;AAChF;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CAAC,EAAE,CAAC,EAAE,UAAU,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,SAAS,CAkBxF"}
|
package/core/breakpoints.js
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
// core/breakpoints.ts
|
|
2
2
|
import fs from "node:fs";
|
|
3
|
+
export const BREAKPOINTS = ["sm", "md", "lg"];
|
|
3
4
|
export function parseBreakpoints(argv) {
|
|
4
5
|
const allIdx = argv.indexOf("--all-breakpoints");
|
|
5
6
|
if (allIdx >= 0)
|
|
6
7
|
return ["sm", "md", "lg"];
|
|
7
8
|
const bpIdx = argv.indexOf("--breakpoint");
|
|
8
|
-
if (bpIdx >= 0)
|
|
9
|
-
|
|
9
|
+
if (bpIdx >= 0) {
|
|
10
|
+
// Validate the value (TASK-035 B): an unknown/missing breakpoint must not be
|
|
11
|
+
// accepted — it would load a non-existent override and validate the BASE
|
|
12
|
+
// tokens as if the breakpoint applied (a confident false green on a typo).
|
|
13
|
+
const val = argv[bpIdx + 1];
|
|
14
|
+
if (!val || !BREAKPOINTS.includes(val)) {
|
|
15
|
+
throw new Error(`Invalid --breakpoint "${val ?? ""}". Expected one of: ${BREAKPOINTS.join(", ")}.`);
|
|
16
|
+
}
|
|
17
|
+
return [val];
|
|
18
|
+
}
|
|
10
19
|
return []; // no BP slicing requested
|
|
11
20
|
}
|
|
12
21
|
export function loadJsonSafe(path) {
|
package/core/breakpoints.ts
CHANGED
|
@@ -4,11 +4,22 @@ import type { TokenNode } from "./flatten.js";
|
|
|
4
4
|
|
|
5
5
|
export type Breakpoint = "sm" | "md" | "lg";
|
|
6
6
|
|
|
7
|
+
export const BREAKPOINTS: readonly Breakpoint[] = ["sm", "md", "lg"];
|
|
8
|
+
|
|
7
9
|
export function parseBreakpoints(argv: string[]): Breakpoint[] {
|
|
8
10
|
const allIdx = argv.indexOf("--all-breakpoints");
|
|
9
11
|
if (allIdx >= 0) return ["sm", "md", "lg"];
|
|
10
12
|
const bpIdx = argv.indexOf("--breakpoint");
|
|
11
|
-
if (bpIdx >= 0)
|
|
13
|
+
if (bpIdx >= 0) {
|
|
14
|
+
// Validate the value (TASK-035 B): an unknown/missing breakpoint must not be
|
|
15
|
+
// accepted — it would load a non-existent override and validate the BASE
|
|
16
|
+
// tokens as if the breakpoint applied (a confident false green on a typo).
|
|
17
|
+
const val = argv[bpIdx + 1];
|
|
18
|
+
if (!val || !BREAKPOINTS.includes(val as Breakpoint)) {
|
|
19
|
+
throw new Error(`Invalid --breakpoint "${val ?? ""}". Expected one of: ${BREAKPOINTS.join(", ")}.`);
|
|
20
|
+
}
|
|
21
|
+
return [val as Breakpoint];
|
|
22
|
+
}
|
|
12
23
|
return []; // no BP slicing requested
|
|
13
24
|
}
|
|
14
25
|
|
package/core/dtcg.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dtcg.d.ts","sourceRoot":"","sources":["dtcg.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,MAAM,MAAM,cAAc,GAAG;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;
|
|
1
|
+
{"version":3,"file":"dtcg.d.ts","sourceRoot":"","sources":["dtcg.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,MAAM,MAAM,cAAc,GAAG;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AA2DF;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAU/E"}
|
package/core/dtcg.js
CHANGED
|
@@ -64,7 +64,13 @@ function isColorObject(obj, type) {
|
|
|
64
64
|
'hex' in obj);
|
|
65
65
|
}
|
|
66
66
|
function isDimensionObject(obj, type) {
|
|
67
|
-
|
|
67
|
+
if ((type ?? '').toLowerCase() === 'dimension')
|
|
68
|
+
return true;
|
|
69
|
+
// A bare { value: <number> } (with or without `unit`) is a dimension even when
|
|
70
|
+
// $type is absent — normalizeDimension defaults the unit to px. Previously this
|
|
71
|
+
// required `unit` too, so a unit-less dimension became an <unsupported> sentinel
|
|
72
|
+
// (TASK-035 E). isColorObject runs first, so a numeric `value` here is a length.
|
|
73
|
+
return 'value' in obj && typeof obj.value === 'number';
|
|
68
74
|
}
|
|
69
75
|
/**
|
|
70
76
|
* Normalize a raw DTCG `$value` to the string/number form the engine expects.
|
package/core/dtcg.ts
CHANGED
|
@@ -80,7 +80,12 @@ function isColorObject(obj: Record<string, unknown>, type?: string): boolean {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
function isDimensionObject(obj: Record<string, unknown>, type?: string): boolean {
|
|
83
|
-
|
|
83
|
+
if ((type ?? '').toLowerCase() === 'dimension') return true;
|
|
84
|
+
// A bare { value: <number> } (with or without `unit`) is a dimension even when
|
|
85
|
+
// $type is absent — normalizeDimension defaults the unit to px. Previously this
|
|
86
|
+
// required `unit` too, so a unit-less dimension became an <unsupported> sentinel
|
|
87
|
+
// (TASK-035 E). isColorObject runs first, so a numeric `value` here is a length.
|
|
88
|
+
return 'value' in obj && typeof obj.value === 'number';
|
|
84
89
|
}
|
|
85
90
|
|
|
86
91
|
/**
|
package/core/patch.d.ts
CHANGED
|
@@ -23,6 +23,13 @@ export interface BuildPatchOptions {
|
|
|
23
23
|
breakpoint?: string;
|
|
24
24
|
includeUnchanged?: boolean;
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Canonical hash of a token set's flattened id→value map. Shared by buildPatch
|
|
28
|
+
* (to stamp baseTokensHash) and patch:apply (to detect drift). They MUST use the
|
|
29
|
+
* exact same serialization — previously patch:apply used a different
|
|
30
|
+
* JSON.stringify form, so the drift warning fired on every apply (TASK-035 D).
|
|
31
|
+
*/
|
|
32
|
+
export declare function computeBaseTokensHash(tokens: TokenNode): string;
|
|
26
33
|
export declare function applyFlatOverrides(tokens: TokenNode, overrides?: Record<string, any>): void;
|
|
27
34
|
export declare function buildPatch(opts: BuildPatchOptions): PatchDocument;
|
|
28
35
|
//# sourceMappingURL=patch.d.ts.map
|
package/core/patch.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"patch.d.ts","sourceRoot":"","sources":["patch.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAG1D,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,CAAC;IACpC,EAAE,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,CAAC;IAClC,IAAI,EAAE,QAAQ,GAAG,KAAK,GAAG,QAAQ,CAAC;CACnC;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,CAAC,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IACrD,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAC5B;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,SAAS,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAMD,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,EAAE,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAuB3F;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,iBAAiB,GAAG,aAAa,
|
|
1
|
+
{"version":3,"file":"patch.d.ts","sourceRoot":"","sources":["patch.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAG1D,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,CAAC;IACpC,EAAE,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,CAAC;IAClC,IAAI,EAAE,QAAQ,GAAG,KAAK,GAAG,QAAQ,CAAC;CACnC;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,CAAC,CAAC;IACX,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IACrD,IAAI,EAAE,MAAM,CAAC;IACb,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAC5B;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,SAAS,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAChC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAMD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,GAAG,MAAM,CAK/D;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,EAAE,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,CAuB3F;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,iBAAiB,GAAG,aAAa,CAwEjE"}
|
package/core/patch.js
CHANGED
|
@@ -3,6 +3,18 @@ import { flattenTokens } from './flatten.js';
|
|
|
3
3
|
function canonicalString(obj) {
|
|
4
4
|
return JSON.stringify(obj, Object.keys(obj).sort(), 2);
|
|
5
5
|
}
|
|
6
|
+
/**
|
|
7
|
+
* Canonical hash of a token set's flattened id→value map. Shared by buildPatch
|
|
8
|
+
* (to stamp baseTokensHash) and patch:apply (to detect drift). They MUST use the
|
|
9
|
+
* exact same serialization — previously patch:apply used a different
|
|
10
|
+
* JSON.stringify form, so the drift warning fired on every apply (TASK-035 D).
|
|
11
|
+
*/
|
|
12
|
+
export function computeBaseTokensHash(tokens) {
|
|
13
|
+
const flat = flattenTokens(JSON.parse(JSON.stringify(tokens))).flat;
|
|
14
|
+
const values = {};
|
|
15
|
+
Object.keys(flat).sort().forEach((id) => { values[id] = flat[id]?.value; });
|
|
16
|
+
return createHash('sha256').update(canonicalString(values)).digest('hex');
|
|
17
|
+
}
|
|
6
18
|
export function applyFlatOverrides(tokens, overrides) {
|
|
7
19
|
if (!overrides)
|
|
8
20
|
return;
|
|
@@ -36,10 +48,9 @@ export function buildPatch(opts) {
|
|
|
36
48
|
const cloned = JSON.parse(JSON.stringify(opts.tokens));
|
|
37
49
|
// Flatten original
|
|
38
50
|
const baseFlat = flattenTokens(cloned).flat;
|
|
39
|
-
// Canonical base tokens hash (id -> value) for drift detection when applying
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const baseTokensHash = createHash('sha256').update(canonicalString(baseFlatValues)).digest('hex');
|
|
51
|
+
// Canonical base tokens hash (id -> value) for drift detection when applying
|
|
52
|
+
// the patch later — uses the SHARED hash so patch:apply agrees (TASK-035 D).
|
|
53
|
+
const baseTokensHash = computeBaseTokensHash(opts.tokens);
|
|
43
54
|
// Apply overrides on a fresh clone for diffing
|
|
44
55
|
const modified = JSON.parse(JSON.stringify(opts.tokens));
|
|
45
56
|
const removalIds = new Set();
|
package/core/patch.ts
CHANGED
|
@@ -33,6 +33,19 @@ function canonicalString(obj: any): string {
|
|
|
33
33
|
return JSON.stringify(obj, Object.keys(obj).sort(), 2);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Canonical hash of a token set's flattened id→value map. Shared by buildPatch
|
|
38
|
+
* (to stamp baseTokensHash) and patch:apply (to detect drift). They MUST use the
|
|
39
|
+
* exact same serialization — previously patch:apply used a different
|
|
40
|
+
* JSON.stringify form, so the drift warning fired on every apply (TASK-035 D).
|
|
41
|
+
*/
|
|
42
|
+
export function computeBaseTokensHash(tokens: TokenNode): string {
|
|
43
|
+
const flat = flattenTokens(JSON.parse(JSON.stringify(tokens))).flat as Record<string, any>;
|
|
44
|
+
const values: Record<string, any> = {};
|
|
45
|
+
Object.keys(flat).sort().forEach((id) => { values[id] = flat[id]?.value; });
|
|
46
|
+
return createHash('sha256').update(canonicalString(values)).digest('hex');
|
|
47
|
+
}
|
|
48
|
+
|
|
36
49
|
export function applyFlatOverrides(tokens: TokenNode, overrides?: Record<string, any>): void {
|
|
37
50
|
if (!overrides) return;
|
|
38
51
|
for (const [id, val] of Object.entries(overrides)) {
|
|
@@ -62,10 +75,9 @@ export function buildPatch(opts: BuildPatchOptions): PatchDocument {
|
|
|
62
75
|
const cloned = JSON.parse(JSON.stringify(opts.tokens));
|
|
63
76
|
// Flatten original
|
|
64
77
|
const baseFlat = flattenTokens(cloned as any).flat as Record<string, any>;
|
|
65
|
-
// Canonical base tokens hash (id -> value) for drift detection when applying
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const baseTokensHash = createHash('sha256').update(canonicalString(baseFlatValues)).digest('hex');
|
|
78
|
+
// Canonical base tokens hash (id -> value) for drift detection when applying
|
|
79
|
+
// the patch later — uses the SHARED hash so patch:apply agrees (TASK-035 D).
|
|
80
|
+
const baseTokensHash = computeBaseTokensHash(opts.tokens);
|
|
69
81
|
// Apply overrides on a fresh clone for diffing
|
|
70
82
|
const modified = JSON.parse(JSON.stringify(opts.tokens));
|
|
71
83
|
const removalIds = new Set<string>();
|
package/package.json
CHANGED
package/server.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "io.github.CseperkePapp/design-constraint-validator",
|
|
4
4
|
"title": "Design Constraint Validator",
|
|
5
5
|
"description": "Validate design tokens for accessibility, scales, and design-system constraint consistency.",
|
|
6
|
-
"version": "2.
|
|
6
|
+
"version": "2.3.0",
|
|
7
7
|
"repository": {
|
|
8
8
|
"url": "https://github.com/CseperkePapp/design-constraint-validator",
|
|
9
9
|
"source": "github"
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "design-constraint-validator",
|
|
15
|
-
"version": "2.
|
|
15
|
+
"version": "2.3.0",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
}
|