@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
@@ -22,12 +22,12 @@ function getTypeForLevel(tree: GoalTree, level: number): string {
22
22
  return gt?.name ?? '';
23
23
  }
24
24
 
25
- function updateDescendantLevels(goals: Goal[], id: number, levelDelta: number): void {
26
- for (const g of goals) {
25
+ function updateDescendantLevels(clone: GoalTree, id: number, levelDelta: number, maxLevel: number): void {
26
+ for (const g of clone.goals) {
27
27
  if (g.parent_id === id) {
28
- g.level += levelDelta;
29
- g.type = '';
30
- updateDescendantLevels(goals, g.id, levelDelta);
28
+ g.level = Math.min(g.level + levelDelta, maxLevel);
29
+ g.type = getTypeForLevel(clone, g.level);
30
+ updateDescendantLevels(clone, g.id, levelDelta, maxLevel);
31
31
  }
32
32
  }
33
33
  }
@@ -70,7 +70,7 @@ export function reparent(tree: GoalTree, sourceId: number, targetId: number): Mu
70
70
  source.parent_id = targetId;
71
71
  source.level = newLevel;
72
72
  source.type = getTypeForLevel(clone, newLevel);
73
- if (levelDelta !== 0) updateDescendantLevels(clone.goals, sourceId, levelDelta);
73
+ if (levelDelta !== 0) updateDescendantLevels(clone, sourceId, levelDelta, maxLevel);
74
74
 
75
75
  const v = validateGoalTree(clone);
76
76
  if (!v.valid) return refusalError(v.errors[0]?.message ?? 'Validation failed after reparent');
@@ -36,6 +36,11 @@ export interface CanonicalGoalsResult extends ValidationResult {
36
36
  const GOAL_ID_RE = /^GOAL(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
37
37
  const GOALS_DOC_ID_RE = /^GOALS(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
38
38
 
39
+ /** Display id for previews — canonical `GOAL-…` when present, else internal numeric id. */
40
+ export function displayGoalId(goal: { id: number; canonical_id?: string }): string {
41
+ return goal.canonical_id ?? String(goal.id);
42
+ }
43
+
39
44
  function isNonEmptyString(v: unknown): v is string {
40
45
  return typeof v === 'string' && v.trim().length > 0;
41
46
  }
@@ -267,6 +272,7 @@ export function parseCanonicalGoals(input: unknown): CanonicalGoalsResult {
267
272
  isNonEmptyString(parent) && idToInternal.has(parent) ? idToInternal.get(parent)! : 0;
268
273
  return {
269
274
  id: idToInternal.get(o['id'] as string)!,
275
+ canonical_id: o['id'] as string,
270
276
  name: o['name'] as string,
271
277
  type: o['type'] as string,
272
278
  level: o['level'] as number,
@@ -10,6 +10,8 @@ export interface Factor {
10
10
 
11
11
  export interface Goal {
12
12
  id: number;
13
+ /** Canonical typed id from the YAML (`GOAL-…`) when parsed from canonical form. */
14
+ canonical_id?: string;
13
15
  name: string;
14
16
  type: string;
15
17
  level: number;
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Block/cell size presets for diagram renderers.
3
+ *
4
+ * Presets drive both layout geometry and shared text-layout char budgets.
5
+ * Smooth per-pixel sliders are intentionally deferred.
6
+ */
7
+
8
+ export type NodeSizePreset = 'compact' | 'normal' | 'wide';
9
+
10
+ export const NODE_SIZE_PRESET_VALUES: readonly NodeSizePreset[] = ['compact', 'normal', 'wide'] as const;
11
+
12
+ export interface BoxDimensions {
13
+ width: number;
14
+ height: number;
15
+ }
16
+
17
+ export interface BlocksLeafDimensions extends BoxDimensions {}
18
+
19
+ export interface ProcessBlueprintDimensions {
20
+ legendColumnWidth: number;
21
+ stageColumnWidth: number;
22
+ stageHeaderHeight: number;
23
+ goalRowHeight: number;
24
+ resultRowHeight: number;
25
+ aspectRowMinHeight: number;
26
+ pillHeight: number;
27
+ pillGap: number;
28
+ cellPadding: number;
29
+ textLineHeight: number;
30
+ textCharWidth: number;
31
+ cellTextPadX: number;
32
+ cellTextPadY: number;
33
+ }
34
+
35
+ export interface CapabilityMapDimensions {
36
+ nodeWidth: number;
37
+ nodeHeight: number;
38
+ }
39
+
40
+ type PresetTable<T> = Record<NodeSizePreset, T>;
41
+
42
+ export const GOALS_NODE_SIZE: PresetTable<BoxDimensions> = {
43
+ compact: { width: 200, height: 72 },
44
+ normal: { width: 250, height: 72 },
45
+ wide: { width: 320, height: 72 },
46
+ };
47
+
48
+ export const DGCA_NODE_SIZE: PresetTable<BoxDimensions> = {
49
+ compact: { width: 200, height: 72 },
50
+ normal: { width: 220, height: 72 },
51
+ wide: { width: 280, height: 72 },
52
+ };
53
+
54
+ export const BLOCKS_LEAF_SIZE: PresetTable<BlocksLeafDimensions> = {
55
+ compact: { width: 140, height: 72 },
56
+ normal: { width: 160, height: 72 },
57
+ wide: { width: 200, height: 72 },
58
+ };
59
+
60
+ export const ACTION_NODE_SIZE: PresetTable<BoxDimensions> = {
61
+ compact: { width: 180, height: 72 },
62
+ normal: { width: 200, height: 80 },
63
+ wide: { width: 260, height: 80 },
64
+ };
65
+
66
+ export const PROCESS_BLUEPRINT_SIZE: PresetTable<ProcessBlueprintDimensions> = {
67
+ compact: {
68
+ legendColumnWidth: 120,
69
+ stageColumnWidth: 180,
70
+ stageHeaderHeight: 36,
71
+ goalRowHeight: 52,
72
+ resultRowHeight: 52,
73
+ aspectRowMinHeight: 56,
74
+ pillHeight: 36,
75
+ pillGap: 4,
76
+ cellPadding: 8,
77
+ textLineHeight: 16,
78
+ textCharWidth: 7.5,
79
+ cellTextPadX: 10,
80
+ cellTextPadY: 10,
81
+ },
82
+ normal: {
83
+ legendColumnWidth: 140,
84
+ stageColumnWidth: 220,
85
+ stageHeaderHeight: 40,
86
+ goalRowHeight: 56,
87
+ resultRowHeight: 56,
88
+ aspectRowMinHeight: 60,
89
+ pillHeight: 40,
90
+ pillGap: 4,
91
+ cellPadding: 8,
92
+ textLineHeight: 17,
93
+ textCharWidth: 7.5,
94
+ cellTextPadX: 10,
95
+ cellTextPadY: 10,
96
+ },
97
+ wide: {
98
+ legendColumnWidth: 160,
99
+ stageColumnWidth: 280,
100
+ stageHeaderHeight: 44,
101
+ goalRowHeight: 60,
102
+ resultRowHeight: 60,
103
+ aspectRowMinHeight: 64,
104
+ pillHeight: 44,
105
+ pillGap: 4,
106
+ cellPadding: 8,
107
+ textLineHeight: 18,
108
+ textCharWidth: 7.5,
109
+ cellTextPadX: 10,
110
+ cellTextPadY: 10,
111
+ },
112
+ };
113
+
114
+ export const CAPABILITY_MAP_NODE_SIZE: PresetTable<CapabilityMapDimensions> = {
115
+ compact: { nodeWidth: 200, nodeHeight: 56 },
116
+ normal: { nodeWidth: 240, nodeHeight: 60 },
117
+ wide: { nodeWidth: 300, nodeHeight: 64 },
118
+ };
119
+
120
+ export type NodeSizeNotation =
121
+ | 'goals'
122
+ | 'dgca'
123
+ | 'dga'
124
+ | 'blocks'
125
+ | 'action'
126
+ | 'processBlueprint'
127
+ | 'capabilityMap';
128
+
129
+ /** Parse a VS Code setting value into a preset id (defaults to `normal`). */
130
+ export function parseNodeSizePreset(value: string | undefined): NodeSizePreset {
131
+ if (value === 'compact' || value === 'wide') return value;
132
+ return 'normal';
133
+ }
134
+
135
+ export function resolveGoalsNodeSize(preset: NodeSizePreset): BoxDimensions {
136
+ return GOALS_NODE_SIZE[preset];
137
+ }
138
+
139
+ export function resolveDgcaNodeSize(preset: NodeSizePreset): BoxDimensions {
140
+ return DGCA_NODE_SIZE[preset];
141
+ }
142
+
143
+ export function resolveBlocksLeafSize(preset: NodeSizePreset): BlocksLeafDimensions {
144
+ return BLOCKS_LEAF_SIZE[preset];
145
+ }
146
+
147
+ export function resolveActionNodeSize(preset: NodeSizePreset): BoxDimensions {
148
+ return ACTION_NODE_SIZE[preset];
149
+ }
150
+
151
+ export function resolveProcessBlueprintSize(preset: NodeSizePreset): ProcessBlueprintDimensions {
152
+ return PROCESS_BLUEPRINT_SIZE[preset];
153
+ }
154
+
155
+ export function resolveCapabilityMapNodeSize(preset: NodeSizePreset): CapabilityMapDimensions {
156
+ return CAPABILITY_MAP_NODE_SIZE[preset];
157
+ }
@@ -18,6 +18,7 @@ import type {
18
18
  StageHeaderCell,
19
19
  StageTextCell,
20
20
  } from './types.js';
21
+ import { wrapTextLines } from '../webview/entity-text-layout.js';
21
22
 
22
23
  const ASPECT_CATEGORIES: AspectCategory[] = ['systems', 'actors', 'equipment', 'information_entities'];
23
24
 
@@ -78,56 +79,11 @@ function resolveOptions(options: ProcessBlueprintLayoutOptions | undefined): Res
78
79
  }
79
80
 
80
81
  /**
81
- * Greedy word-wrap to a max characters-per-line budget. Words longer than the
82
- * budget are hard-split; if the result exceeds `maxLines`, the last kept line
83
- * is truncated with an ellipsis so a single pathological cell can't grow the
84
- * whole row unbounded.
82
+ * Greedy word-wrap to a max characters-per-line budget. Delegates to the shared
83
+ * shared text-layout module.
85
84
  */
86
85
  function wrapText(text: string, maxChars: number, maxLines: number): string[] {
87
- const trimmed = (text ?? '').trim();
88
- if (trimmed.length === 0) return [];
89
- const budget = Math.max(1, maxChars);
90
-
91
- const words = trimmed.split(/\s+/);
92
- const lines: string[] = [];
93
- let cur = '';
94
- const flush = (): void => {
95
- if (cur.length > 0) {
96
- lines.push(cur);
97
- cur = '';
98
- }
99
- };
100
-
101
- for (const word of words) {
102
- if (word.length > budget) {
103
- // Token wider than the cell: hard-break it across lines.
104
- flush();
105
- let rest = word;
106
- while (rest.length > budget) {
107
- lines.push(rest.slice(0, budget));
108
- rest = rest.slice(budget);
109
- }
110
- cur = rest;
111
- continue;
112
- }
113
- const candidate = cur.length > 0 ? `${cur} ${word}` : word;
114
- if (candidate.length > budget) {
115
- flush();
116
- cur = word;
117
- } else {
118
- cur = candidate;
119
- }
120
- }
121
- flush();
122
-
123
- if (lines.length > maxLines) {
124
- const capped = lines.slice(0, maxLines);
125
- const last = capped[maxLines - 1];
126
- const clipped = last.length > budget - 1 ? last.slice(0, budget - 1) : last;
127
- capped[maxLines - 1] = `${clipped.replace(/…$/, '')}…`;
128
- return capped;
129
- }
130
- return lines;
86
+ return wrapTextLines(text, maxChars, maxLines);
131
87
  }
132
88
 
133
89
  function buildStageIndex(stages: Stage[]): Map<string, number> {
@@ -4,9 +4,8 @@
4
4
  // referential integrity, atomicity, ID uniqueness, policy — the checks that the
5
5
  // methodology's Python `.validators/lint.py` owns today. This converges those
6
6
  // checks onto the shared TypeScript model so there is a single validation
7
- // runtime (methodology ADR
8
- // `2026-06-11-validation-two-axis-model.md`; Studio referencing ADR
9
- // `docs/decisions/2026-06-11-validation-runtime-convergence.md`).
7
+ // runtime (methodology ADR "Validation Two-Axis Model"; Studio decision
8
+ // "Validation Runtime Convergence").
10
9
  //
11
10
  // The finding shape is deliberately minimal — `{ scope, id, message }`. The
12
11
  // richer `target` / `category` reporting taxonomy is explicitly DEFERRED per the
@@ -0,0 +1,52 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ layoutCenteredEntityText,
4
+ maxCharsForInnerWidth,
5
+ wrapWords,
6
+ wrapTextLines,
7
+ } from '../entity-text-layout.js';
8
+
9
+ describe('entity-text-layout', () => {
10
+ it('wrapWords respects max lines and truncates with ellipsis', () => {
11
+ const long = 'one two three four five six seven eight nine ten eleven twelve';
12
+ const lines = wrapWords(long, 12, 2);
13
+ expect(lines.length).toBeLessThanOrEqual(2);
14
+ expect(lines[lines.length - 1]).toMatch(/…$/);
15
+ });
16
+
17
+ it('maxCharsForInnerWidth scales with inner width', () => {
18
+ expect(maxCharsForInnerWidth(160, 7)).toBeGreaterThan(maxCharsForInnerWidth(120, 7));
19
+ });
20
+
21
+ it('layoutCenteredEntityText emits more name capacity on wide boxes', () => {
22
+ const narrow = layoutCenteredEntityText({
23
+ boxX: 0,
24
+ boxY: 0,
25
+ boxWidth: 200,
26
+ boxHeight: 72,
27
+ name: 'Improve customer onboarding experience across regions',
28
+ id: 'GOAL-001',
29
+ nameMaxLines: 2,
30
+ idMaxLines: 1,
31
+ });
32
+ const wide = layoutCenteredEntityText({
33
+ boxX: 0,
34
+ boxY: 0,
35
+ boxWidth: 320,
36
+ boxHeight: 72,
37
+ name: 'Improve customer onboarding experience across regions',
38
+ id: 'GOAL-001',
39
+ nameMaxLines: 2,
40
+ idMaxLines: 1,
41
+ });
42
+ const narrowPrimary = narrow.filter((l) => l.cls === 'text-primary').map((l) => l.text).join(' ');
43
+ const widePrimary = wide.filter((l) => l.cls === 'text-primary').map((l) => l.text).join(' ');
44
+ expect(widePrimary.replace(/…/g, '').length).toBeGreaterThanOrEqual(narrowPrimary.replace(/…/g, '').length);
45
+ });
46
+
47
+ it('wrapTextLines hard-breaks oversized tokens (blueprint cells)', () => {
48
+ const lines = wrapTextLines('supercalifragilisticexpialidocious word', 10, 3);
49
+ expect(lines.length).toBeGreaterThan(1);
50
+ expect(lines.every((l) => l.length <= 10)).toBe(true);
51
+ });
52
+ });
@@ -47,9 +47,9 @@ describe('webview/entry — Step 2 host API', () => {
47
47
 
48
48
  it('routes non-goals kinds to their validator after Step 4 (no NOTATION-NOT-WIRED)', () => {
49
49
  // Every supported kind is now wired to its validator + renderer. A
50
- // malformed fgca document must surface a structured validation error from
51
- // the fgca validator — never the Step-2/3 "not wired" placeholder.
52
- const r = render('fgca', 'changes: []\n');
50
+ // malformed dgca document must surface a structured validation error from
51
+ // the dgca validator — never the Step-2/3 "not wired" placeholder.
52
+ const r = render('dgca', 'changes: []\n');
53
53
  expect(r.status).toBe('error');
54
54
  expect(r.errors.length).toBeGreaterThan(0);
55
55
  expect(r.errors.every((e) => e.code !== 'NOTATION-NOT-WIRED')).toBe(true);
@@ -10,15 +10,7 @@ import { describe, expect, it } from 'vitest';
10
10
 
11
11
  import type { BlocksFile } from '../../blocks/types.js';
12
12
  import { renderBlocksSvg } from '../render-blocks.js';
13
-
14
- // Approximate character widths used by the renderer when sizing labels.
15
- // Kept in sync with `CHAR_W` / `CHAR_W_ID` in render-blocks.ts so the test
16
- // can compute the same overflow bounds the renderer uses.
17
- const CHAR_W = 7;
18
- const CHAR_W_ID = 6;
19
-
20
- // Same horizontal margin the renderer reserves so text never abuts the rect.
21
- const TEXT_MARGIN_X = 8;
13
+ import { CHAR_W_PRIMARY as CHAR_W, CHAR_W_ID, TEXT_MARGIN_X } from '../entity-text-layout.js';
22
14
 
23
15
  function leafDoc(name: string, id: string): BlocksFile {
24
16
  return {
@@ -65,8 +65,22 @@ describe('renderGoalsSvg', () => {
65
65
  expect(svg).toContain('height="0"');
66
66
  });
67
67
 
68
- it('omits the title block when treeName is missing', () => {
69
- const svg = renderGoalsSvg(SIMPLE_TREE);
70
- expect(svg).not.toContain('Goal tree —');
68
+ it('shows canonical goal ids in node labels when present', () => {
69
+ const tree: GoalTree = {
70
+ goal_types: [{ name: 'Strategy', level: 0 }],
71
+ goals: [
72
+ {
73
+ id: 1,
74
+ canonical_id: 'GOAL-REVENUE-1',
75
+ name: 'Triple revenue',
76
+ type: 'Strategy',
77
+ level: 0,
78
+ parent_id: 0,
79
+ },
80
+ ],
81
+ };
82
+ const svg = renderGoalsSvg(tree);
83
+ expect(svg).toContain('GOAL-REVENUE-1');
84
+ expect(svg).not.toMatch(/>\s*1\s*</);
71
85
  });
72
86
  });