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