@transitrix/diagrams 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +47 -47
  3. package/dist/__tests__/edge-path.test.js +8 -0
  4. package/dist/activity-card/__tests__/example.test.js +20 -1
  5. package/dist/activity-card/__tests__/layout.test.js +102 -11
  6. package/dist/activity-card/__tests__/resolver.test.js +139 -6
  7. package/dist/activity-card/index.d.ts +1 -1
  8. package/dist/activity-card/layout.d.ts +1 -1
  9. package/dist/activity-card/layout.js +220 -103
  10. package/dist/activity-card/resolver.js +107 -26
  11. package/dist/activity-card/types.d.ts +150 -27
  12. package/dist/activity-card/validate.js +3 -0
  13. package/dist/actor/__tests__/validate.test.js +63 -31
  14. package/dist/actor/index.d.ts +1 -1
  15. package/dist/actor/index.js +1 -1
  16. package/dist/actor/types.d.ts +7 -3
  17. package/dist/actor/types.js +17 -3
  18. package/dist/actor/validate.js +55 -26
  19. package/dist/assertion/types.d.ts +3 -1
  20. package/dist/assertion/types.js +1 -1
  21. package/dist/change/__tests__/validate.test.js +40 -34
  22. package/dist/change/index.d.ts +1 -0
  23. package/dist/change/types.d.ts +1 -1
  24. package/dist/change/types.js +2 -2
  25. package/dist/change/validate.d.ts +6 -1
  26. package/dist/change/validate.js +68 -41
  27. package/dist/compliance/__tests__/classify.test.js +34 -0
  28. package/dist/compliance/__tests__/coverage-metric.test.js +146 -46
  29. package/dist/compliance/__tests__/html.test.js +2 -2
  30. package/dist/compliance/__tests__/impact.test.js +311 -6
  31. package/dist/compliance/classify.d.ts +6 -0
  32. package/dist/compliance/classify.js +9 -1
  33. package/dist/compliance/coverage-metric.d.ts +31 -11
  34. package/dist/compliance/coverage-metric.js +149 -39
  35. package/dist/compliance/html.js +77 -74
  36. package/dist/compliance/impact.d.ts +77 -16
  37. package/dist/compliance/impact.js +193 -25
  38. package/dist/compliance/index.d.ts +2 -2
  39. package/dist/compliance/index.js +1 -1
  40. package/dist/compliance/markdown.js +1 -0
  41. package/dist/compliance/types.d.ts +25 -6
  42. package/dist/edge-path.d.ts +8 -4
  43. package/dist/edge-path.js +11 -6
  44. package/dist/factor/__tests__/validate.test.js +74 -45
  45. package/dist/factor/index.d.ts +3 -2
  46. package/dist/factor/index.js +1 -1
  47. package/dist/factor/types.d.ts +4 -7
  48. package/dist/factor/types.js +10 -5
  49. package/dist/factor/validate.d.ts +6 -1
  50. package/dist/factor/validate.js +68 -50
  51. package/dist/fgca/__tests__/resolver.test.js +185 -0
  52. package/dist/fgca/index.d.ts +2 -0
  53. package/dist/fgca/index.js +1 -0
  54. package/dist/fgca/resolver.d.ts +49 -0
  55. package/dist/fgca/resolver.js +124 -0
  56. package/dist/goals/index.d.ts +1 -1
  57. package/dist/index.d.ts +5 -0
  58. package/dist/index.js +5 -0
  59. package/dist/process-blueprint/__tests__/layout.test.js +37 -0
  60. package/dist/process-blueprint/layout.js +5 -1
  61. package/dist/process-blueprint/types.d.ts +22 -1
  62. package/dist/stakeholder/__tests__/validate.test.js +49 -47
  63. package/dist/stakeholder/index.d.ts +1 -0
  64. package/dist/stakeholder/types.d.ts +3 -4
  65. package/dist/stakeholder/types.js +2 -3
  66. package/dist/stakeholder/validate.d.ts +6 -1
  67. package/dist/stakeholder/validate.js +71 -50
  68. package/dist/target-state/__tests__/validate.test.js +43 -27
  69. package/dist/target-state/index.d.ts +2 -0
  70. package/dist/target-state/index.js +1 -0
  71. package/dist/target-state/types.d.ts +6 -1
  72. package/dist/target-state/types.js +8 -7
  73. package/dist/target-state/validate.d.ts +6 -1
  74. package/dist/target-state/validate.js +69 -45
  75. package/dist/theme/themes.js +69 -69
  76. package/dist/webview/__tests__/entry.test.js +11 -11
  77. package/dist/webview/render-activities.d.ts +3 -1
  78. package/dist/webview/render-activities.js +2 -2
  79. package/dist/webview/render-activity-card.js +104 -77
  80. package/dist/webview/render-fgca.d.ts +4 -0
  81. package/dist/webview/render-fgca.js +2 -5
  82. package/dist/webview/render-goals.d.ts +1 -0
  83. package/dist/webview/render-goals.js +15 -15
  84. package/dist/webview/render-process-blueprint.js +10 -10
  85. package/package.json +40 -40
  86. package/src/__tests__/edge-path.test.ts +58 -46
  87. package/src/__tests__/scope.test.ts +27 -27
  88. package/src/__tests__/yaml-normalize.test.ts +158 -158
  89. package/src/activities/__tests__/cpm.test.ts +164 -164
  90. package/src/activities/__tests__/gantt.test.ts +245 -245
  91. package/src/activities/__tests__/layout.test.ts +112 -112
  92. package/src/activities/__tests__/validate.test.ts +409 -409
  93. package/src/activities/cpm.ts +95 -95
  94. package/src/activities/gantt.ts +276 -276
  95. package/src/activities/index.ts +28 -28
  96. package/src/activities/layout.ts +112 -112
  97. package/src/activities/types.ts +148 -148
  98. package/src/activities/validate.ts +357 -357
  99. package/src/activity-card/__tests__/example.test.ts +85 -66
  100. package/src/activity-card/__tests__/layout.test.ts +169 -63
  101. package/src/activity-card/__tests__/resolver.test.ts +307 -161
  102. package/src/activity-card/__tests__/validate.test.ts +131 -131
  103. package/src/activity-card/index.ts +38 -32
  104. package/src/activity-card/layout.ts +324 -194
  105. package/src/activity-card/resolver.ts +415 -337
  106. package/src/activity-card/types.ts +376 -237
  107. package/src/activity-card/validate.ts +162 -158
  108. package/src/actor/__tests__/validate.test.ts +70 -31
  109. package/src/actor/index.ts +1 -1
  110. package/src/actor/types.ts +21 -7
  111. package/src/actor/validate.ts +59 -27
  112. package/src/applications/__tests__/validate.test.ts +256 -256
  113. package/src/applications/index.ts +11 -11
  114. package/src/applications/types.ts +43 -43
  115. package/src/applications/validate.ts +131 -131
  116. package/src/assertion/__tests__/example.test.ts +37 -37
  117. package/src/assertion/__tests__/validate.test.ts +142 -142
  118. package/src/assertion/index.ts +12 -12
  119. package/src/assertion/types.ts +60 -58
  120. package/src/assertion/validate.ts +145 -145
  121. package/src/blocks/__tests__/layout.test.ts +169 -169
  122. package/src/blocks/__tests__/validate.test.ts +260 -260
  123. package/src/blocks/index.ts +17 -17
  124. package/src/blocks/layout.ts +186 -186
  125. package/src/blocks/types.ts +62 -62
  126. package/src/blocks/validate.ts +206 -206
  127. package/src/capability-map/__tests__/validate.test.ts +238 -238
  128. package/src/capability-map/index.ts +8 -8
  129. package/src/capability-map/types.ts +29 -29
  130. package/src/capability-map/validate.ts +127 -127
  131. package/src/change/__tests__/validate.test.ts +43 -34
  132. package/src/change/index.ts +1 -0
  133. package/src/change/types.ts +5 -3
  134. package/src/change/validate.ts +75 -43
  135. package/src/compliance/__tests__/classify.test.ts +64 -27
  136. package/src/compliance/__tests__/confidence.test.ts +98 -98
  137. package/src/compliance/__tests__/coverage-metric.test.ts +300 -188
  138. package/src/compliance/__tests__/demo-scenario.test.ts +273 -273
  139. package/src/compliance/__tests__/example.test.ts +57 -57
  140. package/src/compliance/__tests__/gap-report.test.ts +128 -128
  141. package/src/compliance/__tests__/html.test.ts +141 -141
  142. package/src/compliance/__tests__/impact.test.ts +840 -481
  143. package/src/compliance/__tests__/markdown.test.ts +94 -94
  144. package/src/compliance/__tests__/views.test.ts +64 -64
  145. package/src/compliance/classify.ts +100 -84
  146. package/src/compliance/confidence.ts +69 -69
  147. package/src/compliance/coverage-metric.ts +404 -263
  148. package/src/compliance/gap-report.ts +94 -94
  149. package/src/compliance/html.ts +298 -295
  150. package/src/compliance/impact.ts +861 -677
  151. package/src/compliance/index.ts +57 -56
  152. package/src/compliance/markdown.ts +136 -135
  153. package/src/compliance/reverse-index.ts +33 -33
  154. package/src/compliance/types.ts +136 -120
  155. package/src/compliance/views.ts +46 -46
  156. package/src/compliance-matrix/__tests__/build.test.ts +200 -200
  157. package/src/compliance-matrix/__tests__/example.test.ts +57 -57
  158. package/src/compliance-matrix/index.ts +11 -11
  159. package/src/compliance-matrix/types.ts +102 -102
  160. package/src/edge-path.ts +50 -44
  161. package/src/factor/__tests__/validate.test.ts +80 -45
  162. package/src/factor/index.ts +3 -7
  163. package/src/factor/types.ts +14 -18
  164. package/src/factor/validate.ts +77 -51
  165. package/src/fgca/__tests__/chain-table.test.ts +172 -172
  166. package/src/fgca/__tests__/layout.test.ts +179 -179
  167. package/src/fgca/__tests__/parse-canonical.test.ts +248 -248
  168. package/src/fgca/__tests__/preview-layout.test.ts +124 -124
  169. package/src/fgca/__tests__/resolver.test.ts +209 -0
  170. package/src/fgca/__tests__/validate.test.ts +142 -142
  171. package/src/fgca/chain-table.ts +225 -225
  172. package/src/fgca/index.ts +47 -45
  173. package/src/fgca/layout.ts +204 -204
  174. package/src/fgca/nodeTypes.ts +11 -11
  175. package/src/fgca/nodes/FGCAActivityNode.tsx +65 -65
  176. package/src/fgca/nodes/FGCAChangeNode.tsx +64 -64
  177. package/src/fgca/nodes/FGCAFactorNode.tsx +64 -64
  178. package/src/fgca/nodes/FGCAGoalNode.tsx +72 -72
  179. package/src/fgca/parse-canonical.ts +400 -400
  180. package/src/fgca/preview-layout.ts +217 -217
  181. package/src/fgca/resolver.ts +157 -0
  182. package/src/fgca/types.ts +50 -50
  183. package/src/fgca/validate.ts +126 -126
  184. package/src/geometry.ts +9 -9
  185. package/src/goals/__tests__/layout.test.ts +175 -175
  186. package/src/goals/__tests__/mutations.test.ts +111 -111
  187. package/src/goals/__tests__/parse-canonical.test.ts +145 -145
  188. package/src/goals/__tests__/validate.test.ts +202 -202
  189. package/src/goals/index.ts +20 -21
  190. package/src/goals/layout.ts +172 -172
  191. package/src/goals/mutations.ts +123 -123
  192. package/src/goals/parse-canonical.ts +286 -286
  193. package/src/goals/types.ts +85 -85
  194. package/src/goals/validate.ts +136 -136
  195. package/src/index.ts +28 -23
  196. package/src/process-blueprint/__tests__/compliance-lane.test.ts +320 -320
  197. package/src/process-blueprint/__tests__/layout.test.ts +240 -195
  198. package/src/process-blueprint/__tests__/validate.test.ts +310 -310
  199. package/src/process-blueprint/index.ts +31 -31
  200. package/src/process-blueprint/layout.ts +535 -531
  201. package/src/process-blueprint/types.ts +263 -242
  202. package/src/process-blueprint/validate.ts +181 -181
  203. package/src/process-map/__tests__/validate.test.ts +223 -223
  204. package/src/process-map/index.ts +10 -10
  205. package/src/process-map/types.ts +36 -36
  206. package/src/process-map/validate.ts +135 -135
  207. package/src/products/__tests__/validate.test.ts +239 -239
  208. package/src/products/index.ts +9 -9
  209. package/src/products/types.ts +31 -31
  210. package/src/products/validate.ts +112 -112
  211. package/src/repo-validate/__tests__/validate-repo.test.ts +149 -149
  212. package/src/repo-validate/index.ts +2 -2
  213. package/src/repo-validate/types.ts +50 -50
  214. package/src/repo-validate/validate-repo.ts +196 -196
  215. package/src/requirement/__tests__/example.test.ts +24 -24
  216. package/src/requirement/__tests__/validate.test.ts +96 -96
  217. package/src/requirement/index.ts +4 -4
  218. package/src/requirement/types.ts +35 -35
  219. package/src/requirement/validate.ts +92 -92
  220. package/src/scenarios/__tests__/validate.test.ts +209 -209
  221. package/src/scenarios/index.ts +15 -15
  222. package/src/scenarios/types.ts +37 -37
  223. package/src/scenarios/validate.ts +122 -122
  224. package/src/schema-version.ts +16 -16
  225. package/src/scope.ts +46 -46
  226. package/src/stakeholder/__tests__/validate.test.ts +51 -47
  227. package/src/stakeholder/index.ts +1 -0
  228. package/src/stakeholder/types.ts +7 -7
  229. package/src/stakeholder/validate.ts +79 -52
  230. package/src/target-state/__tests__/validate.test.ts +48 -27
  231. package/src/target-state/index.ts +2 -0
  232. package/src/target-state/types.ts +11 -7
  233. package/src/target-state/validate.ts +76 -46
  234. package/src/theme/index.ts +4 -4
  235. package/src/theme/themes.ts +237 -237
  236. package/src/theme/tokens.ts +133 -133
  237. package/src/typed-id.ts +56 -56
  238. package/src/validation-types.ts +36 -36
  239. package/src/webview/__tests__/entry-notations.test.ts +77 -77
  240. package/src/webview/__tests__/entry.test.ts +64 -64
  241. package/src/webview/__tests__/render-goals.test.ts +72 -72
  242. package/src/webview/entry.ts +292 -292
  243. package/src/webview/render-activities.ts +5 -3
  244. package/src/webview/render-activity-card.ts +266 -234
  245. package/src/webview/render-fgca.ts +6 -5
  246. package/src/webview/render-goals.ts +111 -110
  247. package/src/webview/render-process-blueprint.ts +207 -207
  248. package/src/webview/styles.css +322 -323
  249. package/src/yaml-normalize.ts +56 -56
  250. package/dist/element-envelope.d.ts +0 -15
  251. package/dist/element-envelope.js +0 -55
  252. package/dist/issues/__tests__/layout.test.js +0 -62
  253. package/dist/issues/__tests__/validate.test.d.ts +0 -1
  254. package/dist/issues/__tests__/validate.test.js +0 -82
  255. package/dist/issues/index.d.ts +0 -4
  256. package/dist/issues/index.js +0 -2
  257. package/dist/issues/layout.d.ts +0 -10
  258. package/dist/issues/layout.js +0 -82
  259. package/dist/issues/types.d.ts +0 -61
  260. package/dist/issues/types.js +0 -6
  261. package/dist/issues/validate.d.ts +0 -3
  262. package/dist/issues/validate.js +0 -143
  263. package/dist/webview/transitrix-render.css +0 -323
  264. package/dist/webview/transitrix-render.js +0 -6452
  265. package/dist/webview/transitrix-render.js.map +0 -7
  266. package/src/element-envelope.ts +0 -76
  267. /package/dist/{issues/__tests__/layout.test.d.ts → fgca/__tests__/resolver.test.d.ts} +0 -0
