@transitrix/diagrams 1.0.0 → 1.1.0

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 (315) 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/activities/cpm.js +11 -2
  5. package/dist/activities/gantt.js +6 -3
  6. package/dist/activities/types.d.ts +1 -0
  7. package/dist/activities/validate.js +3 -3
  8. package/dist/activity-card/__tests__/example.test.js +20 -1
  9. package/dist/activity-card/__tests__/layout.test.js +102 -11
  10. package/dist/activity-card/__tests__/resolver.test.js +161 -7
  11. package/dist/activity-card/index.d.ts +1 -1
  12. package/dist/activity-card/layout.d.ts +1 -1
  13. package/dist/activity-card/layout.js +220 -103
  14. package/dist/activity-card/resolver.js +115 -28
  15. package/dist/activity-card/types.d.ts +150 -27
  16. package/dist/activity-card/validate.js +3 -0
  17. package/dist/actor/__tests__/validate.test.js +63 -31
  18. package/dist/actor/index.d.ts +1 -1
  19. package/dist/actor/index.js +1 -1
  20. package/dist/actor/types.d.ts +7 -3
  21. package/dist/actor/types.js +17 -3
  22. package/dist/actor/validate.js +55 -26
  23. package/dist/assertion/types.d.ts +3 -1
  24. package/dist/assertion/types.js +1 -1
  25. package/dist/change/__tests__/validate.test.js +40 -34
  26. package/dist/change/index.d.ts +1 -0
  27. package/dist/change/types.d.ts +1 -1
  28. package/dist/change/types.js +2 -2
  29. package/dist/change/validate.d.ts +6 -1
  30. package/dist/change/validate.js +68 -41
  31. package/dist/compliance/__tests__/classify.test.js +34 -0
  32. package/dist/compliance/__tests__/coverage-metric.test.js +146 -46
  33. package/dist/compliance/__tests__/html.test.js +2 -2
  34. package/dist/compliance/__tests__/impact.test.js +509 -6
  35. package/dist/compliance/classify.d.ts +6 -0
  36. package/dist/compliance/classify.js +9 -1
  37. package/dist/compliance/coverage-metric.d.ts +31 -11
  38. package/dist/compliance/coverage-metric.js +149 -39
  39. package/dist/compliance/html.js +78 -81
  40. package/dist/compliance/impact.d.ts +100 -16
  41. package/dist/compliance/impact.js +272 -28
  42. package/dist/compliance/index.d.ts +2 -2
  43. package/dist/compliance/index.js +1 -1
  44. package/dist/compliance/markdown.js +1 -0
  45. package/dist/compliance/types.d.ts +25 -6
  46. package/dist/edge-path.d.ts +8 -4
  47. package/dist/edge-path.js +11 -6
  48. package/dist/factor/__tests__/validate.test.js +75 -46
  49. package/dist/factor/index.d.ts +3 -2
  50. package/dist/factor/index.js +1 -1
  51. package/dist/factor/types.d.ts +5 -8
  52. package/dist/factor/types.js +10 -5
  53. package/dist/factor/validate.d.ts +6 -1
  54. package/dist/factor/validate.js +68 -50
  55. package/dist/fgca/__tests__/chain-table.test.js +2 -2
  56. package/dist/fgca/__tests__/layout.test.js +11 -11
  57. package/dist/fgca/__tests__/preview-layout.test.js +7 -7
  58. package/dist/fgca/__tests__/resolver.test.js +185 -0
  59. package/dist/fgca/chain-table.d.ts +1 -1
  60. package/dist/fgca/chain-table.js +4 -4
  61. package/dist/fgca/index.d.ts +4 -2
  62. package/dist/fgca/index.js +2 -1
  63. package/dist/fgca/layout.d.ts +2 -2
  64. package/dist/fgca/layout.js +9 -9
  65. package/dist/fgca/nodeTypes.d.ts +1 -1
  66. package/dist/fgca/nodeTypes.js +2 -2
  67. package/dist/fgca/nodes/FGCAFactorNode.d.ts +3 -3
  68. package/dist/fgca/nodes/FGCAFactorNode.js +5 -5
  69. package/dist/fgca/parse-canonical.js +15 -10
  70. package/dist/fgca/preview-layout.d.ts +1 -1
  71. package/dist/fgca/preview-layout.js +4 -4
  72. package/dist/fgca/resolver.d.ts +49 -0
  73. package/dist/fgca/resolver.js +126 -0
  74. package/dist/fgca/types.d.ts +11 -11
  75. package/dist/fgca/types.js +2 -2
  76. package/dist/fgca/validate.d.ts +2 -2
  77. package/dist/fgca/validate.js +17 -14
  78. package/dist/goals/index.d.ts +1 -1
  79. package/dist/index.d.ts +5 -0
  80. package/dist/index.js +5 -0
  81. package/dist/process-blueprint/__tests__/layout.test.js +37 -0
  82. package/dist/process-blueprint/index.d.ts +1 -1
  83. package/dist/process-blueprint/layout.js +66 -35
  84. package/dist/process-blueprint/types.d.ts +36 -1
  85. package/dist/scenarios/index.d.ts +1 -1
  86. package/dist/scenarios/types.d.ts +4 -4
  87. package/dist/stakeholder/__tests__/validate.test.js +49 -47
  88. package/dist/stakeholder/index.d.ts +1 -0
  89. package/dist/stakeholder/types.d.ts +3 -4
  90. package/dist/stakeholder/types.js +2 -3
  91. package/dist/stakeholder/validate.d.ts +6 -1
  92. package/dist/stakeholder/validate.js +71 -50
  93. package/dist/target-state/__tests__/validate.test.js +43 -27
  94. package/dist/target-state/index.d.ts +2 -0
  95. package/dist/target-state/index.js +1 -0
  96. package/dist/target-state/types.d.ts +6 -1
  97. package/dist/target-state/types.js +8 -7
  98. package/dist/target-state/validate.d.ts +6 -1
  99. package/dist/target-state/validate.js +69 -45
  100. package/dist/theme/themes.js +71 -69
  101. package/dist/theme/tokens.d.ts +7 -1
  102. package/dist/theme/tokens.js +3 -1
  103. package/dist/webview/__tests__/entry-notations.test.js +2 -0
  104. package/dist/webview/__tests__/entry.test.js +11 -11
  105. package/dist/webview/__tests__/render-process.test.js +463 -0
  106. package/dist/webview/__tests__/render-util.test.d.ts +1 -0
  107. package/dist/webview/__tests__/render-util.test.js +54 -0
  108. package/dist/webview/entry.d.ts +1 -1
  109. package/dist/webview/entry.js +9 -5
  110. package/dist/webview/render-activities.d.ts +24 -2
  111. package/dist/webview/render-activities.js +92 -57
  112. package/dist/webview/render-activity-card.d.ts +11 -1
  113. package/dist/webview/render-activity-card.js +115 -79
  114. package/dist/webview/render-blocks.d.ts +24 -1
  115. package/dist/webview/render-blocks.js +58 -24
  116. package/dist/webview/render-fgca.d.ts +31 -0
  117. package/dist/webview/render-fgca.js +30 -19
  118. package/dist/webview/render-goals.d.ts +27 -1
  119. package/dist/webview/render-goals.js +42 -32
  120. package/dist/webview/render-process-blueprint.d.ts +11 -1
  121. package/dist/webview/render-process-blueprint.js +118 -53
  122. package/dist/webview/render-process.d.ts +95 -0
  123. package/dist/webview/render-process.js +392 -0
  124. package/dist/webview/render-scenarios.js +2 -2
  125. package/dist/webview/render-util.d.ts +19 -6
  126. package/dist/webview/render-util.js +23 -15
  127. package/package.json +54 -40
  128. package/src/__tests__/edge-path.test.ts +58 -46
  129. package/src/__tests__/scope.test.ts +27 -27
  130. package/src/__tests__/yaml-normalize.test.ts +158 -158
  131. package/src/activities/__tests__/cpm.test.ts +164 -164
  132. package/src/activities/__tests__/gantt.test.ts +245 -245
  133. package/src/activities/__tests__/layout.test.ts +112 -112
  134. package/src/activities/__tests__/validate.test.ts +409 -409
  135. package/src/activities/cpm.ts +107 -95
  136. package/src/activities/gantt.ts +277 -276
  137. package/src/activities/index.ts +28 -28
  138. package/src/activities/layout.ts +112 -112
  139. package/src/activities/types.ts +149 -148
  140. package/src/activities/validate.ts +357 -357
  141. package/src/activity-card/__tests__/example.test.ts +85 -66
  142. package/src/activity-card/__tests__/layout.test.ts +169 -63
  143. package/src/activity-card/__tests__/resolver.test.ts +330 -161
  144. package/src/activity-card/__tests__/validate.test.ts +131 -131
  145. package/src/activity-card/index.ts +38 -32
  146. package/src/activity-card/layout.ts +324 -194
  147. package/src/activity-card/resolver.ts +422 -337
  148. package/src/activity-card/types.ts +376 -237
  149. package/src/activity-card/validate.ts +162 -158
  150. package/src/actor/__tests__/validate.test.ts +70 -31
  151. package/src/actor/index.ts +1 -1
  152. package/src/actor/types.ts +21 -7
  153. package/src/actor/validate.ts +59 -27
  154. package/src/applications/__tests__/validate.test.ts +256 -256
  155. package/src/applications/index.ts +11 -11
  156. package/src/applications/types.ts +43 -43
  157. package/src/applications/validate.ts +131 -131
  158. package/src/assertion/__tests__/example.test.ts +37 -37
  159. package/src/assertion/__tests__/validate.test.ts +142 -142
  160. package/src/assertion/index.ts +12 -12
  161. package/src/assertion/types.ts +60 -58
  162. package/src/assertion/validate.ts +145 -145
  163. package/src/blocks/__tests__/layout.test.ts +169 -169
  164. package/src/blocks/__tests__/validate.test.ts +260 -260
  165. package/src/blocks/index.ts +17 -17
  166. package/src/blocks/layout.ts +186 -186
  167. package/src/blocks/types.ts +62 -62
  168. package/src/blocks/validate.ts +206 -206
  169. package/src/capability-map/__tests__/validate.test.ts +238 -238
  170. package/src/capability-map/index.ts +8 -8
  171. package/src/capability-map/types.ts +29 -29
  172. package/src/capability-map/validate.ts +127 -127
  173. package/src/change/__tests__/validate.test.ts +43 -34
  174. package/src/change/index.ts +1 -0
  175. package/src/change/types.ts +5 -3
  176. package/src/change/validate.ts +75 -43
  177. package/src/compliance/__tests__/classify.test.ts +64 -27
  178. package/src/compliance/__tests__/confidence.test.ts +98 -98
  179. package/src/compliance/__tests__/coverage-metric.test.ts +300 -188
  180. package/src/compliance/__tests__/demo-scenario.test.ts +273 -273
  181. package/src/compliance/__tests__/example.test.ts +57 -57
  182. package/src/compliance/__tests__/gap-report.test.ts +128 -128
  183. package/src/compliance/__tests__/html.test.ts +141 -141
  184. package/src/compliance/__tests__/impact.test.ts +1059 -481
  185. package/src/compliance/__tests__/markdown.test.ts +94 -94
  186. package/src/compliance/__tests__/views.test.ts +64 -64
  187. package/src/compliance/classify.ts +100 -84
  188. package/src/compliance/confidence.ts +69 -69
  189. package/src/compliance/coverage-metric.ts +404 -263
  190. package/src/compliance/gap-report.ts +94 -94
  191. package/src/compliance/html.ts +291 -295
  192. package/src/compliance/impact.ts +979 -677
  193. package/src/compliance/index.ts +57 -56
  194. package/src/compliance/markdown.ts +136 -135
  195. package/src/compliance/reverse-index.ts +33 -33
  196. package/src/compliance/types.ts +136 -120
  197. package/src/compliance/views.ts +46 -46
  198. package/src/compliance-matrix/__tests__/build.test.ts +200 -200
  199. package/src/compliance-matrix/__tests__/example.test.ts +57 -57
  200. package/src/compliance-matrix/index.ts +11 -11
  201. package/src/compliance-matrix/types.ts +102 -102
  202. package/src/edge-path.ts +50 -44
  203. package/src/factor/__tests__/validate.test.ts +81 -46
  204. package/src/factor/index.ts +3 -7
  205. package/src/factor/types.ts +15 -19
  206. package/src/factor/validate.ts +77 -51
  207. package/src/fgca/__tests__/chain-table.test.ts +172 -172
  208. package/src/fgca/__tests__/layout.test.ts +179 -179
  209. package/src/fgca/__tests__/parse-canonical.test.ts +248 -248
  210. package/src/fgca/__tests__/preview-layout.test.ts +124 -124
  211. package/src/fgca/__tests__/resolver.test.ts +209 -0
  212. package/src/fgca/__tests__/validate.test.ts +142 -142
  213. package/src/fgca/chain-table.ts +225 -225
  214. package/src/fgca/index.ts +47 -45
  215. package/src/fgca/layout.ts +204 -204
  216. package/src/fgca/nodeTypes.ts +11 -11
  217. package/src/fgca/nodes/FGCAActivityNode.tsx +65 -65
  218. package/src/fgca/nodes/FGCAChangeNode.tsx +64 -64
  219. package/src/fgca/nodes/FGCAFactorNode.tsx +64 -64
  220. package/src/fgca/nodes/FGCAGoalNode.tsx +72 -72
  221. package/src/fgca/parse-canonical.ts +405 -400
  222. package/src/fgca/preview-layout.ts +217 -217
  223. package/src/fgca/resolver.ts +159 -0
  224. package/src/fgca/types.ts +50 -50
  225. package/src/fgca/validate.ts +129 -126
  226. package/src/geometry.ts +9 -9
  227. package/src/goals/__tests__/layout.test.ts +175 -175
  228. package/src/goals/__tests__/mutations.test.ts +111 -111
  229. package/src/goals/__tests__/parse-canonical.test.ts +145 -145
  230. package/src/goals/__tests__/validate.test.ts +202 -202
  231. package/src/goals/index.ts +20 -21
  232. package/src/goals/layout.ts +172 -172
  233. package/src/goals/mutations.ts +123 -123
  234. package/src/goals/parse-canonical.ts +286 -286
  235. package/src/goals/types.ts +85 -85
  236. package/src/goals/validate.ts +136 -136
  237. package/src/index.ts +28 -23
  238. package/src/process-blueprint/__tests__/compliance-lane.test.ts +320 -320
  239. package/src/process-blueprint/__tests__/layout.test.ts +240 -195
  240. package/src/process-blueprint/__tests__/validate.test.ts +310 -310
  241. package/src/process-blueprint/index.ts +32 -31
  242. package/src/process-blueprint/layout.ts +564 -531
  243. package/src/process-blueprint/types.ts +278 -242
  244. package/src/process-blueprint/validate.ts +181 -181
  245. package/src/process-map/__tests__/validate.test.ts +223 -223
  246. package/src/process-map/index.ts +10 -10
  247. package/src/process-map/types.ts +36 -36
  248. package/src/process-map/validate.ts +135 -135
  249. package/src/products/__tests__/validate.test.ts +239 -239
  250. package/src/products/index.ts +9 -9
  251. package/src/products/types.ts +31 -31
  252. package/src/products/validate.ts +112 -112
  253. package/src/repo-validate/__tests__/validate-repo.test.ts +149 -149
  254. package/src/repo-validate/index.ts +2 -2
  255. package/src/repo-validate/types.ts +50 -50
  256. package/src/repo-validate/validate-repo.ts +196 -196
  257. package/src/requirement/__tests__/example.test.ts +24 -24
  258. package/src/requirement/__tests__/validate.test.ts +96 -96
  259. package/src/requirement/index.ts +4 -4
  260. package/src/requirement/types.ts +35 -35
  261. package/src/requirement/validate.ts +92 -92
  262. package/src/scenarios/__tests__/validate.test.ts +209 -209
  263. package/src/scenarios/index.ts +15 -15
  264. package/src/scenarios/types.ts +37 -37
  265. package/src/scenarios/validate.ts +122 -122
  266. package/src/schema-version.ts +16 -16
  267. package/src/scope.ts +46 -46
  268. package/src/stakeholder/__tests__/validate.test.ts +51 -47
  269. package/src/stakeholder/index.ts +1 -0
  270. package/src/stakeholder/types.ts +7 -7
  271. package/src/stakeholder/validate.ts +79 -52
  272. package/src/target-state/__tests__/validate.test.ts +48 -27
  273. package/src/target-state/index.ts +2 -0
  274. package/src/target-state/types.ts +11 -7
  275. package/src/target-state/validate.ts +76 -46
  276. package/src/theme/index.ts +4 -4
  277. package/src/theme/themes.ts +239 -237
  278. package/src/theme/tokens.ts +135 -133
  279. package/src/typed-id.ts +56 -56
  280. package/src/validation-types.ts +36 -36
  281. package/src/webview/__tests__/entry-notations.test.ts +79 -77
  282. package/src/webview/__tests__/entry.test.ts +64 -64
  283. package/src/webview/__tests__/render-goals.test.ts +72 -72
  284. package/src/webview/__tests__/render-process.test.ts +538 -0
  285. package/src/webview/__tests__/render-util.test.ts +66 -0
  286. package/src/webview/entry.ts +298 -292
  287. package/src/webview/render-activities.ts +111 -69
  288. package/src/webview/render-activity-card.ts +275 -234
  289. package/src/webview/render-blocks.ts +78 -28
  290. package/src/webview/render-fgca.ts +53 -28
  291. package/src/webview/render-goals.ts +133 -110
  292. package/src/webview/render-process-blueprint.ts +281 -207
  293. package/src/webview/render-process.ts +507 -0
  294. package/src/webview/render-scenarios.ts +3 -3
  295. package/src/webview/render-util.ts +27 -14
  296. package/src/webview/styles.css +322 -323
  297. package/src/yaml-normalize.ts +56 -56
  298. package/dist/element-envelope.d.ts +0 -15
  299. package/dist/element-envelope.js +0 -55
  300. package/dist/issues/__tests__/layout.test.js +0 -62
  301. package/dist/issues/__tests__/validate.test.js +0 -82
  302. package/dist/issues/index.d.ts +0 -4
  303. package/dist/issues/index.js +0 -2
  304. package/dist/issues/layout.d.ts +0 -10
  305. package/dist/issues/layout.js +0 -82
  306. package/dist/issues/types.d.ts +0 -61
  307. package/dist/issues/types.js +0 -6
  308. package/dist/issues/validate.d.ts +0 -3
  309. package/dist/issues/validate.js +0 -143
  310. package/dist/webview/transitrix-render.css +0 -323
  311. package/dist/webview/transitrix-render.js +0 -6452
  312. package/dist/webview/transitrix-render.js.map +0 -7
  313. package/src/element-envelope.ts +0 -76
  314. /package/dist/{issues/__tests__/layout.test.d.ts → fgca/__tests__/resolver.test.d.ts} +0 -0
  315. /package/dist/{issues/__tests__/validate.test.d.ts → webview/__tests__/render-process.test.d.ts} +0 -0
