@transitrix/diagrams 1.6.0 → 1.8.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 (131) hide show
  1. package/dist/activities/layout.js +7 -7
  2. package/dist/activities/types.d.ts +4 -0
  3. package/dist/capability-map/dsm-schema.d.ts +1 -1
  4. package/dist/capability-map/dsm-schema.js +1 -1
  5. package/dist/capability-map/layout-tree.d.ts +5 -1
  6. package/dist/capability-map/layout-tree.js +11 -9
  7. package/dist/capability-map/render-capability-tree.d.ts +2 -9
  8. package/dist/capability-map/render-capability-tree.js +16 -28
  9. package/dist/codex/__tests__/example.test.d.ts +1 -0
  10. package/dist/codex/__tests__/example.test.js +33 -0
  11. package/dist/codex/__tests__/validate.test.d.ts +1 -0
  12. package/dist/codex/__tests__/validate.test.js +68 -0
  13. package/dist/codex/index.d.ts +4 -0
  14. package/dist/codex/index.js +2 -0
  15. package/dist/codex/types.d.ts +7 -0
  16. package/dist/codex/types.js +8 -0
  17. package/dist/codex/validate.d.ts +11 -0
  18. package/dist/codex/validate.js +103 -0
  19. package/dist/compliance/__tests__/canon-catalog.test.d.ts +1 -0
  20. package/dist/compliance/__tests__/canon-catalog.test.js +60 -0
  21. package/dist/compliance/__tests__/view-resolution.test.d.ts +1 -0
  22. package/dist/compliance/__tests__/view-resolution.test.js +59 -0
  23. package/dist/compliance/canon-catalog.d.ts +19 -0
  24. package/dist/compliance/canon-catalog.js +59 -0
  25. package/dist/compliance/coverage-metric.d.ts +12 -0
  26. package/dist/compliance/coverage-metric.js +35 -0
  27. package/dist/compliance/impact.d.ts +7 -1
  28. package/dist/compliance/impact.js +53 -0
  29. package/dist/compliance/index.d.ts +6 -4
  30. package/dist/compliance/index.js +3 -2
  31. package/dist/constraint/__tests__/example.test.d.ts +1 -0
  32. package/dist/constraint/__tests__/example.test.js +14 -0
  33. package/dist/constraint/__tests__/validate.test.d.ts +1 -0
  34. package/dist/constraint/__tests__/validate.test.js +58 -0
  35. package/dist/constraint/index.d.ts +4 -0
  36. package/dist/constraint/index.js +2 -0
  37. package/dist/constraint/types.d.ts +4 -0
  38. package/dist/constraint/types.js +2 -0
  39. package/dist/constraint/validate.d.ts +7 -0
  40. package/dist/constraint/validate.js +115 -0
  41. package/dist/fgca/__tests__/parse-canonical.test.js +31 -0
  42. package/dist/fgca/parse-canonical.d.ts +1 -1
  43. package/dist/fgca/parse-canonical.js +18 -4
  44. package/dist/fgca/preview-layout.d.ts +8 -4
  45. package/dist/fgca/preview-layout.js +13 -13
  46. package/dist/goals/GoalNode.d.ts +2 -1
  47. package/dist/goals/GoalNode.js +7 -2
  48. package/dist/goals/GoalTreeView.d.ts +3 -1
  49. package/dist/goals/GoalTreeView.js +31 -14
  50. package/dist/goals/__tests__/GoalTreeView.test.js +41 -0
  51. package/dist/goals/__tests__/mutations.test.js +39 -0
  52. package/dist/goals/__tests__/parse-canonical.test.js +2 -0
  53. package/dist/goals/layout.js +2 -1
  54. package/dist/goals/mutations.js +6 -6
  55. package/dist/goals/parse-canonical.d.ts +5 -0
  56. package/dist/goals/parse-canonical.js +5 -0
  57. package/dist/goals/types.d.ts +2 -0
  58. package/dist/node-size-presets.d.ts +50 -0
  59. package/dist/node-size-presets.js +103 -0
  60. package/dist/process-blueprint/layout.js +4 -47
  61. package/dist/repo-validate/types.js +2 -3
  62. package/dist/webview/__tests__/entity-text-layout.test.d.ts +1 -0
  63. package/dist/webview/__tests__/entity-text-layout.test.js +43 -0
  64. package/dist/webview/__tests__/entry.test.js +3 -3
  65. package/dist/webview/__tests__/render-blocks.test.js +1 -7
  66. package/dist/webview/__tests__/render-goals.test.js +17 -3
  67. package/dist/webview/entity-text-layout.d.ts +95 -0
  68. package/dist/webview/entity-text-layout.js +279 -0
  69. package/dist/webview/entry.js +7 -3
  70. package/dist/webview/notation-style.d.ts +1 -1
  71. package/dist/webview/notation-style.js +1 -1
  72. package/dist/webview/render-activities.d.ts +3 -1
  73. package/dist/webview/render-activities.js +29 -32
  74. package/dist/webview/render-blocks.d.ts +4 -1
  75. package/dist/webview/render-blocks.js +27 -108
  76. package/dist/webview/render-fgca.d.ts +15 -8
  77. package/dist/webview/render-fgca.js +41 -44
  78. package/dist/webview/render-goals.d.ts +2 -0
  79. package/dist/webview/render-goals.js +23 -37
  80. package/dist/webview/render-process-blueprint.d.ts +4 -10
  81. package/dist/webview/render-process-blueprint.js +18 -64
  82. package/package.json +1 -1
  83. package/src/activities/layout.ts +12 -7
  84. package/src/activities/types.ts +4 -0
  85. package/src/capability-map/dsm-schema.ts +1 -1
  86. package/src/capability-map/index.ts +1 -1
  87. package/src/capability-map/layout-tree.ts +16 -8
  88. package/src/capability-map/render-capability-tree.ts +18 -30
  89. package/src/codex/__tests__/example.test.ts +40 -0
  90. package/src/codex/__tests__/validate.test.ts +84 -0
  91. package/src/codex/index.ts +4 -0
  92. package/src/codex/types.ts +13 -0
  93. package/src/codex/validate.ts +125 -0
  94. package/src/compliance/__tests__/canon-catalog.test.ts +68 -0
  95. package/src/compliance/__tests__/view-resolution.test.ts +80 -0
  96. package/src/compliance/canon-catalog.ts +76 -0
  97. package/src/compliance/coverage-metric.ts +50 -0
  98. package/src/compliance/impact.ts +67 -1
  99. package/src/compliance/index.ts +10 -1
  100. package/src/constraint/__tests__/example.test.ts +19 -0
  101. package/src/constraint/__tests__/validate.test.ts +68 -0
  102. package/src/constraint/index.ts +4 -0
  103. package/src/constraint/types.ts +5 -0
  104. package/src/constraint/validate.ts +131 -0
  105. package/src/fgca/__tests__/parse-canonical.test.ts +34 -0
  106. package/src/fgca/parse-canonical.ts +17 -4
  107. package/src/fgca/preview-layout.ts +21 -15
  108. package/src/goals/GoalNode.tsx +8 -1
  109. package/src/goals/GoalTreeView.tsx +61 -27
  110. package/src/goals/__tests__/GoalTreeView.test.tsx +48 -0
  111. package/src/goals/__tests__/mutations.test.ts +42 -0
  112. package/src/goals/__tests__/parse-canonical.test.ts +2 -0
  113. package/src/goals/layout.ts +2 -1
  114. package/src/goals/mutations.ts +6 -6
  115. package/src/goals/parse-canonical.ts +6 -0
  116. package/src/goals/types.ts +2 -0
  117. package/src/node-size-presets.ts +157 -0
  118. package/src/process-blueprint/layout.ts +4 -48
  119. package/src/repo-validate/types.ts +2 -3
  120. package/src/webview/__tests__/entity-text-layout.test.ts +52 -0
  121. package/src/webview/__tests__/entry.test.ts +3 -3
  122. package/src/webview/__tests__/render-blocks.test.ts +1 -9
  123. package/src/webview/__tests__/render-goals.test.ts +17 -3
  124. package/src/webview/entity-text-layout.ts +355 -0
  125. package/src/webview/entry.ts +8 -4
  126. package/src/webview/notation-style.ts +1 -1
  127. package/src/webview/render-activities.ts +38 -27
  128. package/src/webview/render-blocks.ts +35 -118
  129. package/src/webview/render-fgca.ts +59 -44
  130. package/src/webview/render-goals.ts +24 -35
  131. package/src/webview/render-process-blueprint.ts +30 -65