@@ -1,124 +1,124 @@
1
- import { describe, it, expect } from 'vitest';
2
- import {
3
- layoutFGCAPreview,
4
- FGCA_PAD,
5
- FGCA_NODE_W,
6
- type FGCAPreviewDoc,
7
- } from '../preview-layout.js';
8
-
9
- const doc: FGCAPreviewDoc = {
10
- factors: [
11
- { id: 1, name: 'F1' },
12
- { id: 2, name: 'F2' },
13
- ],
14
- goals: [
15
- { id: 10, name: 'G1', factor: [{ id: 1 }] },
16
- { id: 11, name: 'G2', factor: [{ id: 2 }] },
17
- ],
18
- changes: [{ id: 20, name: 'C1', goal_id: 10, activity_ids: [30] }],
19
- activities: [
20
- { id: 30, name: 'A1', goal_id: 10 },
21
- { id: 31, name: 'A2', goal_id: 11 },
22
- ],
23
- };
24
-
25
- describe('layoutFGCAPreview', () => {
26
- it('lays out all four columns in order (FGCA)', () => {
27
- const layout = layoutFGCAPreview(doc);
28
- expect(layout.columns.map(c => c.col)).toEqual(['factor', 'goal', 'change', 'activity']);
29
- // 2 factors + 2 goals + 1 change + 2 activities
30
- expect(layout.nodes).toHaveLength(7);
31
- expect(layout.width).toBeGreaterThan(0);
32
- expect(layout.height).toBeGreaterThan(0);
33
- });
34
-
35
- it('produces the expected FGCA edge set', () => {
36
- // F1→G1, F2→G2 (2), G1→C1 (1), C1→A1 (1), G2→A2 direct (1) = 5
37
- const layout = layoutFGCAPreview(doc);
38
- expect(layout.edges).toHaveLength(5);
39
- });
40
-
41
- it('hideChanges (FGA) drops the Changes column and links goals to activities', () => {
42
- const layout = layoutFGCAPreview(doc, { hideChanges: true });
43
- expect(layout.columns.map(c => c.col)).toEqual(['factor', 'goal', 'activity']);
44
- // 2 factors + 2 goals + 2 activities
45
- expect(layout.nodes).toHaveLength(6);
46
- // F1→G1, F2→G2 (2), G1→A1, G2→A2 (2) = 4
47
- expect(layout.edges).toHaveLength(4);
48
- });
49
-
50
- it('first column sits at the pad origin', () => {
51
- const layout = layoutFGCAPreview(doc);
52
- expect(layout.columns[0].x).toBe(FGCA_PAD);
53
- });
54
-
55
- it('empty options reproduce the default layout', () => {
56
- const a = layoutFGCAPreview(doc);
57
- const b = layoutFGCAPreview(doc, {});
58
- expect(b.nodes.map(n => `${n.id}:${n.x},${n.y}`)).toEqual(a.nodes.map(n => `${n.id}:${n.x},${n.y}`));
59
- });
60
-
61
- it('larger colGap widens the column step', () => {
62
- const stepOf = (colGap: number) => {
63
- const cols = layoutFGCAPreview(doc, { colGap }).columns;
64
- return cols[1].x - cols[0].x;
65
- };
66
- expect(stepOf(80)).toBe(FGCA_NODE_W + 80);
67
- expect(stepOf(240)).toBe(FGCA_NODE_W + 240);
68
- expect(stepOf(240)).toBeGreaterThan(stepOf(80));
69
- });
70
-
71
- it('larger rowGap increases the gap between stacked nodes', () => {
72
- const gapOf = (rowGap: number) => {
73
- const factors = layoutFGCAPreview(doc, { rowGap }).nodes.filter(n => n.col === 'factor');
74
- return factors[1].y - factors[0].y;
75
- };
76
- expect(gapOf(120)).toBeGreaterThan(gapOf(20));
77
- });
78
-
79
- // vkgeorgia/strategy#77 — scope filtering. FGCA goals are flat, so 'root'
80
- // selects the single matching goal plus the factors/changes/activities that
81
- // touch it.
82
- describe('scope', () => {
83
- const idsOf = (layout: ReturnType<typeof layoutFGCAPreview>) => new Set(layout.nodes.map(n => n.id));
84
-
85
- it("mode 'root' keeps only the root goal and what connects to it", () => {
86
- // root 10 → factor 1 (referenced by G10), change 20 (goal_id 10),
87
- // activity 30 (via change 20). G11/F2/A31 are dropped.
88
- const layout = layoutFGCAPreview(doc, { scope: { mode: 'root', rootGoalId: '10' } });
89
- expect(idsOf(layout)).toEqual(new Set(['factor_1', 'goal_10', 'change_20', 'activity_30']));
90
- });
91
-
92
- it("mode 'root' filters factors and activities to those touching the visible goal", () => {
93
- // root 11 → factor 2, activity 31 (direct goal link); no change touches G11.
94
- const layout = layoutFGCAPreview(doc, { scope: { mode: 'root', rootGoalId: '11' } });
95
- expect(idsOf(layout)).toEqual(new Set(['factor_2', 'goal_11', 'activity_31']));
96
- // F1, A30, C20 (which only touch the hidden G10) are excluded.
97
- expect(layout.nodes.some(n => n.id === 'factor_1')).toBe(false);
98
- expect(layout.nodes.some(n => n.id === 'activity_30')).toBe(false);
99
- });
100
-
101
- it("mode 'level' trims goals above the cap and their connections", () => {
102
- const leveled: FGCAPreviewDoc = {
103
- factors: [{ id: 1, name: 'F1' }, { id: 2, name: 'F2' }],
104
- goals: [
105
- { id: 10, name: 'G1', level: 0, factor: [{ id: 1 }] },
106
- { id: 11, name: 'G2', level: 1, factor: [{ id: 2 }] },
107
- ],
108
- changes: [],
109
- activities: [
110
- { id: 30, name: 'A1', goal_id: 10 },
111
- { id: 31, name: 'A2', goal_id: 11 },
112
- ],
113
- };
114
- const layout = layoutFGCAPreview(leveled, { scope: { mode: 'level', maxLevel: 0 } });
115
- expect(idsOf(layout)).toEqual(new Set(['factor_1', 'goal_10', 'activity_30']));
116
- });
117
-
118
- it("mode 'root' returns an empty layout when the root is absent", () => {
119
- const layout = layoutFGCAPreview(doc, { scope: { mode: 'root', rootGoalId: '999' } });
120
- expect(layout.nodes).toHaveLength(0);
121
- expect(layout.edges).toHaveLength(0);
122
- });
123
- });
124
- });
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ layoutFGCAPreview,
4
+ FGCA_PAD,
5
+ FGCA_NODE_W,
6
+ type FGCAPreviewDoc,
7
+ } from '../preview-layout.js';
8
+
9
+ const doc: FGCAPreviewDoc = {
10
+ factors: [
11
+ { id: 1, name: 'F1' },
12
+ { id: 2, name: 'F2' },
13
+ ],
14
+ goals: [
15
+ { id: 10, name: 'G1', factor: [{ id: 1 }] },
16
+ { id: 11, name: 'G2', factor: [{ id: 2 }] },
17
+ ],
18
+ changes: [{ id: 20, name: 'C1', goal_id: 10, activity_ids: [30] }],
19
+ activities: [
20
+ { id: 30, name: 'A1', goal_id: 10 },
21
+ { id: 31, name: 'A2', goal_id: 11 },
22
+ ],
23
+ };
24
+
25
+ describe('layoutFGCAPreview', () => {
26
+ it('lays out all four columns in order (FGCA)', () => {
27
+ const layout = layoutFGCAPreview(doc);
28
+ expect(layout.columns.map(c => c.col)).toEqual(['factor', 'goal', 'change', 'activity']);
29
+ // 2 factors + 2 goals + 1 change + 2 activities
30
+ expect(layout.nodes).toHaveLength(7);
31
+ expect(layout.width).toBeGreaterThan(0);
32
+ expect(layout.height).toBeGreaterThan(0);
33
+ });
34
+
35
+ it('produces the expected FGCA edge set', () => {
36
+ // F1→G1, F2→G2 (2), G1→C1 (1), C1→A1 (1), G2→A2 direct (1) = 5
37
+ const layout = layoutFGCAPreview(doc);
38
+ expect(layout.edges).toHaveLength(5);
39
+ });
40
+
41
+ it('hideChanges (FGA) drops the Changes column and links goals to activities', () => {
42
+ const layout = layoutFGCAPreview(doc, { hideChanges: true });
43
+ expect(layout.columns.map(c => c.col)).toEqual(['factor', 'goal', 'activity']);
44
+ // 2 factors + 2 goals + 2 activities
45
+ expect(layout.nodes).toHaveLength(6);
46
+ // F1→G1, F2→G2 (2), G1→A1, G2→A2 (2) = 4
47
+ expect(layout.edges).toHaveLength(4);
48
+ });
49
+
50
+ it('first column sits at the pad origin', () => {
51
+ const layout = layoutFGCAPreview(doc);
52
+ expect(layout.columns[0].x).toBe(FGCA_PAD);
53
+ });
54
+
55
+ it('empty options reproduce the default layout', () => {
56
+ const a = layoutFGCAPreview(doc);
57
+ const b = layoutFGCAPreview(doc, {});
58
+ expect(b.nodes.map(n => `${n.id}:${n.x},${n.y}`)).toEqual(a.nodes.map(n => `${n.id}:${n.x},${n.y}`));
59
+ });
60
+
61
+ it('larger colGap widens the column step', () => {
62
+ const stepOf = (colGap: number) => {
63
+ const cols = layoutFGCAPreview(doc, { colGap }).columns;
64
+ return cols[1].x - cols[0].x;
65
+ };
66
+ expect(stepOf(80)).toBe(FGCA_NODE_W + 80);
67
+ expect(stepOf(240)).toBe(FGCA_NODE_W + 240);
68
+ expect(stepOf(240)).toBeGreaterThan(stepOf(80));
69
+ });
70
+
71
+ it('larger rowGap increases the gap between stacked nodes', () => {
72
+ const gapOf = (rowGap: number) => {
73
+ const factors = layoutFGCAPreview(doc, { rowGap }).nodes.filter(n => n.col === 'factor');
74
+ return factors[1].y - factors[0].y;
75
+ };
76
+ expect(gapOf(120)).toBeGreaterThan(gapOf(20));
77
+ });
78
+
79
+ // vkgeorgia/strategy#77 — scope filtering. FGCA goals are flat, so 'root'
80
+ // selects the single matching goal plus the factors/changes/activities that
81
+ // touch it.
82
+ describe('scope', () => {
83
+ const idsOf = (layout: ReturnType<typeof layoutFGCAPreview>) => new Set(layout.nodes.map(n => n.id));
84
+
85
+ it("mode 'root' keeps only the root goal and what connects to it", () => {
86
+ // root 10 → factor 1 (referenced by G10), change 20 (goal_id 10),
87
+ // activity 30 (via change 20). G11/F2/A31 are dropped.
88
+ const layout = layoutFGCAPreview(doc, { scope: { mode: 'root', rootGoalId: '10' } });
89
+ expect(idsOf(layout)).toEqual(new Set(['factor_1', 'goal_10', 'change_20', 'activity_30']));
90
+ });
91
+
92
+ it("mode 'root' filters factors and activities to those touching the visible goal", () => {
93
+ // root 11 → factor 2, activity 31 (direct goal link); no change touches G11.
94
+ const layout = layoutFGCAPreview(doc, { scope: { mode: 'root', rootGoalId: '11' } });
95
+ expect(idsOf(layout)).toEqual(new Set(['factor_2', 'goal_11', 'activity_31']));
96
+ // F1, A30, C20 (which only touch the hidden G10) are excluded.
97
+ expect(layout.nodes.some(n => n.id === 'factor_1')).toBe(false);
98
+ expect(layout.nodes.some(n => n.id === 'activity_30')).toBe(false);
99
+ });
100
+
101
+ it("mode 'level' trims goals above the cap and their connections", () => {
102
+ const leveled: FGCAPreviewDoc = {
103
+ factors: [{ id: 1, name: 'F1' }, { id: 2, name: 'F2' }],
104
+ goals: [
105
+ { id: 10, name: 'G1', level: 0, factor: [{ id: 1 }] },
106
+ { id: 11, name: 'G2', level: 1, factor: [{ id: 2 }] },
107
+ ],
108
+ changes: [],
109
+ activities: [
110
+ { id: 30, name: 'A1', goal_id: 10 },
111
+ { id: 31, name: 'A2', goal_id: 11 },
112
+ ],
113
+ };
114
+ const layout = layoutFGCAPreview(leveled, { scope: { mode: 'level', maxLevel: 0 } });
115
+ expect(idsOf(layout)).toEqual(new Set(['factor_1', 'goal_10', 'activity_30']));
116
+ });
117
+
118
+ it("mode 'root' returns an empty layout when the root is absent", () => {
119
+ const layout = layoutFGCAPreview(doc, { scope: { mode: 'root', rootGoalId: '999' } });
120
+ expect(layout.nodes).toHaveLength(0);
121
+ expect(layout.edges).toHaveLength(0);
122
+ });
123
+ });
124
+ });
@@ -0,0 +1,209 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
4
+ import yaml from 'js-yaml';
5
+ import { resolveFGCA, isFGCAViewDoc } from '../resolver.js';
6
+ import { parseCanonicalFGCA } from '../parse-canonical.js';
7
+
8
+ // ── Inline canon element store (mirrors strategy-2026 data split into per-element form) ─
9
+
10
+ const ELEMENTS = [
11
+ { notation: 'factor', id: 'FACTOR-1', name: 'Competitive market pressure', type: 'external' },
12
+ { notation: 'factor', id: 'FACTOR-2', name: 'Digital adoption acceleration', type: 'external' },
13
+ { notation: 'goal', id: 'GOAL-1', name: 'Grow revenue by 20%', factors: ['FACTOR-1', 'FACTOR-2'] },
14
+ { notation: 'goal', id: 'GOAL-2', name: 'Reduce operational costs', factors: ['FACTOR-2'] },
15
+ { notation: 'goal', id: 'GOAL-3', name: 'Improve customer retention', factors: ['FACTOR-1'] },
16
+ { notation: 'change', id: 'CHANGE-1', name: 'Launch new product line', goals: ['GOAL-1'] },
17
+ { notation: 'change', id: 'CHANGE-2', name: 'Automate manual processes', goals: ['GOAL-2'] },
18
+ { notation: 'change', id: 'CHANGE-3', name: 'Enhance support platform', goals: ['GOAL-3'] },
19
+ { notation: 'activity', id: 'ACTIVITY-1', name: 'Market research', changes: ['CHANGE-1'] },
20
+ { notation: 'activity', id: 'ACTIVITY-2', name: 'MVP development', changes: ['CHANGE-1'] },
21
+ { notation: 'activity', id: 'ACTIVITY-3', name: 'Process audit', changes: ['CHANGE-2'] },
22
+ { notation: 'activity', id: 'ACTIVITY-4', name: 'RPA tooling rollout', changes: ['CHANGE-2'] },
23
+ { notation: 'activity', id: 'ACTIVITY-5', name: 'CRM implementation', changes: ['CHANGE-3'] },
24
+ ];
25
+
26
+ const SOURCES = { elements: ELEMENTS, relations: [] };
27
+
28
+ const VIEW_DOC = {
29
+ notation: 'fgca',
30
+ id: 'FGCA-STRAT-1',
31
+ name: 'Strategy 2026 — FGCA chain',
32
+ spec_version: '0.1',
33
+ view_config: {
34
+ goals: { filter: 'all' },
35
+ factors: { surface: 'derived' },
36
+ changes: { surface: 'derived' },
37
+ activities: { surface: 'derived' },
38
+ },
39
+ };
40
+
41
+ // ── isFGCAViewDoc ─────────────────────────────────────────────────────────────
42
+
43
+ describe('isFGCAViewDoc', () => {
44
+ it('returns true for a view_config-based doc', () => {
45
+ expect(isFGCAViewDoc(VIEW_DOC)).toBe(true);
46
+ });
47
+
48
+ it('returns false for an inline doc with factors/goals arrays', () => {
49
+ expect(isFGCAViewDoc({
50
+ notation: 'fgca', id: 'FGCA-1', name: 'x',
51
+ factors: [], goals: [], changes: [], activities: [],
52
+ })).toBe(false);
53
+ });
54
+
55
+ it('returns false for non-objects', () => {
56
+ expect(isFGCAViewDoc(null)).toBe(false);
57
+ expect(isFGCAViewDoc('string')).toBe(false);
58
+ expect(isFGCAViewDoc(42)).toBe(false);
59
+ });
60
+ });
61
+
62
+ // ── resolveFGCA — goals.filter=all (default) ──────────────────────────────────
63
+
64
+ describe('resolveFGCA — goals.filter=all', () => {
65
+ it('includes all goals, changes, and activities', () => {
66
+ const doc = resolveFGCA(VIEW_DOC, SOURCES);
67
+ expect((doc['goals'] as unknown[]).length).toBe(3);
68
+ expect((doc['changes'] as unknown[]).length).toBe(3);
69
+ expect((doc['activities'] as unknown[]).length).toBe(5);
70
+ });
71
+
72
+ it('derives factors from all goal.factors[] refs', () => {
73
+ const doc = resolveFGCA(VIEW_DOC, SOURCES);
74
+ const factorIds = (doc['factors'] as Array<{ id: string }>).map((f) => f.id);
75
+ expect(factorIds).toContain('FACTOR-1');
76
+ expect(factorIds).toContain('FACTOR-2');
77
+ expect(factorIds.length).toBe(2);
78
+ });
79
+
80
+ it('passes parseCanonicalFGCA and produces a fully valid internal doc', () => {
81
+ const doc = resolveFGCA(VIEW_DOC, SOURCES);
82
+ const r = parseCanonicalFGCA(doc);
83
+ expect(r.valid).toBe(true);
84
+ expect(r.errors).toEqual([]);
85
+ expect(r.parsed?.goals).toHaveLength(3);
86
+ expect(r.parsed?.factors).toHaveLength(2);
87
+ expect(r.parsed?.changes).toHaveLength(3);
88
+ expect(r.parsed?.activities).toHaveLength(5);
89
+ });
90
+
91
+ it('carries view doc metadata (id, name, spec_version)', () => {
92
+ const doc = resolveFGCA(VIEW_DOC, SOURCES);
93
+ expect(doc['notation']).toBe('fgca');
94
+ expect(doc['id']).toBe('FGCA-STRAT-1');
95
+ expect(doc['name']).toBe('Strategy 2026 — FGCA chain');
96
+ expect(doc['spec_version']).toBe('0.1');
97
+ });
98
+ });
99
+
100
+ // ── resolveFGCA — goals.filter=ids ────────────────────────────────────────────
101
+
102
+ describe('resolveFGCA — goals.filter=ids', () => {
103
+ it('narrows to the specified goal and its derived changes + activities', () => {
104
+ const viewDoc = {
105
+ ...VIEW_DOC,
106
+ view_config: { goals: { filter: 'ids', ids: ['GOAL-1'] }, factors: { surface: 'derived' }, changes: { surface: 'derived' }, activities: { surface: 'derived' } },
107
+ };
108
+ const doc = resolveFGCA(viewDoc, SOURCES);
109
+ const goalIds = (doc['goals'] as Array<{ id: string }>).map((g) => g.id);
110
+ expect(goalIds).toEqual(['GOAL-1']);
111
+ const changeIds = (doc['changes'] as Array<{ id: string }>).map((c) => c.id);
112
+ expect(changeIds).toEqual(['CHANGE-1']);
113
+ const actIds = (doc['activities'] as Array<{ id: string }>).map((a) => a.id).sort();
114
+ expect(actIds).toEqual(['ACTIVITY-1', 'ACTIVITY-2']);
115
+ const r = parseCanonicalFGCA(doc);
116
+ expect(r.valid).toBe(true);
117
+ });
118
+
119
+ it('returns empty arrays when none of the requested ids exist in canon', () => {
120
+ const viewDoc = {
121
+ ...VIEW_DOC,
122
+ view_config: { goals: { filter: 'ids', ids: ['GOAL-NONEXISTENT-1'] }, factors: { surface: 'derived' }, changes: { surface: 'derived' }, activities: { surface: 'derived' } },
123
+ };
124
+ const doc = resolveFGCA(viewDoc, SOURCES);
125
+ expect((doc['goals'] as unknown[]).length).toBe(0);
126
+ expect((doc['changes'] as unknown[]).length).toBe(0);
127
+ expect((doc['factors'] as unknown[]).length).toBe(0);
128
+ });
129
+ });
130
+
131
+ // ── resolveFGCA — surface=all ─────────────────────────────────────────────────
132
+
133
+ describe('resolveFGCA — surface=all', () => {
134
+ it('factors.surface=all includes every factor even when only some are referenced', () => {
135
+ const viewDoc = {
136
+ ...VIEW_DOC,
137
+ view_config: { goals: { filter: 'ids', ids: ['GOAL-3'] }, factors: { surface: 'all' }, changes: { surface: 'derived' }, activities: { surface: 'derived' } },
138
+ };
139
+ const doc = resolveFGCA(viewDoc, SOURCES);
140
+ // GOAL-3 only references FACTOR-1, but surface=all → both factors
141
+ expect((doc['factors'] as unknown[]).length).toBe(2);
142
+ });
143
+
144
+ it('changes.surface=all includes every change', () => {
145
+ const viewDoc = {
146
+ ...VIEW_DOC,
147
+ view_config: { goals: { filter: 'ids', ids: ['GOAL-1'] }, factors: { surface: 'derived' }, changes: { surface: 'all' }, activities: { surface: 'derived' } },
148
+ };
149
+ const doc = resolveFGCA(viewDoc, SOURCES);
150
+ expect((doc['changes'] as unknown[]).length).toBe(3);
151
+ });
152
+ });
153
+
154
+ // ── resolveFGCA — empty / degenerate inputs ───────────────────────────────────
155
+
156
+ describe('resolveFGCA — empty sources', () => {
157
+ it('returns empty arrays when canon has no elements', () => {
158
+ const doc = resolveFGCA(VIEW_DOC, { elements: [], relations: [] });
159
+ expect((doc['goals'] as unknown[]).length).toBe(0);
160
+ expect((doc['factors'] as unknown[]).length).toBe(0);
161
+ expect((doc['changes'] as unknown[]).length).toBe(0);
162
+ expect((doc['activities'] as unknown[]).length).toBe(0);
163
+ });
164
+
165
+ it('returns empty arrays for a non-object viewDoc', () => {
166
+ const doc = resolveFGCA(null, SOURCES);
167
+ expect((doc['goals'] as unknown[]).length).toBe(0);
168
+ });
169
+ });
170
+
171
+ // ── File-based regression — resolver + canon fixture + parseCanonicalFGCA ─────
172
+
173
+ describe('resolveFGCA — file-based regression against canon fixture', () => {
174
+ const CANON_ROOT = path.resolve(
175
+ process.cwd(), '..', '..', 'tests', 'fixtures', 'notation-corpus', 'fgca', 'canon',
176
+ );
177
+ const VIEW_DIR = path.join(CANON_ROOT, 'views');
178
+ const ELEM_DIR = path.join(CANON_ROOT, 'elements');
179
+
180
+ function loadYamlsUnder(dir: string): unknown[] {
181
+ if (!fs.existsSync(dir)) return [];
182
+ const out: unknown[] = [];
183
+ function walk(d: string) {
184
+ for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
185
+ const full = path.join(d, entry.name);
186
+ if (entry.isDirectory()) { walk(full); }
187
+ else if (entry.name.endsWith('.yaml')) { out.push(yaml.load(fs.readFileSync(full, 'utf8'))); }
188
+ }
189
+ }
190
+ walk(dir);
191
+ return out;
192
+ }
193
+
194
+ const viewFiles = fs.existsSync(VIEW_DIR)
195
+ ? fs.readdirSync(VIEW_DIR).filter((f) => f.endsWith('.transitrix.yaml'))
196
+ : [];
197
+
198
+ for (const file of viewFiles) {
199
+ it(`resolves ${file} against canon fixtures and passes parseCanonicalFGCA`, () => {
200
+ const viewDoc = yaml.load(fs.readFileSync(path.join(VIEW_DIR, file), 'utf8'));
201
+ expect(isFGCAViewDoc(viewDoc)).toBe(true);
202
+ const elements = loadYamlsUnder(ELEM_DIR);
203
+ const doc = resolveFGCA(viewDoc, { elements, relations: [] });
204
+ const r = parseCanonicalFGCA(doc);
205
+ expect(r.valid).toBe(true);
206
+ expect(r.errors).toEqual([]);
207
+ });
208
+ }
209
+ });