design-constraint-validator 1.0.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/LICENSE +21 -0
- package/README.md +659 -0
- package/adapters/README.md +46 -0
- package/adapters/css.d.ts +44 -0
- package/adapters/css.d.ts.map +1 -0
- package/adapters/css.js +97 -0
- package/adapters/css.ts +116 -0
- package/adapters/js.d.ts +3 -0
- package/adapters/js.d.ts.map +1 -0
- package/adapters/js.js +15 -0
- package/adapters/js.ts +14 -0
- package/adapters/json.d.ts +18 -0
- package/adapters/json.d.ts.map +1 -0
- package/adapters/json.js +35 -0
- package/adapters/json.ts +45 -0
- package/cli/build-css.d.ts +2 -0
- package/cli/build-css.d.ts.map +1 -0
- package/cli/build-css.js +23 -0
- package/cli/build-css.ts +32 -0
- package/cli/commands/build.d.ts +5 -0
- package/cli/commands/build.d.ts.map +1 -0
- package/cli/commands/build.js +89 -0
- package/cli/commands/build.ts +65 -0
- package/cli/commands/graph.d.ts +3 -0
- package/cli/commands/graph.d.ts.map +1 -0
- package/cli/commands/graph.js +219 -0
- package/cli/commands/graph.ts +137 -0
- package/cli/commands/index.d.ts +8 -0
- package/cli/commands/index.d.ts.map +1 -0
- package/cli/commands/index.js +7 -0
- package/cli/commands/index.ts +7 -0
- package/cli/commands/patch-apply.d.ts +3 -0
- package/cli/commands/patch-apply.d.ts.map +1 -0
- package/cli/commands/patch-apply.js +75 -0
- package/cli/commands/patch-apply.ts +80 -0
- package/cli/commands/patch.d.ts +3 -0
- package/cli/commands/patch.d.ts.map +1 -0
- package/cli/commands/patch.js +21 -0
- package/cli/commands/patch.ts +22 -0
- package/cli/commands/set.d.ts +3 -0
- package/cli/commands/set.d.ts.map +1 -0
- package/cli/commands/set.js +286 -0
- package/cli/commands/set.ts +225 -0
- package/cli/commands/utils.d.ts +4 -0
- package/cli/commands/utils.d.ts.map +1 -0
- package/cli/commands/utils.js +51 -0
- package/cli/commands/utils.ts +50 -0
- package/cli/commands/validate.d.ts +3 -0
- package/cli/commands/validate.d.ts.map +1 -0
- package/cli/commands/validate.js +131 -0
- package/cli/commands/validate.ts +115 -0
- package/cli/commands/why.d.ts +3 -0
- package/cli/commands/why.d.ts.map +1 -0
- package/cli/commands/why.js +64 -0
- package/cli/commands/why.ts +46 -0
- package/cli/config-schema.d.ts +238 -0
- package/cli/config-schema.d.ts.map +1 -0
- package/cli/config-schema.js +21 -0
- package/cli/config-schema.ts +27 -0
- package/cli/config.d.ts +4 -0
- package/cli/config.d.ts.map +1 -0
- package/cli/config.js +37 -0
- package/cli/config.ts +35 -0
- package/cli/dcv.d.ts +3 -0
- package/cli/dcv.d.ts.map +1 -0
- package/cli/dcv.js +86 -0
- package/cli/dcv.ts +107 -0
- package/cli/engine-helpers.d.ts +8 -0
- package/cli/engine-helpers.d.ts.map +1 -0
- package/cli/engine-helpers.js +70 -0
- package/cli/engine-helpers.ts +61 -0
- package/cli/graph-poset.d.ts +9 -0
- package/cli/graph-poset.d.ts.map +1 -0
- package/cli/graph-poset.js +58 -0
- package/cli/graph-poset.ts +74 -0
- package/cli/index.d.ts +3 -0
- package/cli/index.d.ts.map +1 -0
- package/cli/index.js +2 -0
- package/cli/index.ts +2 -0
- package/cli/result.d.ts +17 -0
- package/cli/result.d.ts.map +1 -0
- package/cli/result.js +29 -0
- package/cli/result.ts +27 -0
- package/cli/run.d.ts +3 -0
- package/cli/run.d.ts.map +1 -0
- package/cli/run.js +47 -0
- package/cli/run.ts +54 -0
- package/cli/smoke-test.d.ts +2 -0
- package/cli/smoke-test.d.ts.map +1 -0
- package/cli/smoke-test.js +33 -0
- package/cli/smoke-test.ts +40 -0
- package/cli/types.d.ts +86 -0
- package/cli/types.d.ts.map +1 -0
- package/cli/types.js +1 -0
- package/cli/types.ts +78 -0
- package/core/breakpoints.d.ts +12 -0
- package/core/breakpoints.d.ts.map +1 -0
- package/core/breakpoints.js +48 -0
- package/core/breakpoints.ts +50 -0
- package/core/cli-format.d.ts +8 -0
- package/core/cli-format.d.ts.map +1 -0
- package/core/cli-format.js +29 -0
- package/core/cli-format.ts +31 -0
- package/core/color.d.ts +14 -0
- package/core/color.d.ts.map +1 -0
- package/core/color.js +136 -0
- package/core/color.ts +148 -0
- package/core/constraints/cross-axis.d.ts +33 -0
- package/core/constraints/cross-axis.d.ts.map +1 -0
- package/core/constraints/cross-axis.js +93 -0
- package/core/constraints/cross-axis.ts +114 -0
- package/core/constraints/monotonic-lightness.d.ts +5 -0
- package/core/constraints/monotonic-lightness.d.ts.map +1 -0
- package/core/constraints/monotonic-lightness.js +37 -0
- package/core/constraints/monotonic-lightness.ts +38 -0
- package/core/constraints/monotonic.d.ts +7 -0
- package/core/constraints/monotonic.d.ts.map +1 -0
- package/core/constraints/monotonic.js +65 -0
- package/core/constraints/monotonic.ts +74 -0
- package/core/constraints/threshold.d.ts +10 -0
- package/core/constraints/threshold.d.ts.map +1 -0
- package/core/constraints/threshold.js +36 -0
- package/core/constraints/threshold.ts +43 -0
- package/core/constraints/wcag.d.ts +11 -0
- package/core/constraints/wcag.d.ts.map +1 -0
- package/core/constraints/wcag.js +53 -0
- package/core/constraints/wcag.ts +70 -0
- package/core/cross-axis-config.d.ts +5 -0
- package/core/cross-axis-config.d.ts.map +1 -0
- package/core/cross-axis-config.js +144 -0
- package/core/cross-axis-config.ts +152 -0
- package/core/engine.d.ts +32 -0
- package/core/engine.d.ts.map +1 -0
- package/core/engine.js +46 -0
- package/core/engine.ts +65 -0
- package/core/flatten.d.ts +20 -0
- package/core/flatten.d.ts.map +1 -0
- package/core/flatten.js +80 -0
- package/core/flatten.ts +116 -0
- package/core/image-export.d.ts +10 -0
- package/core/image-export.d.ts.map +1 -0
- package/core/image-export.js +43 -0
- package/core/image-export.ts +48 -0
- package/core/index.d.ts +31 -0
- package/core/index.d.ts.map +1 -0
- package/core/index.js +54 -0
- package/core/index.ts +72 -0
- package/core/patch.d.ts +28 -0
- package/core/patch.d.ts.map +1 -0
- package/core/patch.js +110 -0
- package/core/patch.ts +134 -0
- package/core/poset.d.ts +41 -0
- package/core/poset.d.ts.map +1 -0
- package/core/poset.js +275 -0
- package/core/poset.ts +311 -0
- package/core/why.d.ts +17 -0
- package/core/why.d.ts.map +1 -0
- package/core/why.js +45 -0
- package/core/why.ts +63 -0
- package/dist/test-overrides-removal.json +4 -0
- package/dist/tmp.patch.json +35 -0
- package/package.json +90 -0
- package/themes/color.lg.order.json +15 -0
- package/themes/color.md.order.json +15 -0
- package/themes/color.order.json +15 -0
- package/themes/color.sm.order.json +15 -0
- package/themes/cross-axis.rules.json +36 -0
- package/themes/cross-axis.sm.rules.json +12 -0
- package/themes/layout.lg.order.json +18 -0
- package/themes/layout.md.order.json +18 -0
- package/themes/layout.order.json +18 -0
- package/themes/layout.sm.order.json +18 -0
- package/themes/spacing.order.json +14 -0
- package/themes/typography.lg.order.json +15 -0
- package/themes/typography.md.order.json +15 -0
- package/themes/typography.order.json +15 -0
- package/themes/typography.sm.order.json +15 -0
- package/tokens/overrides/base.json +22 -0
- package/tokens/overrides/lg.json +20 -0
- package/tokens/overrides/md.json +16 -0
- package/tokens/overrides/sm.json +16 -0
- package/tokens/overrides/viol.color.json +6 -0
- package/tokens/overrides/viol.typography.json +6 -0
- package/tokens/tokens.demo-violations.json +116 -0
- package/tokens/tokens.example.json +128 -0
- package/tokens/tokens.json +67 -0
- package/tokens/tokens.multi-violations.json +21 -0
- package/tokens/tokens.schema.d.ts +2298 -0
- package/tokens/tokens.schema.d.ts.map +1 -0
- package/tokens/tokens.schema.js +148 -0
- package/tokens/tokens.schema.ts +196 -0
- package/tokens/tokens.test.json +38 -0
- package/tokens/tokens.touch-violation.json +8 -0
- package/tokens/typography.classes.css +11 -0
- package/tokens/typography.css +20 -0
package/core/index.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export class Engine {
|
|
2
|
+
values = new Map();
|
|
3
|
+
graph = new Map();
|
|
4
|
+
plugins = [];
|
|
5
|
+
constructor(initValues, edges) {
|
|
6
|
+
for (const [k, v] of Object.entries(initValues))
|
|
7
|
+
this.values.set(k, v);
|
|
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
|
+
}
|
package/core/index.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
export type TokenId = string; // "dimension.spacing.scale.4"
|
|
2
|
+
export type TokenValue = string | number;
|
|
3
|
+
export type Graph = Map<TokenId, Set<TokenId>>; // edges: id -> dependents
|
|
4
|
+
|
|
5
|
+
export type ConstraintIssue = {
|
|
6
|
+
id: TokenId;
|
|
7
|
+
rule: string; // "wcag-contrast"
|
|
8
|
+
level: "error" | "warn";
|
|
9
|
+
message: string;
|
|
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
|
+
}
|
package/core/patch.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { TokenNode, TokenValue } from './flatten.js';
|
|
2
|
+
export interface PatchChange {
|
|
3
|
+
id: string;
|
|
4
|
+
from: TokenValue | null | undefined;
|
|
5
|
+
to: TokenValue | null | undefined;
|
|
6
|
+
type: 'modify' | 'add' | 'remove';
|
|
7
|
+
}
|
|
8
|
+
export interface PatchDocument {
|
|
9
|
+
version: 1;
|
|
10
|
+
generatedAt: string;
|
|
11
|
+
baseFile?: string;
|
|
12
|
+
breakpoint?: string;
|
|
13
|
+
changes: PatchChange[];
|
|
14
|
+
patch: Record<string, TokenValue | null | undefined>;
|
|
15
|
+
hash: string;
|
|
16
|
+
baseTokensHash?: string;
|
|
17
|
+
meta?: Record<string, any>;
|
|
18
|
+
}
|
|
19
|
+
export interface BuildPatchOptions {
|
|
20
|
+
tokens: TokenNode;
|
|
21
|
+
baseFile?: string;
|
|
22
|
+
overrides?: Record<string, any>;
|
|
23
|
+
breakpoint?: string;
|
|
24
|
+
includeUnchanged?: boolean;
|
|
25
|
+
}
|
|
26
|
+
export declare function applyFlatOverrides(tokens: TokenNode, overrides?: Record<string, any>): void;
|
|
27
|
+
export declare function buildPatch(opts: BuildPatchOptions): PatchDocument;
|
|
28
|
+
//# sourceMappingURL=patch.d.ts.map
|
|
@@ -0,0 +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,CAyEjE"}
|
package/core/patch.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { flattenTokens } from './flatten.js';
|
|
3
|
+
function canonicalString(obj) {
|
|
4
|
+
return JSON.stringify(obj, Object.keys(obj).sort(), 2);
|
|
5
|
+
}
|
|
6
|
+
export function applyFlatOverrides(tokens, overrides) {
|
|
7
|
+
if (!overrides)
|
|
8
|
+
return;
|
|
9
|
+
for (const [id, val] of Object.entries(overrides)) {
|
|
10
|
+
const parts = id.split('.');
|
|
11
|
+
let cur = tokens;
|
|
12
|
+
for (let i = 0; i < parts.length; i++) {
|
|
13
|
+
const p = parts[i];
|
|
14
|
+
if (cur == null || typeof cur !== 'object')
|
|
15
|
+
break;
|
|
16
|
+
if (!(p in cur))
|
|
17
|
+
break;
|
|
18
|
+
if (i === parts.length - 1) {
|
|
19
|
+
const leaf = cur[p];
|
|
20
|
+
if (leaf && typeof leaf === 'object' && Object.prototype.hasOwnProperty.call(leaf, '$value')) {
|
|
21
|
+
if (val === null) {
|
|
22
|
+
// Do not delete here; handled later to keep reference resolution intact.
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
leaf.$value = val;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
cur = cur[p];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export function buildPatch(opts) {
|
|
36
|
+
const cloned = JSON.parse(JSON.stringify(opts.tokens));
|
|
37
|
+
// Flatten original
|
|
38
|
+
const baseFlat = flattenTokens(cloned).flat;
|
|
39
|
+
// Canonical base tokens hash (id -> value) for drift detection when applying patch later
|
|
40
|
+
const baseFlatValues = {};
|
|
41
|
+
Object.keys(baseFlat).sort().forEach(id => { baseFlatValues[id] = baseFlat[id]?.value; });
|
|
42
|
+
const baseTokensHash = createHash('sha256').update(canonicalString(baseFlatValues)).digest('hex');
|
|
43
|
+
// Apply overrides on a fresh clone for diffing
|
|
44
|
+
const modified = JSON.parse(JSON.stringify(opts.tokens));
|
|
45
|
+
const removalIds = new Set();
|
|
46
|
+
if (opts.overrides) {
|
|
47
|
+
for (const [id, v] of Object.entries(opts.overrides)) {
|
|
48
|
+
if (v === null)
|
|
49
|
+
removalIds.add(id);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
applyFlatOverrides(modified, opts.overrides);
|
|
53
|
+
const modFlat = flattenTokens(modified).flat;
|
|
54
|
+
// Post-process removals: remove from modFlat so diff sees them as missing
|
|
55
|
+
for (const id of removalIds) {
|
|
56
|
+
delete modFlat[id];
|
|
57
|
+
}
|
|
58
|
+
const changes = [];
|
|
59
|
+
const patch = {};
|
|
60
|
+
const visited = new Set();
|
|
61
|
+
for (const id of Object.keys(baseFlat)) {
|
|
62
|
+
visited.add(id);
|
|
63
|
+
const before = baseFlat[id]?.value;
|
|
64
|
+
const after = modFlat[id]?.value;
|
|
65
|
+
if (removalIds.has(id)) {
|
|
66
|
+
// Skip here; removal handled later
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (before !== after) {
|
|
70
|
+
changes.push({ id, from: before, to: after, type: 'modify' });
|
|
71
|
+
patch[id] = after;
|
|
72
|
+
}
|
|
73
|
+
else if (opts.includeUnchanged) {
|
|
74
|
+
changes.push({ id, from: before, to: after, type: 'modify' });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Added ids (override referencing unknown id -> treat as add)
|
|
78
|
+
if (opts.overrides) {
|
|
79
|
+
for (const id of Object.keys(opts.overrides)) {
|
|
80
|
+
if (visited.has(id))
|
|
81
|
+
continue;
|
|
82
|
+
patch[id] = opts.overrides[id];
|
|
83
|
+
changes.push({ id, from: null, to: opts.overrides[id], type: 'add' });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Removed ids (present in base, absent after overrides application)
|
|
87
|
+
for (const id of Object.keys(baseFlat)) {
|
|
88
|
+
if (!(id in modFlat)) {
|
|
89
|
+
const before = baseFlat[id]?.value;
|
|
90
|
+
patch[id] = null;
|
|
91
|
+
changes.push({ id, from: before, to: null, type: 'remove' });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Sort deterministic
|
|
95
|
+
changes.sort((a, b) => a.id.localeCompare(b.id));
|
|
96
|
+
const patchSorted = {};
|
|
97
|
+
Object.keys(patch).sort().forEach(k => { patchSorted[k] = patch[k]; });
|
|
98
|
+
const hash = createHash('sha256').update(canonicalString(patchSorted)).digest('hex');
|
|
99
|
+
return {
|
|
100
|
+
version: 1,
|
|
101
|
+
generatedAt: new Date().toISOString(),
|
|
102
|
+
baseFile: opts.baseFile,
|
|
103
|
+
breakpoint: opts.breakpoint,
|
|
104
|
+
changes,
|
|
105
|
+
patch: patchSorted,
|
|
106
|
+
hash,
|
|
107
|
+
baseTokensHash,
|
|
108
|
+
meta: { changeCount: changes.length }
|
|
109
|
+
};
|
|
110
|
+
}
|
package/core/patch.ts
ADDED
|
@@ -0,0 +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
|
+
}
|
package/core/poset.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export type Id = string;
|
|
2
|
+
export type Comp = "<=" | ">=";
|
|
3
|
+
export type Order = [Id, Comp, Id];
|
|
4
|
+
export type Digraph = Map<Id, Set<Id>>;
|
|
5
|
+
export type EdgeLabels = Map<string, string>;
|
|
6
|
+
/** Safe ID for Mermaid/DOT node identifiers */
|
|
7
|
+
export declare function sanitizeId(id: string): string;
|
|
8
|
+
export type Highlight = {
|
|
9
|
+
nodes?: Set<string>;
|
|
10
|
+
edges?: Set<string>;
|
|
11
|
+
color?: string;
|
|
12
|
+
};
|
|
13
|
+
export declare function buildPoset(orders: Order[]): Digraph;
|
|
14
|
+
export declare function transitiveReduction(g: Digraph): Digraph;
|
|
15
|
+
export declare function toMermaidHasse(gHasse: Digraph, { title }?: {
|
|
16
|
+
title?: string | undefined;
|
|
17
|
+
}): string;
|
|
18
|
+
export declare function toMermaidHasseStyled(g: Digraph, opts?: {
|
|
19
|
+
title?: string;
|
|
20
|
+
highlight?: Highlight;
|
|
21
|
+
labels?: EdgeLabels;
|
|
22
|
+
}): string;
|
|
23
|
+
export declare function toDotHasse(gHasse: Digraph, opts?: {
|
|
24
|
+
title?: string;
|
|
25
|
+
labels?: EdgeLabels;
|
|
26
|
+
}): string;
|
|
27
|
+
export declare function toDotHasseStyled(g: Digraph, opts?: {
|
|
28
|
+
title?: string;
|
|
29
|
+
highlight?: Highlight;
|
|
30
|
+
labels?: EdgeLabels;
|
|
31
|
+
}): string;
|
|
32
|
+
export declare function validatePoset(g: Digraph): {
|
|
33
|
+
valid: boolean;
|
|
34
|
+
cycles?: Id[][];
|
|
35
|
+
};
|
|
36
|
+
export declare function filterDigraph(g: Digraph, predicate: (id: string) => boolean): Digraph;
|
|
37
|
+
export declare function filterByPrefix(g: Digraph, prefixes: string[]): Digraph;
|
|
38
|
+
export declare function filterExcludePrefix(g: Digraph, prefixes: string[]): Digraph;
|
|
39
|
+
export declare function khopSubgraph(g: Digraph, seeds: Set<string>, k?: number): Digraph;
|
|
40
|
+
export declare function pickSeedsByPattern(nodes: Iterable<string>, pattern: string): Set<string>;
|
|
41
|
+
//# sourceMappingURL=poset.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"poset.d.ts","sourceRoot":"","sources":["poset.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,EAAE,GAAG,MAAM,CAAC;AACxB,MAAM,MAAM,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAC/B,MAAM,MAAM,KAAK,GAAG,CAAC,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC;AAEnC,MAAM,MAAM,OAAO,GAAG,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;AAGvC,MAAM,MAAM,UAAU,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAM7C,+CAA+C;AAC/C,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAE7C;AAED,MAAM,MAAM,SAAS,GAAG;IACtB,KAAK,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACpB,KAAK,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,wBAAgB,UAAU,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,OAAO,CAYnD;AAED,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,OAAO,GAAG,OAAO,CAwBvD;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,OAAO,EAAE,EAAC,KAAe,EAAC;;CAAK,GAAG,MAAM,CAQ9E;AAGD,wBAAgB,oBAAoB,CAClC,CAAC,EAAE,OAAO,EACV,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,SAAS,CAAC;IAAC,MAAM,CAAC,EAAE,UAAU,CAAA;CAAO,GACxE,MAAM,CAuCR;AAED,wBAAgB,UAAU,CACxB,MAAM,EAAE,OAAO,EACf,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,UAAU,CAAA;CAAO,GACjD,MAAM,CAoBR;AAGD,wBAAgB,gBAAgB,CAC9B,CAAC,EAAE,OAAO,EACV,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,SAAS,CAAC;IAAC,MAAM,CAAC,EAAE,UAAU,CAAA;CAAO,GACxE,MAAM,CAyCR;AAGD,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,GAAG;IAAE,KAAK,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,EAAE,EAAE,EAAE,CAAA;CAAE,CA2C7E;AAED,wBAAgB,aAAa,CAC3B,CAAC,EAAE,OAAO,EACV,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,GACjC,OAAO,CAYT;AAED,wBAAgB,cAAc,CAAC,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAKtE;AAED,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,OAAO,CAK3E;AAED,wBAAgB,YAAY,CAAC,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,SAAI,GAAG,OAAO,CA8B3E;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAOxF"}
|