design-constraint-validator 1.0.0 → 1.1.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.
Files changed (116) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +215 -659
  3. package/adapters/README.md +46 -46
  4. package/adapters/css.ts +116 -116
  5. package/adapters/js.ts +14 -14
  6. package/adapters/json.ts +45 -45
  7. package/cli/build-css.ts +32 -32
  8. package/cli/commands/build.ts +65 -65
  9. package/cli/commands/graph.d.ts.map +1 -1
  10. package/cli/commands/graph.js +26 -10
  11. package/cli/commands/graph.ts +180 -137
  12. package/cli/commands/index.ts +7 -7
  13. package/cli/commands/patch-apply.ts +80 -80
  14. package/cli/commands/patch.ts +22 -22
  15. package/cli/commands/set.d.ts.map +1 -1
  16. package/cli/commands/set.js +12 -4
  17. package/cli/commands/set.ts +239 -225
  18. package/cli/commands/utils.ts +50 -50
  19. package/cli/commands/validate.d.ts.map +1 -1
  20. package/cli/commands/validate.js +86 -33
  21. package/cli/commands/validate.ts +176 -115
  22. package/cli/commands/why.d.ts.map +1 -1
  23. package/cli/commands/why.js +86 -20
  24. package/cli/commands/why.ts +158 -46
  25. package/cli/config-schema.ts +27 -27
  26. package/cli/config.ts +35 -35
  27. package/cli/constraint-registry.d.ts +101 -0
  28. package/cli/constraint-registry.d.ts.map +1 -0
  29. package/cli/constraint-registry.js +225 -0
  30. package/cli/constraint-registry.ts +304 -0
  31. package/cli/constraints-loader.d.ts +30 -0
  32. package/cli/constraints-loader.d.ts.map +1 -0
  33. package/cli/constraints-loader.js +58 -0
  34. package/cli/constraints-loader.ts +83 -0
  35. package/cli/cross-axis-loader.d.ts +91 -0
  36. package/cli/cross-axis-loader.d.ts.map +1 -0
  37. package/cli/cross-axis-loader.js +222 -0
  38. package/cli/cross-axis-loader.ts +289 -0
  39. package/cli/dcv.js +4 -0
  40. package/cli/dcv.ts +111 -107
  41. package/cli/engine-helpers.d.ts +33 -0
  42. package/cli/engine-helpers.d.ts.map +1 -1
  43. package/cli/engine-helpers.js +87 -22
  44. package/cli/engine-helpers.ts +133 -61
  45. package/cli/graph-poset.ts +74 -74
  46. package/cli/json-output.d.ts +64 -0
  47. package/cli/json-output.d.ts.map +1 -0
  48. package/cli/json-output.js +107 -0
  49. package/cli/json-output.ts +177 -0
  50. package/cli/result.ts +27 -27
  51. package/cli/run.ts +54 -54
  52. package/cli/smoke-test.ts +40 -40
  53. package/cli/types.d.ts +6 -0
  54. package/cli/types.d.ts.map +1 -1
  55. package/cli/types.ts +84 -78
  56. package/core/breakpoints.ts +50 -50
  57. package/core/cli-format.ts +31 -31
  58. package/core/color.ts +148 -148
  59. package/core/constraints/cross-axis.ts +114 -114
  60. package/core/constraints/monotonic-lightness.ts +38 -38
  61. package/core/constraints/monotonic.ts +74 -74
  62. package/core/constraints/threshold.ts +43 -43
  63. package/core/constraints/wcag.ts +70 -70
  64. package/core/cross-axis-config.d.ts +29 -0
  65. package/core/cross-axis-config.d.ts.map +1 -1
  66. package/core/cross-axis-config.js +29 -0
  67. package/core/cross-axis-config.ts +181 -151
  68. package/core/engine.d.ts +95 -0
  69. package/core/engine.d.ts.map +1 -1
  70. package/core/engine.js +22 -0
  71. package/core/engine.ts +167 -65
  72. package/core/flatten.ts +116 -116
  73. package/core/image-export.ts +48 -48
  74. package/core/index.d.ts +9 -30
  75. package/core/index.d.ts.map +1 -1
  76. package/core/index.js +7 -54
  77. package/core/index.ts +10 -72
  78. package/core/patch.ts +134 -134
  79. package/core/poset.ts +311 -311
  80. package/core/why.ts +63 -63
  81. package/package.json +96 -90
  82. package/themes/color.lg.order.json +15 -15
  83. package/themes/color.md.order.json +15 -15
  84. package/themes/color.order.json +15 -15
  85. package/themes/color.sm.order.json +15 -15
  86. package/themes/cross-axis.rules.json +35 -35
  87. package/themes/cross-axis.sm.rules.json +12 -12
  88. package/themes/layout.lg.order.json +18 -18
  89. package/themes/layout.md.order.json +18 -18
  90. package/themes/layout.order.json +18 -18
  91. package/themes/layout.sm.order.json +18 -18
  92. package/themes/spacing.order.json +14 -14
  93. package/themes/typography.lg.order.json +15 -15
  94. package/themes/typography.md.order.json +15 -15
  95. package/themes/typography.order.json +15 -15
  96. package/themes/typography.sm.order.json +15 -15
  97. package/dist/test-overrides-removal.json +0 -4
  98. package/dist/tmp.patch.json +0 -35
  99. package/tokens/overrides/base.json +0 -22
  100. package/tokens/overrides/lg.json +0 -20
  101. package/tokens/overrides/md.json +0 -16
  102. package/tokens/overrides/sm.json +0 -16
  103. package/tokens/overrides/viol.color.json +0 -6
  104. package/tokens/overrides/viol.typography.json +0 -6
  105. package/tokens/tokens.demo-violations.json +0 -116
  106. package/tokens/tokens.example.json +0 -128
  107. package/tokens/tokens.json +0 -67
  108. package/tokens/tokens.multi-violations.json +0 -21
  109. package/tokens/tokens.schema.d.ts +0 -2298
  110. package/tokens/tokens.schema.d.ts.map +0 -1
  111. package/tokens/tokens.schema.js +0 -148
  112. package/tokens/tokens.schema.ts +0 -196
  113. package/tokens/tokens.test.json +0 -38
  114. package/tokens/tokens.touch-violation.json +0 -8
  115. package/tokens/typography.classes.css +0 -11
  116. package/tokens/typography.css +0 -20
