@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
@@ -10,45 +10,24 @@
10
10
  * self-contained. No `vscode`, no `node:*`, no host APIs.
11
11
  */
12
12
  import { layoutProcessBlueprint } from '../process-blueprint/layout.js';
13
+ import { parseNodeSizePreset, resolveProcessBlueprintSize, } from '../node-size-presets.js';
13
14
  import { generateSvgEmbedCss } from '../theme/index.js';
15
+ import { layoutCenteredPillText, layoutLeftCellLines, maxCharsForInnerWidth, truncateLine, } from './entity-text-layout.js';
14
16
  import { escXml } from './render-util.js';
15
17
  const PAD = 24;
16
- // Blueprint-specific rules not carried by the shared theme CSS. The outer
17
- // border is drawn last on top of the clipped inner content; the row
18
- // backgrounds are transparent (pills/chips carry the colour). Mirrors
19
- // `BLUEPRINT_CSS` from the VS Code preview so the self-contained JCEF output
20
- // matches the editor.
21
18
  const BP_EMBED_CSS = `
22
19
  .bp-row-bg { fill: none; }
23
20
  .bp-border { fill: none; stroke: var(--ts-border, #cbd5e1); stroke-width: 1; }`;
24
- function truncate(text, maxChars) {
25
- if (text.length <= maxChars)
26
- return text;
27
- return text.slice(0, Math.max(0, maxChars - 1)) + '…';
28
- }
29
- /**
30
- * Render a left-anchored, vertically centred multi-line text cell. `lines` are
31
- * pre-wrapped by the layout; the block is centred within the cell height.
32
- */
33
21
  function textCellSvg(lines, cls, x, cellTop, cellHeight, lineHeight) {
22
+ const specs = layoutLeftCellLines(lines, x, cellTop, cellHeight, lineHeight, cls);
34
23
  const ls = lines.length > 0 ? lines : [''];
35
24
  const first = cellTop + cellHeight / 2 - ((ls.length - 1) / 2) * lineHeight;
36
25
  const tspans = ls
37
26
  .map((ln, i) => `<tspan x="${x}" y="${first + i * lineHeight}">${escXml(ln)}</tspan>`)
38
27
  .join('');
28
+ void specs;
39
29
  return `<text class="${cls}" dominant-baseline="central">${tspans}</text>`;
40
30
  }
