@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,337 +1,415 @@
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 in canon.
172
+ if (!projectRec) {
173
+ errors.push({
174
+ code: 'PC-001',
175
+ message: `activity_card.project "${projectId}" does not resolve to an ACTIVITY element in the canon element store`,
176
+ });
177
+ return { valid: false, errors, warnings };
178
+ }
179
+
180
+ // LIFECYCLE-001..003 on the project Activity.
181
+ const validFrom = projectRec['valid_from'];
182
+ const validToRaw = projectRec['valid_to'];
183
+ let validFromOk = false;
184
+ if (validFrom === undefined || validFrom === null) {
185
+ errors.push({ code: 'LIFECYCLE-001', message: `Activity "${projectId}" valid_from is required` });
186
+ } else if (!isIsoDate(validFrom)) {
187
+ errors.push({
188
+ code: 'LIFECYCLE-001',
189
+ message: `Activity "${projectId}" valid_from "${String(validFrom)}" is not a parseable ISO 8601 date`,
190
+ });
191
+ } else {
192
+ validFromOk = true;
193
+ }
194
+
195
+ let validToOk = false;
196
+ const validToPresent = validToRaw !== undefined && validToRaw !== null;
197
+ if (validToPresent) {
198
+ if (!isIsoDate(validToRaw)) {
199
+ errors.push({
200
+ code: 'LIFECYCLE-002',
201
+ message: `Activity "${projectId}" valid_to "${String(validToRaw)}" is not a parseable ISO 8601 date`,
202
+ });
203
+ } else {
204
+ validToOk = true;
205
+ if (validFromOk && (validToRaw as string) < (validFrom as string)) {
206
+ errors.push({
207
+ code: 'LIFECYCLE-003',
208
+ message: `Activity "${projectId}" valid_to "${String(validToRaw)}" is earlier than valid_from "${String(validFrom)}"`,
209
+ });
210
+ }
211
+ }
212
+ }
213
+
214
+ // A failed lifecycle/PC-001 stops us producing a card view-model, but we still
215
+ // return every error collected so the panel lists them all at once.
216
+ const projectChanges = new Set(strArray(projectRec['delivers_changes']));
217
+ // Project goals: prefer the first-class `activity_goal` relations (the
218
+ // canonical, time-aware home for an Activity→Goal link); fall back to the
219
+ // activity element's transitional inline `goals[]` when no relation exists
220
+ // (07-activities.md §"Time-aware relations").
221
+ const relGoalIds = activeActivityGoals(sources.relations, projectId);
222
+ const projectGoalIds = relGoalIds.length > 0 ? relGoalIds : strArray(projectRec['goals']);
223
+
224
+ // PC-003 — each milestone change must be a subset of the project's changes.
225
+ const rawMilestones = Array.isArray(card.milestones) ? card.milestones : [];
226
+ const milestones: ResolvedMilestone[] = [];
227
+ for (const m of rawMilestones) {
228
+ if (!isObject(m)) continue;
229
+ const mid = str(m['id']);
230
+ const mname = str(m['name']);
231
+ const mdate = str(m['date']);
232
+ if (!mid || !mname || !mdate) continue; // structural validator already flagged
233
+ const deliversChanges = strArray(m['delivers_changes']);
234
+ for (const c of deliversChanges) {
235
+ if (!projectChanges.has(c)) {
236
+ errors.push({
237
+ code: 'PC-003',
238
+ message: `Milestone "${mid}" delivers_changes "${c}" is not in project "${projectId}" delivers_changes`,
239
+ });
240
+ }
241
+ }
242
+ // PC-004 — milestone date should fall within the project lifecycle window.
243
+ if (validFromOk && mdate < (validFrom as string)) {
244
+ warnings.push({
245
+ code: 'PC-004',
246
+ message: `Milestone "${mid}" date "${mdate}" is before project valid_from "${String(validFrom)}"`,
247
+ });
248
+ }
249
+ if (validToOk && mdate > (validToRaw as string)) {
250
+ warnings.push({
251
+ code: 'PC-004',
252
+ message: `Milestone "${mid}" date "${mdate}" is after project valid_to "${String(validToRaw)}"`,
253
+ });
254
+ }
255
+ milestones.push({
256
+ id: mid,
257
+ name: mname,
258
+ date: mdate,
259
+ description: str(m['description']),
260
+ deliversChanges,
261
+ });
262
+ }
263
+ milestones.sort((a, b) => (a.date < b.date ? -1 : a.date > b.date ? 1 : 0));
264
+
265
+ // If we have hard errors, surface them without an assembled card.
266
+ if (errors.length > 0) {
267
+ return { valid: false, errors, warnings };
268
+ }
269
+
270
+ // ── Motivation chain — expand against canon DRIVER / GOAL / CHANGE elements ──
271
+ // Note: YAML corpus still uses `notation: factor` and `goal.factors` field names;
272
+ // the YAML keys are stable. The resolved TypeScript API uses "driver/driverIds".
273
+ const driverElems = collectByNotation(sources.elements, 'factor');
274
+ const goalElems = collectByNotation(sources.elements, 'goal');
275
+ const changeElems = collectByNotation(sources.elements, 'change');
276
+
277
+ const changes: ResolvedChange[] = [];
278
+ for (const cid of projectChanges) {
279
+ const rec = changeElems.get(cid);
280
+ if (!rec) {
281
+ warnings.push({
282
+ code: 'PC-001',
283
+ message: `Project change "${cid}" not found as a CHANGE element in canon; omitted from the motivation chain`,
284
+ });
285
+ continue;
286
+ }
287
+ changes.push({ id: cid, name: str(rec['name']) ?? cid, goalIds: strArray(rec['goals']) });
288
+ }
289
+
290
+ // Goals shown = the project's declared goals ∪ goals referenced by in-scope
291
+ // changes (so the D→G→C chain stays connected).
292
+ const goalIdSet = new Set<string>(projectGoalIds);
293
+ for (const ch of changes) for (const g of ch.goalIds) goalIdSet.add(g);
294
+
295
+ const goals: ResolvedGoal[] = [];
296
+ for (const gid of goalIdSet) {
297
+ const rec = goalElems.get(gid);
298
+ if (!rec) {
299
+ warnings.push({
300
+ code: 'PC-001',
301
+ message: `Goal "${gid}" not found as a GOAL element in canon; omitted from the motivation chain`,
302
+ });
303
+ continue;
304
+ }
305
+ goals.push({ id: gid, name: str(rec['name']) ?? gid, driverIds: strArray(rec['factors']) });
306
+ }
307
+
308
+ // Drivers shown = those referenced by the in-scope goals.
309
+ const driverIdSet = new Set<string>();
310
+ for (const g of goals) for (const d of g.driverIds) driverIdSet.add(d);
311
+
312
+ const drivers: ResolvedDriver[] = [];
313
+ for (const did of driverIdSet) {
314
+ const rec = driverElems.get(did);
315
+ drivers.push({ id: did, name: rec ? str(rec['name']) ?? did : did });
316
+ }
317
+
318
+ // ── Assessments — findings that assess an in-scope Driver ────────────────────
319
+ // Assessments answer "what specific problem are we solving?" They are resolved
320
+ // from ASSESSMENT elements whose `assesses` field matches a Driver id that
321
+ // appeared in the motivation chain above. Empty when none exist — the gap is
322
+ // intentionally visible on the card so the author knows it needs filling.
323
+ const driverIdLookup = new Set(drivers.map((d) => d.id));
324
+ const assessmentElems = collectByNotation(sources.elements, 'assessment');
325
+ const assessments: ResolvedAssessment[] = [];
326
+ for (const [aid, rec] of assessmentElems) {
327
+ const driverId = str(rec['assesses']);
328
+ if (!driverId || !driverIdLookup.has(driverId)) continue;
329
+ assessments.push({
330
+ id: aid,
331
+ name: str(rec['name']) ?? aid,
332
+ driverId,
333
+ description: str(rec['description']),
334
+ observed_at: str(rec['observed_at']),
335
+ });
336
+ }
337
+ assessments.sort((a, b) => {
338
+ if (!a.observed_at && !b.observed_at) return 0;
339
+ if (!a.observed_at) return 1;
340
+ if (!b.observed_at) return -1;
341
+ return a.observed_at < b.observed_at ? -1 : a.observed_at > b.observed_at ? 1 : 0;
342
+ });
343
+
344
+ // ── Child activities — parent = project id ──────────────────────────────────
345
+ const childActivities: ResolvedChildActivity[] = [];
346
+ for (const [id, rec] of activities) {
347
+ if (str(rec['parent']) !== projectId) continue;
348
+ childActivities.push({
349
+ id,
350
+ name: str(rec['name']) ?? id,
351
+ activity_type: str(rec['activity_type']),
352
+ start_date: str(rec['start_date']),
353
+ end_date: str(rec['end_date']),
354
+ owner: str(rec['owner']),
355
+ });
356
+ }
357
+ childActivities.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
358
+
359
+ // ── Project goal text field — names of the DIRECTLY-served goals ────────────
360
+ // The "Project goal" field shows the goals the project itself serves (the
361
+ // `activity_goal` targets), not the wider F→G→C chain (which also pulls in
362
+ // goals referenced only by in-scope changes). Resolve each id to its GOAL
363
+ // element name, falling back to the id when the element is absent.
364
+ const goalNames = projectGoalIds.map((gid) => str(goalElems.get(gid)?.['name']) ?? gid);
365
+
366
+ // ── Stakeholders — active `activity_stakeholder` relations from the project ──
367
+ const stakeholderElems = collectByNotation(sources.elements, 'stakeholder');
368
+ const stakeholders: ResolvedStakeholder[] = [];
369
+ for (const { id: sid, role } of activeActivityStakeholders(sources.relations, projectId)) {
370
+ const rec = stakeholderElems.get(sid);
371
+ if (!rec) {
372
+ warnings.push({
373
+ code: 'PC-001',
374
+ message: `Stakeholder "${sid}" not found as a STAKEHOLDER element in canon; shown by id`,
375
+ });
376
+ stakeholders.push({ id: sid, name: sid, role });
377
+ continue;
378
+ }
379
+ stakeholders.push({
380
+ id: sid,
381
+ name: str(rec['name']) ?? sid,
382
+ type: str(rec['type']),
383
+ interest: str(rec['interest']),
384
+ influence: str(rec['influence']),
385
+ role,
386
+ });
387
+ }
388
+
389
+ const project: ResolvedProject = {
390
+ id: projectId,
391
+ name: str(projectRec['name']) ?? projectId,
392
+ description: str(projectRec['description']),
393
+ valid_from: validFromOk ? (validFrom as string) : undefined,
394
+ valid_to: validToOk ? (validToRaw as string) : undefined,
395
+ start_date: str(projectRec['start_date']),
396
+ end_date: str(projectRec['end_date']),
397
+ activity_type: str(projectRec['activity_type']),
398
+ status: str(projectRec['status']),
399
+ };
400
+
401
+ const resolved: ResolvedActivityCard = {
402
+ cardId: str(card.id) ?? '',
403
+ cardDescription: str(card.description),
404
+ project,
405
+ milestones,
406
+ motivation: { drivers, goals, changes },
407
+ assessments,
408
+ childActivities,
409
+ goalNames,
410
+ stakeholders,
411
+ notes: str(card.notes),
412
+ };
413
+
414
+ return { valid: true, errors, warnings, resolved };
415
+ }