@@ -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,64 @@
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
+ }
30
+ export interface ValidationReceipt extends ValidationResult {
31
+ environment: {
32
+ nodeVersion: string;
33
+ platform: string;
34
+ arch: string;
35
+ };
36
+ inputs: {
37
+ tokensFile: string;
38
+ tokensHash: string;
39
+ constraintsDir: string;
40
+ constraintHashes: Record<string, string>;
41
+ breakpoint?: string;
42
+ };
43
+ config: {
44
+ failOn: 'off' | 'warn' | 'error';
45
+ overrides?: string[];
46
+ };
47
+ }
48
+ /**
49
+ * Convert internal ConstraintIssue to standardized ConstraintViolation format
50
+ */
51
+ export declare function formatViolation(issue: ConstraintIssue): ConstraintViolation;
52
+ /**
53
+ * Generate a ValidationResult from collected issues
54
+ */
55
+ export declare function createValidationResult(errors: ConstraintIssue[], warnings: ConstraintIssue[], durationMs: number, engineVersion: string): ValidationResult;
56
+ /**
57
+ * Generate a ValidationReceipt with full audit trail
58
+ */
59
+ export declare function createValidationReceipt(result: ValidationResult, tokensFile: string, constraintsDir: string, breakpoint: string | undefined, failOn: 'off' | 'warn' | 'error'): ValidationReceipt;
60
+ /**
61
+ * Write JSON output to file or stdout
62
+ */
63
+ export declare function writeJsonOutput(data: unknown, outputPath?: string): void;
64
+ //# 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;AAKzD,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;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,CAmBlB;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,107 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { createHash } from 'node:crypto';
4
+ /**
5
+ * Convert internal ConstraintIssue to standardized ConstraintViolation format
6
+ */
7
+ export function formatViolation(issue) {
8
+ const violation = {
9
+ ruleId: issue.rule,
10
+ level: issue.level === 'error' ? 'error' : 'warn',
11
+ message: issue.message,
12
+ };
13
+ if (issue.id) {
14
+ violation.nodes = [issue.id];
15
+ }
16
+ // Add context from where field or other metadata
17
+ if (issue.where) {
18
+ violation.context = { where: issue.where };
19
+ }
20
+ return violation;
21
+ }
22
+ /**
23
+ * Generate a ValidationResult from collected issues
24
+ */
25
+ export function createValidationResult(errors, warnings, durationMs, engineVersion) {
26
+ const violations = errors.map(formatViolation);
27
+ const warningViolations = warnings.map(formatViolation);
28
+ return {
29
+ ok: errors.length === 0,
30
+ counts: {
31
+ checked: errors.length + warnings.length,
32
+ violations: errors.length,
33
+ warnings: warnings.length,
34
+ },
35
+ violations,
36
+ warnings: warningViolations.length > 0 ? warningViolations : undefined,
37
+ stats: {
38
+ durationMs: Math.round(durationMs),
39
+ engineVersion,
40
+ timestamp: new Date().toISOString(),
41
+ },
42
+ };
43
+ }
44
+ /**
45
+ * Calculate SHA-256 hash of a file
46
+ */
47
+ function hashFile(filePath) {
48
+ try {
49
+ const content = fs.readFileSync(filePath);
50
+ return 'sha256:' + createHash('sha256').update(content).digest('hex').slice(0, 16);
51
+ }
52
+ catch {
53
+ return 'sha256:unknown';
54
+ }
55
+ }
56
+ /**
57
+ * Generate a ValidationReceipt with full audit trail
58
+ */
59
+ export function createValidationReceipt(result, tokensFile, constraintsDir, breakpoint, failOn) {
60
+ // Hash the tokens file
61
+ const tokensHash = hashFile(tokensFile);
62
+ // Hash all constraint files in the directory
63
+ const constraintHashes = {};
64
+ try {
65
+ const files = fs.readdirSync(constraintsDir);
66
+ for (const file of files) {
67
+ if (file.endsWith('.json')) {
68
+ const filePath = path.join(constraintsDir, file);
69
+ constraintHashes[file] = hashFile(filePath);
70
+ }
71
+ }
72
+ }
73
+ catch {
74
+ // If constraintsDir doesn't exist, just use empty hashes
75
+ }
76
+ return {
77
+ ...result,
78
+ environment: {
79
+ nodeVersion: process.version,
80
+ platform: process.platform,
81
+ arch: process.arch,
82
+ },
83
+ inputs: {
84
+ tokensFile,
85
+ tokensHash,
86
+ constraintsDir,
87
+ constraintHashes,
88
+ breakpoint,
89
+ },
90
+ config: {
91
+ failOn,
92
+ },
93
+ };
94
+ }
95
+ /**
96
+ * Write JSON output to file or stdout
97
+ */
98
+ export function writeJsonOutput(data, outputPath) {
99
+ const json = JSON.stringify(data, null, 2);
100
+ if (outputPath) {
101
+ fs.writeFileSync(outputPath, json, 'utf-8');
102
+ console.error(`✓ Output written to ${outputPath}`);
103
+ }
104
+ else {
105
+ console.log(json);
106
+ }
107
+ }
@@ -0,0 +1,177 @@
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
+
6
+ export interface ConstraintViolation {
7
+ ruleId: string;
8
+ level: 'error' | 'warn';
9
+ message: string;
10
+ nodes?: string[];
11
+ edges?: [string, string][];
12
+ context?: {
13
+ actual?: unknown;
14
+ expected?: unknown;
15
+ threshold?: number;
16
+ [key: string]: unknown;
17
+ };
18
+ }
19
+
20
+ export interface ValidationResult {
21
+ ok: boolean;
22
+ counts: {
23
+ checked: number;
24
+ violations: number;
25
+ warnings: number;
26
+ };
27
+ violations: ConstraintViolation[];
28
+ warnings?: ConstraintViolation[];
29
+ stats: {
30
+ durationMs: number;
31
+ engineVersion: string;
32
+ timestamp: string;
33
+ };
34
+ }
35
+
36
+ export interface ValidationReceipt extends ValidationResult {
37
+ environment: {
38
+ nodeVersion: string;
39
+ platform: string;
40
+ arch: string;
41
+ };
42
+ inputs: {
43
+ tokensFile: string;
44
+ tokensHash: string;
45
+ constraintsDir: string;
46
+ constraintHashes: Record<string, string>;
47
+ breakpoint?: string;
48
+ };
49
+ config: {
50
+ failOn: 'off' | 'warn' | 'error';
51
+ overrides?: string[];
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Convert internal ConstraintIssue to standardized ConstraintViolation format
57
+ */
58
+ export function formatViolation(issue: ConstraintIssue): ConstraintViolation {
59
+ const violation: ConstraintViolation = {
60
+ ruleId: issue.rule,
61
+ level: issue.level === 'error' ? 'error' : 'warn',
62
+ message: issue.message,
63
+ };
64
+
65
+ if (issue.id) {
66
+ violation.nodes = [issue.id];
67
+ }
68
+
69
+ // Add context from where field or other metadata
70
+ if (issue.where) {
71
+ violation.context = { where: issue.where };
72
+ }
73
+
74
+ return violation;
75
+ }
76
+
77
+ /**
78
+ * Generate a ValidationResult from collected issues
79
+ */
80
+ export function createValidationResult(
81
+ errors: ConstraintIssue[],
82
+ warnings: ConstraintIssue[],
83
+ durationMs: number,
84
+ engineVersion: string
85
+ ): ValidationResult {
86
+ const violations = errors.map(formatViolation);
87
+ const warningViolations = warnings.map(formatViolation);
88
+
89
+ return {
90
+ ok: errors.length === 0,
91
+ counts: {
92
+ checked: errors.length + warnings.length,
93
+ violations: errors.length,
94
+ warnings: warnings.length,
95
+ },
96
+ violations,
97
+ warnings: warningViolations.length > 0 ? warningViolations : undefined,
98
+ stats: {
99
+ durationMs: Math.round(durationMs),
100
+ engineVersion,
101
+ timestamp: new Date().toISOString(),
102
+ },
103
+ };
104
+ }
105
+
106
+ /**
107
+ * Calculate SHA-256 hash of a file
108
+ */
109
+ function hashFile(filePath: string): string {
110
+ try {
111
+ const content = fs.readFileSync(filePath);
112
+ return 'sha256:' + createHash('sha256').update(content).digest('hex').slice(0, 16);
113
+ } catch {
114
+ return 'sha256:unknown';
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Generate a ValidationReceipt with full audit trail
120
+ */
121
+ export function createValidationReceipt(
122
+ result: ValidationResult,
123
+ tokensFile: string,
124
+ constraintsDir: string,
125
+ breakpoint: string | undefined,
126
+ failOn: 'off' | 'warn' | 'error'
127
+ ): ValidationReceipt {
128
+ // Hash the tokens file
129
+ const tokensHash = hashFile(tokensFile);
130
+
131
+ // Hash all constraint files in the directory
132
+ const constraintHashes: Record<string, string> = {};
133
+ try {
134
+ const files = fs.readdirSync(constraintsDir);
135
+ for (const file of files) {
136
+ if (file.endsWith('.json')) {
137
+ const filePath = path.join(constraintsDir, file);
138
+ constraintHashes[file] = hashFile(filePath);
139
+ }
140
+ }
141
+ } catch {
142
+ // If constraintsDir doesn't exist, just use empty hashes
143
+ }
144
+
145
+ return {
146
+ ...result,
147
+ environment: {
148
+ nodeVersion: process.version,
149
+ platform: process.platform,
150
+ arch: process.arch,
151
+ },
152
+ inputs: {
153
+ tokensFile,
154
+ tokensHash,
155
+ constraintsDir,
156
+ constraintHashes,
157
+ breakpoint,
158
+ },
159
+ config: {
160
+ failOn,
161
+ },
162
+ };
163
+ }
164
+
165
+ /**
166
+ * Write JSON output to file or stdout
167
+ */
168
+ export function writeJsonOutput(data: unknown, outputPath?: string): void {
169
+ const json = JSON.stringify(data, null, 2);
170
+
171
+ if (outputPath) {
172
+ fs.writeFileSync(outputPath, json, 'utf-8');
173
+ console.error(`✓ Output written to ${outputPath}`);
174
+ } else {
175
+ console.log(json);
176
+ }
177
+ }
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
+ })();