@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,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 = [];
@@ -127,19 +156,6 @@ export function resolveActivityCard(doc, sources) {
127
156
  });
128
157
  return { valid: false, errors, warnings };
129
158
  }
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
159
  // LIFECYCLE-001..003 on the project Activity.
144
160
  const validFrom = projectRec['valid_from'];
145
161
  const validToRaw = projectRec['valid_to'];
@@ -230,8 +246,10 @@ export function resolveActivityCard(doc, sources) {
230
246
  if (errors.length > 0) {
231
247
  return { valid: false, errors, warnings };
232
248
  }
233
- // ── Motivation chain — expand against canon FACTOR / GOAL / CHANGE elements ──
234
- const factorElems = collectByNotation(sources.elements, 'factor');
249
+ // ── Motivation chain — expand against canon DRIVER / GOAL / CHANGE elements ──
250
+ // Note: YAML corpus still uses `notation: factor` and `goal.factors` field names;
251
+ // the YAML keys are stable. The resolved TypeScript API uses "driver/driverIds".
252
+ const driverElems = collectByNotation(sources.elements, 'factor');
235
253
  const goalElems = collectByNotation(sources.elements, 'goal');
236
254
  const changeElems = collectByNotation(sources.elements, 'change');
237
255
  const changes = [];
@@ -247,7 +265,7 @@ export function resolveActivityCard(doc, sources) {
247
265
  changes.push({ id: cid, name: str(rec['name']) ?? cid, goalIds: strArray(rec['goals']) });
248
266
  }
249
267
  // Goals shown = the project's declared goals ∪ goals referenced by in-scope
250
- // changes (so the F→G→C chain stays connected).
268
+ // changes (so the D→G→C chain stays connected).
251
269
  const goalIdSet = new Set(projectGoalIds);
252
270
  for (const ch of changes)
253
271
  for (const g of ch.goalIds)
@@ -262,18 +280,47 @@ export function resolveActivityCard(doc, sources) {
262
280
  });
263
281
  continue;
264
282
  }
265
- goals.push({ id: gid, name: str(rec['name']) ?? gid, factorIds: strArray(rec['factors']) });
283
+ goals.push({ id: gid, name: str(rec['name']) ?? gid, driverIds: strArray(rec['factors']) });
266
284
  }
267
- // Factors shown = those referenced by the in-scope goals.
268
- const factorIdSet = new Set();
285
+ // Drivers shown = those referenced by the in-scope goals.
286
+ const driverIdSet = new Set();
269
287
  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 });
288
+ for (const d of g.driverIds)
289
+ driverIdSet.add(d);
290
+ const drivers = [];
291
+ for (const did of driverIdSet) {
292
+ const rec = driverElems.get(did);
293
+ drivers.push({ id: did, name: rec ? str(rec['name']) ?? did : did });
294
+ }
295
+ // ── Assessments — findings that assess an in-scope Driver ────────────────────
296
+ // Assessments answer "what specific problem are we solving?" They are resolved
297
+ // from ASSESSMENT elements whose `assesses` field matches a Driver id that
298
+ // appeared in the motivation chain above. Empty when none exist — the gap is
299
+ // intentionally visible on the card so the author knows it needs filling.
300
+ const driverIdLookup = new Set(drivers.map((d) => d.id));
301
+ const assessmentElems = collectByNotation(sources.elements, 'assessment');
302
+ const assessments = [];
303
+ for (const [aid, rec] of assessmentElems) {
304
+ const driverId = str(rec['assesses']);
305
+ if (!driverId || !driverIdLookup.has(driverId))
306
+ continue;
307
+ assessments.push({
308
+ id: aid,
309
+ name: str(rec['name']) ?? aid,
310
+ driverId,
311
+ description: str(rec['description']),
312
+ observed_at: str(rec['observed_at']),
313
+ });
276
314
  }
315
+ assessments.sort((a, b) => {
316
+ if (!a.observed_at && !b.observed_at)
317
+ return 0;
318
+ if (!a.observed_at)
319
+ return 1;
320
+ if (!b.observed_at)
321
+ return -1;
322
+ return a.observed_at < b.observed_at ? -1 : a.observed_at > b.observed_at ? 1 : 0;
323
+ });
277
324
  // ── Child activities — parent = project id ──────────────────────────────────
278
325
  const childActivities = [];
279
326
  for (const [id, rec] of activities) {
@@ -289,6 +336,34 @@ export function resolveActivityCard(doc, sources) {
289
336
  });
290
337
  }
291
338
  childActivities.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
339
+ // ── Project goal text field — names of the DIRECTLY-served goals ────────────
340
+ // The "Project goal" field shows the goals the project itself serves (the
341
+ // `activity_goal` targets), not the wider F→G→C chain (which also pulls in
342
+ // goals referenced only by in-scope changes). Resolve each id to its GOAL
343
+ // element name, falling back to the id when the element is absent.
344
+ const goalNames = projectGoalIds.map((gid) => str(goalElems.get(gid)?.['name']) ?? gid);
345
+ // ── Stakeholders — active `activity_stakeholder` relations from the project ──
346
+ const stakeholderElems = collectByNotation(sources.elements, 'stakeholder');
347
+ const stakeholders = [];
348
+ for (const { id: sid, role } of activeActivityStakeholders(sources.relations, projectId)) {
349
+ const rec = stakeholderElems.get(sid);
350
+ if (!rec) {
351
+ warnings.push({
352
+ code: 'PC-001',
353
+ message: `Stakeholder "${sid}" not found as a STAKEHOLDER element in canon; shown by id`,
354
+ });
355
+ stakeholders.push({ id: sid, name: sid, role });
356
+ continue;
357
+ }
358
+ stakeholders.push({
359
+ id: sid,
360
+ name: str(rec['name']) ?? sid,
361
+ type: str(rec['type']),
362
+ interest: str(rec['interest']),
363
+ influence: str(rec['influence']),
364
+ role,
365
+ });
366
+ }
292
367
  const project = {
293
368
  id: projectId,
294
369
  name: str(projectRec['name']) ?? projectId,
@@ -297,14 +372,20 @@ export function resolveActivityCard(doc, sources) {
297
372
  valid_to: validToOk ? validToRaw : undefined,
298
373
  start_date: str(projectRec['start_date']),
299
374
  end_date: str(projectRec['end_date']),
375
+ activity_type: str(projectRec['activity_type']),
376
+ status: str(projectRec['status']),
300
377
  };
301
378
  const resolved = {
302
379
  cardId: str(card.id) ?? '',
303
380
  cardDescription: str(card.description),
304
381
  project,
305
382
  milestones,
306
- motivation: { factors, goals, changes },
383
+ motivation: { drivers, goals, changes },
384
+ assessments,
307
385
  childActivities,
386
+ goalNames,
387
+ stakeholders,
388
+ notes: str(card.notes),
308
389
  };
309
390
  return { valid: true, errors, warnings, resolved };
310
391
  }