design-constraint-validator 1.0.0 → 2.0.1
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/LICENSE +21 -21
- package/README.md +229 -659
- package/adapters/README.md +46 -46
- package/adapters/css.ts +116 -116
- package/adapters/decisionthemes.d.ts +44 -0
- package/adapters/decisionthemes.d.ts.map +1 -0
- package/adapters/decisionthemes.js +35 -0
- package/adapters/decisionthemes.ts +59 -0
- package/adapters/js.ts +14 -14
- package/adapters/json.ts +45 -45
- package/cli/build-css.ts +32 -32
- package/cli/commands/build.ts +65 -65
- package/cli/commands/graph.d.ts.map +1 -1
- package/cli/commands/graph.js +26 -10
- package/cli/commands/graph.ts +180 -137
- package/cli/commands/index.ts +7 -7
- package/cli/commands/patch-apply.ts +80 -80
- package/cli/commands/patch.ts +22 -22
- package/cli/commands/set.d.ts.map +1 -1
- package/cli/commands/set.js +12 -4
- package/cli/commands/set.ts +239 -225
- package/cli/commands/utils.ts +50 -50
- package/cli/commands/validate.d.ts.map +1 -1
- package/cli/commands/validate.js +89 -33
- package/cli/commands/validate.ts +180 -115
- package/cli/commands/why.d.ts.map +1 -1
- package/cli/commands/why.js +86 -20
- package/cli/commands/why.ts +158 -46
- package/cli/config-schema.ts +27 -27
- package/cli/config.ts +35 -35
- package/cli/constraint-registry.d.ts +101 -0
- package/cli/constraint-registry.d.ts.map +1 -0
- package/cli/constraint-registry.js +225 -0
- package/cli/constraint-registry.ts +304 -0
- package/cli/constraints-loader.d.ts.map +1 -0
- package/cli/cross-axis-loader.d.ts +91 -0
- package/cli/cross-axis-loader.d.ts.map +1 -0
- package/cli/cross-axis-loader.js +222 -0
- package/cli/cross-axis-loader.ts +289 -0
- package/cli/dcv.js +4 -0
- package/cli/dcv.ts +111 -107
- package/cli/engine-helpers.d.ts.map +1 -1
- package/cli/graph-poset.ts +74 -74
- package/cli/json-output.d.ts +69 -0
- package/cli/json-output.d.ts.map +1 -0
- package/cli/json-output.js +109 -0
- package/cli/json-output.ts +184 -0
- package/cli/result.ts +27 -27
- package/cli/run.ts +54 -54
- package/cli/smoke-test.ts +40 -40
- package/cli/types.d.ts +6 -0
- package/cli/types.d.ts.map +1 -1
- package/cli/types.ts +84 -78
- package/cli/version-banner.d.ts +20 -0
- package/cli/version-banner.d.ts.map +1 -0
- package/cli/version-banner.js +49 -0
- package/cli/version-banner.ts +61 -0
- package/core/breakpoints.ts +50 -50
- package/core/cli-format.ts +31 -31
- package/core/color.ts +148 -148
- package/core/constraints/cross-axis.ts +114 -114
- package/core/constraints/monotonic-lightness.ts +38 -38
- package/core/constraints/monotonic.ts +74 -74
- package/core/constraints/threshold.ts +43 -43
- package/core/constraints/wcag.ts +70 -70
- package/core/cross-axis-config.d.ts.map +1 -1
- package/core/engine.d.ts +95 -0
- package/core/engine.d.ts.map +1 -1
- package/core/engine.js +22 -0
- package/core/engine.ts +167 -65
- package/core/flatten.ts +116 -116
- package/core/image-export.ts +48 -48
- package/core/index.d.ts +9 -30
- package/core/index.d.ts.map +1 -1
- package/core/index.js +7 -54
- package/core/index.ts +10 -72
- package/core/patch.ts +134 -134
- package/core/poset.ts +311 -311
- package/core/why.ts +63 -63
- package/package.json +96 -90
- package/themes/color.lg.order.json +15 -15
- package/themes/color.md.order.json +15 -15
- package/themes/color.order.json +15 -15
- package/themes/color.sm.order.json +15 -15
- package/themes/cross-axis.rules.json +35 -35
- package/themes/cross-axis.sm.rules.json +12 -12
- package/themes/layout.lg.order.json +18 -18
- package/themes/layout.md.order.json +18 -18
- package/themes/layout.order.json +18 -18
- package/themes/layout.sm.order.json +18 -18
- package/themes/spacing.order.json +14 -14
- package/themes/typography.lg.order.json +15 -15
- package/themes/typography.md.order.json +15 -15
- package/themes/typography.order.json +15 -15
- package/themes/typography.sm.order.json +15 -15
- package/cli/engine-helpers.d.ts +0 -8
- package/cli/engine-helpers.js +0 -70
- package/cli/engine-helpers.ts +0 -61
- package/core/cross-axis-config.d.ts +0 -5
- package/core/cross-axis-config.js +0 -144
- package/core/cross-axis-config.ts +0 -152
- package/dist/test-overrides-removal.json +0 -4
- package/dist/tmp.patch.json +0 -35
- package/tokens/overrides/base.json +0 -22
- package/tokens/overrides/lg.json +0 -20
- package/tokens/overrides/md.json +0 -16
- package/tokens/overrides/sm.json +0 -16
- package/tokens/overrides/viol.color.json +0 -6
- package/tokens/overrides/viol.typography.json +0 -6
- package/tokens/tokens.demo-violations.json +0 -116
- package/tokens/tokens.example.json +0 -128
- package/tokens/tokens.json +0 -67
- package/tokens/tokens.multi-violations.json +0 -21
- package/tokens/tokens.schema.d.ts +0 -2298
- package/tokens/tokens.schema.d.ts.map +0 -1
- package/tokens/tokens.schema.js +0 -148
- package/tokens/tokens.schema.ts +0 -196
- package/tokens/tokens.test.json +0 -38
- package/tokens/tokens.touch-violation.json +0 -8
- package/tokens/typography.classes.css +0 -11
- package/tokens/typography.css +0 -20
package/core/index.js
CHANGED
|
@@ -1,54 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
for (const [from, to] of edges) {
|
|
9
|
-
if (!this.graph.has(from))
|
|
10
|
-
this.graph.set(from, new Set());
|
|
11
|
-
this.graph.get(from).add(to);
|
|
12
|
-
if (!this.graph.has(to))
|
|
13
|
-
this.graph.set(to, new Set());
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
set(id, value) { this.values.set(id, value); }
|
|
17
|
-
get(id) { return this.values.get(id); }
|
|
18
|
-
use(plugin) {
|
|
19
|
-
this.plugins.push(plugin);
|
|
20
|
-
return this;
|
|
21
|
-
}
|
|
22
|
-
affected(start) {
|
|
23
|
-
const seen = new Set();
|
|
24
|
-
const stack = [start];
|
|
25
|
-
while (stack.length) {
|
|
26
|
-
const n = stack.pop();
|
|
27
|
-
if (seen.has(n))
|
|
28
|
-
continue;
|
|
29
|
-
seen.add(n);
|
|
30
|
-
for (const d of this.graph.get(n) ?? [])
|
|
31
|
-
stack.push(d);
|
|
32
|
-
}
|
|
33
|
-
seen.delete(start);
|
|
34
|
-
return seen;
|
|
35
|
-
}
|
|
36
|
-
evaluate(ids) {
|
|
37
|
-
const candidates = new Set(ids);
|
|
38
|
-
const issues = [];
|
|
39
|
-
// Run all registered constraint plugins
|
|
40
|
-
for (const plugin of this.plugins) {
|
|
41
|
-
issues.push(...plugin.evaluate(this, candidates));
|
|
42
|
-
}
|
|
43
|
-
return issues;
|
|
44
|
-
}
|
|
45
|
-
/** Apply a single change and return a batch: affected set + issues + patch */
|
|
46
|
-
commit(id, value) {
|
|
47
|
-
this.set(id, value);
|
|
48
|
-
const A = this.affected(id);
|
|
49
|
-
const issues = this.evaluate([id, ...A]);
|
|
50
|
-
const patch = {};
|
|
51
|
-
patch[id] = value; // include dependents if you compute derived values
|
|
52
|
-
return { affected: Array.from(A), issues, patch };
|
|
53
|
-
}
|
|
54
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Public API exports for Design Constraint Validator core.
|
|
3
|
+
*
|
|
4
|
+
* This file re-exports the main types and classes from the core engine.
|
|
5
|
+
* See engine.ts for full implementation with Phase 3C enhancements.
|
|
6
|
+
*/
|
|
7
|
+
export { Engine } from "./engine.js";
|
package/core/index.ts
CHANGED
|
@@ -1,72 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export type ConstraintPlugin = {
|
|
13
|
-
id: string;
|
|
14
|
-
evaluate(engine: Engine, candidates: Set<TokenId>): ConstraintIssue[];
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export class Engine {
|
|
18
|
-
private values = new Map<TokenId, TokenValue>();
|
|
19
|
-
private graph: Graph = new Map();
|
|
20
|
-
private plugins: ConstraintPlugin[] = [];
|
|
21
|
-
|
|
22
|
-
constructor(initValues: Record<TokenId, TokenValue>, edges: Array<[TokenId, TokenId]>) {
|
|
23
|
-
for (const [k,v] of Object.entries(initValues)) this.values.set(k, v);
|
|
24
|
-
for (const [from,to] of edges) {
|
|
25
|
-
if (!this.graph.has(from)) this.graph.set(from, new Set());
|
|
26
|
-
this.graph.get(from)!.add(to);
|
|
27
|
-
if (!this.graph.has(to)) this.graph.set(to, new Set());
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
set(id: TokenId, value: TokenValue) { this.values.set(id, value); }
|
|
32
|
-
get(id: TokenId) { return this.values.get(id); }
|
|
33
|
-
|
|
34
|
-
use(plugin: ConstraintPlugin): this {
|
|
35
|
-
this.plugins.push(plugin);
|
|
36
|
-
return this;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
affected(start: TokenId): Set<TokenId> {
|
|
40
|
-
const seen = new Set<TokenId>(); const stack = [start];
|
|
41
|
-
while (stack.length) {
|
|
42
|
-
const n = stack.pop()!;
|
|
43
|
-
if (seen.has(n)) continue;
|
|
44
|
-
seen.add(n);
|
|
45
|
-
for (const d of this.graph.get(n) ?? []) stack.push(d);
|
|
46
|
-
}
|
|
47
|
-
seen.delete(start);
|
|
48
|
-
return seen;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
evaluate(ids: Iterable<TokenId>): ConstraintIssue[] {
|
|
52
|
-
const candidates = new Set(ids);
|
|
53
|
-
const issues: ConstraintIssue[] = [];
|
|
54
|
-
|
|
55
|
-
// Run all registered constraint plugins
|
|
56
|
-
for (const plugin of this.plugins) {
|
|
57
|
-
issues.push(...plugin.evaluate(this, candidates));
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return issues;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** Apply a single change and return a batch: affected set + issues + patch */
|
|
64
|
-
commit(id: TokenId, value: TokenValue) {
|
|
65
|
-
this.set(id, value);
|
|
66
|
-
const A = this.affected(id);
|
|
67
|
-
const issues = this.evaluate([id, ...A]);
|
|
68
|
-
const patch: Record<TokenId, TokenValue> = {};
|
|
69
|
-
patch[id] = value; // include dependents if you compute derived values
|
|
70
|
-
return { affected: Array.from(A), issues, patch };
|
|
71
|
-
}
|
|
72
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Public API exports for Design Constraint Validator core.
|
|
3
|
+
*
|
|
4
|
+
* This file re-exports the main types and classes from the core engine.
|
|
5
|
+
* See engine.ts for full implementation with Phase 3C enhancements.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type { TokenId, TokenValue } from "./flatten.js";
|
|
9
|
+
export type { ConstraintIssue, ConstraintPlugin, Graph } from "./engine.js";
|
|
10
|
+
export { Engine } from "./engine.js";
|
package/core/patch.ts
CHANGED
|
@@ -1,134 +1,134 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import type { TokenNode, TokenValue } from './flatten.js';
|
|
3
|
-
import { flattenTokens } from './flatten.js';
|
|
4
|
-
|
|
5
|
-
export interface PatchChange {
|
|
6
|
-
id: string;
|
|
7
|
-
from: TokenValue | null | undefined;
|
|
8
|
-
to: TokenValue | null | undefined;
|
|
9
|
-
type: 'modify' | 'add' | 'remove';
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface PatchDocument {
|
|
13
|
-
version: 1;
|
|
14
|
-
generatedAt: string; // ISO
|
|
15
|
-
baseFile?: string;
|
|
16
|
-
breakpoint?: string;
|
|
17
|
-
changes: PatchChange[];
|
|
18
|
-
patch: Record<string, TokenValue | null | undefined>; // id -> to value
|
|
19
|
-
hash: string; // sha256 of canonical patch object (patch + ids)
|
|
20
|
-
baseTokensHash?: string; // sha256 of canonical flattened base tokens (id -> value)
|
|
21
|
-
meta?: Record<string, any>;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface BuildPatchOptions {
|
|
25
|
-
tokens: TokenNode; // raw hierarchical tokens
|
|
26
|
-
baseFile?: string; // filename hint
|
|
27
|
-
overrides?: Record<string, any>; // flat override map id->value
|
|
28
|
-
breakpoint?: string; // reserved for future responsive diffing
|
|
29
|
-
includeUnchanged?: boolean; // debug flag
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function canonicalString(obj: any): string {
|
|
33
|
-
return JSON.stringify(obj, Object.keys(obj).sort(), 2);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function applyFlatOverrides(tokens: TokenNode, overrides?: Record<string, any>): void {
|
|
37
|
-
if (!overrides) return;
|
|
38
|
-
for (const [id, val] of Object.entries(overrides)) {
|
|
39
|
-
const parts = id.split('.');
|
|
40
|
-
let cur: any = tokens;
|
|
41
|
-
for (let i = 0; i < parts.length; i++) {
|
|
42
|
-
const p = parts[i];
|
|
43
|
-
if (cur == null || typeof cur !== 'object') break;
|
|
44
|
-
if (!(p in cur)) break;
|
|
45
|
-
if (i === parts.length - 1) {
|
|
46
|
-
const leaf = cur[p];
|
|
47
|
-
if (leaf && typeof leaf === 'object' && Object.prototype.hasOwnProperty.call(leaf, '$value')) {
|
|
48
|
-
if (val === null) {
|
|
49
|
-
// Do not delete here; handled later to keep reference resolution intact.
|
|
50
|
-
} else {
|
|
51
|
-
leaf.$value = val;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
} else {
|
|
55
|
-
cur = cur[p];
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function buildPatch(opts: BuildPatchOptions): PatchDocument {
|
|
62
|
-
const cloned = JSON.parse(JSON.stringify(opts.tokens));
|
|
63
|
-
// Flatten original
|
|
64
|
-
const baseFlat = flattenTokens(cloned as any).flat as Record<string, any>;
|
|
65
|
-
// Canonical base tokens hash (id -> value) for drift detection when applying patch later
|
|
66
|
-
const baseFlatValues: Record<string, any> = {};
|
|
67
|
-
Object.keys(baseFlat).sort().forEach(id => { baseFlatValues[id] = baseFlat[id]?.value; });
|
|
68
|
-
const baseTokensHash = createHash('sha256').update(canonicalString(baseFlatValues)).digest('hex');
|
|
69
|
-
// Apply overrides on a fresh clone for diffing
|
|
70
|
-
const modified = JSON.parse(JSON.stringify(opts.tokens));
|
|
71
|
-
const removalIds = new Set<string>();
|
|
72
|
-
if (opts.overrides) {
|
|
73
|
-
for (const [id, v] of Object.entries(opts.overrides)) {
|
|
74
|
-
if (v === null) removalIds.add(id);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
applyFlatOverrides(modified as any, opts.overrides);
|
|
78
|
-
const modFlat = flattenTokens(modified as any).flat as Record<string, any>;
|
|
79
|
-
// Post-process removals: remove from modFlat so diff sees them as missing
|
|
80
|
-
for (const id of removalIds) {
|
|
81
|
-
delete modFlat[id];
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const changes: PatchChange[] = [];
|
|
85
|
-
const patch: Record<string, TokenValue | null | undefined> = {};
|
|
86
|
-
const visited = new Set<string>();
|
|
87
|
-
for (const id of Object.keys(baseFlat)) {
|
|
88
|
-
visited.add(id);
|
|
89
|
-
const before = baseFlat[id]?.value;
|
|
90
|
-
const after = modFlat[id]?.value;
|
|
91
|
-
if (removalIds.has(id)) {
|
|
92
|
-
// Skip here; removal handled later
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
if (before !== after) {
|
|
96
|
-
changes.push({ id, from: before, to: after, type: 'modify' });
|
|
97
|
-
patch[id] = after;
|
|
98
|
-
} else if (opts.includeUnchanged) {
|
|
99
|
-
changes.push({ id, from: before, to: after, type: 'modify' });
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
// Added ids (override referencing unknown id -> treat as add)
|
|
103
|
-
if (opts.overrides) {
|
|
104
|
-
for (const id of Object.keys(opts.overrides)) {
|
|
105
|
-
if (visited.has(id)) continue;
|
|
106
|
-
patch[id] = opts.overrides[id];
|
|
107
|
-
changes.push({ id, from: null, to: opts.overrides[id], type: 'add' });
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
// Removed ids (present in base, absent after overrides application)
|
|
111
|
-
for (const id of Object.keys(baseFlat)) {
|
|
112
|
-
if (!(id in modFlat)) {
|
|
113
|
-
const before = baseFlat[id]?.value;
|
|
114
|
-
patch[id] = null;
|
|
115
|
-
changes.push({ id, from: before, to: null, type: 'remove' });
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
// Sort deterministic
|
|
119
|
-
changes.sort((a, b) => a.id.localeCompare(b.id));
|
|
120
|
-
const patchSorted: Record<string, any> = {};
|
|
121
|
-
Object.keys(patch).sort().forEach(k => { patchSorted[k] = patch[k]; });
|
|
122
|
-
const hash = createHash('sha256').update(canonicalString(patchSorted)).digest('hex');
|
|
123
|
-
return {
|
|
124
|
-
version: 1,
|
|
125
|
-
generatedAt: new Date().toISOString(),
|
|
126
|
-
baseFile: opts.baseFile,
|
|
127
|
-
breakpoint: opts.breakpoint,
|
|
128
|
-
changes,
|
|
129
|
-
patch: patchSorted,
|
|
130
|
-
hash,
|
|
131
|
-
baseTokensHash,
|
|
132
|
-
meta: { changeCount: changes.length }
|
|
133
|
-
};
|
|
134
|
-
}
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import type { TokenNode, TokenValue } from './flatten.js';
|
|
3
|
+
import { flattenTokens } from './flatten.js';
|
|
4
|
+
|
|
5
|
+
export interface PatchChange {
|
|
6
|
+
id: string;
|
|
7
|
+
from: TokenValue | null | undefined;
|
|
8
|
+
to: TokenValue | null | undefined;
|
|
9
|
+
type: 'modify' | 'add' | 'remove';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface PatchDocument {
|
|
13
|
+
version: 1;
|
|
14
|
+
generatedAt: string; // ISO
|
|
15
|
+
baseFile?: string;
|
|
16
|
+
breakpoint?: string;
|
|
17
|
+
changes: PatchChange[];
|
|
18
|
+
patch: Record<string, TokenValue | null | undefined>; // id -> to value
|
|
19
|
+
hash: string; // sha256 of canonical patch object (patch + ids)
|
|
20
|
+
baseTokensHash?: string; // sha256 of canonical flattened base tokens (id -> value)
|
|
21
|
+
meta?: Record<string, any>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface BuildPatchOptions {
|
|
25
|
+
tokens: TokenNode; // raw hierarchical tokens
|
|
26
|
+
baseFile?: string; // filename hint
|
|
27
|
+
overrides?: Record<string, any>; // flat override map id->value
|
|
28
|
+
breakpoint?: string; // reserved for future responsive diffing
|
|
29
|
+
includeUnchanged?: boolean; // debug flag
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function canonicalString(obj: any): string {
|
|
33
|
+
return JSON.stringify(obj, Object.keys(obj).sort(), 2);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function applyFlatOverrides(tokens: TokenNode, overrides?: Record<string, any>): void {
|
|
37
|
+
if (!overrides) return;
|
|
38
|
+
for (const [id, val] of Object.entries(overrides)) {
|
|
39
|
+
const parts = id.split('.');
|
|
40
|
+
let cur: any = tokens;
|
|
41
|
+
for (let i = 0; i < parts.length; i++) {
|
|
42
|
+
const p = parts[i];
|
|
43
|
+
if (cur == null || typeof cur !== 'object') break;
|
|
44
|
+
if (!(p in cur)) break;
|
|
45
|
+
if (i === parts.length - 1) {
|
|
46
|
+
const leaf = cur[p];
|
|
47
|
+
if (leaf && typeof leaf === 'object' && Object.prototype.hasOwnProperty.call(leaf, '$value')) {
|
|
48
|
+
if (val === null) {
|
|
49
|
+
// Do not delete here; handled later to keep reference resolution intact.
|
|
50
|
+
} else {
|
|
51
|
+
leaf.$value = val;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
cur = cur[p];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function buildPatch(opts: BuildPatchOptions): PatchDocument {
|
|
62
|
+
const cloned = JSON.parse(JSON.stringify(opts.tokens));
|
|
63
|
+
// Flatten original
|
|
64
|
+
const baseFlat = flattenTokens(cloned as any).flat as Record<string, any>;
|
|
65
|
+
// Canonical base tokens hash (id -> value) for drift detection when applying patch later
|
|
66
|
+
const baseFlatValues: Record<string, any> = {};
|
|
67
|
+
Object.keys(baseFlat).sort().forEach(id => { baseFlatValues[id] = baseFlat[id]?.value; });
|
|
68
|
+
const baseTokensHash = createHash('sha256').update(canonicalString(baseFlatValues)).digest('hex');
|
|
69
|
+
// Apply overrides on a fresh clone for diffing
|
|
70
|
+
const modified = JSON.parse(JSON.stringify(opts.tokens));
|
|
71
|
+
const removalIds = new Set<string>();
|
|
72
|
+
if (opts.overrides) {
|
|
73
|
+
for (const [id, v] of Object.entries(opts.overrides)) {
|
|
74
|
+
if (v === null) removalIds.add(id);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
applyFlatOverrides(modified as any, opts.overrides);
|
|
78
|
+
const modFlat = flattenTokens(modified as any).flat as Record<string, any>;
|
|
79
|
+
// Post-process removals: remove from modFlat so diff sees them as missing
|
|
80
|
+
for (const id of removalIds) {
|
|
81
|
+
delete modFlat[id];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const changes: PatchChange[] = [];
|
|
85
|
+
const patch: Record<string, TokenValue | null | undefined> = {};
|
|
86
|
+
const visited = new Set<string>();
|
|
87
|
+
for (const id of Object.keys(baseFlat)) {
|
|
88
|
+
visited.add(id);
|
|
89
|
+
const before = baseFlat[id]?.value;
|
|
90
|
+
const after = modFlat[id]?.value;
|
|
91
|
+
if (removalIds.has(id)) {
|
|
92
|
+
// Skip here; removal handled later
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
if (before !== after) {
|
|
96
|
+
changes.push({ id, from: before, to: after, type: 'modify' });
|
|
97
|
+
patch[id] = after;
|
|
98
|
+
} else if (opts.includeUnchanged) {
|
|
99
|
+
changes.push({ id, from: before, to: after, type: 'modify' });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Added ids (override referencing unknown id -> treat as add)
|
|
103
|
+
if (opts.overrides) {
|
|
104
|
+
for (const id of Object.keys(opts.overrides)) {
|
|
105
|
+
if (visited.has(id)) continue;
|
|
106
|
+
patch[id] = opts.overrides[id];
|
|
107
|
+
changes.push({ id, from: null, to: opts.overrides[id], type: 'add' });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Removed ids (present in base, absent after overrides application)
|
|
111
|
+
for (const id of Object.keys(baseFlat)) {
|
|
112
|
+
if (!(id in modFlat)) {
|
|
113
|
+
const before = baseFlat[id]?.value;
|
|
114
|
+
patch[id] = null;
|
|
115
|
+
changes.push({ id, from: before, to: null, type: 'remove' });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
// Sort deterministic
|
|
119
|
+
changes.sort((a, b) => a.id.localeCompare(b.id));
|
|
120
|
+
const patchSorted: Record<string, any> = {};
|
|
121
|
+
Object.keys(patch).sort().forEach(k => { patchSorted[k] = patch[k]; });
|
|
122
|
+
const hash = createHash('sha256').update(canonicalString(patchSorted)).digest('hex');
|
|
123
|
+
return {
|
|
124
|
+
version: 1,
|
|
125
|
+
generatedAt: new Date().toISOString(),
|
|
126
|
+
baseFile: opts.baseFile,
|
|
127
|
+
breakpoint: opts.breakpoint,
|
|
128
|
+
changes,
|
|
129
|
+
patch: patchSorted,
|
|
130
|
+
hash,
|
|
131
|
+
baseTokensHash,
|
|
132
|
+
meta: { changeCount: changes.length }
|
|
133
|
+
};
|
|
134
|
+
}
|