@@ -1,5 +1,5 @@
1
1
  import type { ActivityCardLayout, ActivityCardLayoutOptions, ResolvedActivityCard } from './types.js';
2
- /** ArchiMate class per §5.1 — derived from element TYPE, never stored. */
2
+ /** §5.1 ArchiMate class — derived from element TYPE, never stored. */
3
3
  export declare const ARCHIMATE_CLASS: {
4
4
  readonly ACTIVITY: "Work Package";
5
5
  readonly MILESTONE: "Implementation Event";
@@ -1,138 +1,244 @@
1
1
  // Activity Card — single-page layout (pure geometry; the preview paints SVG).
2
2
  //
3
- // Top-to-bottom card sections (spec §4 / §5):
4
- // 1. Title — project name
5
- // 2. Dates band — Initiation (valid_from) · Planned start · Planned end
6
- // 3. Milestones — narrative timeline, markers left→right by date
7
- // 4. Motivation — Factors → Goals → Changes columns with F→G→C edges
8
- // 5. Child activities — activities whose parent = the project
3
+ // Card structure (top to bottom):
4
+ // Header — title row (name + type badge + status badge)
5
+ // dates row (Initiation · Planned start · Planned end)
6
+ // roles row (Initiator · Owner · Sponsor · PM)
7
+ // Body — description (optional)
8
+ // chain: Drivers → Assessments → Goals → Changes (vertical)
9
+ // milestones
10
+ // child activities
11
+ // Footer — notes (optional)
9
12
  //
10
- // The §5.1 ArchiMate-class convention ("(Work Package)", "(Implementation
11
- // Event)") is applied HERE, derived from element TYPE — there is no
12
- // archimate_class data field.
13
+ // The §5.1 ArchiMate-class convention is applied here, derived from element
14
+ // TYPE — there is no archimate_class data field.
13
15
  const DEFAULTS = {
14
16
  cardWidth: 780,
15
- columnGap: 28,
16
- rowGap: 12,
17
+ rowGap: 10,
17
18
  };
18
- // Fixed metrics (px). Kept local — the card has no user-tunable spacing yet.
19
+ // Fixed metrics (px).
19
20
  const PAD = 20;
20
- const TITLE_H = 38;
21
- const DATES_H = 56;
22
- const SECTION_HEADER_H = 26;
21
+ const TITLE_H = 44;
22
+ const BADGE_H = 22;
23
+ const BADGE_HPAD = 12;
24
+ const BADGE_GAP = 8;
25
+ const DATES_H = 62;
26
+ const ROLES_H = 58;
27
+ const CELL_GAP = 12;
23
28
  const SECTION_GAP = 16;
29
+ const CONNECTOR_H = 20;
30
+ const CHAIN_HEADER_H = 24;
31
+ const CHAIN_NODE_H = 48;
32
+ const CHAIN_GAP_H = 32;
33
+ const CHAIN_INNER_PAD = 12;
24
34
  const MILESTONE_W = 168;
25
- const MILESTONE_H = 64;
35
+ const MILESTONE_H = 72;
26
36
  const MILESTONE_GAP = 16;
27
- const CHAIN_NODE_H = 46;
28
37
  const CHILD_ROW_H = 42;
29
- /** ArchiMate class per §5.1 — derived from element TYPE, never stored. */
38
+ const INFO_LINE_H = 18;
39
+ const INFO_ROW_BASE_H = 50;
40
+ const INFO_CHAR_W = 7;
41
+ /** §5.1 ArchiMate class — derived from element TYPE, never stored. */
30
42
  export const ARCHIMATE_CLASS = {
31
43
  ACTIVITY: 'Work Package',
32
44
  MILESTONE: 'Implementation Event',
33
45
  };
46
+ function wrapText(text, maxChars, maxLines) {
47
+ const trimmed = (text ?? '').trim();
48
+ if (trimmed.length === 0)
49
+ return [];
50
+ const budget = Math.max(1, maxChars);
51
+ const words = trimmed.split(/\s+/);
52
+ const lines = [];
53
+ let cur = '';
54
+ const flush = () => {
55
+ if (cur.length > 0) {
56
+ lines.push(cur);
57
+ cur = '';
58
+ }
59
+ };
60
+ for (const word of words) {
61
+ if (word.length > budget) {
62
+ flush();
63
+ let rest = word;
64
+ while (rest.length > budget) {
65
+ lines.push(rest.slice(0, budget));
66
+ rest = rest.slice(budget);
67
+ }
68
+ cur = rest;
69
+ continue;
70
+ }
71
+ const candidate = cur.length > 0 ? `${cur} ${word}` : word;
72
+ if (candidate.length > budget) {
73
+ flush();
74
+ cur = word;
75
+ }
76
+ else {
77
+ cur = candidate;
78
+ }
79
+ }
80
+ flush();
81
+ if (lines.length > maxLines) {
82
+ const capped = lines.slice(0, maxLines);
83
+ const last = capped[maxLines - 1];
84
+ const clipped = last.length > budget - 1 ? last.slice(0, budget - 1) : last;
85
+ capped[maxLines - 1] = `${clipped.replace(/…$/, '')}…`;
86
+ return capped;
87
+ }
88
+ return lines;
89
+ }
90
+ function formatBadgeLabel(raw) {
91
+ return raw.replace(/_/g, ' ');
92
+ }
93
+ function badgeWidth(label) {
94
+ return Math.max(56, label.length * 7 + BADGE_HPAD * 2);
95
+ }
34
96
  export function layoutActivityCard(card, options) {
35
97
  const opts = { ...DEFAULTS, ...(options ?? {}) };
36
98
  const contentX = PAD;
37
99
  const contentW = opts.cardWidth - PAD * 2;
100
+ const maxChars = Math.max(8, Math.floor(contentW / INFO_CHAR_W));
38
101
  let cursorY = PAD;
39
- // 1. Title
40
- const title = { name: card.project.name, x: contentX, y: cursorY + TITLE_H / 2 };
102
+ // ── 1. Title row — name + type badge + status badge ──────────────────────
103
+ const titleRow = { name: card.project.name, x: contentX, y: cursorY + TITLE_H / 2 };
104
+ let badgeRightX = contentX + contentW;
105
+ let statusBadge;
106
+ let activityTypeBadge;
107
+ if (card.project.status) {
108
+ const label = formatBadgeLabel(card.project.status);
109
+ const w = badgeWidth(label);
110
+ badgeRightX -= w;
111
+ statusBadge = { label, x: badgeRightX, y: cursorY + (TITLE_H - BADGE_H) / 2, width: w, height: BADGE_H };
112
+ badgeRightX -= BADGE_GAP;
113
+ }
114
+ if (card.project.activity_type) {
115
+ const label = formatBadgeLabel(card.project.activity_type);
116
+ const w = badgeWidth(label);
117
+ badgeRightX -= w;
118
+ activityTypeBadge = { label, x: badgeRightX, y: cursorY + (TITLE_H - BADGE_H) / 2, width: w, height: BADGE_H };
119
+ }
41
120
  cursorY += TITLE_H;
42
- // 2. Dates band — three equal cells.
121
+ // ── 2. Dates row ─────────────────────────────────────────────────────────
43
122
  const dateDefs = [
44
123
  { label: 'Initiation', value: card.project.valid_from ?? '—' },
45
124
  { label: 'Planned start', value: card.project.start_date ?? '—' },
46
125
  { label: 'Planned end', value: card.project.end_date ?? '—' },
47
126
  ];
48
- const dateGap = 12;
49
- const dateCellW = (contentW - dateGap * (dateDefs.length - 1)) / dateDefs.length;
127
+ const dateCellW = (contentW - CELL_GAP * (dateDefs.length - 1)) / dateDefs.length;
50
128
  const dateFields = dateDefs.map((d, i) => ({
51
- label: d.label,
52
- value: d.value,
53
- x: contentX + i * (dateCellW + dateGap),
54
- y: cursorY,
55
- width: dateCellW,
56
- height: DATES_H,
129
+ label: d.label, value: d.value,
130
+ x: contentX + i * (dateCellW + CELL_GAP), y: cursorY,
131
+ width: dateCellW, height: DATES_H,
57
132
  }));
58
- cursorY += DATES_H + SECTION_GAP;
59
- const sectionHeaders = [];
60
- const pushHeader = (label) => {
61
- sectionHeaders.push({ label, x: contentX, y: cursorY, width: contentW, height: SECTION_HEADER_H });
62
- cursorY += SECTION_HEADER_H + 8;
63
- };
64
- // 3. Milestones — timeline markers, wrapped left→right.
133
+ cursorY += DATES_H + CELL_GAP;
134
+ // ── 3. Stakeholder role slots — 2-column grid ────────────────────────────
135
+ const ROLE_DEFS = [
136
+ { role: 'initiator', label: 'Initiator' },
137
+ { role: 'owner', label: 'Owner' },
138
+ { role: 'sponsor', label: 'Sponsor' },
139
+ { role: 'project_manager', label: 'PM' },
140
+ ];
141
+ const ROLE_COLS = 2;
142
+ const roleSlotW = (contentW - CELL_GAP * (ROLE_COLS - 1)) / ROLE_COLS;
143
+ const stakeholderRoleSlots = ROLE_DEFS.map(({ role, label }, i) => {
144
+ const col = i % ROLE_COLS;
145
+ const row = Math.floor(i / ROLE_COLS);
146
+ const match = (card.stakeholders ?? []).find((s) => s.role === role);
147
+ return {
148
+ role: label,
149
+ name: match?.name ?? '—',
150
+ x: contentX + col * (roleSlotW + CELL_GAP),
151
+ y: cursorY + row * (ROLES_H + CELL_GAP),
152
+ width: roleSlotW, height: ROLES_H,
153
+ };
154
+ });
155
+ const roleRows = Math.ceil(ROLE_DEFS.length / ROLE_COLS);
156
+ cursorY += roleRows * (ROLES_H + CELL_GAP) - CELL_GAP + SECTION_GAP;
157
+ // ── 4. Description row (optional) ────────────────────────────────────────
158
+ let descriptionRow;
159
+ const description = (card.cardDescription ?? '').trim();
160
+ if (description.length > 0) {
161
+ const valueLines = wrapText(description, maxChars, 5);
162
+ const lines = valueLines.length > 0 ? valueLines : ['—'];
163
+ const height = INFO_ROW_BASE_H + lines.length * INFO_LINE_H;
164
+ descriptionRow = { label: 'Description', valueLines: lines, x: contentX, y: cursorY, width: contentW, height };
165
+ cursorY += height + SECTION_GAP;
166
+ }
167
+ // ── 5. Chain: Drivers → Assessments → Goals → Changes ────────────────────
168
+ // Each section is a full-width band with a header label and stacked node rows.
169
+ // An empty section shows a gap indicator ("— not on file") so the author
170
+ // sees which parts of the narrative are missing.
171
+ const chainSections = [];
172
+ const chainEdges = [];
173
+ function pushChainSection(type, label, subtitle, rawNodes) {
174
+ const isEmpty = rawNodes.length === 0;
175
+ const sectionY = cursorY;
176
+ const nodeStartY = sectionY + CHAIN_HEADER_H + 8;
177
+ const nodeW = contentW - CHAIN_INNER_PAD * 2;
178
+ const nodes = isEmpty ? [] : rawNodes.map((n, i) => ({
179
+ id: n.id,
180
+ name: n.name,
181
+ meta: n.meta,
182
+ x: contentX + CHAIN_INNER_PAD,
183
+ y: nodeStartY + i * (CHAIN_NODE_H + opts.rowGap),
184
+ width: nodeW,
185
+ height: CHAIN_NODE_H,
186
+ }));
187
+ const bodyH = isEmpty
188
+ ? CHAIN_GAP_H
189
+ : rawNodes.length * (CHAIN_NODE_H + opts.rowGap) - opts.rowGap;
190
+ const sectionH = CHAIN_HEADER_H + 8 + bodyH + 8;
191
+ chainSections.push({ type, label, subtitle, nodes, isEmpty, x: contentX, y: sectionY, width: contentW, height: sectionH });
192
+ cursorY += sectionH + CONNECTOR_H;
193
+ }
194
+ const { drivers, goals, changes } = card.motivation;
195
+ const assessments = card.assessments ?? [];
196
+ pushChainSection('drivers', 'Drivers', 'What prompted this initiative?', drivers.map((d) => ({ id: d.id, name: d.name })));
197
+ pushChainSection('assessments', 'Assessments', 'What is the current gap?', assessments.map((a) => ({ id: a.id, name: a.name, meta: a.observed_at })));
198
+ pushChainSection('goals', 'Goals', 'What outcome do we target?', goals.map((g) => ({ id: g.id, name: g.name })));
199
+ pushChainSection('changes', 'Changes', 'What will change?', changes.map((c) => ({ id: c.id, name: c.name })));
200
+ // Remove trailing CONNECTOR_H — last section connects to milestones with SECTION_GAP.
201
+ cursorY = cursorY - CONNECTOR_H + SECTION_GAP;
202
+ // Node-level chain edges.
203
+ const driverIdSet = new Set(drivers.map((d) => d.id));
204
+ const goalIdSet = new Set(goals.map((g) => g.id));
205
+ for (const a of assessments) {
206
+ if (driverIdSet.has(a.driverId))
207
+ chainEdges.push({ sourceId: a.driverId, targetId: a.id });
208
+ }
209
+ for (const g of goals) {
210
+ for (const did of g.driverIds) {
211
+ if (driverIdSet.has(did))
212
+ chainEdges.push({ sourceId: did, targetId: g.id });
213
+ }
214
+ }
215
+ for (const c of changes) {
216
+ for (const gid of c.goalIds) {
217
+ if (goalIdSet.has(gid))
218
+ chainEdges.push({ sourceId: gid, targetId: c.id });
219
+ }
220
+ }
221
+ // ── 6. Milestones ────────────────────────────────────────────────────────
65
222
  const milestones = [];
66
223
  if (card.milestones.length > 0) {
67
- pushHeader('Milestones');
68
224
  const perRow = Math.max(1, Math.floor((contentW + MILESTONE_GAP) / (MILESTONE_W + MILESTONE_GAP)));
69
- const rowStartY = cursorY;
70
225
  card.milestones.forEach((m, i) => {
71
226
  const col = i % perRow;
72
227
  const row = Math.floor(i / perRow);
73
228
  milestones.push({
74
- id: m.id,
75
- name: m.name,
76
- date: m.date,
229
+ id: m.id, name: m.name, date: m.date,
77
230
  archimateClass: ARCHIMATE_CLASS.MILESTONE,
78
231
  x: contentX + col * (MILESTONE_W + MILESTONE_GAP),
79
- y: rowStartY + row * (MILESTONE_H + opts.rowGap),
80
- width: MILESTONE_W,
81
- height: MILESTONE_H,
232
+ y: cursorY + row * (MILESTONE_H + opts.rowGap),
233
+ width: MILESTONE_W, height: MILESTONE_H,
82
234
  });
83
235
  });
84
236
  const rows = Math.ceil(card.milestones.length / perRow);
85
- cursorY = rowStartY + rows * (MILESTONE_H + opts.rowGap) - opts.rowGap + SECTION_GAP;
237
+ cursorY += rows * (MILESTONE_H + opts.rowGap) - opts.rowGap + SECTION_GAP;
86
238
  }
87
- // 4. Motivation chain — Factors | Goals | Changes columns.
88
- const chainEdges = [];
89
- const factorsCol = [];
90
- const goalsCol = [];
91
- const changesCol = [];
92
- const { factors, goals, changes } = card.motivation;
93
- if (factors.length + goals.length + changes.length > 0) {
94
- pushHeader('Motivation — Factors → Goals → Changes');
95
- const colW = (contentW - opts.columnGap * 2) / 3;
96
- const colX = [contentX, contentX + colW + opts.columnGap, contentX + (colW + opts.columnGap) * 2];
97
- const colStartY = cursorY;
98
- const stack = (items, colIndex, into) => {
99
- items.forEach((it, i) => {
100
- into.push({
101
- id: it.id,
102
- name: it.name,
103
- x: colX[colIndex],
104
- y: colStartY + i * (CHAIN_NODE_H + opts.rowGap),
105
- width: colW,
106
- height: CHAIN_NODE_H,
107
- });
108
- });
109
- };
110
- stack(factors, 0, factorsCol);
111
- stack(goals, 1, goalsCol);
112
- stack(changes, 2, changesCol);
113
- // Edges: factor→goal (goal.factorIds), goal→change (change.goalIds).
114
- const factorIds = new Set(factorsCol.map((n) => n.id));
115
- const goalIds = new Set(goalsCol.map((n) => n.id));
116
- for (const g of goals) {
117
- for (const fid of g.factorIds) {
118
- if (factorIds.has(fid))
119
- chainEdges.push({ sourceId: fid, targetId: g.id });
120
- }
121
- }
122
- for (const c of changes) {
123
- for (const gid of c.goalIds) {
124
- if (goalIds.has(gid))
125
- chainEdges.push({ sourceId: gid, targetId: c.id });
126
- }
127
- }
128
- const tallest = Math.max(factorsCol.length, goalsCol.length, changesCol.length);
129
- cursorY = colStartY + tallest * (CHAIN_NODE_H + opts.rowGap) - opts.rowGap + SECTION_GAP;
130
- }
131
- // 5. Child activities — full-width rows.
239
+ // ── 7. Child activities ───────────────────────────────────────────────────
132
240
  const childActivities = [];
133
241
  if (card.childActivities.length > 0) {
134
- pushHeader('Child activities');
135
- const rowStartY = cursorY;
136
242
  card.childActivities.forEach((a, i) => {
137
243
  const metaBits = [];
138
244
  if (a.start_date || a.end_date)
@@ -140,28 +246,39 @@ export function layoutActivityCard(card, options) {
140
246
  if (a.owner)
141
247
  metaBits.push(a.owner);
142
248
  childActivities.push({
143
- id: a.id,
144
- name: a.name,
249
+ id: a.id, name: a.name,
145
250
  archimateClass: ARCHIMATE_CLASS.ACTIVITY,
146
251
  meta: metaBits.join(' · '),
147
252
  x: contentX,
148
- y: rowStartY + i * (CHILD_ROW_H + opts.rowGap),
149
- width: contentW,
150
- height: CHILD_ROW_H,
253
+ y: cursorY + i * (CHILD_ROW_H + opts.rowGap),
254
+ width: contentW, height: CHILD_ROW_H,
151
255
  });
152
256
  });
153
- cursorY =
154
- rowStartY + card.childActivities.length * (CHILD_ROW_H + opts.rowGap) - opts.rowGap + SECTION_GAP;
257
+ cursorY += card.childActivities.length * (CHILD_ROW_H + opts.rowGap) - opts.rowGap + SECTION_GAP;
258
+ }
259
+ // ── 8. Footer — notes ────────────────────────────────────────────────────
260
+ let footerRow;
261
+ const notes = (card.notes ?? '').trim();
262
+ if (notes.length > 0) {
263
+ const valueLines = wrapText(notes, maxChars, 10);
264
+ const lines = valueLines.length > 0 ? valueLines : ['—'];
265
+ const height = INFO_ROW_BASE_H + lines.length * INFO_LINE_H;
266
+ footerRow = { label: 'Notes', valueLines: lines, x: contentX, y: cursorY, width: contentW, height };
267
+ cursorY += height + SECTION_GAP;
155
268
  }
156
269
  const totalHeight = cursorY - SECTION_GAP + PAD;
157
270
  return {
158
271
  bounds: { x: 0, y: 0, width: opts.cardWidth, height: Math.max(totalHeight, PAD * 2 + TITLE_H) },
159
- title,
272
+ titleRow,
273
+ activityTypeBadge,
274
+ statusBadge,
160
275
  dateFields,
161
- sectionHeaders,
162
- milestones,
163
- chainColumns: { factors: factorsCol, goals: goalsCol, changes: changesCol },
276
+ stakeholderRoleSlots,
277
+ descriptionRow,
278
+ chainSections,
164
279
  chainEdges,
280
+ milestones,
165
281
  childActivities,
282
+ footerRow,
166
283
  };
167
284
  }
@@ -107,6 +107,35 @@ function activeActivityGoals(relations, fromId) {
107
107
  }
108
108
  return out;
109
109
  }
110
+ /**
111
+ * STAKEHOLDER ids + roles reached by an *active* `activity_stakeholder`
112
+ * relation originating at `fromId`. A relation with a non-null `valid_to` has
113
+ * ended and is excluded, so the card shows the project's currently-engaged
114
+ * stakeholders. The optional `role` field on the relation carries the
115
+ * project-role (initiator | owner | sponsor | project_manager).
116
+ */
117
+ function activeActivityStakeholders(relations, fromId) {
118
+ const out = [];
119
+ for (const doc of relations) {
120
+ if (!isObject(doc))
121
+ continue;
122
+ if (str(doc['notation']) !== 'relation')
123
+ continue;
124
+ if (str(doc['type']) !== 'activity_stakeholder')
125
+ continue;
126
+ if (str(doc['from']) !== fromId)
127
+ continue;
128
+ const to = str(doc['to']);
129
+ if (!to)
130
+ continue;
131
+ const validTo = doc['valid_to'];
132
+ if (validTo !== undefined && validTo !== null)
133
+ continue; // ended relation
134
+ if (!out.some((s) => s.id === to))
135
+ out.push({ id: to, role: str(doc['role']) });
136
+ }
137
+ return out;
138
+ }
110
139
  export function resolveActivityCard(doc, sources) {
111
140
  const errors = [];
112
141
  const warnings = [];
@@ -119,27 +148,19 @@ export function resolveActivityCard(doc, sources) {
119
148
  }
120
149
  const activities = collectByNotation(sources.elements, 'activity');
121
150
  const projectRec = activities.get(projectId);
122
- // PC-001 — project must resolve to an admitted ACTIVITY element in canon.
151
+ // PC-001 — project must resolve to an admitted ACTIVITY element after
152
+ // exhausting the canonical resolution scope (§6.1): canon/elements/**
153
+ // recursively first, then canon/views/activities/** as secondary fallback.
123
154
  if (!projectRec) {
124
155
  errors.push({
125
156
  code: 'PC-001',
126
- message: `activity_card.project "${projectId}" does not resolve to an ACTIVITY element in the canon element store`,
157
+ message: `activity_card.project "${projectId}" not found after searching ` +
158
+ `canon/elements/** and canon/views/activities/** — ` +
159
+ `add a YAML file with notation: activity and id: "${projectId}" ` +
160
+ `(e.g. canon/elements/05_implementation/activities/${projectId}.yaml)`,
127
161
  });
128
162
  return { valid: false, errors, warnings };
129
163
  }
130
- // PC-002 — the resolved Activity should be the project scale. In the element
131
- // model all activity scales (initiative / programme / project / task) share
132
- // one ACTIVITY TYPE and `activity_type` references an ActivityType element,
133
- // so a missing/non-literal value is acceptable; only an explicit, clearly
134
- // non-project marker is flagged. (Canonical project-identification semantics
135
- // are tracked as a follow-up internally.)
136
- const activityType = str(projectRec['activity_type']);
137
- if (activityType !== undefined && activityType !== 'Project' && activityType !== 'project') {
138
- errors.push({
139
- code: 'PC-002',
140
- message: `Activity "${projectId}" has activity_type "${activityType}", expected the project scale`,
141
- });
142
- }
143
164
  // LIFECYCLE-001..003 on the project Activity.
144
165
  const validFrom = projectRec['valid_from'];
145
166
  const validToRaw = projectRec['valid_to'];
@@ -230,8 +251,11 @@ export function resolveActivityCard(doc, sources) {
230
251
  if (errors.length > 0) {
231
252
  return { valid: false, errors, warnings };
232
253
  }
233
- // ── Motivation chain — expand against canon FACTOR / GOAL / CHANGE elements ──
234
- const factorElems = collectByNotation(sources.elements, 'factor');
254
+ // ── Motivation chain — expand against canon DRIVER / GOAL / CHANGE elements ──
255
+ // Canon elements use `notation: driver`; `notation: factor` accepted for backward compat.
256
+ const driverElemsLegacy = collectByNotation(sources.elements, 'factor');
257
+ const driverElemsNew = collectByNotation(sources.elements, 'driver');
258
+ const driverElems = new Map([...driverElemsLegacy, ...driverElemsNew]);
235
259
  const goalElems = collectByNotation(sources.elements, 'goal');
236
260
  const changeElems = collectByNotation(sources.elements, 'change');
237
261
  const changes = [];
@@ -247,7 +271,7 @@ export function resolveActivityCard(doc, sources) {
247
271
  changes.push({ id: cid, name: str(rec['name']) ?? cid, goalIds: strArray(rec['goals']) });
248
272
  }
249
273
  // Goals shown = the project's declared goals ∪ goals referenced by in-scope
250
- // changes (so the F→G→C chain stays connected).
274
+ // changes (so the D→G→C chain stays connected).
251
275
  const goalIdSet = new Set(projectGoalIds);
252
276
  for (const ch of changes)
253
277
  for (const g of ch.goalIds)
@@ -262,18 +286,47 @@ export function resolveActivityCard(doc, sources) {
262
286
  });
263
287
  continue;
264
288
  }
265
- goals.push({ id: gid, name: str(rec['name']) ?? gid, factorIds: strArray(rec['factors']) });
289
+ goals.push({ id: gid, name: str(rec['name']) ?? gid, driverIds: strArray(rec['factors']) });
266
290
  }