@@ -1,10 +1,10 @@
1
1
  import { computeCpm } from './cpm.js';
2
- const NODE_W = 200;
3
- const NODE_H = 80;
2
+ const DEFAULT_NODE_W = 200;
3
+ const DEFAULT_NODE_H = 80;
4
4
  const H_GAP = 80;
5
5
  const V_GAP = 24;
6
6
  export function layoutActivities(doc, options = {}) {
7
- const { horizontalGap = H_GAP, verticalGap = V_GAP } = options;
7
+ const { horizontalGap = H_GAP, verticalGap = V_GAP, nodeWidth = DEFAULT_NODE_W, nodeHeight = DEFAULT_NODE_H, } = options;
8
8
  const activities = doc.activities;
9
9
  if (activities.length === 0) {
10
10
  return { nodes: [], edges: [], bounds: { x: 0, y: 0, width: 0, height: 0 } };
@@ -64,21 +64,21 @@ export function layoutActivities(doc, options = {}) {
64
64
  const colCount = Math.max(0, ...[...cols.keys()]) + 1;
65
65
  for (let c = 0; c < colCount; c++) {
66
66
  const list = cols.get(c) ?? [];
67
- const x = c * (NODE_W + horizontalGap);
67
+ const x = c * (nodeWidth + horizontalGap);
68
68
  let y = 0;
69
69
  for (const a of list) {
70
70
  const node = {
71
71
  id: a.id,
72
72
  x,
73
73
  y,
74
- width: NODE_W,
75
- height: NODE_H,
74
+ width: nodeWidth,
75
+ height: nodeHeight,
76
76
  data: a,
77
77
  cpm: cpm.get(a.id),
78
78
  };
79
79
  nodes.push(node);
80
80
  nodeMap.set(a.id, node);
81
- y += NODE_H + verticalGap;
81
+ y += nodeHeight + verticalGap;
82
82
  }
83
83
  }
84
84
  // Build edges
@@ -86,6 +86,10 @@ export interface ActivitiesLayoutOptions {
86
86
  horizontalGap?: number;
87
87
  /** Vertical gap (px) between stacked activity nodes. Default matches the historical hardcoded value. */
88
88
  verticalGap?: number;
89
+ /** Activity node width (px). Default 200. */
90
+ nodeWidth?: number;
91
+ /** Activity node height (px). Default 80. */
92
+ nodeHeight?: number;
89
93
  }
90
94
  export type GanttMode = 'computed' | 'pinned';
91
95
  export type GanttBarKind = 'leaf' | 'milestone' | 'phase';
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Types for the DSM-migration capability map (docs/extraction/capability-map.md).
2
+ // Types for the DSM-migration capability map (docs/internal/extraction/capability-map.md).
3
3
  *
4
4
  * Distinct from `./types.ts` (`CapabilityNode`/`CapabilityMapFile`), which is
5
5
  * Transitrix's own native `*.capability-map.transitrix.yaml` notation — a
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Types for the DSM-migration capability map (docs/extraction/capability-map.md).
2
+ // Types for the DSM-migration capability map (docs/internal/extraction/capability-map.md).
3
3
  *
4
4
  * Distinct from `./types.ts` (`CapabilityNode`/`CapabilityMapFile`), which is
5
5
  * Transitrix's own native `*.capability-map.transitrix.yaml` notation — a
@@ -39,4 +39,8 @@ export interface CapabilityTreeLayout {
39
39
  };
40
40
  levelCounts: CapabilityTreeLevelCounts;
41
41
  }
42
- export declare function layoutCapabilityTree(map: CapabilityMapHeader, collapsedIds?: Set<string>): CapabilityTreeLayout;
42
+ export interface CapabilityTreeLayoutOptions {
43
+ nodeWidth?: number;
44
+ nodeHeight?: number;
45
+ }
46
+ export declare function layoutCapabilityTree(map: CapabilityMapHeader, collapsedIds?: Set<string>, options?: CapabilityTreeLayoutOptions): CapabilityTreeLayout;
@@ -4,7 +4,9 @@ export const TREE_NODE_HEIGHT = 64;
4
4
  export const TREE_RANK_SEP = 80;
5
5
  /** Vertical gap between sibling nodes. */
6
6
  export const TREE_NODE_SEP = 24;
7
- export function layoutCapabilityTree(map, collapsedIds = new Set()) {
7
+ export function layoutCapabilityTree(map, collapsedIds = new Set(), options = {}) {
8
+ const nodeWidth = options.nodeWidth ?? TREE_NODE_WIDTH;
9
+ const nodeHeight = options.nodeHeight ?? TREE_NODE_HEIGHT;
8
10
  const nodes = [];
9
11
  const edges = [];
10
12
  const colNextY = new Map();
@@ -18,12 +20,12 @@ export function layoutCapabilityTree(map, collapsedIds = new Set()) {
18
20
  const isCollapsed = collapsedIds.has(node.id);
19
21
  const hasChildren = Boolean(node.children?.length);
20
22
  const visibleChildren = !isCollapsed && node.children?.length ? node.children : [];
21
- const x = depth * (TREE_NODE_WIDTH + TREE_RANK_SEP);
23
+ const x = depth * (nodeWidth + TREE_RANK_SEP);
22
24
  if (visibleChildren.length === 0) {
23
25
  const y = getNextY(depth);
24
- advanceY(depth, TREE_NODE_HEIGHT + TREE_NODE_SEP);
25
- nodes.push({ id: node.id, x, y, width: TREE_NODE_WIDTH, height: TREE_NODE_HEIGHT, data: node, depth, hasChildren, isCollapsed });
26
- return { top: y, bottom: y + TREE_NODE_HEIGHT };
26
+ advanceY(depth, nodeHeight + TREE_NODE_SEP);
27
+ nodes.push({ id: node.id, x, y, width: nodeWidth, height: nodeHeight, data: node, depth, hasChildren, isCollapsed });
28
+ return { top: y, bottom: y + nodeHeight };
27
29
  }
28
30
  const childSpans = [];
29
31
  for (const child of visibleChildren) {
@@ -32,12 +34,12 @@ export function layoutCapabilityTree(map, collapsedIds = new Set()) {
32
34
  }
33
35
  const spanTop = Math.min(...childSpans.map(s => s.top));
34
36
  const spanBottom = Math.max(...childSpans.map(s => s.bottom));
35
- const parentY = spanTop + (spanBottom - spanTop) / 2 - TREE_NODE_HEIGHT / 2;
37
+ const parentY = spanTop + (spanBottom - spanTop) / 2 - nodeHeight / 2;
36
38
  const curColY = getNextY(depth);
37
39
  const finalY = Math.max(parentY, curColY);
38
- advanceY(depth, finalY - curColY + TREE_NODE_HEIGHT + TREE_NODE_SEP);
39
- nodes.push({ id: node.id, x, y: finalY, width: TREE_NODE_WIDTH, height: TREE_NODE_HEIGHT, data: node, depth, hasChildren, isCollapsed });
40
- return { top: Math.min(finalY, spanTop), bottom: Math.max(finalY + TREE_NODE_HEIGHT, spanBottom) };
40
+ advanceY(depth, finalY - curColY + nodeHeight + TREE_NODE_SEP);
41
+ nodes.push({ id: node.id, x, y: finalY, width: nodeWidth, height: nodeHeight, data: node, depth, hasChildren, isCollapsed });
42
+ return { top: Math.min(finalY, spanTop), bottom: Math.max(finalY + nodeHeight, spanBottom) };
41
43
  }
42
44
  for (const root of map.capabilities) {
43
45
  placeNode(root, 0);
@@ -1,15 +1,8 @@
1
1
  import type { CapabilityMapHeader } from './types.js';
2
+ import { type NodeSizePreset } from '../node-size-presets.js';
2
3
  export interface RenderCapabilityTreeOptions {
3
4
  collapsedIds?: Set<string>;
4
5
  curvature?: number;
6
+ nodeSizePreset?: NodeSizePreset;
5
7
  }
6
- /**
7
- * Renders a capability-map as a left-to-right SVG node tree with DSM-matching
8
- * visual design: depth-banded fills, maturity badges, and +/− collapse buttons.
9
- *
10
- * The SVG uses CSS classes (`tree-level-N`, `tree-maturity-N`, `tree-collapse-btn`)
11
- * that must be resolved by the host's stylesheet (injected via `diagramClassCss()`
12
- * in themes.ts). Collapse buttons carry `data-node-id` / `data-collapsed` attributes
13
- * for the webview click handler.
14
- */
15
8
  export declare function renderCapabilityTreeSvg(map: CapabilityMapHeader, opts?: RenderCapabilityTreeOptions): string;
@@ -1,11 +1,11 @@
1
- import { layoutCapabilityTree, TREE_NODE_WIDTH, TREE_NODE_HEIGHT, TREE_NODE_SEP } from './layout-tree.js';
1
+ import { layoutCapabilityTree, TREE_NODE_SEP } from './layout-tree.js';
2
2
  import { horizontalCubicEdgePath, DEFAULT_EDGE_CURVATURE } from '../edge-path.js';
3
+ import { parseNodeSizePreset, resolveCapabilityMapNodeSize } from '../node-size-presets.js';
4
+ import { maxCharsForInnerWidth, truncateLine } from '../webview/entity-text-layout.js';
3
5
  import { escXml } from '../webview/render-util.js';
4
6
  const PAD = 24;
5
7
  const BTN_R = 8;
6
8
  const LEGEND_H = 40;
7
- const LABEL_MAX_CHARS = 28;
8
- const LABEL_TRIM_CHARS = 26;
9
9
  const BAND_LABELS = ['Levels 0–2', 'Levels 3–4', 'Levels 5+'];
10
10
  function depthBand(depth) {
11
11
  if (depth <= 2)
@@ -14,21 +14,10 @@ function depthBand(depth) {
14
14
  return 1;
15
15
  return 2;
16
16
  }
17
- function truncate(s, max, trim) {
18
- return s.length > max ? s.slice(0, trim) + '…' : s;
19
- }
20
- /**
21
- * Renders a capability-map as a left-to-right SVG node tree with DSM-matching
22
- * visual design: depth-banded fills, maturity badges, and +/− collapse buttons.
23
- *
24
- * The SVG uses CSS classes (`tree-level-N`, `tree-maturity-N`, `tree-collapse-btn`)
25
- * that must be resolved by the host's stylesheet (injected via `diagramClassCss()`
26
- * in themes.ts). Collapse buttons carry `data-node-id` / `data-collapsed` attributes
27
- * for the webview click handler.
28
- */
29
17
  export function renderCapabilityTreeSvg(map, opts = {}) {
30
- const { collapsedIds = new Set(), curvature = DEFAULT_EDGE_CURVATURE } = opts;
31
- const layout = layoutCapabilityTree(map, collapsedIds);
18
+ const { collapsedIds = new Set(), curvature = DEFAULT_EDGE_CURVATURE, nodeSizePreset = 'normal' } = opts;
19
+ const nodeSize = resolveCapabilityMapNodeSize(parseNodeSizePreset(nodeSizePreset));
20
+ const layout = layoutCapabilityTree(map, collapsedIds, nodeSize);
32
21
  if (layout.nodes.length === 0) {
33
22
  return `<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 0 0"></svg>`;
34
23
  }
@@ -53,26 +42,25 @@ export function renderCapabilityTreeSvg(map, opts = {}) {
53
42
  const y = n.y + oy;
54
43
  const band = depthBand(n.depth);
55
44
  const mat = Math.max(1, Math.min(5, n.data.current_maturity | 0));
56
- const nameText = truncate(n.data.name, LABEL_MAX_CHARS, LABEL_TRIM_CHARS);
57
- const nameTitle = n.data.name.length > LABEL_MAX_CHARS ? escXml(n.data.name) : '';
58
- // Maturity badge: left side of node
59
45
  const badgeW = 28;
60
46
  const badgeH = 18;
47
+ const textAreaW = Math.max(0, n.width - badgeW - 26);
48
+ const maxChars = maxCharsForInnerWidth(textAreaW, 7);
49
+ const nameText = truncateLine(n.data.name, maxChars);
50
+ const nameTitle = n.data.name.length > maxChars ? escXml(n.data.name) : '';
61
51
  const badgeX = x + 10;
62
- const badgeY = y + (TREE_NODE_HEIGHT - badgeH) / 2;
63
- // Text area: right of badge
52
+ const badgeY = y + (n.height - badgeH) / 2;
64
53
  const textX = badgeX + badgeW + 8;
65
- const nameY = y + TREE_NODE_HEIGHT / 2 - 8;
66
- const idY = y + TREE_NODE_HEIGHT / 2 + 9;
67
- // Collapse button: centered on right edge of node
68
- const btnCx = x + TREE_NODE_WIDTH;
69
- const btnCy = y + TREE_NODE_HEIGHT / 2;
54
+ const nameY = y + n.height / 2 - 8;
55
+ const idY = y + n.height / 2 + 9;
56
+ const btnCx = x + n.width;
57
+ const btnCy = y + n.height / 2;
70
58
  const collapseBtn = n.hasChildren
71
59
  ? `<circle class="tree-collapse-btn" cx="${btnCx}" cy="${btnCy}" r="${BTN_R}" data-node-id="${escXml(n.id)}" data-collapsed="${n.isCollapsed}"/>
72
60
  <text class="tree-collapse-lbl" x="${btnCx}" y="${btnCy}" text-anchor="middle">${n.isCollapsed ? '+' : '−'}</text>`
73
61
  : '';
74
62
  return `<g${nameTitle ? ` title="${nameTitle}"` : ''}>
75
- <rect class="tree-level-${band}" x="${x}" y="${y}" width="${TREE_NODE_WIDTH}" height="${TREE_NODE_HEIGHT}" rx="11" stroke="var(--ts-node-stroke,#94a3b8)" stroke-width="3"/>
63
+ <rect class="tree-level-${band}" x="${x}" y="${y}" width="${n.width}" height="${n.height}" rx="11" stroke="var(--ts-node-stroke,#94a3b8)" stroke-width="3"/>
76
64
  <rect class="tree-maturity-${mat}" x="${badgeX}" y="${badgeY}" width="${badgeW}" height="${badgeH}" rx="4"/>
77
65
  <text class="text-pill" x="${badgeX + badgeW / 2}" y="${badgeY + badgeH / 2}" text-anchor="middle" fill="white">L${mat}</text>
78
66
  <text class="text-primary" x="${textX}" y="${nameY}" dominant-baseline="central">${escXml(nameText)}</text>
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,33 @@
1
+ // Conformance test for CODEX worked examples under
2
+ // `tests/fixtures/notation-corpus/codex/` — pins #518 Phase C2 corpus signal.
3
+ import { describe, it, expect } from 'vitest';
4
+ import { readFileSync, readdirSync } from 'node:fs';
5
+ import { fileURLToPath } from 'node:url';
6
+ import path from 'node:path';
7
+ import yaml from 'js-yaml';
8
+ import { validateCodex, folderJurisdictionFromPath } from '../validate.js';
9
+ const corpusRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../../../tests/fixtures/notation-corpus/codex');
10
+ function codexFixturePaths() {
11
+ const out = [];
12
+ for (const zone of ['external/EU', 'internal']) {
13
+ const dir = path.join(corpusRoot, zone);
14
+ for (const f of readdirSync(dir).filter((x) => x.endsWith('.yaml'))) {
15
+ const rel = path.join('codex', zone, f).replace(/\\/g, '/');
16
+ out.push({
17
+ rel,
18
+ folderJurisdiction: folderJurisdictionFromPath(rel),
19
+ });
20
+ }
21
+ }
22
+ return out;
23
+ }
24
+ describe('codex worked examples (notation-corpus)', () => {
25
+ for (const { rel, folderJurisdiction } of codexFixturePaths()) {
26
+ it(`validates ${rel} without error`, () => {
27
+ const data = yaml.load(readFileSync(path.join(corpusRoot, rel.replace(/^codex\//, '')), 'utf-8'));
28
+ const v = validateCodex(data, { folderJurisdiction });
29
+ expect(v.valid, JSON.stringify(v.errors)).toBe(true);
30
+ expect(v.errors).toHaveLength(0);
31
+ });
32
+ }
33
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,68 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { validateCodex, isCodexDoc, folderJurisdictionFromPath, } from '../validate.js';
3
+ const VALID_EXTERNAL = {
4
+ zone: 'codex',
5
+ id: 'LAW-GDPR-1',
6
+ name: 'General Data Protection Regulation',
7
+ type: 'LAW',
8
+ jurisdiction: 'eu',
9
+ effective_date: '2018-05-25',
10
+ admitted_at: '2026-06-01',
11
+ admitted_by: 'legal.team',
12
+ gate_checks: { uniqueness: 'pass' },
13
+ };
14
+ const VALID_INTERNAL = {
15
+ zone: 'codex',
16
+ id: 'INTERNAL_STANDARD-coding-conventions-1',
17
+ name: 'Engineering Coding Conventions',
18
+ type: 'INTERNAL_STANDARD',
19
+ issuing_authority: 'VP Engineering',
20
+ effective_date: '2026-01-01',
21
+ admitted_at: '2026-05-27',
22
+ admitted_by: 'vp.engineering',
23
+ gate_checks: { source_authority: 'VP Engineering' },
24
+ };
25
+ describe('validateCodex', () => {
26
+ it('accepts a valid external artefact', () => {
27
+ const r = validateCodex(VALID_EXTERNAL, { folderJurisdiction: 'eu' });
28
+ expect(r.valid, JSON.stringify(r.errors)).toBe(true);
29
+ });
30
+ it('accepts a valid internal artefact', () => {
31
+ expect(validateCodex(VALID_INTERNAL).valid).toBe(true);
32
+ });
33
+ it('CODEX-001 rejects non-mapping input', () => {
34
+ expect(validateCodex(null).errors.some((e) => e.code === 'CODEX-001')).toBe(true);
35
+ });
36
+ it('CODEX-001 rejects wrong zone', () => {
37
+ const r = validateCodex({ ...VALID_EXTERNAL, zone: 'canon' });
38
+ expect(r.errors.some((e) => e.code === 'CODEX-001' && e.path === 'zone')).toBe(true);
39
+ });
40
+ it('CODEX-002 rejects type/id prefix mismatch', () => {
41
+ const r = validateCodex({ ...VALID_EXTERNAL, type: 'REGULATION' });
42
+ expect(r.errors.some((e) => e.code === 'CODEX-002')).toBe(true);
43
+ });
44
+ it('CODEX-003 requires jurisdiction on external artefacts', () => {
45
+ const r = validateCodex({ ...VALID_EXTERNAL, jurisdiction: '' });
46
+ expect(r.errors.some((e) => e.code === 'CODEX-003' && e.path === 'jurisdiction')).toBe(true);
47
+ });
48
+ it('CODEX-005 enforces folder jurisdiction match', () => {
49
+ const r = validateCodex(VALID_EXTERNAL, { folderJurisdiction: 'ge' });
50
+ expect(r.errors.some((e) => e.code === 'CODEX-005')).toBe(true);
51
+ });
52
+ it('CODEX-004 requires issuing_authority on internal artefacts', () => {
53
+ const r = validateCodex({ ...VALID_INTERNAL, issuing_authority: '' });
54
+ expect(r.errors.some((e) => e.code === 'CODEX-004')).toBe(true);
55
+ });
56
+ });
57
+ describe('isCodexDoc', () => {
58
+ it('detects zone: codex', () => {
59
+ expect(isCodexDoc({ zone: 'codex', id: 'LAW-1' })).toBe(true);
60
+ expect(isCodexDoc({ zone: 'canon' })).toBe(false);
61
+ });
62
+ });
63
+ describe('folderJurisdictionFromPath', () => {
64
+ it('parses codex/external/<jurisdiction>/ paths', () => {
65
+ expect(folderJurisdictionFromPath('codex/external/eu/LAW-GDPR-1.yaml')).toBe('eu');
66
+ expect(folderJurisdictionFromPath('codex/internal/POLICY-1.yaml')).toBeUndefined();
67
+ });
68
+ });
@@ -0,0 +1,4 @@
1
+ export { CODEX_ARTEFACT_TYPES, EXTERNAL_CODEX_TYPES, INTERNAL_CODEX_TYPES } from './types.js';
2
+ export type { CodexArtefactType } from './types.js';
3
+ export { validateCodex, isCodexDoc, folderJurisdictionFromPath } from './validate.js';
4
+ export type { CodexValidateOptions } from './validate.js';
@@ -0,0 +1,2 @@
1
+ export { CODEX_ARTEFACT_TYPES, EXTERNAL_CODEX_TYPES, INTERNAL_CODEX_TYPES } from './types.js';
2
+ export { validateCodex, isCodexDoc, folderJurisdictionFromPath } from './validate.js';
@@ -0,0 +1,7 @@
1
+ /** TYPE prefixes admitted in the codex zone (REQ-003 / 14-codex.md). */
2
+ export declare const CODEX_ARTEFACT_TYPES: readonly ["LAW", "REGULATION", "POLICY", "INTERNAL_STANDARD"];
3
+ export type CodexArtefactType = (typeof CODEX_ARTEFACT_TYPES)[number];
4
+ /** External codex artefacts (codex/external/<jurisdiction>/). */
5
+ export declare const EXTERNAL_CODEX_TYPES: readonly CodexArtefactType[];
6
+ /** Internal codex artefacts (codex/internal/). */
7
+ export declare const INTERNAL_CODEX_TYPES: readonly CodexArtefactType[];
@@ -0,0 +1,8 @@
1
+ // Codex artefact — external laws/regulations and internal policies/standards.
2
+ // Schema: methodology notations/elements/14-codex.md §3–§4.
3
+ /** TYPE prefixes admitted in the codex zone (REQ-003 / 14-codex.md). */
4
+ export const CODEX_ARTEFACT_TYPES = ['LAW', 'REGULATION', 'POLICY', 'INTERNAL_STANDARD'];
5
+ /** External codex artefacts (codex/external/<jurisdiction>/). */
6
+ export const EXTERNAL_CODEX_TYPES = ['LAW', 'REGULATION'];
7
+ /** Internal codex artefacts (codex/internal/). */
8
+ export const INTERNAL_CODEX_TYPES = ['POLICY', 'INTERNAL_STANDARD'];
@@ -0,0 +1,11 @@
1
+ import type { ValidationResult } from '../validation-types.js';
2
+ export interface CodexValidateOptions {
3
+ /** Lowercased jurisdiction folder segment from `codex/external/<jurisdiction>/`. */
4
+ folderJurisdiction?: string;
5
+ }
6
+ export declare function validateCodex(input: unknown, options?: CodexValidateOptions): ValidationResult;
7
+ /** True when a parsed document is a codex-zone artefact candidate. */
8
+ export declare function isCodexDoc(data: unknown): boolean;
9
+ /** Infer codex folder jurisdiction from a repo-relative path such as
10
+ * `codex/external/eu/LAW-GDPR-1.yaml` → `eu`. */
11
+ export declare function folderJurisdictionFromPath(filePath: string): string | undefined;
@@ -0,0 +1,103 @@
1
+ // CODEX validator — methodology notations/elements/14-codex.md §4.
2
+ //
3
+ // CODEX-001 — shape, zone, id grammar, admission envelope.
4
+ // CODEX-002 — `type` field matches the id TYPE prefix when present.
5
+ // CODEX-003 — external artefact frontmatter (jurisdiction, effective_date).
6
+ // CODEX-004 — internal artefact frontmatter (issuing_authority, effective_date).
7
+ // CODEX-005 — jurisdiction must match the parent folder under codex/external/
8
+ // (when `folderJurisdiction` is supplied by the repo sweep).
9
+ import { isCanonicalId, typeOfId } from '../typed-id.js';
10
+ import { CODEX_ARTEFACT_TYPES, EXTERNAL_CODEX_TYPES, INTERNAL_CODEX_TYPES, } from './types.js';
11
+ function isCodexType(type) {
12
+ return type !== null && CODEX_ARTEFACT_TYPES.includes(type);
13
+ }
14
+ function normalizeType(value) {
15
+ if (typeof value !== 'string' || value.trim() === '')
16
+ return undefined;
17
+ return value.trim().toUpperCase();
18
+ }
19
+ export function validateCodex(input, options = {}) {
20
+ const errors = [];
21
+ const warnings = [];
22
+ if (input === null || typeof input !== 'object' || Array.isArray(input)) {
23
+ errors.push({ code: 'CODEX-001', message: 'Codex artefact must be a YAML mapping.' });
24
+ return { valid: false, errors, warnings };
25
+ }
26
+ const c = input;
27
+ if (c.zone !== 'codex') {
28
+ errors.push({ code: 'CODEX-001', message: 'zone must be the fixed value "codex".', path: 'zone' });
29
+ }
30
+ const idType = typeOfId(c.id);
31
+ if (!isCanonicalId(c.id) || !isCodexType(idType)) {
32
+ errors.push({
33
+ code: 'CODEX-001',
34
+ message: `id "${String(c.id)}" must be a typed codex id (LAW, REGULATION, POLICY, or INTERNAL_STANDARD).`,
35
+ path: 'id',
36
+ });
37
+ }
38
+ if (typeof c.name !== 'string' || c.name.trim() === '') {
39
+ errors.push({ code: 'CODEX-001', message: 'name is required.', path: 'name' });
40
+ }
41
+ for (const field of ['admitted_at', 'admitted_by']) {
42
+ const v = c[field];
43
+ if (typeof v !== 'string' || v.trim() === '') {
44
+ errors.push({ code: 'CODEX-001', message: `${field} is required.`, path: field });
45
+ }
46
+ }
47
+ if (c.gate_checks === null || typeof c.gate_checks !== 'object' || Array.isArray(c.gate_checks)) {
48
+ errors.push({ code: 'CODEX-001', message: 'gate_checks is required and must be a mapping.', path: 'gate_checks' });
49
+ }
50
+ const declaredType = normalizeType(c.type);
51
+ if (declaredType !== undefined && idType && declaredType !== idType) {
52
+ errors.push({
53
+ code: 'CODEX-002',
54
+ message: `type "${String(c.type)}" does not match the id TYPE prefix "${idType}".`,
55
+ path: 'type',
56
+ });
57
+ }
58
+ const artefactType = (declaredType ?? idType);
59
+ if (artefactType && EXTERNAL_CODEX_TYPES.includes(artefactType)) {
60
+ const jurisdiction = typeof c.jurisdiction === 'string' ? c.jurisdiction.trim() : '';
61
+ if (!jurisdiction) {
62
+ errors.push({ code: 'CODEX-003', message: 'jurisdiction is required for external codex artefacts.', path: 'jurisdiction' });
63
+ }
64
+ else if (options.folderJurisdiction && jurisdiction.toLowerCase() !== options.folderJurisdiction.toLowerCase()) {
65
+ errors.push({
66
+ code: 'CODEX-005',
67
+ message: `jurisdiction "${jurisdiction}" must match the parent folder "${options.folderJurisdiction}" (CODEX-001).`,
68
+ path: 'jurisdiction',
69
+ });
70
+ }
71
+ if (typeof c.effective_date !== 'string' || c.effective_date.trim() === '') {
72
+ errors.push({ code: 'CODEX-003', message: 'effective_date is required for external codex artefacts.', path: 'effective_date' });
73
+ }
74
+ }
75
+ if (artefactType && INTERNAL_CODEX_TYPES.includes(artefactType)) {
76
+ if (typeof c.issuing_authority !== 'string' || c.issuing_authority.trim() === '') {
77
+ errors.push({
78
+ code: 'CODEX-004',
79
+ message: 'issuing_authority is required for internal codex artefacts.',
80
+ path: 'issuing_authority',
81
+ });
82
+ }
83
+ if (typeof c.effective_date !== 'string' || c.effective_date.trim() === '') {
84
+ errors.push({ code: 'CODEX-004', message: 'effective_date is required for internal codex artefacts.', path: 'effective_date' });
85
+ }
86
+ }
87
+ return { valid: errors.length === 0, errors, warnings };
88
+ }
89
+ /** True when a parsed document is a codex-zone artefact candidate. */
90
+ export function isCodexDoc(data) {
91
+ if (data === null || typeof data !== 'object' || Array.isArray(data))
92
+ return false;
93
+ return data.zone === 'codex';
94
+ }
95
+ /** Infer codex folder jurisdiction from a repo-relative path such as
96
+ * `codex/external/eu/LAW-GDPR-1.yaml` → `eu`. */
97
+ export function folderJurisdictionFromPath(filePath) {
98
+ const parts = filePath.replace(/\\/g, '/').toLowerCase().split('/');
99
+ const extIdx = parts.indexOf('external');
100
+ if (extIdx < 0 || extIdx + 1 >= parts.length)
101
+ return undefined;
102
+ return parts[extIdx + 1] || undefined;
103
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,60 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { admitDocumentToCatalog, buildComplianceScan, catalogFromMap, } from '../canon-catalog.js';
3
+ describe('buildComplianceScan', () => {
4
+ it('admits codex, requirement, and assertion ids into the catalogue', () => {
5
+ const docs = [
6
+ {
7
+ path: 'codex/external/ge/LAW-PERSONAL-DATA-2017-1.yaml',
8
+ data: {
9
+ zone: 'codex',
10
+ id: 'LAW-PERSONAL-DATA-2017-1',
11
+ name: 'Law',
12
+ type: 'LAW',
13
+ },
14
+ },
15
+ {
16
+ path: 'canon/elements/01_motivation/requirements/REQUIREMENT-DATA-ERASURE-1.yaml',
17
+ data: {
18
+ notation: 'requirement',
19
+ id: 'REQUIREMENT-DATA-ERASURE-1',
20
+ name: 'Erasure',
21
+ derived_from: ['LAW-PERSONAL-DATA-2017-1'],
22
+ },
23
+ },
24
+ {
25
+ path: 'canon/elements/02_business/products/PRODUCT-MOBILE-1.yaml',
26
+ data: { notation: 'product', id: 'PRODUCT-MOBILE-1', name: 'Mobile' },
27
+ },
28
+ {
29
+ path: 'canon/assertions/ASSERTION-MOBILE-DATA-ERASURE-1.yaml',
30
+ data: {
31
+ notation: 'assertion',
32
+ id: 'ASSERTION-MOBILE-DATA-ERASURE-1',
33
+ about: 'REQUIREMENT-DATA-ERASURE-1',
34
+ subject: 'PRODUCT-MOBILE-1',
35
+ status: 'compliant',
36
+ },
37
+ },
38
+ ];
39
+ const { catalog, complianceCanon, pathById } = buildComplianceScan(docs);
40
+ expect(catalog.typeOf('LAW-PERSONAL-DATA-2017-1')).toBe('LAW');
41
+ expect(catalog.typeOf('REQUIREMENT-DATA-ERASURE-1')).toBe('REQUIREMENT');
42
+ expect(catalog.typeOf('PRODUCT-MOBILE-1')).toBe('PRODUCT');
43
+ expect(complianceCanon.requirements).toHaveLength(1);
44
+ expect(complianceCanon.assertions).toHaveLength(1);
45
+ expect(complianceCanon.codex).toHaveLength(1);
46
+ expect(pathById.get('REQUIREMENT-DATA-ERASURE-1')).toContain('requirements/');
47
+ });
48
+ });
49
+ describe('admitDocumentToCatalog', () => {
50
+ it('uses explicit codex type when the id prefix is ambiguous', () => {
51
+ const map = new Map();
52
+ admitDocumentToCatalog(map, {
53
+ zone: 'codex',
54
+ id: 'INTERNAL_STANDARD-coding-conventions-1',
55
+ type: 'INTERNAL_STANDARD',
56
+ });
57
+ const catalog = catalogFromMap(map);
58
+ expect(catalog.typeOf('INTERNAL_STANDARD-coding-conventions-1')).toBe('INTERNAL_STANDARD');
59
+ });
60
+ });
@@ -0,0 +1,59 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { emptyCanon } from '../classify.js';
3
+ import { catalogFromMap } from '../canon-catalog.js';
4
+ import { collectImpactViewResolutionFindings } from '../impact.js';
5
+ import { collectCoverageViewResolutionFindings } from '../coverage-metric.js';
6
+ const catalog = catalogFromMap(new Map([
7
+ ['PRODUCT-ECOMM-1', 'PRODUCT'],
8
+ ['REQUIREMENT-GDPR-1', 'REQUIREMENT'],
9
+ ['LAW-GDPR-1', 'LAW'],
10
+ ]));
11
+ const complianceCanon = emptyCanon();
12
+ complianceCanon.products.push({ id: 'PRODUCT-ECOMM-1', name: 'E-Commerce' });
13
+ complianceCanon.requirements.push({ id: 'REQUIREMENT-GDPR-1', name: 'GDPR req' });
14
+ complianceCanon.codex.push({ id: 'LAW-GDPR-1', name: 'GDPR' });
15
+ describe('collectImpactViewResolutionFindings', () => {
16
+ const base = {
17
+ id: 'test-impact',
18
+ name: 'Test',
19
+ subjects: { products: ['PRODUCT-ECOMM-1'], processes: [], capabilities: [] },
20
+ obligations: { filter: { derived_from_codex: ['LAW-GDPR-1'] } },
21
+ status_display: { show: ['compliant'] },
22
+ empty_cells: {},
23
+ order_rows_by: 'id',
24
+ };
25
+ it('passes when subjects and regimes resolve', () => {
26
+ expect(collectImpactViewResolutionFindings(base, catalog, complianceCanon)).toEqual([]);
27
+ });
28
+ it('flags unresolved subjects and regimes', () => {
29
+ const findings = collectImpactViewResolutionFindings({
30
+ ...base,
31
+ subjects: { products: ['PRODUCT-MISSING-1'], processes: [], capabilities: [] },
32
+ obligations: { include: ['REQUIREMENT-MISSING-1'], filter: { derived_from_codex: ['LAW-MISSING-1'] } },
33
+ }, catalog, complianceCanon);
34
+ expect(findings.some((f) => f.code === 'COMPIMP-REF' && f.message.includes('PRODUCT-MISSING-1'))).toBe(true);
35
+ expect(findings.some((f) => f.message.includes('REQUIREMENT-MISSING-1'))).toBe(true);
36
+ expect(findings.some((f) => f.message.includes('LAW-MISSING-1'))).toBe(true);
37
+ });
38
+ });
39
+ describe('collectCoverageViewResolutionFindings', () => {
40
+ const base = {
41
+ id: 'test-coverage',
42
+ name: 'Test',
43
+ thresholds: { green: 0.9, amber: 0.7 },
44
+ subjects: { products: ['PRODUCT-ECOMM-1'] },
45
+ regimes: { include: ['LAW-GDPR-1'] },
46
+ };
47
+ it('passes when products and regimes resolve', () => {
48
+ expect(collectCoverageViewResolutionFindings(base, catalog, complianceCanon)).toEqual([]);
49
+ });
50
+ it('flags unresolved product and regime refs', () => {
51
+ const findings = collectCoverageViewResolutionFindings({
52
+ ...base,
53
+ subjects: { products: ['PRODUCT-MISSING-1'] },
54
+ regimes: { include: ['LAW-MISSING-1'] },
55
+ }, catalog, complianceCanon);
56
+ expect(findings.some((f) => f.code === 'COVMET-REF')).toBe(true);
57
+ expect(findings).toHaveLength(2);
58
+ });
59
+ });
@@ -0,0 +1,19 @@
1
+ import { type CanonCatalog } from '../typed-id.js';
2
+ import { type ComplianceCanon } from './classify.js';
3
+ export interface ScannedYamlDoc {
4
+ path: string;
5
+ data: unknown;
6
+ }
7
+ export interface ComplianceScanResult {
8
+ complianceCanon: ComplianceCanon;
9
+ catalog: CanonCatalog;
10
+ /** First path where each admitted id was seen (for gap-dashboard warnings). */
11
+ pathById: Map<string, string>;
12
+ }
13
+ /** Admit one parsed document into the id → TYPE map. */
14
+ export declare function admitDocumentToCatalog(map: Map<string, string>, doc: unknown): string | undefined;
15
+ /** Build a read-only `CanonCatalog` from a populated id → TYPE map. */
16
+ export declare function catalogFromMap(map: Map<string, string>): CanonCatalog;
17
+ /** Ingest compliance projections and admit cross-reference targets from a batch
18
+ * of already-parsed YAML documents (no IO). */
19
+ export declare function buildComplianceScan(docs: ScannedYamlDoc[]): ComplianceScanResult;