@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.
- package/LICENSE +21 -21
- package/README.md +47 -47
- package/dist/__tests__/edge-path.test.js +8 -0
- 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 +139 -6
- 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 +107 -26
- 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 +311 -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 +77 -74
- package/dist/compliance/impact.d.ts +77 -16
- package/dist/compliance/impact.js +193 -25
- 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 +74 -45
- package/dist/factor/index.d.ts +3 -2
- package/dist/factor/index.js +1 -1
- package/dist/factor/types.d.ts +4 -7
- 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__/resolver.test.js +185 -0
- package/dist/fgca/index.d.ts +2 -0
- package/dist/fgca/index.js +1 -0
- package/dist/fgca/resolver.d.ts +49 -0
- package/dist/fgca/resolver.js +124 -0
- 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/layout.js +5 -1
- package/dist/process-blueprint/types.d.ts +22 -1
- 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 +69 -69
- package/dist/webview/__tests__/entry.test.js +11 -11
- package/dist/webview/render-activities.d.ts +3 -1
- package/dist/webview/render-activities.js +2 -2
- package/dist/webview/render-activity-card.js +104 -77
- package/dist/webview/render-fgca.d.ts +4 -0
- package/dist/webview/render-fgca.js +2 -5
- package/dist/webview/render-goals.d.ts +1 -0
- package/dist/webview/render-goals.js +15 -15
- package/dist/webview/render-process-blueprint.js +10 -10
- package/package.json +40 -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 +95 -95
- package/src/activities/gantt.ts +276 -276
- package/src/activities/index.ts +28 -28
- package/src/activities/layout.ts +112 -112
- package/src/activities/types.ts +148 -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 +307 -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 +415 -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 +840 -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 +298 -295
- package/src/compliance/impact.ts +861 -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 +80 -45
- package/src/factor/index.ts +3 -7
- package/src/factor/types.ts +14 -18
- 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 +400 -400
- package/src/fgca/preview-layout.ts +217 -217
- package/src/fgca/resolver.ts +157 -0
- package/src/fgca/types.ts +50 -50
- package/src/fgca/validate.ts +126 -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 +31 -31
- package/src/process-blueprint/layout.ts +535 -531
- package/src/process-blueprint/types.ts +263 -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 +237 -237
- package/src/theme/tokens.ts +133 -133
- package/src/typed-id.ts +56 -56
- package/src/validation-types.ts +36 -36
- package/src/webview/__tests__/entry-notations.test.ts +77 -77
- package/src/webview/__tests__/entry.test.ts +64 -64
- package/src/webview/__tests__/render-goals.test.ts +72 -72
- package/src/webview/entry.ts +292 -292
- package/src/webview/render-activities.ts +5 -3
- package/src/webview/render-activity-card.ts +266 -234
- package/src/webview/render-fgca.ts +6 -5
- package/src/webview/render-goals.ts +111 -110
- package/src/webview/render-process-blueprint.ts +207 -207
- 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.d.ts +0 -1
- 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
|
@@ -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 = [];
|
|
@@ -127,19 +156,6 @@ export function resolveActivityCard(doc, sources) {
|
|
|
127
156
|
});
|
|
128
157
|
return { valid: false, errors, warnings };
|
|
129
158
|
}
|
|
130
|
-
// PC-002 — the resolved Activity should be the project scale. In the element
|
|
131
|
-
// model all activity scales (initiative / programme / project / task) share
|
|
132
|
-
// one ACTIVITY TYPE and `activity_type` references an ActivityType element,
|
|
133
|
-
// so a missing/non-literal value is acceptable; only an explicit, clearly
|
|
134
|
-
// non-project marker is flagged. (Canonical project-identification semantics
|
|
135
|
-
// are tracked as a follow-up internally.)
|
|
136
|
-
const activityType = str(projectRec['activity_type']);
|
|
137
|
-
if (activityType !== undefined && activityType !== 'Project' && activityType !== 'project') {
|
|
138
|
-
errors.push({
|
|
139
|
-
code: 'PC-002',
|
|
140
|
-
message: `Activity "${projectId}" has activity_type "${activityType}", expected the project scale`,
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
159
|
// LIFECYCLE-001..003 on the project Activity.
|
|
144
160
|
const validFrom = projectRec['valid_from'];
|
|
145
161
|
const validToRaw = projectRec['valid_to'];
|
|
@@ -230,8 +246,10 @@ export function resolveActivityCard(doc, sources) {
|
|
|
230
246
|
if (errors.length > 0) {
|
|
231
247
|
return { valid: false, errors, warnings };
|
|
232
248
|
}
|
|
233
|
-
// ── Motivation chain — expand against canon
|
|
234
|
-
|
|
249
|
+
// ── Motivation chain — expand against canon DRIVER / GOAL / CHANGE elements ──
|
|
250
|
+
// Note: YAML corpus still uses `notation: factor` and `goal.factors` field names;
|
|
251
|
+
// the YAML keys are stable. The resolved TypeScript API uses "driver/driverIds".
|
|
252
|
+
const driverElems = collectByNotation(sources.elements, 'factor');
|
|
235
253
|
const goalElems = collectByNotation(sources.elements, 'goal');
|
|
236
254
|
const changeElems = collectByNotation(sources.elements, 'change');
|
|
237
255
|
const changes = [];
|
|
@@ -247,7 +265,7 @@ export function resolveActivityCard(doc, sources) {
|
|
|
247
265
|
changes.push({ id: cid, name: str(rec['name']) ?? cid, goalIds: strArray(rec['goals']) });
|
|
248
266
|
}
|
|
249
267
|
// Goals shown = the project's declared goals ∪ goals referenced by in-scope
|
|
250
|
-
// changes (so the
|
|
268
|
+
// changes (so the D→G→C chain stays connected).
|
|
251
269
|
const goalIdSet = new Set(projectGoalIds);
|
|
252
270
|
for (const ch of changes)
|
|
253
271
|
for (const g of ch.goalIds)
|
|
@@ -262,18 +280,47 @@ export function resolveActivityCard(doc, sources) {
|
|
|
262
280
|
});
|
|
263
281
|
continue;
|
|
264
282
|
}
|
|
265
|
-
goals.push({ id: gid, name: str(rec['name']) ?? gid,
|
|
283
|
+
goals.push({ id: gid, name: str(rec['name']) ?? gid, driverIds: strArray(rec['factors']) });
|
|
266
284
|
}
|
|
267
|
-
//
|
|
268
|
-
const
|
|
285
|
+
// Drivers shown = those referenced by the in-scope goals.
|
|
286
|
+
const driverIdSet = new Set();
|
|
269
287
|
for (const g of goals)
|
|
270
|
-
for (const
|
|
271
|
-
|
|
272
|
-
const
|
|
273
|
-
for (const
|
|
274
|
-
const rec =
|
|
275
|
-
|
|
288
|
+
for (const d of g.driverIds)
|
|
289
|
+
driverIdSet.add(d);
|
|
290
|
+
const drivers = [];
|
|
291
|
+
for (const did of driverIdSet) {
|
|
292
|
+
const rec = driverElems.get(did);
|
|
293
|
+
drivers.push({ id: did, name: rec ? str(rec['name']) ?? did : did });
|
|
294
|
+
}
|
|
295
|
+
// ── Assessments — findings that assess an in-scope Driver ────────────────────
|
|
296
|
+
// Assessments answer "what specific problem are we solving?" They are resolved
|
|
297
|
+
// from ASSESSMENT elements whose `assesses` field matches a Driver id that
|
|
298
|
+
// appeared in the motivation chain above. Empty when none exist — the gap is
|
|
299
|
+
// intentionally visible on the card so the author knows it needs filling.
|
|
300
|
+
const driverIdLookup = new Set(drivers.map((d) => d.id));
|
|
301
|
+
const assessmentElems = collectByNotation(sources.elements, 'assessment');
|
|
302
|
+
const assessments = [];
|
|
303
|
+
for (const [aid, rec] of assessmentElems) {
|
|
304
|
+
const driverId = str(rec['assesses']);
|
|
305
|
+
if (!driverId || !driverIdLookup.has(driverId))
|
|
306
|
+
continue;
|
|
307
|
+
assessments.push({
|
|
308
|
+
id: aid,
|
|
309
|
+
name: str(rec['name']) ?? aid,
|
|
310
|
+
driverId,
|
|
311
|
+
description: str(rec['description']),
|
|
312
|
+
observed_at: str(rec['observed_at']),
|
|
313
|
+
});
|
|
276
314
|
}
|
|
315
|
+
assessments.sort((a, b) => {
|
|
316
|
+
if (!a.observed_at && !b.observed_at)
|
|
317
|
+
return 0;
|
|
318
|
+
if (!a.observed_at)
|
|
319
|
+
return 1;
|
|
320
|
+
if (!b.observed_at)
|
|
321
|
+
return -1;
|
|
322
|
+
return a.observed_at < b.observed_at ? -1 : a.observed_at > b.observed_at ? 1 : 0;
|
|
323
|
+
});
|
|
277
324
|
// ── Child activities — parent = project id ──────────────────────────────────
|
|
278
325
|
const childActivities = [];
|
|
279
326
|
for (const [id, rec] of activities) {
|
|
@@ -289,6 +336,34 @@ export function resolveActivityCard(doc, sources) {
|
|
|
289
336
|
});
|
|
290
337
|
}
|
|
291
338
|
childActivities.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
339
|
+
// ── Project goal text field — names of the DIRECTLY-served goals ────────────
|
|
340
|
+
// The "Project goal" field shows the goals the project itself serves (the
|
|
341
|
+
// `activity_goal` targets), not the wider F→G→C chain (which also pulls in
|
|
342
|
+
// goals referenced only by in-scope changes). Resolve each id to its GOAL
|
|
343
|
+
// element name, falling back to the id when the element is absent.
|
|
344
|
+
const goalNames = projectGoalIds.map((gid) => str(goalElems.get(gid)?.['name']) ?? gid);
|
|
345
|
+
// ── Stakeholders — active `activity_stakeholder` relations from the project ──
|
|
346
|
+
const stakeholderElems = collectByNotation(sources.elements, 'stakeholder');
|
|
347
|
+
const stakeholders = [];
|
|
348
|
+
for (const { id: sid, role } of activeActivityStakeholders(sources.relations, projectId)) {
|
|
349
|
+
const rec = stakeholderElems.get(sid);
|
|
350
|
+
if (!rec) {
|
|
351
|
+
warnings.push({
|
|
352
|
+
code: 'PC-001',
|
|
353
|
+
message: `Stakeholder "${sid}" not found as a STAKEHOLDER element in canon; shown by id`,
|
|
354
|
+
});
|
|
355
|
+
stakeholders.push({ id: sid, name: sid, role });
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
stakeholders.push({
|
|
359
|
+
id: sid,
|
|
360
|
+
name: str(rec['name']) ?? sid,
|
|
361
|
+
type: str(rec['type']),
|
|
362
|
+
interest: str(rec['interest']),
|
|
363
|
+
influence: str(rec['influence']),
|
|
364
|
+
role,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
292
367
|
const project = {
|
|
293
368
|
id: projectId,
|
|
294
369
|
name: str(projectRec['name']) ?? projectId,
|
|
@@ -297,14 +372,20 @@ export function resolveActivityCard(doc, sources) {
|
|
|
297
372
|
valid_to: validToOk ? validToRaw : undefined,
|
|
298
373
|
start_date: str(projectRec['start_date']),
|
|
299
374
|
end_date: str(projectRec['end_date']),
|
|
375
|
+
activity_type: str(projectRec['activity_type']),
|
|
376
|
+
status: str(projectRec['status']),
|
|
300
377
|
};
|
|
301
378
|
const resolved = {
|
|
302
379
|
cardId: str(card.id) ?? '',
|
|
303
380
|
cardDescription: str(card.description),
|
|
304
381
|
project,
|
|
305
382
|
milestones,
|
|
306
|
-
motivation: {
|
|
383
|
+
motivation: { drivers, goals, changes },
|
|
384
|
+
assessments,
|
|
307
385
|
childActivities,
|
|
386
|
+
goalNames,
|
|
387
|
+
stakeholders,
|
|
388
|
+
notes: str(card.notes),
|
|
308
389
|
};
|
|
309
390
|
return { valid: true, errors, warnings, resolved };
|
|
310
391
|
}
|