267
- // Factors shown = those referenced by the in-scope goals.
268
- const factorIdSet = new Set();
291
+ // Drivers shown = those referenced by the in-scope goals.
292
+ const driverIdSet = new Set();
269
293
  for (const g of goals)
270
- for (const f of g.factorIds)
271
- factorIdSet.add(f);
272
- const factors = [];
273
- for (const fid of factorIdSet) {
274
- const rec = factorElems.get(fid);
275
- factors.push({ id: fid, name: rec ? str(rec['name']) ?? fid : fid });
294
+ for (const d of g.driverIds)
295
+ driverIdSet.add(d);
296
+ const drivers = [];
297
+ for (const did of driverIdSet) {
298
+ const rec = driverElems.get(did);
299
+ drivers.push({ id: did, name: rec ? str(rec['name']) ?? did : did });
300
+ }
301
+ // ── Assessments — findings that assess an in-scope Driver ────────────────────
302
+ // Assessments answer "what specific problem are we solving?" They are resolved
303
+ // from ASSESSMENT elements whose `assesses` field matches a Driver id that
304
+ // appeared in the motivation chain above. Empty when none exist — the gap is
305
+ // intentionally visible on the card so the author knows it needs filling.
306
+ const driverIdLookup = new Set(drivers.map((d) => d.id));
307
+ const assessmentElems = collectByNotation(sources.elements, 'assessment');
308
+ const assessments = [];
309
+ for (const [aid, rec] of assessmentElems) {
310
+ const driverId = str(rec['assesses']);
311
+ if (!driverId || !driverIdLookup.has(driverId))
312
+ continue;
313
+ assessments.push({
314
+ id: aid,
315
+ name: str(rec['name']) ?? aid,
316
+ driverId,
317
+ description: str(rec['description']),
318
+ observed_at: str(rec['observed_at']),
319
+ });
276
320
  }
321
+ assessments.sort((a, b) => {
322
+ if (!a.observed_at && !b.observed_at)
323
+ return 0;
324
+ if (!a.observed_at)
325
+ return 1;
326
+ if (!b.observed_at)
327
+ return -1;
328
+ return a.observed_at < b.observed_at ? -1 : a.observed_at > b.observed_at ? 1 : 0;
329
+ });
277
330
  // ── Child activities — parent = project id ──────────────────────────────────
278
331
  const childActivities = [];
279
332
  for (const [id, rec] of activities) {
@@ -289,6 +342,34 @@ export function resolveActivityCard(doc, sources) {
289
342
  });
290
343
  }
