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/cli/graph-poset.ts
CHANGED
|
@@ -1,74 +1,74 @@
|
|
|
1
|
-
// cli/graph-poset.ts
|
|
2
|
-
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
3
|
-
import { buildPoset, transitiveReduction, toMermaidHasse, toDotHasse, validatePoset, type Order } from "../core/poset.js";
|
|
4
|
-
|
|
5
|
-
export interface OrderFile {
|
|
6
|
-
$description?: string;
|
|
7
|
-
order: Order[];
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function loadOrderFile(path: string): OrderFile {
|
|
11
|
-
if (!existsSync(path)) {
|
|
12
|
-
throw new Error(`Order file not found: ${path}`);
|
|
13
|
-
}
|
|
14
|
-
return JSON.parse(readFileSync(path, "utf8"));
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function generateHasseDiagram(orderPath: string, outputPath: string, format: 'mermaid' | 'dot' = 'mermaid'): void {
|
|
18
|
-
const orderData = loadOrderFile(orderPath);
|
|
19
|
-
const { order, $description } = orderData;
|
|
20
|
-
|
|
21
|
-
// Build and validate poset
|
|
22
|
-
const poset = buildPoset(order);
|
|
23
|
-
const validation = validatePoset(poset);
|
|
24
|
-
|
|
25
|
-
if (!validation.valid) {
|
|
26
|
-
console.error("⚠️ Warning: Poset contains cycles:");
|
|
27
|
-
for (const cycle of validation.cycles || []) {
|
|
28
|
-
console.error(` ${cycle.join(" → ")}`);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Generate Hasse diagram (transitive reduction)
|
|
33
|
-
const hasse = transitiveReduction(poset);
|
|
34
|
-
|
|
35
|
-
const title = $description || "Poset Hierarchy";
|
|
36
|
-
const diagram = format === 'mermaid'
|
|
37
|
-
? toMermaidHasse(hasse, { title })
|
|
38
|
-
: toDotHasse(hasse, { title });
|
|
39
|
-
|
|
40
|
-
// Ensure output directory exists
|
|
41
|
-
mkdirSync(outputPath.split('/').slice(0, -1).join('/'), { recursive: true });
|
|
42
|
-
|
|
43
|
-
writeFileSync(outputPath, diagram);
|
|
44
|
-
console.log(`✅ Generated ${format.toUpperCase()} Hasse diagram: ${outputPath}`);
|
|
45
|
-
|
|
46
|
-
if (!validation.valid) {
|
|
47
|
-
console.log(`⚠️ Note: Diagram shows cycles that should be resolved`);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function generateAllHasseDiagrams(themesDir: string = "themes", outputDir: string = "dist/graphs"): void {
|
|
52
|
-
const orderFiles = [
|
|
53
|
-
{ file: "typography.order.json", name: "typography" },
|
|
54
|
-
{ file: "spacing.order.json", name: "spacing" }
|
|
55
|
-
];
|
|
56
|
-
|
|
57
|
-
mkdirSync(outputDir, { recursive: true });
|
|
58
|
-
|
|
59
|
-
for (const { file, name } of orderFiles) {
|
|
60
|
-
const orderPath = `${themesDir}/${file}`;
|
|
61
|
-
|
|
62
|
-
if (existsSync(orderPath)) {
|
|
63
|
-
try {
|
|
64
|
-
// Generate both Mermaid and DOT formats
|
|
65
|
-
generateHasseDiagram(orderPath, `${outputDir}/${name}-hasse.mmd`, 'mermaid');
|
|
66
|
-
generateHasseDiagram(orderPath, `${outputDir}/${name}-hasse.dot`, 'dot');
|
|
67
|
-
} catch (error) {
|
|
68
|
-
console.error(`❌ Failed to generate diagram for ${name}:`, error);
|
|
69
|
-
}
|
|
70
|
-
} else {
|
|
71
|
-
console.log(`⏭️ Skipping ${name} (no order file found)`);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
1
|
+
// cli/graph-poset.ts
|
|
2
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
3
|
+
import { buildPoset, transitiveReduction, toMermaidHasse, toDotHasse, validatePoset, type Order } from "../core/poset.js";
|
|
4
|
+
|
|
5
|
+
export interface OrderFile {
|
|
6
|
+
$description?: string;
|
|
7
|
+
order: Order[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function loadOrderFile(path: string): OrderFile {
|
|
11
|
+
if (!existsSync(path)) {
|
|
12
|
+
throw new Error(`Order file not found: ${path}`);
|
|
13
|
+
}
|
|
14
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function generateHasseDiagram(orderPath: string, outputPath: string, format: 'mermaid' | 'dot' = 'mermaid'): void {
|
|
18
|
+
const orderData = loadOrderFile(orderPath);
|
|
19
|
+
const { order, $description } = orderData;
|
|
20
|
+
|
|
21
|
+
// Build and validate poset
|
|
22
|
+
const poset = buildPoset(order);
|
|
23
|
+
const validation = validatePoset(poset);
|
|
24
|
+
|
|
25
|
+
if (!validation.valid) {
|
|
26
|
+
console.error("⚠️ Warning: Poset contains cycles:");
|
|
27
|
+
for (const cycle of validation.cycles || []) {
|
|
28
|
+
console.error(` ${cycle.join(" → ")}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Generate Hasse diagram (transitive reduction)
|
|
33
|
+
const hasse = transitiveReduction(poset);
|
|
34
|
+
|
|
35
|
+
const title = $description || "Poset Hierarchy";
|
|
36
|
+
const diagram = format === 'mermaid'
|
|
37
|
+
? toMermaidHasse(hasse, { title })
|
|
38
|
+
: toDotHasse(hasse, { title });
|
|
39
|
+
|
|
40
|
+
// Ensure output directory exists
|
|
41
|
+
mkdirSync(outputPath.split('/').slice(0, -1).join('/'), { recursive: true });
|
|
42
|
+
|
|
43
|
+
writeFileSync(outputPath, diagram);
|
|
44
|
+
console.log(`✅ Generated ${format.toUpperCase()} Hasse diagram: ${outputPath}`);
|
|
45
|
+
|
|
46
|
+
if (!validation.valid) {
|
|
47
|
+
console.log(`⚠️ Note: Diagram shows cycles that should be resolved`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function generateAllHasseDiagrams(themesDir: string = "themes", outputDir: string = "dist/graphs"): void {
|
|
52
|
+
const orderFiles = [
|
|
53
|
+
{ file: "typography.order.json", name: "typography" },
|
|
54
|
+
{ file: "spacing.order.json", name: "spacing" }
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
mkdirSync(outputDir, { recursive: true });
|
|
58
|
+
|
|
59
|
+
for (const { file, name } of orderFiles) {
|
|
60
|
+
const orderPath = `${themesDir}/${file}`;
|
|
61
|
+
|
|
62
|
+
if (existsSync(orderPath)) {
|
|
63
|
+
try {
|
|
64
|
+
// Generate both Mermaid and DOT formats
|
|
65
|
+
generateHasseDiagram(orderPath, `${outputDir}/${name}-hasse.mmd`, 'mermaid');
|
|
66
|
+
generateHasseDiagram(orderPath, `${outputDir}/${name}-hasse.dot`, 'dot');
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error(`❌ Failed to generate diagram for ${name}:`, error);
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
console.log(`⏭️ Skipping ${name} (no order file found)`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { ConstraintIssue } from '../core/engine.js';
|
|
2
|
+
export interface ConstraintViolation {
|
|
3
|
+
ruleId: string;
|
|
4
|
+
level: 'error' | 'warn';
|
|
5
|
+
message: string;
|
|
6
|
+
nodes?: string[];
|
|
7
|
+
edges?: [string, string][];
|
|
8
|
+
context?: {
|
|
9
|
+
actual?: unknown;
|
|
10
|
+
expected?: unknown;
|
|
11
|
+
threshold?: number;
|
|
12
|
+
[key: string]: unknown;
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export interface ValidationResult {
|
|
16
|
+
ok: boolean;
|
|
17
|
+
counts: {
|
|
18
|
+
checked: number;
|
|
19
|
+
violations: number;
|
|
20
|
+
warnings: number;
|
|
21
|
+
};
|
|
22
|
+
violations: ConstraintViolation[];
|
|
23
|
+
warnings?: ConstraintViolation[];
|
|
24
|
+
stats: {
|
|
25
|
+
durationMs: number;
|
|
26
|
+
engineVersion: string;
|
|
27
|
+
timestamp: string;
|
|
28
|
+
};
|
|
29
|
+
dcv: {
|
|
30
|
+
name: string;
|
|
31
|
+
version: string;
|
|
32
|
+
repository: string;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export interface ValidationReceipt extends ValidationResult {
|
|
36
|
+
environment: {
|
|
37
|
+
nodeVersion: string;
|
|
38
|
+
platform: string;
|
|
39
|
+
arch: string;
|
|
40
|
+
};
|
|
41
|
+
inputs: {
|
|
42
|
+
tokensFile: string;
|
|
43
|
+
tokensHash: string;
|
|
44
|
+
constraintsDir: string;
|
|
45
|
+
constraintHashes: Record<string, string>;
|
|
46
|
+
breakpoint?: string;
|
|
47
|
+
};
|
|
48
|
+
config: {
|
|
49
|
+
failOn: 'off' | 'warn' | 'error';
|
|
50
|
+
overrides?: string[];
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Convert internal ConstraintIssue to standardized ConstraintViolation format
|
|
55
|
+
*/
|
|
56
|
+
export declare function formatViolation(issue: ConstraintIssue): ConstraintViolation;
|
|
57
|
+
/**
|
|
58
|
+
* Generate a ValidationResult from collected issues
|
|
59
|
+
*/
|
|
60
|
+
export declare function createValidationResult(errors: ConstraintIssue[], warnings: ConstraintIssue[], durationMs: number, engineVersion: string): ValidationResult;
|
|
61
|
+
/**
|
|
62
|
+
* Generate a ValidationReceipt with full audit trail
|
|
63
|
+
*/
|
|
64
|
+
export declare function createValidationReceipt(result: ValidationResult, tokensFile: string, constraintsDir: string, breakpoint: string | undefined, failOn: 'off' | 'warn' | 'error'): ValidationReceipt;
|
|
65
|
+
/**
|
|
66
|
+
* Write JSON output to file or stdout
|
|
67
|
+
*/
|
|
68
|
+
export declare function writeJsonOutput(data: unknown, outputPath?: string): void;
|
|
69
|
+
//# sourceMappingURL=json-output.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json-output.d.ts","sourceRoot":"","sources":["json-output.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAMzD,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC;IAC3B,OAAO,CAAC,EAAE;QACR,MAAM,CAAC,EAAE,OAAO,CAAC;QACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;KACxB,CAAC;CACH;AAED,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE;QACN,OAAO,EAAE,MAAM,CAAC;QAChB,UAAU,EAAE,MAAM,CAAC;QACnB,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,UAAU,EAAE,mBAAmB,EAAE,CAAC;IAClC,QAAQ,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACjC,KAAK,EAAE;QACL,UAAU,EAAE,MAAM,CAAC;QACnB,aAAa,EAAE,MAAM,CAAC;QACtB,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,GAAG,EAAE;QACH,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC;CACH;AAED,MAAM,WAAW,iBAAkB,SAAQ,gBAAgB;IACzD,WAAW,EAAE;QACX,WAAW,EAAE,MAAM,CAAC;QACpB,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;IACF,MAAM,EAAE;QACN,UAAU,EAAE,MAAM,CAAC;QACnB,UAAU,EAAE,MAAM,CAAC;QACnB,cAAc,EAAE,MAAM,CAAC;QACvB,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACzC,UAAU,CAAC,EAAE,MAAM,CAAC;KACrB,CAAC;IACF,MAAM,EAAE;QACN,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,CAAC;QACjC,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;KACtB,CAAC;CACH;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,eAAe,GAAG,mBAAmB,CAiB3E;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,eAAe,EAAE,EACzB,QAAQ,EAAE,eAAe,EAAE,EAC3B,UAAU,EAAE,MAAM,EAClB,aAAa,EAAE,MAAM,GACpB,gBAAgB,CAoBlB;AAcD;;GAEG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE,gBAAgB,EACxB,UAAU,EAAE,MAAM,EAClB,cAAc,EAAE,MAAM,EACtB,UAAU,EAAE,MAAM,GAAG,SAAS,EAC9B,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,GAC/B,iBAAiB,CAoCnB;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,OAAO,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CASxE"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { getVersionInfo } from './version-banner.js';
|
|
5
|
+
/**
|
|
6
|
+
* Convert internal ConstraintIssue to standardized ConstraintViolation format
|
|
7
|
+
*/
|
|
8
|
+
export function formatViolation(issue) {
|
|
9
|
+
const violation = {
|
|
10
|
+
ruleId: issue.rule,
|
|
11
|
+
level: issue.level === 'error' ? 'error' : 'warn',
|
|
12
|
+
message: issue.message,
|
|
13
|
+
};
|
|
14
|
+
if (issue.id) {
|
|
15
|
+
violation.nodes = [issue.id];
|
|
16
|
+
}
|
|
17
|
+
// Add context from where field or other metadata
|
|
18
|
+
if (issue.where) {
|
|
19
|
+
violation.context = { where: issue.where };
|
|
20
|
+
}
|
|
21
|
+
return violation;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Generate a ValidationResult from collected issues
|
|
25
|
+
*/
|
|
26
|
+
export function createValidationResult(errors, warnings, durationMs, engineVersion) {
|
|
27
|
+
const violations = errors.map(formatViolation);
|
|
28
|
+
const warningViolations = warnings.map(formatViolation);
|
|
29
|
+
return {
|
|
30
|
+
ok: errors.length === 0,
|
|
31
|
+
counts: {
|
|
32
|
+
checked: errors.length + warnings.length,
|
|
33
|
+
violations: errors.length,
|
|
34
|
+
warnings: warnings.length,
|
|
35
|
+
},
|
|
36
|
+
violations,
|
|
37
|
+
warnings: warningViolations.length > 0 ? warningViolations : undefined,
|
|
38
|
+
stats: {
|
|
39
|
+
durationMs: Math.round(durationMs),
|
|
40
|
+
engineVersion,
|
|
41
|
+
timestamp: new Date().toISOString(),
|
|
42
|
+
},
|
|
43
|
+
dcv: getVersionInfo(),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Calculate SHA-256 hash of a file
|
|
48
|
+
*/
|
|
49
|
+
function hashFile(filePath) {
|
|
50
|
+
try {
|
|
51
|
+
const content = fs.readFileSync(filePath);
|
|
52
|
+
return 'sha256:' + createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return 'sha256:unknown';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Generate a ValidationReceipt with full audit trail
|
|
60
|
+
*/
|
|
61
|
+
export function createValidationReceipt(result, tokensFile, constraintsDir, breakpoint, failOn) {
|
|
62
|
+
// Hash the tokens file
|
|
63
|
+
const tokensHash = hashFile(tokensFile);
|
|
64
|
+
// Hash all constraint files in the directory
|
|
65
|
+
const constraintHashes = {};
|
|
66
|
+
try {
|
|
67
|
+
const files = fs.readdirSync(constraintsDir);
|
|
68
|
+
for (const file of files) {
|
|
69
|
+
if (file.endsWith('.json')) {
|
|
70
|
+
const filePath = path.join(constraintsDir, file);
|
|
71
|
+
constraintHashes[file] = hashFile(filePath);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// If constraintsDir doesn't exist, just use empty hashes
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
...result,
|
|
80
|
+
environment: {
|
|
81
|
+
nodeVersion: process.version,
|
|
82
|
+
platform: process.platform,
|
|
83
|
+
arch: process.arch,
|
|
84
|
+
},
|
|
85
|
+
inputs: {
|
|
86
|
+
tokensFile,
|
|
87
|
+
tokensHash,
|
|
88
|
+
constraintsDir,
|
|
89
|
+
constraintHashes,
|
|
90
|
+
breakpoint,
|
|
91
|
+
},
|
|
92
|
+
config: {
|
|
93
|
+
failOn,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Write JSON output to file or stdout
|
|
99
|
+
*/
|
|
100
|
+
export function writeJsonOutput(data, outputPath) {
|
|
101
|
+
const json = JSON.stringify(data, null, 2);
|
|
102
|
+
if (outputPath) {
|
|
103
|
+
fs.writeFileSync(outputPath, json, 'utf-8');
|
|
104
|
+
console.error(`✓ Output written to ${outputPath}`);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
console.log(json);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import type { ConstraintIssue } from '../core/engine.js';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { getVersionInfo } from './version-banner.js';
|
|
6
|
+
|
|
7
|
+
export interface ConstraintViolation {
|
|
8
|
+
ruleId: string;
|
|
9
|
+
level: 'error' | 'warn';
|
|
10
|
+
message: string;
|
|
11
|
+
nodes?: string[];
|
|
12
|
+
edges?: [string, string][];
|
|
13
|
+
context?: {
|
|
14
|
+
actual?: unknown;
|
|
15
|
+
expected?: unknown;
|
|
16
|
+
threshold?: number;
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ValidationResult {
|
|
22
|
+
ok: boolean;
|
|
23
|
+
counts: {
|
|
24
|
+
checked: number;
|
|
25
|
+
violations: number;
|
|
26
|
+
warnings: number;
|
|
27
|
+
};
|
|
28
|
+
violations: ConstraintViolation[];
|
|
29
|
+
warnings?: ConstraintViolation[];
|
|
30
|
+
stats: {
|
|
31
|
+
durationMs: number;
|
|
32
|
+
engineVersion: string;
|
|
33
|
+
timestamp: string;
|
|
34
|
+
};
|
|
35
|
+
dcv: {
|
|
36
|
+
name: string;
|
|
37
|
+
version: string;
|
|
38
|
+
repository: string;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ValidationReceipt extends ValidationResult {
|
|
43
|
+
environment: {
|
|
44
|
+
nodeVersion: string;
|
|
45
|
+
platform: string;
|
|
46
|
+
arch: string;
|
|
47
|
+
};
|
|
48
|
+
inputs: {
|
|
49
|
+
tokensFile: string;
|
|
50
|
+
tokensHash: string;
|
|
51
|
+
constraintsDir: string;
|
|
52
|
+
constraintHashes: Record<string, string>;
|
|
53
|
+
breakpoint?: string;
|
|
54
|
+
};
|
|
55
|
+
config: {
|
|
56
|
+
failOn: 'off' | 'warn' | 'error';
|
|
57
|
+
overrides?: string[];
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Convert internal ConstraintIssue to standardized ConstraintViolation format
|
|
63
|
+
*/
|
|
64
|
+
export function formatViolation(issue: ConstraintIssue): ConstraintViolation {
|
|
65
|
+
const violation: ConstraintViolation = {
|
|
66
|
+
ruleId: issue.rule,
|
|
67
|
+
level: issue.level === 'error' ? 'error' : 'warn',
|
|
68
|
+
message: issue.message,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
if (issue.id) {
|
|
72
|
+
violation.nodes = [issue.id];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Add context from where field or other metadata
|
|
76
|
+
if (issue.where) {
|
|
77
|
+
violation.context = { where: issue.where };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return violation;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Generate a ValidationResult from collected issues
|
|
85
|
+
*/
|
|
86
|
+
export function createValidationResult(
|
|
87
|
+
errors: ConstraintIssue[],
|
|
88
|
+
warnings: ConstraintIssue[],
|
|
89
|
+
durationMs: number,
|
|
90
|
+
engineVersion: string
|
|
91
|
+
): ValidationResult {
|
|
92
|
+
const violations = errors.map(formatViolation);
|
|
93
|
+
const warningViolations = warnings.map(formatViolation);
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
ok: errors.length === 0,
|
|
97
|
+
counts: {
|
|
98
|
+
checked: errors.length + warnings.length,
|
|
99
|
+
violations: errors.length,
|
|
100
|
+
warnings: warnings.length,
|
|
101
|
+
},
|
|
102
|
+
violations,
|
|
103
|
+
warnings: warningViolations.length > 0 ? warningViolations : undefined,
|
|
104
|
+
stats: {
|
|
105
|
+
durationMs: Math.round(durationMs),
|
|
106
|
+
engineVersion,
|
|
107
|
+
timestamp: new Date().toISOString(),
|
|
108
|
+
},
|
|
109
|
+
dcv: getVersionInfo(),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Calculate SHA-256 hash of a file
|
|
115
|
+
*/
|
|
116
|
+
function hashFile(filePath: string): string {
|
|
117
|
+
try {
|
|
118
|
+
const content = fs.readFileSync(filePath);
|
|
119
|
+
return 'sha256:' + createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
120
|
+
} catch {
|
|
121
|
+
return 'sha256:unknown';
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Generate a ValidationReceipt with full audit trail
|
|
127
|
+
*/
|
|
128
|
+
export function createValidationReceipt(
|
|
129
|
+
result: ValidationResult,
|
|
130
|
+
tokensFile: string,
|
|
131
|
+
constraintsDir: string,
|
|
132
|
+
breakpoint: string | undefined,
|
|
133
|
+
failOn: 'off' | 'warn' | 'error'
|
|
134
|
+
): ValidationReceipt {
|
|
135
|
+
// Hash the tokens file
|
|
136
|
+
const tokensHash = hashFile(tokensFile);
|
|
137
|
+
|
|
138
|
+
// Hash all constraint files in the directory
|
|
139
|
+
const constraintHashes: Record<string, string> = {};
|
|
140
|
+
try {
|
|
141
|
+
const files = fs.readdirSync(constraintsDir);
|
|
142
|
+
for (const file of files) {
|
|
143
|
+
if (file.endsWith('.json')) {
|
|
144
|
+
const filePath = path.join(constraintsDir, file);
|
|
145
|
+
constraintHashes[file] = hashFile(filePath);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
} catch {
|
|
149
|
+
// If constraintsDir doesn't exist, just use empty hashes
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return {
|
|
153
|
+
...result,
|
|
154
|
+
environment: {
|
|
155
|
+
nodeVersion: process.version,
|
|
156
|
+
platform: process.platform,
|
|
157
|
+
arch: process.arch,
|
|
158
|
+
},
|
|
159
|
+
inputs: {
|
|
160
|
+
tokensFile,
|
|
161
|
+
tokensHash,
|
|
162
|
+
constraintsDir,
|
|
163
|
+
constraintHashes,
|
|
164
|
+
breakpoint,
|
|
165
|
+
},
|
|
166
|
+
config: {
|
|
167
|
+
failOn,
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Write JSON output to file or stdout
|
|
174
|
+
*/
|
|
175
|
+
export function writeJsonOutput(data: unknown, outputPath?: string): void {
|
|
176
|
+
const json = JSON.stringify(data, null, 2);
|
|
177
|
+
|
|
178
|
+
if (outputPath) {
|
|
179
|
+
fs.writeFileSync(outputPath, json, 'utf-8');
|
|
180
|
+
console.error(`✓ Output written to ${outputPath}`);
|
|
181
|
+
} else {
|
|
182
|
+
console.log(json);
|
|
183
|
+
}
|
|
184
|
+
}
|
package/cli/result.ts
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
export type Ok<T> = { ok: true; value: T };
|
|
2
|
-
export type Err<E> = { ok: false; error: E };
|
|
3
|
-
export type Result<T, E = Error> = Ok<T> | Err<E>;
|
|
4
|
-
|
|
5
|
-
export function ok<T>(value: T): Ok<T> { return { ok: true, value }; }
|
|
6
|
-
export function err<E>(error: E): Err<E> { return { ok: false, error }; }
|
|
7
|
-
|
|
8
|
-
export function wrap<T>(fn: () => T): Result<T, unknown> {
|
|
9
|
-
try { return ok(fn()); } catch (e) { return err(e); }
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export async function wrapAsync<T>(fn: () => Promise<T>): Promise<Result<T, unknown>> {
|
|
13
|
-
try { return ok(await fn()); } catch (e) { return err(e); }
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function map<T, U, E>(r: Result<T, E>, f: (v: T) => U): Result<U, E> {
|
|
17
|
-
return r.ok ? ok(f(r.value)) : r;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function chain<T, U, E>(r: Result<T, E>, f: (v: T) => Result<U, E>): Result<U, E> {
|
|
21
|
-
return r.ok ? f(r.value) : r;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function getOrThrow<T, E>(r: Result<T, E>): T {
|
|
25
|
-
if (!r.ok) throw r.error as any; // consumer decides how to handle
|
|
26
|
-
return r.value;
|
|
27
|
-
}
|
|
1
|
+
export type Ok<T> = { ok: true; value: T };
|
|
2
|
+
export type Err<E> = { ok: false; error: E };
|
|
3
|
+
export type Result<T, E = Error> = Ok<T> | Err<E>;
|
|
4
|
+
|
|
5
|
+
export function ok<T>(value: T): Ok<T> { return { ok: true, value }; }
|
|
6
|
+
export function err<E>(error: E): Err<E> { return { ok: false, error }; }
|
|
7
|
+
|
|
8
|
+
export function wrap<T>(fn: () => T): Result<T, unknown> {
|
|
9
|
+
try { return ok(fn()); } catch (e) { return err(e); }
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function wrapAsync<T>(fn: () => Promise<T>): Promise<Result<T, unknown>> {
|
|
13
|
+
try { return ok(await fn()); } catch (e) { return err(e); }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function map<T, U, E>(r: Result<T, E>, f: (v: T) => U): Result<U, E> {
|
|
17
|
+
return r.ok ? ok(f(r.value)) : r;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function chain<T, U, E>(r: Result<T, E>, f: (v: T) => Result<U, E>): Result<U, E> {
|
|
21
|
+
return r.ok ? f(r.value) : r;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getOrThrow<T, E>(r: Result<T, E>): T {
|
|
25
|
+
if (!r.ok) throw r.error as any; // consumer decides how to handle
|
|
26
|
+
return r.value;
|
|
27
|
+
}
|
package/cli/run.ts
CHANGED
|
@@ -1,54 +1,54 @@
|
|
|
1
|
-
#!/usr/bin/env ts-node
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
3
|
-
import { flattenTokens } from "../core/flatten.js";
|
|
4
|
-
import { Engine } from "../core/engine.js";
|
|
5
|
-
import { WcagContrastPlugin } from "../core/constraints/wcag.js";
|
|
6
|
-
|
|
7
|
-
const tokensRoot = JSON.parse(readFileSync("tokens/tokens.example.json","utf8"));
|
|
8
|
-
const { flat, edges } = flattenTokens(tokensRoot);
|
|
9
|
-
|
|
10
|
-
// Build init values map
|
|
11
|
-
const init: Record<string, string|number> = {};
|
|
12
|
-
for (const [id, t] of Object.entries(flat)) init[id] = t.value;
|
|
13
|
-
|
|
14
|
-
// Engine + plugin (example pairs; adjust IDs to your roles)
|
|
15
|
-
const engine = new Engine(init, edges).use(
|
|
16
|
-
WcagContrastPlugin([
|
|
17
|
-
{ fg: "color.role.text.default", bg: "color.role.surface.default", min: 4.5, where: "Body text" },
|
|
18
|
-
{ fg: "color.role.accent.default", bg: "color.role.surface.default", min: 3.0, where: "Accent on surface" }
|
|
19
|
-
])
|
|
20
|
-
);
|
|
21
|
-
|
|
22
|
-
// Apply overrides if present
|
|
23
|
-
let overrides: Record<string, string|number> = {};
|
|
24
|
-
try {
|
|
25
|
-
const maybe = JSON.parse(readFileSync("tokens/overrides/local.json","utf8"));
|
|
26
|
-
overrides = maybe.overrides ?? maybe;
|
|
27
|
-
} catch {
|
|
28
|
-
// File doesn't exist or invalid JSON, use empty overrides
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
import { patchToJson } from "../adapters/json.js";
|
|
32
|
-
import yargs from "yargs/yargs";
|
|
33
|
-
import { hideBin } from "yargs/helpers";
|
|
34
|
-
|
|
35
|
-
const argvPromise = yargs(hideBin(process.argv)).option('json', {
|
|
36
|
-
alias: 'j',
|
|
37
|
-
type: 'boolean',
|
|
38
|
-
description: 'Output result as JSON'
|
|
39
|
-
}).argv;
|
|
40
|
-
|
|
41
|
-
// ... existing code ...
|
|
42
|
-
|
|
43
|
-
(async () => {
|
|
44
|
-
const argv = await argvPromise;
|
|
45
|
-
for (const [id, val] of Object.entries(overrides)) {
|
|
46
|
-
const res = engine.commit(id, val);
|
|
47
|
-
if (argv.json) {
|
|
48
|
-
const jsonOutput = patchToJson(res);
|
|
49
|
-
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
50
|
-
} else {
|
|
51
|
-
console.log(JSON.stringify(res, null, 2));
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
})();
|
|
1
|
+
#!/usr/bin/env ts-node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { flattenTokens } from "../core/flatten.js";
|
|
4
|
+
import { Engine } from "../core/engine.js";
|
|
5
|
+
import { WcagContrastPlugin } from "../core/constraints/wcag.js";
|
|
6
|
+
|
|
7
|
+
const tokensRoot = JSON.parse(readFileSync("tokens/tokens.example.json","utf8"));
|
|
8
|
+
const { flat, edges } = flattenTokens(tokensRoot);
|
|
9
|
+
|
|
10
|
+
// Build init values map
|
|
11
|
+
const init: Record<string, string|number> = {};
|
|
12
|
+
for (const [id, t] of Object.entries(flat)) init[id] = t.value;
|
|
13
|
+
|
|
14
|
+
// Engine + plugin (example pairs; adjust IDs to your roles)
|
|
15
|
+
const engine = new Engine(init, edges).use(
|
|
16
|
+
WcagContrastPlugin([
|
|
17
|
+
{ fg: "color.role.text.default", bg: "color.role.surface.default", min: 4.5, where: "Body text" },
|
|
18
|
+
{ fg: "color.role.accent.default", bg: "color.role.surface.default", min: 3.0, where: "Accent on surface" }
|
|
19
|
+
])
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
// Apply overrides if present
|
|
23
|
+
let overrides: Record<string, string|number> = {};
|
|
24
|
+
try {
|
|
25
|
+
const maybe = JSON.parse(readFileSync("tokens/overrides/local.json","utf8"));
|
|
26
|
+
overrides = maybe.overrides ?? maybe;
|
|
27
|
+
} catch {
|
|
28
|
+
// File doesn't exist or invalid JSON, use empty overrides
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
import { patchToJson } from "../adapters/json.js";
|
|
32
|
+
import yargs from "yargs/yargs";
|
|
33
|
+
import { hideBin } from "yargs/helpers";
|
|
34
|
+
|
|
35
|
+
const argvPromise = yargs(hideBin(process.argv)).option('json', {
|
|
36
|
+
alias: 'j',
|
|
37
|
+
type: 'boolean',
|
|
38
|
+
description: 'Output result as JSON'
|
|
39
|
+
}).argv;
|
|
40
|
+
|
|
41
|
+
// ... existing code ...
|
|
42
|
+
|
|
43
|
+
(async () => {
|
|
44
|
+
const argv = await argvPromise;
|
|
45
|
+
for (const [id, val] of Object.entries(overrides)) {
|
|
46
|
+
const res = engine.commit(id, val);
|
|
47
|
+
if (argv.json) {
|
|
48
|
+
const jsonOutput = patchToJson(res);
|
|
49
|
+
console.log(JSON.stringify(jsonOutput, null, 2));
|
|
50
|
+
} else {
|
|
51
|
+
console.log(JSON.stringify(res, null, 2));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
})();
|