@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,337 +1,422 @@
1
- // Activity Card — cross-document resolver (the novel piece of this notation).
2
- //
3
- // Unlike every other Studio preview, the Activity Card is assembled from
4
- // MULTIPLE documents. The card YAML names a project Activity; the resolver
5
- // pulls the rest of the card's content BY REFERENCE from the canonical
6
- // ELEMENT and RELATION store — never from other view documents (view-purity:
7
- // a view is a projection over elements + relations, methodology
8
- // ELEMENT_PRIMITIVES.md §1):
9
- //
10
- // canon/elements/** → one element per file. The project ACTIVITY (name,
11
- // lifecycle + schedule dates, delivers_changes[],
12
- // parent) and its child ACTIVITY elements (parent =
13
- // project id); the FACTOR / GOAL / CHANGE elements that
14
- // the motivation chain expands (goal.factors,
15
- // change.goals carried inline on the elements).
16
- // canon/relations/** → `notation: relation` files. The project's goals come
17
- // from the first-class `activity_goal` REL (from =
18
- // project id), preferred over the activity element's
19
- // transitional inline `goals[]` when present
20
- // (07-activities.md §"Time-aware relations").
21
- //
22
- // The extension reads + parses the element and relation files (walking the
23
- // org's `canon/elements` and `canon/relations` trees) and hands the parsed
24
- // docs in. This resolver is filesystem-free and pure so it stays unit-testable.
25
- //
26
- // Cross-document validation lives here (the rules that need the sibling docs):
27
- // PC-001 project does not resolve to an admitted Activity
28
- // PC-002 resolved Activity has activity_type other than "Project"
29
- // PC-003 milestone.delivers_changes[] entry not in the project's own
30
- // delivers_changes[]
31
- // PC-004 (warning) milestone.date outside [valid_from, valid_to]
32
- // LIFECYCLE-001 project valid_from missing / not a parseable ISO date
33
- // LIFECYCLE-002 project valid_to present, not null, not a parseable date
34
- // LIFECYCLE-003 project valid_to earlier than valid_from
35
- //
36
- // LIFECYCLE-* are scoped to the RESOLVED PROJECT Activity only — the card is
37
- // built around the project's initiation date, so a missing valid_from there is
38
- // a hard error. Child activities and FGCA elements are not lifecycle-checked
39
- // here (out of this notation's scope; today's Studio activities docs may
40
- // legitimately omit lifecycle fields).
41
-
42
- import type {
43
- ActivityCardDoc,
44
- ActivityCardSources,
45
- ResolvedActivityCard,
46
- ResolvedChange,
47
- ResolvedChildActivity,
48
- ResolvedFactor,
49
- ResolvedGoal,
50
- ResolvedMilestone,
51
- ResolvedProject,
52
- } from './types.js';
53
- import type { ValidationError, ValidationWarning, ValidationResult } from '../validation-types.js';
54
-
55
- const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
56
-
57
- export interface ActivityCardResolution extends ValidationResult {
58
- /** Present only when resolution succeeded (project found + valid_from OK). */
59
- resolved?: ResolvedActivityCard;
60
- }
61
-
62
- function isObject(v: unknown): v is Record<string, unknown> {
63
- return !!v && typeof v === 'object' && !Array.isArray(v);
64
- }
65
-
66
- function str(v: unknown): string | undefined {
67
- return typeof v === 'string' && v.trim().length > 0 ? v : undefined;
68
- }
69
-
70
- function strArray(v: unknown): string[] {
71
- if (!Array.isArray(v)) return [];
72
- return v.filter((x): x is string => typeof x === 'string' && x.trim().length > 0);
73
- }
74
-
75
- /** A parseable quoted ISO 8601 date, calendar-checked (rejects 2026-13-40). */
76
- function isIsoDate(v: unknown): v is string {
77
- if (typeof v !== 'string' || !DATE_RE.test(v)) return false;
78
- const t = Date.parse(`${v}T00:00:00Z`);
79
- if (Number.isNaN(t)) return false;
80
- // Round-trip guard so 2026-02-30 (which Date.parse may roll forward) is rejected.
81
- return new Date(t).toISOString().slice(0, 10) === v;
82
- }
83
-
84
- /**
85
- * Index canon element documents of one `notation` by their `id`. Each element
86
- * is its own single-element file (`notation: activity|factor|goal|change|…`
87
- * with a top-level `id`); first definition wins.
88
- */
89
- function collectByNotation(
90
- docs: unknown[],
91
- notation: string,
92
- ): Map<string, Record<string, unknown>> {
93
- const out = new Map<string, Record<string, unknown>>();
94
- for (const doc of docs) {
95
- if (!isObject(doc)) continue;
96
- if (str(doc['notation']) !== notation) continue;
97
- const id = str(doc['id']);
98
- if (!id) continue;
99
- if (!out.has(id)) out.set(id, doc); // first definition wins
100
- }
101
- return out;
102
- }
103
-
104
- /**
105
- * GOAL ids reached by an *active* `activity_goal` relation originating at
106
- * `fromId`. A relation with a non-null `valid_to` has ended (the activity was
107
- * re-aimed) and is excluded — the card shows the currently-served goals.
108
- */
109
- function activeActivityGoals(relations: unknown[], fromId: string): string[] {
110
- const out: string[] = [];
111
- for (const doc of relations) {
112
- if (!isObject(doc)) continue;
113
- if (str(doc['notation']) !== 'relation') continue;
114
- if (str(doc['type']) !== 'activity_goal') continue;
115
- if (str(doc['from']) !== fromId) continue;
116
- const to = str(doc['to']);
117
- if (!to) continue;
118
- const validTo = doc['valid_to'];
119
- if (validTo !== undefined && validTo !== null) continue; // ended relation
120
- if (!out.includes(to)) out.push(to);
121
- }
122
- return out;
123
- }
124
-
125
- export function resolveActivityCard(
126
- doc: ActivityCardDoc,
127
- sources: ActivityCardSources,
128
- ): ActivityCardResolution {
129
- const errors: ValidationError[] = [];
130
- const warnings: ValidationWarning[] = [];
131
-
132
- const card = doc?.activity_card;
133
- const projectId = str(card?.project);
134
- if (!projectId) {
135
- // Structural validator already flags this; nothing to resolve.
136
- errors.push({ code: 'PC-001', message: 'activity_card.project is missing' });
137
- return { valid: false, errors, warnings };
138
- }
139
-
140
- const activities = collectByNotation(sources.elements, 'activity');
141
- const projectRec = activities.get(projectId);
142
-
143
- // PC-001 — project must resolve to an admitted ACTIVITY element in canon.
144
- if (!projectRec) {
145
- errors.push({
146
- code: 'PC-001',
147
- message: `activity_card.project "${projectId}" does not resolve to an ACTIVITY element in the canon element store`,
148
- });
149
- return { valid: false, errors, warnings };
150
- }
151
-
152
- // PC-002 — the resolved Activity should be the project scale. In the element
153
- // model all activity scales (initiative / programme / project / task) share
154
- // one ACTIVITY TYPE and `activity_type` references an ActivityType element,
155
- // so a missing/non-literal value is acceptable; only an explicit, clearly
156
- // non-project marker is flagged. (Canonical project-identification semantics
157
- // are tracked as a follow-up internally.)
158
- const activityType = str(projectRec['activity_type']);
159
- if (activityType !== undefined && activityType !== 'Project' && activityType !== 'project') {
160
- errors.push({
161
- code: 'PC-002',
162
- message: `Activity "${projectId}" has activity_type "${activityType}", expected the project scale`,
163
- });
164
- }
165
-
166
- // LIFECYCLE-001..003 on the project Activity.
167
- const validFrom = projectRec['valid_from'];
168
- const validToRaw = projectRec['valid_to'];
169
- let validFromOk = false;
170
- if (validFrom === undefined || validFrom === null) {
171
- errors.push({ code: 'LIFECYCLE-001', message: `Activity "${projectId}" valid_from is required` });
172
- } else if (!isIsoDate(validFrom)) {
173
- errors.push({
174
- code: 'LIFECYCLE-001',
175
- message: `Activity "${projectId}" valid_from "${String(validFrom)}" is not a parseable ISO 8601 date`,
176
- });
177
- } else {
178
- validFromOk = true;
179
- }
180
-
181
- let validToOk = false;
182
- const validToPresent = validToRaw !== undefined && validToRaw !== null;
183
- if (validToPresent) {
184
- if (!isIsoDate(validToRaw)) {
185
- errors.push({
186
- code: 'LIFECYCLE-002',
187
- message: `Activity "${projectId}" valid_to "${String(validToRaw)}" is not a parseable ISO 8601 date`,
188
- });
189
- } else {
190
- validToOk = true;
191
- if (validFromOk && (validToRaw as string) < (validFrom as string)) {
192
- errors.push({
193
- code: 'LIFECYCLE-003',
194
- message: `Activity "${projectId}" valid_to "${String(validToRaw)}" is earlier than valid_from "${String(validFrom)}"`,
195
- });
196
- }
197
- }
198
- }
199
-
200
- // A failed lifecycle/PC-001 stops us producing a card view-model, but we still
201
- // return every error collected so the panel lists them all at once.
202
- const projectChanges = new Set(strArray(projectRec['delivers_changes']));
203
- // Project goals: prefer the first-class `activity_goal` relations (the
204
- // canonical, time-aware home for an Activity→Goal link); fall back to the
205
- // activity element's transitional inline `goals[]` when no relation exists
206
- // (07-activities.md §"Time-aware relations").
207
- const relGoalIds = activeActivityGoals(sources.relations, projectId);
208
- const projectGoalIds = relGoalIds.length > 0 ? relGoalIds : strArray(projectRec['goals']);
209
-
210
- // PC-003 — each milestone change must be a subset of the project's changes.
211
- const rawMilestones = Array.isArray(card.milestones) ? card.milestones : [];
212
- const milestones: ResolvedMilestone[] = [];
213
- for (const m of rawMilestones) {
214
- if (!isObject(m)) continue;
215
- const mid = str(m['id']);
216
- const mname = str(m['name']);
217
- const mdate = str(m['date']);
218
- if (!mid || !mname || !mdate) continue; // structural validator already flagged
219
- const deliversChanges = strArray(m['delivers_changes']);
220
- for (const c of deliversChanges) {
221
- if (!projectChanges.has(c)) {
222
- errors.push({
223
- code: 'PC-003',
224
- message: `Milestone "${mid}" delivers_changes "${c}" is not in project "${projectId}" delivers_changes`,
225
- });
226
- }
227
- }
228
- // PC-004 — milestone date should fall within the project lifecycle window.
229
- if (validFromOk && mdate < (validFrom as string)) {
230
- warnings.push({
231
- code: 'PC-004',
232
- message: `Milestone "${mid}" date "${mdate}" is before project valid_from "${String(validFrom)}"`,
233
- });
234
- }
235
- if (validToOk && mdate > (validToRaw as string)) {
236
- warnings.push({
237
- code: 'PC-004',
238
- message: `Milestone "${mid}" date "${mdate}" is after project valid_to "${String(validToRaw)}"`,
239
- });
240
- }
241
- milestones.push({
242
- id: mid,
243
- name: mname,
244
- date: mdate,
245
- description: str(m['description']),
246
- deliversChanges,
247
- });
248
- }
249
- milestones.sort((a, b) => (a.date < b.date ? -1 : a.date > b.date ? 1 : 0));
250
-
251
- // If we have hard errors, surface them without an assembled card.
252
- if (errors.length > 0) {
253
- return { valid: false, errors, warnings };
254
- }
255
-
256
- // ── Motivation chain — expand against canon FACTOR / GOAL / CHANGE elements ──
257
- const factorElems = collectByNotation(sources.elements, 'factor');
258
- const goalElems = collectByNotation(sources.elements, 'goal');
259
- const changeElems = collectByNotation(sources.elements, 'change');
260
-
261
- const changes: ResolvedChange[] = [];
262
- for (const cid of projectChanges) {
263
- const rec = changeElems.get(cid);
264
- if (!rec) {
265
- warnings.push({
266
- code: 'PC-001',
267
- message: `Project change "${cid}" not found as a CHANGE element in canon; omitted from the motivation chain`,
268
- });
269
- continue;
270
- }
271
- changes.push({ id: cid, name: str(rec['name']) ?? cid, goalIds: strArray(rec['goals']) });
272
- }
273
-
274
- // Goals shown = the project's declared goals ∪ goals referenced by in-scope
275
- // changes (so the F→G→C chain stays connected).
276
- const goalIdSet = new Set<string>(projectGoalIds);
277
- for (const ch of changes) for (const g of ch.goalIds) goalIdSet.add(g);
278
-
279
- const goals: ResolvedGoal[] = [];
280
- for (const gid of goalIdSet) {
281
- const rec = goalElems.get(gid);
282
- if (!rec) {
283
- warnings.push({
284
- code: 'PC-001',
285
- message: `Goal "${gid}" not found as a GOAL element in canon; omitted from the motivation chain`,
286
- });
287
- continue;
288
- }
289
- goals.push({ id: gid, name: str(rec['name']) ?? gid, factorIds: strArray(rec['factors']) });
290
- }
291
-
292
- // Factors shown = those referenced by the in-scope goals.
293
- const factorIdSet = new Set<string>();
294
- for (const g of goals) for (const f of g.factorIds) factorIdSet.add(f);
295
-
296
- const factors: ResolvedFactor[] = [];
297
- for (const fid of factorIdSet) {
298
- const rec = factorElems.get(fid);
299
- factors.push({ id: fid, name: rec ? str(rec['name']) ?? fid : fid });
300
- }
301
-
302
- // ── Child activities — parent = project id ──────────────────────────────────
303
- const childActivities: ResolvedChildActivity[] = [];
304
- for (const [id, rec] of activities) {
305
- if (str(rec['parent']) !== projectId) continue;
306
- childActivities.push({
307
- id,
308
- name: str(rec['name']) ?? id,
309
- activity_type: str(rec['activity_type']),
310
- start_date: str(rec['start_date']),
311
- end_date: str(rec['end_date']),
312
- owner: str(rec['owner']),
313
- });
314
- }
315
- childActivities.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
316
-
317
- const project: ResolvedProject = {
318
- id: projectId,
319
- name: str(projectRec['name']) ?? projectId,
320
- description: str(projectRec['description']),
321
- valid_from: validFromOk ? (validFrom as string) : undefined,
322
- valid_to: validToOk ? (validToRaw as string) : undefined,
323
- start_date: str(projectRec['start_date']),
324
- end_date: str(projectRec['end_date']),
325
- };
326
-
327
- const resolved: ResolvedActivityCard = {
328
- cardId: str(card.id) ?? '',
329
- cardDescription: str(card.description),
330
- project,
331
- milestones,
332
- motivation: { factors, goals, changes },
333
- childActivities,
334
- };
335
-
336
- return { valid: true, errors, warnings, resolved };
337
- }
1
+ // Activity Card — cross-document resolver (the novel piece of this notation).
2
+ //
3
+ // Unlike every other Studio preview, the Activity Card is assembled from
4
+ // MULTIPLE documents. The card YAML names a project Activity; the resolver
5
+ // pulls the rest of the card's content BY REFERENCE from the canonical
6
+ // ELEMENT and RELATION store — never from other view documents (view-purity:
7
+ // a view is a projection over elements + relations, methodology
8
+ // ELEMENT_PRIMITIVES.md §1):
9
+ //
10
+ // canon/elements/** → one element per file. The project ACTIVITY (name,
11
+ // lifecycle + schedule dates, delivers_changes[],
12
+ // parent) and its child ACTIVITY elements (parent =
13
+ // project id); the FACTOR / GOAL / CHANGE elements that
14
+ // the motivation chain expands (goal.factors,
15
+ // change.goals carried inline on the elements).
16
+ // canon/relations/** → `notation: relation` files. The project's goals come
17
+ // from the first-class `activity_goal` REL (from =
18
+ // project id), preferred over the activity element's
19
+ // transitional inline `goals[]` when present
20
+ // (07-activities.md §"Time-aware relations").
21
+ //
22
+ // The extension reads + parses the element and relation files (walking the
23
+ // org's `canon/elements` and `canon/relations` trees) and hands the parsed
24
+ // docs in. This resolver is filesystem-free and pure so it stays unit-testable.
25
+ //
26
+ // Cross-document validation lives here (the rules that need the sibling docs):
27
+ // PC-001 project does not resolve to an admitted Activity
28
+ // PC-002 resolved Activity has activity_type other than "Project"
29
+ // PC-003 milestone.delivers_changes[] entry not in the project's own
30
+ // delivers_changes[]
31
+ // PC-004 (warning) milestone.date outside [valid_from, valid_to]
32
+ // LIFECYCLE-001 project valid_from missing / not a parseable ISO date
33
+ // LIFECYCLE-002 project valid_to present, not null, not a parseable date
34
+ // LIFECYCLE-003 project valid_to earlier than valid_from
35
+ //
36
+ // LIFECYCLE-* are scoped to the RESOLVED PROJECT Activity only — the card is
37
+ // built around the project's initiation date, so a missing valid_from there is
38
+ // a hard error. Child activities and FGCA elements are not lifecycle-checked
39
+ // here (out of this notation's scope; today's Studio activities docs may
40
+ // legitimately omit lifecycle fields).
41
+
42
+ import type {
43
+ ActivityCardDoc,
44
+ ActivityCardSources,
45
+ ResolvedActivityCard,
46
+ ResolvedAssessment,
47
+ ResolvedChange,
48
+ ResolvedChildActivity,
49
+ ResolvedDriver,
50
+ ResolvedGoal,
51
+ ResolvedMilestone,
52
+ ResolvedProject,
53
+ ResolvedStakeholder,
54
+ } from './types.js';
55
+ import type { ValidationError, ValidationWarning, ValidationResult } from '../validation-types.js';
56
+
57
+ const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
58
+
59
+ export interface ActivityCardResolution extends ValidationResult {
60
+ /** Present only when resolution succeeded (project found + valid_from OK). */
61
+ resolved?: ResolvedActivityCard;
62
+ }
63
+
64
+ function isObject(v: unknown): v is Record<string, unknown> {
65
+ return !!v && typeof v === 'object' && !Array.isArray(v);
66
+ }
67
+
68
+ function str(v: unknown): string | undefined {
69
+ return typeof v === 'string' && v.trim().length > 0 ? v : undefined;
70
+ }
71
+
72
+ function strArray(v: unknown): string[] {
73
+ if (!Array.isArray(v)) return [];
74
+ return v.filter((x): x is string => typeof x === 'string' && x.trim().length > 0);
75
+ }
76
+
77
+ /** A parseable quoted ISO 8601 date, calendar-checked (rejects 2026-13-40). */
78
+ function isIsoDate(v: unknown): v is string {
79
+ if (typeof v !== 'string' || !DATE_RE.test(v)) return false;
80
+ const t = Date.parse(`${v}T00:00:00Z`);
81
+ if (Number.isNaN(t)) return false;
82
+ // Round-trip guard so 2026-02-30 (which Date.parse may roll forward) is rejected.
83
+ return new Date(t).toISOString().slice(0, 10) === v;
84
+ }
85
+
86
+ /**
87
+ * Index canon element documents of one `notation` by their `id`. Each element
88
+ * is its own single-element file (`notation: activity|factor|goal|change|…`
89
+ * with a top-level `id`); first definition wins.
90
+ */
91
+ function collectByNotation(
92
+ docs: unknown[],
93
+ notation: string,
94
+ ): Map<string, Record<string, unknown>> {
95
+ const out = new Map<string, Record<string, unknown>>();
96
+ for (const doc of docs) {
97
+ if (!isObject(doc)) continue;
98
+ if (str(doc['notation']) !== notation) continue;
99
+ const id = str(doc['id']);
100
+ if (!id) continue;
101
+ if (!out.has(id)) out.set(id, doc); // first definition wins
102
+ }
103
+ return out;
104
+ }
105
+
106
+ /**
107
+ * GOAL ids reached by an *active* `activity_goal` relation originating at
108
+ * `fromId`. A relation with a non-null `valid_to` has ended (the activity was
109
+ * re-aimed) and is excluded — the card shows the currently-served goals.
110
+ */
111
+ function activeActivityGoals(relations: unknown[], fromId: string): string[] {
112
+ const out: string[] = [];
113
+ for (const doc of relations) {
114
+ if (!isObject(doc)) continue;
115
+ if (str(doc['notation']) !== 'relation') continue;
116
+ if (str(doc['type']) !== 'activity_goal') continue;
117
+ if (str(doc['from']) !== fromId) continue;
118
+ const to = str(doc['to']);
119
+ if (!to) continue;
120
+ const validTo = doc['valid_to'];
121
+ if (validTo !== undefined && validTo !== null) continue; // ended relation
122
+ if (!out.includes(to)) out.push(to);
123
+ }
124
+ return out;
125
+ }
126
+
127
+ /**
128
+ * STAKEHOLDER ids + roles reached by an *active* `activity_stakeholder`
129
+ * relation originating at `fromId`. A relation with a non-null `valid_to` has
130
+ * ended and is excluded, so the card shows the project's currently-engaged
131
+ * stakeholders. The optional `role` field on the relation carries the
132
+ * project-role (initiator | owner | sponsor | project_manager).
133
+ */
134
+ function activeActivityStakeholders(
135
+ relations: unknown[],
136
+ fromId: string,
137
+ ): Array<{ id: string; role?: string }> {
138
+ const out: Array<{ id: string; role?: string }> = [];
139
+ for (const doc of relations) {
140
+ if (!isObject(doc)) continue;
141
+ if (str(doc['notation']) !== 'relation') continue;
142
+ if (str(doc['type']) !== 'activity_stakeholder') continue;
143
+ if (str(doc['from']) !== fromId) continue;
144
+ const to = str(doc['to']);
145
+ if (!to) continue;
146
+ const validTo = doc['valid_to'];
147
+ if (validTo !== undefined && validTo !== null) continue; // ended relation
148
+ if (!out.some((s) => s.id === to)) out.push({ id: to, role: str(doc['role']) });
149
+ }
150
+ return out;
151
+ }
152
+
153
+ export function resolveActivityCard(
154
+ doc: ActivityCardDoc,
155
+ sources: ActivityCardSources,
156
+ ): ActivityCardResolution {
157
+ const errors: ValidationError[] = [];
158
+ const warnings: ValidationWarning[] = [];
159
+
160
+ const card = doc?.activity_card;
161
+ const projectId = str(card?.project);
162
+ if (!projectId) {
163
+ // Structural validator already flags this; nothing to resolve.
164
+ errors.push({ code: 'PC-001', message: 'activity_card.project is missing' });
165
+ return { valid: false, errors, warnings };
166
+ }
167
+
168
+ const activities = collectByNotation(sources.elements, 'activity');
169
+ const projectRec = activities.get(projectId);
170
+
171
+ // PC-001 — project must resolve to an admitted ACTIVITY element after
172
+ // exhausting the canonical resolution scope (§6.1): canon/elements/**
173
+ // recursively first, then canon/views/activities/** as secondary fallback.
174
+ if (!projectRec) {
175
+ errors.push({
176
+ code: 'PC-001',
177
+ message:
178
+ `activity_card.project "${projectId}" not found after searching ` +
179
+ `canon/elements/** and canon/views/activities/** — ` +
180
+ `add a YAML file with notation: activity and id: "${projectId}" ` +
181
+ `(e.g. canon/elements/05_implementation/activities/${projectId}.yaml)`,
182
+ });
183
+ return { valid: false, errors, warnings };
184
+ }
185
+
186
+ // LIFECYCLE-001..003 on the project Activity.
187
+ const validFrom = projectRec['valid_from'];
188
+ const validToRaw = projectRec['valid_to'];
189
+ let validFromOk = false;
190
+ if (validFrom === undefined || validFrom === null) {
191
+ errors.push({ code: 'LIFECYCLE-001', message: `Activity "${projectId}" valid_from is required` });
192
+ } else if (!isIsoDate(validFrom)) {
193
+ errors.push({
194
+ code: 'LIFECYCLE-001',
195
+ message: `Activity "${projectId}" valid_from "${String(validFrom)}" is not a parseable ISO 8601 date`,
196
+ });
197
+ } else {
198
+ validFromOk = true;
199
+ }
200
+
201
+ let validToOk = false;
202
+ const validToPresent = validToRaw !== undefined && validToRaw !== null;
203
+ if (validToPresent) {
204
+ if (!isIsoDate(validToRaw)) {
205
+ errors.push({
206
+ code: 'LIFECYCLE-002',
207
+ message: `Activity "${projectId}" valid_to "${String(validToRaw)}" is not a parseable ISO 8601 date`,
208
+ });
209
+ } else {
210
+ validToOk = true;
211
+ if (validFromOk && (validToRaw as string) < (validFrom as string)) {
212
+ errors.push({
213
+ code: 'LIFECYCLE-003',
214
+ message: `Activity "${projectId}" valid_to "${String(validToRaw)}" is earlier than valid_from "${String(validFrom)}"`,
215
+ });
216
+ }
217
+ }
218
+ }
219
+
220
+ // A failed lifecycle/PC-001 stops us producing a card view-model, but we still
221
+ // return every error collected so the panel lists them all at once.
222
+ const projectChanges = new Set(strArray(projectRec['delivers_changes']));
223
+ // Project goals: prefer the first-class `activity_goal` relations (the
224
+ // canonical, time-aware home for an Activity→Goal link); fall back to the
225
+ // activity element's transitional inline `goals[]` when no relation exists
226
+ // (07-activities.md §"Time-aware relations").
227
+ const relGoalIds = activeActivityGoals(sources.relations, projectId);
228
+ const projectGoalIds = relGoalIds.length > 0 ? relGoalIds : strArray(projectRec['goals']);
229
+
230
+ // PC-003 — each milestone change must be a subset of the project's changes.
231
+ const rawMilestones = Array.isArray(card.milestones) ? card.milestones : [];
232
+ const milestones: ResolvedMilestone[] = [];
233
+ for (const m of rawMilestones) {
234
+ if (!isObject(m)) continue;
235
+ const mid = str(m['id']);
236
+ const mname = str(m['name']);
237
+ const mdate = str(m['date']);
238
+ if (!mid || !mname || !mdate) continue; // structural validator already flagged
239
+ const deliversChanges = strArray(m['delivers_changes']);
240
+ for (const c of deliversChanges) {
241
+ if (!projectChanges.has(c)) {
242
+ errors.push({
243
+ code: 'PC-003',
244
+ message: `Milestone "${mid}" delivers_changes "${c}" is not in project "${projectId}" delivers_changes`,
245
+ });
246
+ }
247
+ }
248
+ // PC-004 — milestone date should fall within the project lifecycle window.
249
+ if (validFromOk && mdate < (validFrom as string)) {
250
+ warnings.push({
251
+ code: 'PC-004',
252
+ message: `Milestone "${mid}" date "${mdate}" is before project valid_from "${String(validFrom)}"`,
253
+ });
254
+ }
255
+ if (validToOk && mdate > (validToRaw as string)) {
256
+ warnings.push({
257
+ code: 'PC-004',
258
+ message: `Milestone "${mid}" date "${mdate}" is after project valid_to "${String(validToRaw)}"`,
259
+ });
260
+ }
261
+ milestones.push({
262
+ id: mid,
263
+ name: mname,
264
+ date: mdate,
265
+ description: str(m['description']),
266
+ deliversChanges,
267
+ });
268
+ }
269
+ milestones.sort((a, b) => (a.date < b.date ? -1 : a.date > b.date ? 1 : 0));
270
+
271
+ // If we have hard errors, surface them without an assembled card.
272
+ if (errors.length > 0) {
273
+ return { valid: false, errors, warnings };
274
+ }
275
+
276
+ // ── Motivation chain — expand against canon DRIVER / GOAL / CHANGE elements ──
277
+ // Canon elements use `notation: driver`; `notation: factor` accepted for backward compat.
278
+ const driverElemsLegacy = collectByNotation(sources.elements, 'factor');
279
+ const driverElemsNew = collectByNotation(sources.elements, 'driver');
280
+ const driverElems = new Map([...driverElemsLegacy, ...driverElemsNew]);
281
+ const goalElems = collectByNotation(sources.elements, 'goal');
282
+ const changeElems = collectByNotation(sources.elements, 'change');
283
+
284
+ const changes: ResolvedChange[] = [];
285
+ for (const cid of projectChanges) {
286
+ const rec = changeElems.get(cid);
287
+ if (!rec) {
288
+ warnings.push({
289
+ code: 'PC-001',
290
+ message: `Project change "${cid}" not found as a CHANGE element in canon; omitted from the motivation chain`,
291
+ });
292
+ continue;
293
+ }
294
+ changes.push({ id: cid, name: str(rec['name']) ?? cid, goalIds: strArray(rec['goals']) });
295
+ }
296
+
297
+ // Goals shown = the project's declared goals ∪ goals referenced by in-scope
298
+ // changes (so the D→G→C chain stays connected).
299
+ const goalIdSet = new Set<string>(projectGoalIds);
300
+ for (const ch of changes) for (const g of ch.goalIds) goalIdSet.add(g);
301
+
302
+ const goals: ResolvedGoal[] = [];
303
+ for (const gid of goalIdSet) {
304
+ const rec = goalElems.get(gid);
305
+ if (!rec) {
306
+ warnings.push({
307
+ code: 'PC-001',
308
+ message: `Goal "${gid}" not found as a GOAL element in canon; omitted from the motivation chain`,
309
+ });
310
+ continue;
311
+ }
312
+ goals.push({ id: gid, name: str(rec['name']) ?? gid, driverIds: strArray(rec['factors']) });
313
+ }
314
+
315
+ // Drivers shown = those referenced by the in-scope goals.
316
+ const driverIdSet = new Set<string>();
317
+ for (const g of goals) for (const d of g.driverIds) driverIdSet.add(d);
318
+
319
+ const drivers: ResolvedDriver[] = [];
320
+ for (const did of driverIdSet) {
321
+ const rec = driverElems.get(did);
322
+ drivers.push({ id: did, name: rec ? str(rec['name']) ?? did : did });
323
+ }
324
+
325
+ // ── Assessments — findings that assess an in-scope Driver ────────────────────
326
+ // Assessments answer "what specific problem are we solving?" They are resolved
327
+ // from ASSESSMENT elements whose `assesses` field matches a Driver id that
328
+ // appeared in the motivation chain above. Empty when none exist — the gap is
329
+ // intentionally visible on the card so the author knows it needs filling.
330
+ const driverIdLookup = new Set(drivers.map((d) => d.id));
331
+ const assessmentElems = collectByNotation(sources.elements, 'assessment');
332
+ const assessments: ResolvedAssessment[] = [];
333
+ for (const [aid, rec] of assessmentElems) {
334
+ const driverId = str(rec['assesses']);
335
+ if (!driverId || !driverIdLookup.has(driverId)) continue;
336
+ assessments.push({
337
+ id: aid,
338
+ name: str(rec['name']) ?? aid,
339
+ driverId,
340
+ description: str(rec['description']),
341
+ observed_at: str(rec['observed_at']),
342
+ });
343
+ }
344
+ assessments.sort((a, b) => {
345
+ if (!a.observed_at && !b.observed_at) return 0;
346
+ if (!a.observed_at) return 1;
347
+ if (!b.observed_at) return -1;
348
+ return a.observed_at < b.observed_at ? -1 : a.observed_at > b.observed_at ? 1 : 0;
349
+ });
350
+
351
+ // ── Child activities — parent = project id ──────────────────────────────────
352
+ const childActivities: ResolvedChildActivity[] = [];
353
+ for (const [id, rec] of activities) {
354
+ if (str(rec['parent']) !== projectId) continue;
355
+ childActivities.push({
356
+ id,
357
+ name: str(rec['name']) ?? id,
358
+ activity_type: str(rec['activity_type']),
359
+ start_date: str(rec['start_date']),
360
+ end_date: str(rec['end_date']),
361
+ owner: str(rec['owner']),
362
+ });
363
+ }
364
+ childActivities.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
365
+
366
+ // ── Project goal text field — names of the DIRECTLY-served goals ────────────
367
+ // The "Project goal" field shows the goals the project itself serves (the
368
+ // `activity_goal` targets), not the wider F→G→C chain (which also pulls in
369
+ // goals referenced only by in-scope changes). Resolve each id to its GOAL
370
+ // element name, falling back to the id when the element is absent.
371
+ const goalNames = projectGoalIds.map((gid) => str(goalElems.get(gid)?.['name']) ?? gid);
372
+
373
+ // ── Stakeholders — active `activity_stakeholder` relations from the project ──
374
+ const stakeholderElems = collectByNotation(sources.elements, 'stakeholder');
375
+ const stakeholders: ResolvedStakeholder[] = [];
376
+ for (const { id: sid, role } of activeActivityStakeholders(sources.relations, projectId)) {
377
+ const rec = stakeholderElems.get(sid);
378
+ if (!rec) {
379
+ warnings.push({
380
+ code: 'PC-001',
381
+ message: `Stakeholder "${sid}" not found as a STAKEHOLDER element in canon; shown by id`,
382
+ });
383
+ stakeholders.push({ id: sid, name: sid, role });
384
+ continue;
385
+ }
386
+ stakeholders.push({
387
+ id: sid,
388
+ name: str(rec['name']) ?? sid,
389
+ type: str(rec['type']),
390
+ interest: str(rec['interest']),
391
+ influence: str(rec['influence']),
392
+ role,
393
+ });
394
+ }
395
+
396
+ const project: ResolvedProject = {
397
+ id: projectId,
398
+ name: str(projectRec['name']) ?? projectId,
399
+ description: str(projectRec['description']),
400
+ valid_from: validFromOk ? (validFrom as string) : undefined,
401
+ valid_to: validToOk ? (validToRaw as string) : undefined,
402
+ start_date: str(projectRec['start_date']),
403
+ end_date: str(projectRec['end_date']),
404
+ activity_type: str(projectRec['activity_type']),
405
+ status: str(projectRec['status']),
406
+ };
407
+
408
+ const resolved: ResolvedActivityCard = {
409
+ cardId: str(card.id) ?? '',
410
+ cardDescription: str(card.description),
411
+ project,
412
+ milestones,
413
+ motivation: { drivers, goals, changes },
414
+ assessments,
415
+ childActivities,
416
+ goalNames,
417
+ stakeholders,
418
+ notes: str(card.notes),
419
+ };
420
+
421
+ return { valid: true, errors, warnings, resolved };
422
+ }