291
344
  childActivities.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
345
+ // ── Project goal text field — names of the DIRECTLY-served goals ────────────
346
+ // The "Project goal" field shows the goals the project itself serves (the
347
+ // `activity_goal` targets), not the wider F→G→C chain (which also pulls in
348
+ // goals referenced only by in-scope changes). Resolve each id to its GOAL
349
+ // element name, falling back to the id when the element is absent.
350
+ const goalNames = projectGoalIds.map((gid) => str(goalElems.get(gid)?.['name']) ?? gid);
351
+ // ── Stakeholders — active `activity_stakeholder` relations from the project ──
352
+ const stakeholderElems = collectByNotation(sources.elements, 'stakeholder');
353
+ const stakeholders = [];
354
+ for (const { id: sid, role } of activeActivityStakeholders(sources.relations, projectId)) {
355
+ const rec = stakeholderElems.get(sid);
356
+ if (!rec) {
357
+ warnings.push({
358
+ code: 'PC-001',
359
+ message: `Stakeholder "${sid}" not found as a STAKEHOLDER element in canon; shown by id`,
360
+ });
361
+ stakeholders.push({ id: sid, name: sid, role });
362
+ continue;
363
+ }
364
+ stakeholders.push({
365
+ id: sid,
366
+ name: str(rec['name']) ?? sid,
367
+ type: str(rec['type']),
368
+ interest: str(rec['interest']),
369
+ influence: str(rec['influence']),
370
+ role,
371
+ });
372
+ }
292
373
  const project = {
293
374
  id: projectId,
294
375
  name: str(projectRec['name']) ?? projectId,
@@ -297,14 +378,20 @@ export function resolveActivityCard(doc, sources) {
297
378
  valid_to: validToOk ? validToRaw : undefined,
298
379
  start_date: str(projectRec['start_date']),
299
380
  end_date: str(projectRec['end_date']),
381
+ activity_type: str(projectRec['activity_type']),
382
+ status: str(projectRec['status']),
300
383
  };
301
384
  const resolved = {
302
385
  cardId: str(card.id) ?? '',
303
386
  cardDescription: str(card.description),
304
387
  project,
305
388
  milestones,
306
- motivation: { factors, goals, changes },
389
+ motivation: { drivers, goals, changes },
390
+ assessments,
307
391
  childActivities,
392
+ goalNames,
393
+ stakeholders,
394
+ notes: str(card.notes),
308
395
  };
309
396
  return { valid: true, errors, warnings, resolved };
310
397
  }