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.
Files changed (121) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +229 -659
  3. package/adapters/README.md +46 -46
  4. package/adapters/css.ts +116 -116
  5. package/adapters/decisionthemes.d.ts +44 -0
  6. package/adapters/decisionthemes.d.ts.map +1 -0
  7. package/adapters/decisionthemes.js +35 -0
  8. package/adapters/decisionthemes.ts +59 -0
  9. package/adapters/js.ts +14 -14
  10. package/adapters/json.ts +45 -45
  11. package/cli/build-css.ts +32 -32
  12. package/cli/commands/build.ts +65 -65
  13. package/cli/commands/graph.d.ts.map +1 -1
  14. package/cli/commands/graph.js +26 -10
  15. package/cli/commands/graph.ts +180 -137
  16. package/cli/commands/index.ts +7 -7
  17. package/cli/commands/patch-apply.ts +80 -80
  18. package/cli/commands/patch.ts +22 -22
  19. package/cli/commands/set.d.ts.map +1 -1
  20. package/cli/commands/set.js +12 -4
  21. package/cli/commands/set.ts +239 -225
  22. package/cli/commands/utils.ts +50 -50
  23. package/cli/commands/validate.d.ts.map +1 -1
  24. package/cli/commands/validate.js +89 -33
  25. package/cli/commands/validate.ts +180 -115
  26. package/cli/commands/why.d.ts.map +1 -1
  27. package/cli/commands/why.js +86 -20
  28. package/cli/commands/why.ts +158 -46
  29. package/cli/config-schema.ts +27 -27
  30. package/cli/config.ts +35 -35
  31. package/cli/constraint-registry.d.ts +101 -0
  32. package/cli/constraint-registry.d.ts.map +1 -0
  33. package/cli/constraint-registry.js +225 -0
  34. package/cli/constraint-registry.ts +304 -0
  35. package/cli/constraints-loader.d.ts.map +1 -0
  36. package/cli/cross-axis-loader.d.ts +91 -0
  37. package/cli/cross-axis-loader.d.ts.map +1 -0
  38. package/cli/cross-axis-loader.js +222 -0
  39. package/cli/cross-axis-loader.ts +289 -0
  40. package/cli/dcv.js +4 -0
  41. package/cli/dcv.ts +111 -107
  42. package/cli/engine-helpers.d.ts.map +1 -1
  43. package/cli/graph-poset.ts +74 -74
  44. package/cli/json-output.d.ts +69 -0
  45. package/cli/json-output.d.ts.map +1 -0
  46. package/cli/json-output.js +109 -0
  47. package/cli/json-output.ts +184 -0
  48. package/cli/result.ts +27 -27
  49. package/cli/run.ts +54 -54
  50. package/cli/smoke-test.ts +40 -40
  51. package/cli/types.d.ts +6 -0
  52. package/cli/types.d.ts.map +1 -1
  53. package/cli/types.ts +84 -78
  54. package/cli/version-banner.d.ts +20 -0
  55. package/cli/version-banner.d.ts.map +1 -0
  56. package/cli/version-banner.js +49 -0
  57. package/cli/version-banner.ts +61 -0
  58. package/core/breakpoints.ts +50 -50
  59. package/core/cli-format.ts +31 -31
  60. package/core/color.ts +148 -148
  61. package/core/constraints/cross-axis.ts +114 -114
  62. package/core/constraints/monotonic-lightness.ts +38 -38
  63. package/core/constraints/monotonic.ts +74 -74
  64. package/core/constraints/threshold.ts +43 -43
  65. package/core/constraints/wcag.ts +70 -70
  66. package/core/cross-axis-config.d.ts.map +1 -1
  67. package/core/engine.d.ts +95 -0
  68. package/core/engine.d.ts.map +1 -1
  69. package/core/engine.js +22 -0
  70. package/core/engine.ts +167 -65
  71. package/core/flatten.ts +116 -116
  72. package/core/image-export.ts +48 -48
  73. package/core/index.d.ts +9 -30
  74. package/core/index.d.ts.map +1 -1
  75. package/core/index.js +7 -54
  76. package/core/index.ts +10 -72
  77. package/core/patch.ts +134 -134
  78. package/core/poset.ts +311 -311
  79. package/core/why.ts +63 -63
  80. package/package.json +96 -90
  81. package/themes/color.lg.order.json +15 -15
  82. package/themes/color.md.order.json +15 -15
  83. package/themes/color.order.json +15 -15
  84. package/themes/color.sm.order.json +15 -15
  85. package/themes/cross-axis.rules.json +35 -35
  86. package/themes/cross-axis.sm.rules.json +12 -12
  87. package/themes/layout.lg.order.json +18 -18
  88. package/themes/layout.md.order.json +18 -18
  89. package/themes/layout.order.json +18 -18
  90. package/themes/layout.sm.order.json +18 -18
  91. package/themes/spacing.order.json +14 -14
  92. package/themes/typography.lg.order.json +15 -15
  93. package/themes/typography.md.order.json +15 -15
  94. package/themes/typography.order.json +15 -15
  95. package/themes/typography.sm.order.json +15 -15
  96. package/cli/engine-helpers.d.ts +0 -8
  97. package/cli/engine-helpers.js +0 -70
  98. package/cli/engine-helpers.ts +0 -61
  99. package/core/cross-axis-config.d.ts +0 -5
  100. package/core/cross-axis-config.js +0 -144
  101. package/core/cross-axis-config.ts +0 -152
  102. package/dist/test-overrides-removal.json +0 -4
  103. package/dist/tmp.patch.json +0 -35
  104. package/tokens/overrides/base.json +0 -22
  105. package/tokens/overrides/lg.json +0 -20
  106. package/tokens/overrides/md.json +0 -16
  107. package/tokens/overrides/sm.json +0 -16
  108. package/tokens/overrides/viol.color.json +0 -6
  109. package/tokens/overrides/viol.typography.json +0 -6
  110. package/tokens/tokens.demo-violations.json +0 -116
  111. package/tokens/tokens.example.json +0 -128
  112. package/tokens/tokens.json +0 -67
  113. package/tokens/tokens.multi-violations.json +0 -21
  114. package/tokens/tokens.schema.d.ts +0 -2298
  115. package/tokens/tokens.schema.d.ts.map +0 -1
  116. package/tokens/tokens.schema.js +0 -148
  117. package/tokens/tokens.schema.ts +0 -196
  118. package/tokens/tokens.test.json +0 -38
  119. package/tokens/tokens.touch-violation.json +0 -8
  120. package/tokens/typography.classes.css +0 -11
  121. 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,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
+ })();