41
- /**
42
- * Render a single compliance law chip wrapped in a `<g data-chip-law…>` so the
43
- * VS Code host can wire click-to-drill-down. Other hosts (JCEF) simply ignore
44
- * the data attributes.
45
- *
46
- * Three orthogonal decorations are expressed as SVG overrides:
47
- * - `new` — dashed stroke border (`stroke-dasharray="4 2"`)
48
- * - `gap` — warning-level fill (`class="…compliance-gap"`)
49
- * - `deadline` — urgent fill override (`class="…compliance-deadline"`) + a small
50
- * badge circle in the top-right corner
51
- */
52
31
  function complianceChipSvg(chip, ox, oy, stageId) {
53
32
  const { x, y, width, height, lawId, decorations } = chip;
54
33
  const ax = x + ox;
@@ -62,9 +41,10 @@ function complianceChipSvg(chip, ox, oy, stageId) {
62
41
  else if (hasGap)
63
42
  rectClass += ' compliance-gap';
64
43
  const strokeDash = hasNew ? ' stroke-dasharray="4 2"' : '';
44
+ const maxChars = maxCharsForInnerWidth(Math.max(0, width - 8), 8);
65
45
  const parts = [];
66
46
  parts.push(`<rect class="${rectClass}" x="${ax}" y="${ay}" width="${width}" height="${height}" rx="6"${strokeDash}/>`);
67
- parts.push(`<text class="text-pill" x="${ax + width / 2}" y="${ay + height / 2}" text-anchor="middle" dominant-baseline="central">${escXml(truncate(lawId, Math.floor(width / 8)))}</text>`);
47
+ parts.push(`<text class="text-pill" x="${ax + width / 2}" y="${ay + height / 2}" text-anchor="middle" dominant-baseline="central">${escXml(truncateLine(lawId, maxChars))}</text>`);
68
48
  if (hasDeadline) {
69
49
  const br = 5;
70
50
  const bx = ax + width - br - 3;
@@ -74,38 +54,23 @@ function complianceChipSvg(chip, ox, oy, stageId) {
74
54
  }
75
55
  return `<g data-chip-law="${escXml(lawId)}" data-chip-stage="${escXml(stageId)}">\n${parts.join('\n')}\n</g>`;
76
56
  }
77
- /**
78
- * Canonical Process Blueprint body — everything inside the `<svg>` except the
79
- * host-specific title block. Shared verbatim by the VS Code preview and the
80
- * host-neutral wrapper below so both surfaces stay pixel-identical.
81
- *
82
- * Layering: a clipped group holds the row fills and grid lines; pills and
83
- * compliance chips render on top (so grid lines sit under pill content); the
84
- * outer rounded border is drawn last so corner cells never overflow it.
85
- */
86
57
  export function renderProcessBlueprintBody(layout, ox, oy, clipId = 'bp-clip') {
87
58
  const tw = layout.bounds.width;
88
59
  const th = layout.bounds.height;
89
60
  const parts = [];
90
- // Pills and chips collected separately so they render after (on top of) grid lines.
91
61
  const topParts = [];
92
- // Outer background (fill only; border drawn last on top).
93
62
  parts.push(`<rect class="diagram-node level-0 bp-row-bg" x="${ox}" y="${oy}" width="${tw}" height="${th}" rx="6" stroke="none"/>`);
94
- // ClipPath — clips all inner content to the outer rounded rect so corner cells
95
- // don't visually overflow the rx=6 boundary.
96
63
  parts.push(`<defs><clipPath id="${clipId}"><rect x="${ox}" y="${oy}" width="${tw}" height="${th}" rx="6"/></clipPath></defs>`);
97
64
  const inner = [];
98
- // Stage headers (top row).
65
+ const headerMaxChars = Math.max(8, Math.floor((layout.stageColumnWidth - 16) / 7));
99
66
  for (const s of layout.stageHeaders) {
100
67
  inner.push(`<rect class="diagram-node level-1" x="${s.x + ox}" y="${s.y + oy}" width="${s.width}" height="${s.height}" stroke="none"/>`);
101
- inner.push(`<text class="text-header" x="${s.x + ox + s.width / 2}" y="${s.y + oy + s.height / 2}" text-anchor="middle" dominant-baseline="central">${escXml(truncate(s.name, 28))}</text>`);
68
+ inner.push(`<text class="text-header" x="${s.x + ox + s.width / 2}" y="${s.y + oy + s.height / 2}" text-anchor="middle" dominant-baseline="central">${escXml(truncateLine(s.name, headerMaxChars))}</text>`);
102
69
  }
103
- // Legend column (row labels).
104
70
  for (const l of layout.legend) {
105
71
  inner.push(`<rect class="diagram-node level-2 bp-row-bg" x="${ox}" y="${l.y + oy}" width="${layout.legendColumnWidth}" height="${l.height}" stroke="none"/>`);
106
72
  inner.push(`<text class="text-primary" x="${ox + 12}" y="${l.y + oy + l.height / 2}" dominant-baseline="central">${escXml(l.label)}</text>`);
107
73
  }
108
- // Goal and result cells.
109
74
  const textX = layout.cellTextPadX;
110
75
  for (const c of layout.goalCells) {
111
76
  inner.push(`<rect class="diagram-node level-3 bp-row-bg" x="${c.x + ox}" y="${c.y + oy}" width="${c.width}" height="${c.height}" stroke="none"/>`);
@@ -115,32 +80,22 @@ export function renderProcessBlueprintBody(layout, ox, oy, clipId = 'bp-clip') {
115
80
  inner.push(`<rect class="diagram-node level-4 bp-row-bg" x="${c.x + ox}" y="${c.y + oy}" width="${c.width}" height="${c.height}" stroke="none"/>`);
116
81
  inner.push(textCellSvg(c.lines, 'text-secondary', c.x + ox + textX, c.y + oy, c.height, layout.textLineHeight));
117
82
  }
118
- // Aspect rows — transparent background; pills carry the colour.
119
83
  for (let r = 0; r < layout.aspectRows.length; r++) {
120
84
  const row = layout.aspectRows[r];
121
85
  const level = 5 + (r % 3);
122
86
  inner.push(`<rect class="diagram-node level-${level} bp-row-bg" x="${layout.legendColumnWidth + ox}" y="${row.y + oy}" width="${tw - layout.legendColumnWidth}" height="${row.height}" stroke="none"/>`);
123
87
  for (const p of row.pills) {
124
- const cx = p.x + ox + p.width / 2;
125
- const pillLH = 15;
126
- // p.lines: name lines (possibly wrapped), optionally with id as last line.
127
88
  const hasIdLine = !!(p.id && p.lines.length > 0 && p.lines[p.lines.length - 1] === p.id);
128
89
  const nameLines = hasIdLine ? p.lines.slice(0, -1) : p.lines;
129
90
  const idLine = hasIdLine ? p.id : undefined;
130
- const pillFirstY = p.y + oy + p.height / 2 - ((p.lines.length - 1) / 2) * pillLH;
91
+ const pillSpecs = layoutCenteredPillText(p.x + ox, p.y + oy, p.width, p.height, nameLines, idLine);
131
92
  topParts.push(`<rect class="diagram-node level-${level}" x="${p.x + ox}" y="${p.y + oy}" width="${p.width}" height="${p.height}" rx="6"/>`);
132
- if (nameLines.length > 0) {
133
- const tspans = nameLines
134
- .map((ln, i) => `<tspan x="${cx}" y="${pillFirstY + i * pillLH}">${escXml(ln)}</tspan>`)
135
- .join('');
136
- topParts.push(`<text class="text-pill" text-anchor="middle" dominant-baseline="central">${tspans}</text>`);
137
- }
138
- if (idLine) {
139
- topParts.push(`<text class="text-secondary" x="${cx}" y="${pillFirstY + nameLines.length * pillLH}" text-anchor="middle" dominant-baseline="central">${escXml(idLine)}</text>`);
93
+ for (const spec of pillSpecs) {
94
+ const anchor = spec.cls === 'text-pill' || spec.cls === 'text-secondary' ? ' text-anchor="middle"' : '';
95
+ topParts.push(`<text class="${spec.cls}" x="${spec.x}" y="${spec.y}"${anchor} dominant-baseline="central">${escXml(spec.text)}</text>`);
140
96
  }
141
97
  }
142
98
  }
143
- // Compliance row (optional).
144
99
  if (layout.complianceRow) {
145
100
  const row = layout.complianceRow;
146
101
  inner.push(`<rect class="diagram-node level-5 bp-row-bg" x="${layout.legendColumnWidth + ox}" y="${row.y + oy}" width="${tw - layout.legendColumnWidth}" height="${row.height}" stroke="none"/>`);
@@ -149,7 +104,6 @@ export function renderProcessBlueprintBody(layout, ox, oy, clipId = 'bp-clip') {
149
104
  topParts.push(complianceChipSvg(chip, ox, oy, stageId));
150
105
  }
151
106
  }
152
- // Grid lines — drawn before pills so they appear under pill content.
153
107
  const gridX1 = ox;
154
108
  const gridX2 = ox + tw;
155
109
  const gridY1 = oy;
@@ -168,15 +122,17 @@ export function renderProcessBlueprintBody(layout, ox, oy, clipId = 'bp-clip') {
168
122
  const stageLineX = ox + layout.legendColumnWidth + i * layout.stageColumnWidth;
169
123
  inner.push(`<line class="diagram-edge" x1="${stageLineX}" y1="${gridY1}" x2="${stageLineX}" y2="${gridY2}"/>`);
170
124
  }
171
- // Inner content (fills + grid lines) clipped to rounded outer rect.
172
125
  parts.push(`<g clip-path="url(#${clipId})">${inner.join('\n')}${topParts.join('\n')}</g>`);
173
- // Outer border on top — fill="none" as attribute so PNG export (no external CSS) never shows black.
174
126
  parts.push(`<rect class="bp-border" x="${ox}" y="${oy}" width="${tw}" height="${th}" rx="6" fill="none"/>`);
175
127
  return parts.join('\n');
176
128
  }
177
129
  export function renderProcessBlueprintSvg(doc, options = {}) {
178
- const { title = '' } = options;
179
- const layout = layoutProcessBlueprint(doc);
130
+ const { title = '', nodeSizePreset = 'normal', layoutOptions } = options;
131
+ const sizing = resolveProcessBlueprintSize(parseNodeSizePreset(nodeSizePreset));
132
+ const layout = layoutProcessBlueprint(doc, {
133
+ ...sizing,
134
+ ...layoutOptions,
135
+ });
180
136
  if (layout.bounds.width === 0 || layout.bounds.height === 0) {
181
137
  return `<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 0 0"></svg>`;
182
138
  }
@@ -189,8 +145,6 @@ export function renderProcessBlueprintSvg(doc, options = {}) {
189
145
  const titleSvg = title
190
146
  ? `<text class="text-header" x="${PAD}" y="${PAD - 6}">${escXml(title)}</text>`
191
147
  : '';
192
- // Embed the shared theme CSS plus the blueprint-specific rules inside the SVG
193
- // so the rendered output is self-contained for the JCEF host.
194
148
  const embedCss = generateSvgEmbedCss('transitrix') + BP_EMBED_CSS;
195
149
  return `<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
196
150
  <style>${embedCss}</style>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@transitrix/diagrams",
3
- "version": "1.6.0",
3
+ "version": "1.8.1",
4
4
  "type": "module",
5
5
  "description": "Shared rendering, validation, and pure mutations for Transitrix custom diagram formats.",
6
6
  "license": "MIT",
@@ -1,13 +1,18 @@
1
1
  import type { ActivityDoc, Activity, ActivitiesLayout, ActivitiesLayoutOptions, LayoutNode, LayoutEdge } from './types.js';
2
2
  import { computeCpm } from './cpm.js';
3
3
 
4
- const NODE_W = 200;
5
- const NODE_H = 80;
4
+ const DEFAULT_NODE_W = 200;
5
+ const DEFAULT_NODE_H = 80;
6
6
  const H_GAP = 80;
7
7
  const V_GAP = 24;
8
8
 
9
9
  export function layoutActivities(doc: ActivityDoc, options: ActivitiesLayoutOptions = {}): ActivitiesLayout {
10
- const { horizontalGap = H_GAP, verticalGap = V_GAP } = options;
10
+ const {
11
+ horizontalGap = H_GAP,
12
+ verticalGap = V_GAP,
13
+ nodeWidth = DEFAULT_NODE_W,
14
+ nodeHeight = DEFAULT_NODE_H,
15
+ } = options;
11
16
  const activities = doc.activities;
12
17
  if (activities.length === 0) {
13
18
  return { nodes: [], edges: [], bounds: { x: 0, y: 0, width: 0, height: 0 } };
@@ -70,21 +75,21 @@ export function layoutActivities(doc: ActivityDoc, options: ActivitiesLayoutOpti
70
75
 
71
76
  for (let c = 0; c < colCount; c++) {
72
77
  const list = cols.get(c) ?? [];
73
- const x = c * (NODE_W + horizontalGap);
78
+ const x = c * (nodeWidth + horizontalGap);
74
79
  let y = 0;
75
80
  for (const a of list) {
76
81
  const node: LayoutNode = {
77
82
  id: a.id,
78
83
  x,
79
84
  y,
80
- width: NODE_W,
81
- height: NODE_H,
85
+ width: nodeWidth,
86
+ height: nodeHeight,
82
87
  data: a,
83
88
  cpm: cpm.get(a.id),
84
89
  };
85
90
  nodes.push(node);
86
91
  nodeMap.set(a.id, node);
87
- y += NODE_H + verticalGap;
92
+ y += nodeHeight + verticalGap;
88
93
  }
89
94
  }
90
95
 
@@ -100,6 +100,10 @@ export interface ActivitiesLayoutOptions {
100
100
  horizontalGap?: number;
101
101
  /** Vertical gap (px) between stacked activity nodes. Default matches the historical hardcoded value. */
102
102
  verticalGap?: number;
103
+ /** Activity node width (px). Default 200. */
104
+ nodeWidth?: number;
105
+ /** Activity node height (px). Default 80. */
106
+ nodeHeight?: number;
103
107
  }
104
108
 
105
109
  // ── Gantt view (spec §9) ─────────────────────────────────────────────────────
@@ -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
@@ -18,7 +18,7 @@ export { layoutCapabilityTree, TREE_NODE_WIDTH, TREE_NODE_HEIGHT, TREE_RANK_SEP,
18
18
  export type { RenderCapabilityTreeOptions } from './render-capability-tree.js';
19
19
  export { renderCapabilityTreeSvg } from './render-capability-tree.js';
20
20
 
21
- // ── DSM-migration capability map (docs/extraction/capability-map.md) ───────
21
+ // ── DSM-migration capability map (docs/internal/extraction/capability-map.md) ───────
22
22
  // A separate, flat, address-triple data model from CapabilityNode above —
23
23
  // see dsm-schema.ts for why the two coexist in this folder. Named
24
24
  // `validateCapabilityMapData` rather than `validateCapabilityMap` because
@@ -40,10 +40,18 @@ export interface CapabilityTreeLayout {
40
40
  levelCounts: CapabilityTreeLevelCounts;
41
41
  }
42
42
 
43
+ export interface CapabilityTreeLayoutOptions {
44
+ nodeWidth?: number;
45
+ nodeHeight?: number;
46
+ }
47
+
43
48
  export function layoutCapabilityTree(
44
49
  map: CapabilityMapHeader,
45
50
  collapsedIds: Set<string> = new Set(),
51
+ options: CapabilityTreeLayoutOptions = {},
46
52
  ): CapabilityTreeLayout {
53
+ const nodeWidth = options.nodeWidth ?? TREE_NODE_WIDTH;
54
+ const nodeHeight = options.nodeHeight ?? TREE_NODE_HEIGHT;
47
55
  const nodes: CapabilityTreeNode[] = [];
48
56
  const edges: CapabilityTreeEdge[] = [];
49
57
 
@@ -60,13 +68,13 @@ export function layoutCapabilityTree(
60
68
  const hasChildren = Boolean(node.children?.length);
61
69
  const visibleChildren = !isCollapsed && node.children?.length ? node.children : [];
62
70
 
63
- const x = depth * (TREE_NODE_WIDTH + TREE_RANK_SEP);
71
+ const x = depth * (nodeWidth + TREE_RANK_SEP);
64
72
 
65
73
  if (visibleChildren.length === 0) {
66
74
  const y = getNextY(depth);
67
- advanceY(depth, TREE_NODE_HEIGHT + TREE_NODE_SEP);
68
- nodes.push({ id: node.id, x, y, width: TREE_NODE_WIDTH, height: TREE_NODE_HEIGHT, data: node, depth, hasChildren, isCollapsed });
69
- return { top: y, bottom: y + TREE_NODE_HEIGHT };
75
+ advanceY(depth, nodeHeight + TREE_NODE_SEP);
76
+ nodes.push({ id: node.id, x, y, width: nodeWidth, height: nodeHeight, data: node, depth, hasChildren, isCollapsed });
77
+ return { top: y, bottom: y + nodeHeight };
70
78
  }
71
79
 
72
80
  const childSpans: Array<{ top: number; bottom: number }> = [];
@@ -77,14 +85,14 @@ export function layoutCapabilityTree(
77
85
 
78
86
  const spanTop = Math.min(...childSpans.map(s => s.top));
79
87
  const spanBottom = Math.max(...childSpans.map(s => s.bottom));
80
- const parentY = spanTop + (spanBottom - spanTop) / 2 - TREE_NODE_HEIGHT / 2;
88
+ const parentY = spanTop + (spanBottom - spanTop) / 2 - nodeHeight / 2;
81
89
 
82
90
  const curColY = getNextY(depth);
83
91
  const finalY = Math.max(parentY, curColY);
84
- advanceY(depth, finalY - curColY + TREE_NODE_HEIGHT + TREE_NODE_SEP);
92
+ advanceY(depth, finalY - curColY + nodeHeight + TREE_NODE_SEP);
85
93
 
86
- nodes.push({ id: node.id, x, y: finalY, width: TREE_NODE_WIDTH, height: TREE_NODE_HEIGHT, data: node, depth, hasChildren, isCollapsed });
87
- return { top: Math.min(finalY, spanTop), bottom: Math.max(finalY + TREE_NODE_HEIGHT, spanBottom) };
94
+ nodes.push({ id: node.id, x, y: finalY, width: nodeWidth, height: nodeHeight, data: node, depth, hasChildren, isCollapsed });
95
+ return { top: Math.min(finalY, spanTop), bottom: Math.max(finalY + nodeHeight, spanBottom) };
88
96
  }
89
97
 
90
98
  for (const root of map.capabilities) {
@@ -1,13 +1,13 @@
1
1
  import type { CapabilityMapHeader } from './types.js';
2
- import { layoutCapabilityTree, TREE_NODE_WIDTH, TREE_NODE_HEIGHT, TREE_NODE_SEP } from './layout-tree.js';
2
+ import { layoutCapabilityTree, TREE_NODE_SEP } from './layout-tree.js';
3
3
  import { horizontalCubicEdgePath, DEFAULT_EDGE_CURVATURE } from '../edge-path.js';
4
+ import { parseNodeSizePreset, resolveCapabilityMapNodeSize, type NodeSizePreset } from '../node-size-presets.js';
5
+ import { maxCharsForInnerWidth, truncateLine } from '../webview/entity-text-layout.js';
4
6
  import { escXml } from '../webview/render-util.js';
5
7
 
6
8
  const PAD = 24;
7
9
  const BTN_R = 8;
8
10
  const LEGEND_H = 40;
9
- const LABEL_MAX_CHARS = 28;
10
- const LABEL_TRIM_CHARS = 26;
11
11
 
12
12
  const BAND_LABELS = ['Levels 0–2', 'Levels 3–4', 'Levels 5+'] as const;
13
13
 
@@ -17,30 +17,19 @@ function depthBand(depth: number): 0 | 1 | 2 {
17
17
  return 2;
18
18
  }
19
19
 
20
- function truncate(s: string, max: number, trim: number): string {
21
- return s.length > max ? s.slice(0, trim) + '…' : s;
22
- }
23
-
24
20
  export interface RenderCapabilityTreeOptions {
25
21
  collapsedIds?: Set<string>;
26
22
  curvature?: number;
23
+ nodeSizePreset?: NodeSizePreset;
27
24
  }
28
25
 
29
- /**
30
- * Renders a capability-map as a left-to-right SVG node tree with DSM-matching
31
- * visual design: depth-banded fills, maturity badges, and +/− collapse buttons.
32
- *
33
- * The SVG uses CSS classes (`tree-level-N`, `tree-maturity-N`, `tree-collapse-btn`)
34
- * that must be resolved by the host's stylesheet (injected via `diagramClassCss()`
35
- * in themes.ts). Collapse buttons carry `data-node-id` / `data-collapsed` attributes
36
- * for the webview click handler.
37
- */
38
26
  export function renderCapabilityTreeSvg(
39
27
  map: CapabilityMapHeader,
40
28
  opts: RenderCapabilityTreeOptions = {},
41
29
  ): string {
42
- const { collapsedIds = new Set(), curvature = DEFAULT_EDGE_CURVATURE } = opts;
43
- const layout = layoutCapabilityTree(map, collapsedIds);
30
+ const { collapsedIds = new Set(), curvature = DEFAULT_EDGE_CURVATURE, nodeSizePreset = 'normal' } = opts;
31
+ const nodeSize = resolveCapabilityMapNodeSize(parseNodeSizePreset(nodeSizePreset));
32
+ const layout = layoutCapabilityTree(map, collapsedIds, nodeSize);
44
33
 
45
34
  if (layout.nodes.length === 0) {
46
35
  return `<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" viewBox="0 0 0 0"></svg>`;
@@ -70,23 +59,22 @@ export function renderCapabilityTreeSvg(
70
59
  const band = depthBand(n.depth);
71
60
  const mat = Math.max(1, Math.min(5, n.data.current_maturity | 0));
72
61
 
73
- const nameText = truncate(n.data.name, LABEL_MAX_CHARS, LABEL_TRIM_CHARS);
74
- const nameTitle = n.data.name.length > LABEL_MAX_CHARS ? escXml(n.data.name) : '';
75
-
76
- // Maturity badge: left side of node
77
62
  const badgeW = 28;
78
63
  const badgeH = 18;
64
+ const textAreaW = Math.max(0, n.width - badgeW - 26);
65
+ const maxChars = maxCharsForInnerWidth(textAreaW, 7);
66
+ const nameText = truncateLine(n.data.name, maxChars);
67
+ const nameTitle = n.data.name.length > maxChars ? escXml(n.data.name) : '';
68
+
79
69
  const badgeX = x + 10;
80
- const badgeY = y + (TREE_NODE_HEIGHT - badgeH) / 2;
70
+ const badgeY = y + (n.height - badgeH) / 2;
81
71
 
82
- // Text area: right of badge
83
72
  const textX = badgeX + badgeW + 8;
84
- const nameY = y + TREE_NODE_HEIGHT / 2 - 8;
85
- const idY = y + TREE_NODE_HEIGHT / 2 + 9;
73
+ const nameY = y + n.height / 2 - 8;
74
+ const idY = y + n.height / 2 + 9;
86
75
 
87
- // Collapse button: centered on right edge of node
88
- const btnCx = x + TREE_NODE_WIDTH;
89
- const btnCy = y + TREE_NODE_HEIGHT / 2;
76
+ const btnCx = x + n.width;
77
+ const btnCy = y + n.height / 2;
90
78
 
91
79
  const collapseBtn = n.hasChildren
92
80
  ? `<circle class="tree-collapse-btn" cx="${btnCx}" cy="${btnCy}" r="${BTN_R}" data-node-id="${escXml(n.id)}" data-collapsed="${n.isCollapsed}"/>
@@ -94,7 +82,7 @@ export function renderCapabilityTreeSvg(
94
82
  : '';
95
83
 
96
84
  return `<g${nameTitle ? ` title="${nameTitle}"` : ''}>
97
- <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"/>
85
+ <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"/>
98
86
  <rect class="tree-maturity-${mat}" x="${badgeX}" y="${badgeY}" width="${badgeW}" height="${badgeH}" rx="4"/>
99
87
  <text class="text-pill" x="${badgeX + badgeW / 2}" y="${badgeY + badgeH / 2}" text-anchor="middle" fill="white">L${mat}</text>
100
88
  <text class="text-primary" x="${textX}" y="${nameY}" dominant-baseline="central">${escXml(nameText)}</text>
@@ -0,0 +1,40 @@
1
+ // Conformance test for CODEX worked examples under
2
+ // `tests/fixtures/notation-corpus/codex/` — pins #518 Phase C2 corpus signal.
3
+
4
+ import { describe, it, expect } from 'vitest';
5
+ import { readFileSync, readdirSync } from 'node:fs';
6
+ import { fileURLToPath } from 'node:url';
7
+ import path from 'node:path';
8
+ import yaml from 'js-yaml';
9
+ import { validateCodex, folderJurisdictionFromPath } from '../validate.js';
10
+
11
+ const corpusRoot = path.resolve(
12
+ path.dirname(fileURLToPath(import.meta.url)),
13
+ '../../../../../tests/fixtures/notation-corpus/codex',
14
+ );
15
+
16
+ function codexFixturePaths(): Array<{ rel: string; folderJurisdiction?: string }> {
17
+ const out: Array<{ rel: string; folderJurisdiction?: string }> = [];
18
+ for (const zone of ['external/EU', 'internal']) {
19
+ const dir = path.join(corpusRoot, zone);
20
+ for (const f of readdirSync(dir).filter((x) => x.endsWith('.yaml'))) {
21
+ const rel = path.join('codex', zone, f).replace(/\\/g, '/');
22
+ out.push({
23
+ rel,
24
+ folderJurisdiction: folderJurisdictionFromPath(rel),
25
+ });
26
+ }
27
+ }
28
+ return out;
29
+ }
30
+
31
+ describe('codex worked examples (notation-corpus)', () => {
32
+ for (const { rel, folderJurisdiction } of codexFixturePaths()) {
33
+ it(`validates ${rel} without error`, () => {
34
+ const data = yaml.load(readFileSync(path.join(corpusRoot, rel.replace(/^codex\//, '')), 'utf-8'));
35
+ const v = validateCodex(data, { folderJurisdiction });
36
+ expect(v.valid, JSON.stringify(v.errors)).toBe(true);
37
+ expect(v.errors).toHaveLength(0);
38
+ });
39
+ }
40
+ });
@@ -0,0 +1,84 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ validateCodex,
4
+ isCodexDoc,
5
+ folderJurisdictionFromPath,
6
+ } from '../validate.js';
7
+
8
+ const VALID_EXTERNAL = {
9
+ zone: 'codex',
10
+ id: 'LAW-GDPR-1',
11
+ name: 'General Data Protection Regulation',
12
+ type: 'LAW',
13
+ jurisdiction: 'eu',
14
+ effective_date: '2018-05-25',
15
+ admitted_at: '2026-06-01',
16
+ admitted_by: 'legal.team',
17
+ gate_checks: { uniqueness: 'pass' },
18
+ };
19
+
20
+ const VALID_INTERNAL = {
21
+ zone: 'codex',
22
+ id: 'INTERNAL_STANDARD-coding-conventions-1',
23
+ name: 'Engineering Coding Conventions',
24
+ type: 'INTERNAL_STANDARD',
25
+ issuing_authority: 'VP Engineering',
26
+ effective_date: '2026-01-01',
27
+ admitted_at: '2026-05-27',
28
+ admitted_by: 'vp.engineering',
29
+ gate_checks: { source_authority: 'VP Engineering' },
30
+ };
31
+
32
+ describe('validateCodex', () => {
33
+ it('accepts a valid external artefact', () => {
34
+ const r = validateCodex(VALID_EXTERNAL, { folderJurisdiction: 'eu' });
35
+ expect(r.valid, JSON.stringify(r.errors)).toBe(true);
36
+ });
37
+
38
+ it('accepts a valid internal artefact', () => {
39
+ expect(validateCodex(VALID_INTERNAL).valid).toBe(true);
40
+ });
41
+
42
+ it('CODEX-001 rejects non-mapping input', () => {
43
+ expect(validateCodex(null).errors.some((e) => e.code === 'CODEX-001')).toBe(true);
44
+ });
45
+
46
+ it('CODEX-001 rejects wrong zone', () => {
47
+ const r = validateCodex({ ...VALID_EXTERNAL, zone: 'canon' });
48
+ expect(r.errors.some((e) => e.code === 'CODEX-001' && e.path === 'zone')).toBe(true);
49
+ });
50
+
51
+ it('CODEX-002 rejects type/id prefix mismatch', () => {
52
+ const r = validateCodex({ ...VALID_EXTERNAL, type: 'REGULATION' });
53
+ expect(r.errors.some((e) => e.code === 'CODEX-002')).toBe(true);
54
+ });
55
+
56
+ it('CODEX-003 requires jurisdiction on external artefacts', () => {
57
+ const r = validateCodex({ ...VALID_EXTERNAL, jurisdiction: '' });
58
+ expect(r.errors.some((e) => e.code === 'CODEX-003' && e.path === 'jurisdiction')).toBe(true);
59
+ });
60
+
61
+ it('CODEX-005 enforces folder jurisdiction match', () => {
62
+ const r = validateCodex(VALID_EXTERNAL, { folderJurisdiction: 'ge' });
63
+ expect(r.errors.some((e) => e.code === 'CODEX-005')).toBe(true);
64
+ });
65
+
66
+ it('CODEX-004 requires issuing_authority on internal artefacts', () => {
67
+ const r = validateCodex({ ...VALID_INTERNAL, issuing_authority: '' });
68
+ expect(r.errors.some((e) => e.code === 'CODEX-004')).toBe(true);
69
+ });
70
+ });
71
+
72
+ describe('isCodexDoc', () => {
73
+ it('detects zone: codex', () => {
74
+ expect(isCodexDoc({ zone: 'codex', id: 'LAW-1' })).toBe(true);
75
+ expect(isCodexDoc({ zone: 'canon' })).toBe(false);
76
+ });
77
+ });
78
+
79
+ describe('folderJurisdictionFromPath', () => {
80
+ it('parses codex/external/<jurisdiction>/ paths', () => {
81
+ expect(folderJurisdictionFromPath('codex/external/eu/LAW-GDPR-1.yaml')).toBe('eu');
82
+ expect(folderJurisdictionFromPath('codex/internal/POLICY-1.yaml')).toBeUndefined();
83
+ });
84
+ });
@@ -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,13 @@
1
+ // Codex artefact — external laws/regulations and internal policies/standards.
2
+ // Schema: methodology notations/elements/14-codex.md §3–§4.
3
+
4
+ /** TYPE prefixes admitted in the codex zone (REQ-003 / 14-codex.md). */
5
+ export const CODEX_ARTEFACT_TYPES = ['LAW', 'REGULATION', 'POLICY', 'INTERNAL_STANDARD'] as const;
6
+
7
+ export type CodexArtefactType = (typeof CODEX_ARTEFACT_TYPES)[number];
8
+
9
+ /** External codex artefacts (codex/external/<jurisdiction>/). */
10
+ export const EXTERNAL_CODEX_TYPES: readonly CodexArtefactType[] = ['LAW', 'REGULATION'];
11
+
12
+ /** Internal codex artefacts (codex/internal/). */
13
+ export const INTERNAL_CODEX_TYPES: readonly CodexArtefactType[] = ['POLICY', 'INTERNAL_STANDARD'];