@transitrix/diagrams 1.1.0 → 1.2.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/dist/activities/__tests__/validate.test.js +32 -1
- package/dist/activities/validate.js +21 -5
- package/dist/activity-card/__tests__/resolver.test.js +1 -1
- package/dist/activity-card/__tests__/validate.test.js +23 -1
- package/dist/activity-card/resolver.js +23 -21
- package/dist/activity-card/validate.js +22 -11
- package/dist/capability-map/index.d.ts +4 -0
- package/dist/capability-map/index.js +2 -0
- package/dist/capability-map/layout-tree.d.ts +42 -0
- package/dist/capability-map/layout-tree.js +65 -0
- package/dist/capability-map/render-capability-tree.d.ts +15 -0
- package/dist/capability-map/render-capability-tree.js +108 -0
- package/dist/capability-map/types.d.ts +2 -0
- package/dist/fgca/__tests__/validate.test.js +2 -2
- package/dist/fgca/parse-canonical.js +9 -3
- package/dist/fgca/types.d.ts +2 -0
- package/dist/fgca/validate.js +19 -11
- package/dist/theme/index.d.ts +1 -1
- package/dist/theme/index.js +1 -1
- package/dist/theme/themes.js +26 -3
- package/dist/theme/tokens.d.ts +40 -0
- package/dist/theme/tokens.js +28 -0
- package/dist/webview/render-fgca.js +1 -1
- package/package.json +1 -1
- package/src/activities/__tests__/validate.test.ts +35 -1
- package/src/activities/validate.ts +21 -5
- package/src/activity-card/__tests__/resolver.test.ts +1 -1
- package/src/activity-card/__tests__/validate.test.ts +25 -1
- package/src/activity-card/resolver.ts +23 -21
- package/src/activity-card/validate.ts +20 -11
- package/src/capability-map/index.ts +11 -0
- package/src/capability-map/layout-tree.ts +114 -0
- package/src/capability-map/render-capability-tree.ts +131 -0
- package/src/capability-map/types.ts +2 -0
- package/src/fgca/__tests__/validate.test.ts +2 -2
- package/src/fgca/parse-canonical.ts +9 -3
- package/src/fgca/types.ts +2 -0
- package/src/fgca/validate.ts +16 -8
- package/src/theme/index.ts +1 -1
- package/src/theme/themes.ts +27 -2
- package/src/theme/tokens.ts +30 -0
- package/src/webview/render-fgca.ts +1 -1
package/dist/fgca/validate.js
CHANGED
|
@@ -22,10 +22,18 @@ export function validateFGCADoc(input) {
|
|
|
22
22
|
errors.push({ code: 'SCHEMA_INVALID', message: 'goals must be an array', path: 'goals' });
|
|
23
23
|
if (!Array.isArray(raw.changes))
|
|
24
24
|
errors.push({ code: 'SCHEMA_INVALID', message: 'changes must be an array', path: 'changes' });
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
// Accept canonical `actions:` or deprecated `activities:`; warn on the latter.
|
|
26
|
+
const hasActions = Array.isArray(raw.actions);
|
|
27
|
+
const hasActivities = Array.isArray(raw.activities);
|
|
28
|
+
if (!hasActions && !hasActivities) {
|
|
29
|
+
errors.push({ code: 'SCHEMA_INVALID', message: 'actions must be an array', path: 'actions' });
|
|
30
|
+
}
|
|
31
|
+
else if ('activities' in raw) {
|
|
32
|
+
warnings.push({ code: 'DEPRECATED_NOTATION', message: '"activities" key is deprecated — rename to "actions"' });
|
|
33
|
+
}
|
|
27
34
|
if (errors.length > 0)
|
|
28
35
|
return { valid: false, errors, warnings };
|
|
36
|
+
const actionsArr = (hasActions ? raw.actions : raw.activities);
|
|
29
37
|
const doc = raw;
|
|
30
38
|
const collectIds = (arr) => {
|
|
31
39
|
const out = new Set();
|
|
@@ -40,7 +48,7 @@ export function validateFGCADoc(input) {
|
|
|
40
48
|
};
|
|
41
49
|
const factorIds = collectIds(doc.factors);
|
|
42
50
|
const goalIds = collectIds(doc.goals);
|
|
43
|
-
const activityIds = collectIds(
|
|
51
|
+
const activityIds = collectIds(actionsArr);
|
|
44
52
|
for (let i = 0; i < doc.factors.length; i++) {
|
|
45
53
|
const f = doc.factors[i];
|
|
46
54
|
if (!f || typeof f !== 'object') {
|
|
@@ -95,17 +103,17 @@ export function validateFGCADoc(input) {
|
|
|
95
103
|
}
|
|
96
104
|
}
|
|
97
105
|
}
|
|
98
|
-
for (let i = 0; i <
|
|
99
|
-
const a =
|
|
106
|
+
for (let i = 0; i < actionsArr.length; i++) {
|
|
107
|
+
const a = actionsArr[i];
|
|
100
108
|
if (!a || typeof a !== 'object') {
|
|
101
|
-
errors.push({ code: 'SCHEMA_INVALID', message: '
|
|
109
|
+
errors.push({ code: 'SCHEMA_INVALID', message: 'action entry must be an object', path: `actions[${i}]` });
|
|
102
110
|
continue;
|
|
103
111
|
}
|
|
104
|
-
const
|
|
105
|
-
if (typeof
|
|
106
|
-
errors.push({ code: 'SCHEMA_INVALID', message: '
|
|
107
|
-
if (typeof
|
|
108
|
-
errors.push({ code: 'EMPTY_NAME', message: `
|
|
112
|
+
const action = a;
|
|
113
|
+
if (typeof action.id !== 'number' && typeof action.id !== 'string')
|
|
114
|
+
errors.push({ code: 'SCHEMA_INVALID', message: 'action id must be a number or string', path: `actions[${i}]` });
|
|
115
|
+
if (typeof action.name !== 'string' || !action.name.trim())
|
|
116
|
+
errors.push({ code: 'EMPTY_NAME', message: `Action ${String(action.id)} has empty name`, path: `actions[${i}]` });
|
|
109
117
|
}
|
|
110
118
|
return { valid: errors.length === 0, errors, warnings };
|
|
111
119
|
}
|
package/dist/theme/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export type { AdaptiveTokens } from './tokens.js';
|
|
2
|
-
export { BRAND, FUNCTIONAL, LAYER_COLORS, LEVEL_COLORS, MATURITY_COLORS, STRUCTURAL, TYPOGRAPHY, CSS_VAR, getBaseResetCss } from './tokens.js';
|
|
2
|
+
export { BRAND, FUNCTIONAL, LAYER_COLORS, LEVEL_COLORS, MATURITY_COLORS, TREE_LEVEL_COLORS, TREE_MATURITY_COLORS, STRUCTURAL, TYPOGRAPHY, CSS_VAR, getBaseResetCss } from './tokens.js';
|
|
3
3
|
export type { ThemeId } from './themes.js';
|
|
4
4
|
export { generateWebviewCss, generateSvgEmbedCss } from './themes.js';
|
package/dist/theme/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { BRAND, FUNCTIONAL, LAYER_COLORS, LEVEL_COLORS, MATURITY_COLORS, STRUCTURAL, TYPOGRAPHY, CSS_VAR, getBaseResetCss } from './tokens.js';
|
|
1
|
+
export { BRAND, FUNCTIONAL, LAYER_COLORS, LEVEL_COLORS, MATURITY_COLORS, TREE_LEVEL_COLORS, TREE_MATURITY_COLORS, STRUCTURAL, TYPOGRAPHY, CSS_VAR, getBaseResetCss } from './tokens.js';
|
|
2
2
|
export { generateWebviewCss, generateSvgEmbedCss } from './themes.js';
|
package/dist/theme/themes.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LAYER_COLORS, LEVEL_COLORS, MATURITY_COLORS, STRUCTURAL, TYPOGRAPHY, CSS_VAR, getBaseResetCss, } from './tokens.js';
|
|
1
|
+
import { LAYER_COLORS, LEVEL_COLORS, MATURITY_COLORS, TREE_LEVEL_COLORS, TREE_MATURITY_COLORS, STRUCTURAL, TYPOGRAPHY, CSS_VAR, getBaseResetCss, } from './tokens.js';
|
|
2
2
|
const TRANSITRIX_LIGHT = {
|
|
3
3
|
bg: '#ffffff',
|
|
4
4
|
bgSurface: '#f8fafc',
|
|
@@ -60,7 +60,19 @@ function diagramVars(variant) {
|
|
|
60
60
|
const sc = STRUCTURAL;
|
|
61
61
|
const lv = LEVEL_COLORS[variant];
|
|
62
62
|
const mat = MATURITY_COLORS[variant];
|
|
63
|
+
const treeLevel = TREE_LEVEL_COLORS;
|
|
64
|
+
const treeMat = TREE_MATURITY_COLORS[variant];
|
|
63
65
|
const levelVars = lv.map((c, i) => `--ts-level-${i}:${c}`).join(';') + ';';
|
|
66
|
+
const treeVars = [
|
|
67
|
+
`${CSS_VAR.treeLevel0}:${treeLevel.band0[variant]}`,
|
|
68
|
+
`${CSS_VAR.treeLevel1}:${treeLevel.band1[variant]}`,
|
|
69
|
+
`${CSS_VAR.treeLevel2}:${treeLevel.band2[variant]}`,
|
|
70
|
+
`${CSS_VAR.treeMaturity1}:${treeMat[0]}`,
|
|
71
|
+
`${CSS_VAR.treeMaturity2}:${treeMat[1]}`,
|
|
72
|
+
`${CSS_VAR.treeMaturity3}:${treeMat[2]}`,
|
|
73
|
+
`${CSS_VAR.treeMaturity4}:${treeMat[3]}`,
|
|
74
|
+
`${CSS_VAR.treeMaturity5}:${treeMat[4]}`,
|
|
75
|
+
].join(';') + ';';
|
|
64
76
|
return [
|
|
65
77
|
`${CSS_VAR.layerDriver}:${lc.driver[variant]}`,
|
|
66
78
|
`${CSS_VAR.layerFactor}:${lc.factor[variant]}`,
|
|
@@ -77,7 +89,7 @@ function diagramVars(variant) {
|
|
|
77
89
|
`${CSS_VAR.maturity3}:${mat[2]}`,
|
|
78
90
|
`${CSS_VAR.maturity4}:${mat[3]}`,
|
|
79
91
|
`${CSS_VAR.maturity5}:${mat[4]}`,
|
|
80
|
-
].join(';') + ';' + levelVars;
|
|
92
|
+
].join(';') + ';' + levelVars + treeVars;
|
|
81
93
|
}
|
|
82
94
|
/** CSS for the webview shell (toolbar, canvas). */
|
|
83
95
|
function shellCss() {
|
|
@@ -136,7 +148,18 @@ ${levelRules}
|
|
|
136
148
|
.compliance-deadline{fill:var(${cv.statusErrorBg});stroke:var(${cv.statusErrorFg});}
|
|
137
149
|
.compliance-deadline text{fill:var(${cv.statusErrorFg});}
|
|
138
150
|
.compliance-badge{fill:var(${cv.statusErrorFg});}
|
|
139
|
-
.compliance-badge-text{fill:var(${cv.textInverse});font-family:${t.fontFamily};font-size:8px;font-weight:700;}
|
|
151
|
+
.compliance-badge-text{fill:var(${cv.textInverse});font-family:${t.fontFamily};font-size:8px;font-weight:700;}
|
|
152
|
+
.tree-level-0{fill:var(${cv.treeLevel0});}
|
|
153
|
+
.tree-level-1{fill:var(${cv.treeLevel1});}
|
|
154
|
+
.tree-level-2{fill:var(${cv.treeLevel2});}
|
|
155
|
+
.tree-maturity-1{fill:var(${cv.treeMaturity1});}
|
|
156
|
+
.tree-maturity-2{fill:var(${cv.treeMaturity2});}
|
|
157
|
+
.tree-maturity-3{fill:var(${cv.treeMaturity3});}
|
|
158
|
+
.tree-maturity-4{fill:var(${cv.treeMaturity4});}
|
|
159
|
+
.tree-maturity-5{fill:var(${cv.treeMaturity5});}
|
|
160
|
+
.tree-collapse-btn{cursor:pointer;fill:white;stroke:var(${cv.nodeStroke});stroke-width:1.5;}
|
|
161
|
+
.tree-collapse-btn:hover{fill:var(${cv.bgElevated});}
|
|
162
|
+
.tree-collapse-lbl{fill:var(${cv.textSecondary});font-family:${t.fontFamily};font-size:12px;font-weight:700;dominant-baseline:central;pointer-events:none;}`;
|
|
140
163
|
}
|
|
141
164
|
/**
|
|
142
165
|
* Generates a self-contained CSS block for embedding inside a standalone SVG file.
|
package/dist/theme/tokens.d.ts
CHANGED
|
@@ -82,6 +82,38 @@ export declare const MATURITY_COLORS: {
|
|
|
82
82
|
readonly dark: readonly ["#7f1d1d", "#92400e", "#854d0e", "#3f6212", "#14532d"];
|
|
83
83
|
readonly hc: readonly ["#f87171", "#fb923c", "#facc15", "#a3e635", "#4ade80"];
|
|
84
84
|
};
|
|
85
|
+
/**
|
|
86
|
+
* DSM-matching capability tree node fill colors, grouped by depth band.
|
|
87
|
+
* band0: depth 0–2 (pink)
|
|
88
|
+
* band1: depth 3–4 (yellow)
|
|
89
|
+
* band2: depth 5+ (blue)
|
|
90
|
+
*/
|
|
91
|
+
export declare const TREE_LEVEL_COLORS: {
|
|
92
|
+
readonly band0: {
|
|
93
|
+
readonly light: "#fee0e0";
|
|
94
|
+
readonly dark: "#3d0f0f";
|
|
95
|
+
readonly hc: "#ff9999";
|
|
96
|
+
};
|
|
97
|
+
readonly band1: {
|
|
98
|
+
readonly light: "#ffffcb";
|
|
99
|
+
readonly dark: "#3a3500";
|
|
100
|
+
readonly hc: "#ffff88";
|
|
101
|
+
};
|
|
102
|
+
readonly band2: {
|
|
103
|
+
readonly light: "#c2f0ff";
|
|
104
|
+
readonly dark: "#003d4d";
|
|
105
|
+
readonly hc: "#66ccff";
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
/**
|
|
109
|
+
* DSM-matching maturity badge fills for the capability tree (L1–L5).
|
|
110
|
+
* Distinct from MATURITY_COLORS which drives the cards / compliance views.
|
|
111
|
+
*/
|
|
112
|
+
export declare const TREE_MATURITY_COLORS: {
|
|
113
|
+
readonly light: readonly ["#c1c1c1", "#ff3d67", "#ff9a59", "#fdef59", "#83cca3"];
|
|
114
|
+
readonly dark: readonly ["#555555", "#cc1040", "#d06020", "#c8c000", "#4d9970"];
|
|
115
|
+
readonly hc: readonly ["#cccccc", "#ff6699", "#ffbb77", "#ffff44", "#88ddaa"];
|
|
116
|
+
};
|
|
85
117
|
/**
|
|
86
118
|
* Typography hierarchy. All previews resolve text styling from these roles
|
|
87
119
|
* — never from inline font-* attributes on individual <text> elements.
|
|
@@ -160,5 +192,13 @@ export declare const CSS_VAR: {
|
|
|
160
192
|
readonly maturity3: "--ts-maturity-3";
|
|
161
193
|
readonly maturity4: "--ts-maturity-4";
|
|
162
194
|
readonly maturity5: "--ts-maturity-5";
|
|
195
|
+
readonly treeLevel0: "--ts-tree-level-0";
|
|
196
|
+
readonly treeLevel1: "--ts-tree-level-1";
|
|
197
|
+
readonly treeLevel2: "--ts-tree-level-2";
|
|
198
|
+
readonly treeMaturity1: "--ts-tree-maturity-1";
|
|
199
|
+
readonly treeMaturity2: "--ts-tree-maturity-2";
|
|
200
|
+
readonly treeMaturity3: "--ts-tree-maturity-3";
|
|
201
|
+
readonly treeMaturity4: "--ts-tree-maturity-4";
|
|
202
|
+
readonly treeMaturity5: "--ts-tree-maturity-5";
|
|
163
203
|
};
|
|
164
204
|
export declare function getBaseResetCss(): string;
|
package/dist/theme/tokens.js
CHANGED
|
@@ -42,6 +42,26 @@ export const MATURITY_COLORS = {
|
|
|
42
42
|
dark: ['#7f1d1d', '#92400e', '#854d0e', '#3f6212', '#14532d'],
|
|
43
43
|
hc: ['#f87171', '#fb923c', '#facc15', '#a3e635', '#4ade80'],
|
|
44
44
|
};
|
|
45
|
+
/**
|
|
46
|
+
* DSM-matching capability tree node fill colors, grouped by depth band.
|
|
47
|
+
* band0: depth 0–2 (pink)
|
|
48
|
+
* band1: depth 3–4 (yellow)
|
|
49
|
+
* band2: depth 5+ (blue)
|
|
50
|
+
*/
|
|
51
|
+
export const TREE_LEVEL_COLORS = {
|
|
52
|
+
band0: { light: '#fee0e0', dark: '#3d0f0f', hc: '#ff9999' },
|
|
53
|
+
band1: { light: '#ffffcb', dark: '#3a3500', hc: '#ffff88' },
|
|
54
|
+
band2: { light: '#c2f0ff', dark: '#003d4d', hc: '#66ccff' },
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* DSM-matching maturity badge fills for the capability tree (L1–L5).
|
|
58
|
+
* Distinct from MATURITY_COLORS which drives the cards / compliance views.
|
|
59
|
+
*/
|
|
60
|
+
export const TREE_MATURITY_COLORS = {
|
|
61
|
+
light: ['#c1c1c1', '#ff3d67', '#ff9a59', '#fdef59', '#83cca3'],
|
|
62
|
+
dark: ['#555555', '#cc1040', '#d06020', '#c8c000', '#4d9970'],
|
|
63
|
+
hc: ['#cccccc', '#ff6699', '#ffbb77', '#ffff44', '#88ddaa'],
|
|
64
|
+
};
|
|
45
65
|
/**
|
|
46
66
|
* Typography hierarchy. All previews resolve text styling from these roles
|
|
47
67
|
* — never from inline font-* attributes on individual <text> elements.
|
|
@@ -101,6 +121,14 @@ export const CSS_VAR = {
|
|
|
101
121
|
maturity3: '--ts-maturity-3',
|
|
102
122
|
maturity4: '--ts-maturity-4',
|
|
103
123
|
maturity5: '--ts-maturity-5',
|
|
124
|
+
treeLevel0: '--ts-tree-level-0',
|
|
125
|
+
treeLevel1: '--ts-tree-level-1',
|
|
126
|
+
treeLevel2: '--ts-tree-level-2',
|
|
127
|
+
treeMaturity1: '--ts-tree-maturity-1',
|
|
128
|
+
treeMaturity2: '--ts-tree-maturity-2',
|
|
129
|
+
treeMaturity3: '--ts-tree-maturity-3',
|
|
130
|
+
treeMaturity4: '--ts-tree-maturity-4',
|
|
131
|
+
treeMaturity5: '--ts-tree-maturity-5',
|
|
104
132
|
};
|
|
105
133
|
export function getBaseResetCss() {
|
|
106
134
|
return `*,*:before,*:after{box-sizing:border-box;}html,body{margin:0;padding:0;}`;
|
package/package.json
CHANGED
|
@@ -15,9 +15,43 @@ const minimalValid = {
|
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
describe('validateActivities', () => {
|
|
18
|
-
it('ACT-001 — accepts
|
|
18
|
+
it('ACT-001 — accepts notation: activities (deprecated)', () => {
|
|
19
19
|
const r = validateActivities(minimalValid);
|
|
20
20
|
expect(r.valid).toBe(true);
|
|
21
|
+
// deprecated notation emits a warning, not an error
|
|
22
|
+
expect(r.warnings.some(w => w.code === 'DEPRECATED_NOTATION')).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('ACT-001 — accepts notation: action (canonical)', () => {
|
|
26
|
+
const r = validateActivities({
|
|
27
|
+
notation: 'action',
|
|
28
|
+
actions: [
|
|
29
|
+
{ id: 'ACTION-001', name: 'Start', duration: 3 },
|
|
30
|
+
{ id: 'ACTION-002', name: 'End', duration: 2, predecessors: ['ACTION-001'] },
|
|
31
|
+
],
|
|
32
|
+
});
|
|
33
|
+
expect(r.valid).toBe(true);
|
|
34
|
+
expect(r.warnings.some(w => w.code === 'DEPRECATED_NOTATION')).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('ACT-001 — accepts notation: action with deprecated activities: key (emits field warning)', () => {
|
|
38
|
+
const r = validateActivities({
|
|
39
|
+
notation: 'action',
|
|
40
|
+
activities: [{ id: 'ACTION-001', name: 'Foo', duration: 1 }],
|
|
41
|
+
});
|
|
42
|
+
expect(r.valid).toBe(true);
|
|
43
|
+
expect(r.warnings.some(w => w.code === 'DEPRECATED_FIELD')).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('ACT-001 — normalises action_type to activity_type for downstream layout', () => {
|
|
47
|
+
const input: Record<string, unknown> = {
|
|
48
|
+
notation: 'action',
|
|
49
|
+
actions: [{ id: 'ACTION-001', name: 'Foo', duration: 1, action_type: 'project' }],
|
|
50
|
+
};
|
|
51
|
+
const r = validateActivities(input);
|
|
52
|
+
expect(r.valid).toBe(true);
|
|
53
|
+
const entry = (input.actions as Array<Record<string, unknown>>)[0];
|
|
54
|
+
expect(entry['activity_type']).toBe('project');
|
|
21
55
|
});
|
|
22
56
|
|
|
23
57
|
it('ACT-001 — rejects missing notation', () => {
|
|
@@ -19,20 +19,31 @@ export function validateActivities(input: unknown): ActivityValidationResult {
|
|
|
19
19
|
|
|
20
20
|
const raw = input as Record<string, unknown>;
|
|
21
21
|
|
|
22
|
-
// ACT-001: notation must
|
|
22
|
+
// ACT-001: notation must be "action" (canonical) or "activities" (deprecated)
|
|
23
23
|
if (raw.notation === undefined) {
|
|
24
24
|
errors.push({ code: 'ACT-001', message: 'notation field is required' });
|
|
25
25
|
return { valid: false, errors, warnings };
|
|
26
26
|
}
|
|
27
|
-
if (raw.notation !== 'activities') {
|
|
28
|
-
errors.push({ code: 'ACT-001', message: `notation must be "
|
|
27
|
+
if (raw.notation !== 'action' && raw.notation !== 'activities') {
|
|
28
|
+
errors.push({ code: 'ACT-001', message: `notation must be "action", got "${String(raw.notation)}"` });
|
|
29
29
|
return { valid: false, errors, warnings };
|
|
30
30
|
}
|
|
31
|
+
if (raw.notation === 'activities') {
|
|
32
|
+
warnings.push({ code: 'DEPRECATED_NOTATION', message: 'notation: "activities" is deprecated — migrate to notation: "action"' });
|
|
33
|
+
}
|
|
31
34
|
|
|
32
|
-
|
|
33
|
-
|
|
35
|
+
// Accept "actions:" (canonical) or "activities:" (deprecated)
|
|
36
|
+
const rawArray = Array.isArray(raw.actions) ? raw.actions : (Array.isArray(raw.activities) ? raw.activities : undefined);
|
|
37
|
+
if (Array.isArray(raw.activities) && !Array.isArray(raw.actions)) {
|
|
38
|
+
warnings.push({ code: 'DEPRECATED_FIELD', message: 'Root key "activities:" is deprecated — rename to "actions:"' });
|
|
39
|
+
}
|
|
40
|
+
if (!rawArray) {
|
|
41
|
+
const canonKey = raw.notation === 'action' ? 'actions' : 'activities';
|
|
42
|
+
errors.push({ code: 'SCHEMA_INVALID', message: `${canonKey} must be an array`, path: canonKey });
|
|
34
43
|
return { valid: false, errors, warnings };
|
|
35
44
|
}
|
|
45
|
+
// Normalise onto raw.activities for the rest of this function (avoids touching downstream code)
|
|
46
|
+
if (!Array.isArray(raw.activities)) raw.activities = raw.actions;
|
|
36
47
|
|
|
37
48
|
// ACT-014 / ACT-015: project.calendar validation (run before per-activity loop;
|
|
38
49
|
// surfaces clearly even when activities also have issues).
|
|
@@ -99,6 +110,11 @@ export function validateActivities(input: unknown): ActivityValidationResult {
|
|
|
99
110
|
}
|
|
100
111
|
const act = a as Record<string, unknown>;
|
|
101
112
|
|
|
113
|
+
// Normalise action_type → activity_type (canonical rename; old name kept for compat)
|
|
114
|
+
if (act['action_type'] !== undefined && act['activity_type'] === undefined) {
|
|
115
|
+
act['activity_type'] = act['action_type'];
|
|
116
|
+
}
|
|
117
|
+
|
|
102
118
|
// ACT-010: reject single-value forms
|
|
103
119
|
if ('goal' in act) {
|
|
104
120
|
errors.push({ code: 'ACT-010', message: `Use "goals: []" array form instead of "goal:" (activity at index ${i})`, path });
|
|
@@ -241,7 +241,7 @@ describe('resolveActivityCard', () => {
|
|
|
241
241
|
expect(pc001!.message).toContain('canon/elements/**');
|
|
242
242
|
expect(pc001!.message).toContain('canon/views/activities/**');
|
|
243
243
|
// Must include actionable hint with expected file pattern (§7 PC-001)
|
|
244
|
-
expect(pc001!.message).toContain('notation:
|
|
244
|
+
expect(pc001!.message).toContain('notation: action');
|
|
245
245
|
expect(pc001!.message).toContain(`id: "${CARD.activity_card.project}"`);
|
|
246
246
|
});
|
|
247
247
|
|
|
@@ -20,10 +20,34 @@ const VALID = {
|
|
|
20
20
|
};
|
|
21
21
|
|
|
22
22
|
describe('validateActivityCard', () => {
|
|
23
|
-
it('accepts a well-formed card', () => {
|
|
23
|
+
it('accepts a well-formed card (deprecated notation: activity-card)', () => {
|
|
24
24
|
const r = validateActivityCard(VALID);
|
|
25
25
|
expect(r.valid).toBe(true);
|
|
26
26
|
expect(r.errors).toHaveLength(0);
|
|
27
|
+
expect(r.warnings.some(w => w.code === 'DEPRECATED_NOTATION')).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('accepts notation: action-card with action_card block (canonical)', () => {
|
|
31
|
+
const r = validateActivityCard({
|
|
32
|
+
notation: 'action-card',
|
|
33
|
+
action_card: {
|
|
34
|
+
id: 'ACTION_CARD-EU-PROGRAMME-1',
|
|
35
|
+
project: 'ACTION-EU-PROGRAMME-1',
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
expect(r.valid).toBe(true);
|
|
39
|
+
expect(r.warnings.some(w => w.code === 'DEPRECATED_NOTATION')).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('accepts action_card block with legacy ACTIVITY_CARD id prefix', () => {
|
|
43
|
+
const r = validateActivityCard({
|
|
44
|
+
notation: 'action-card',
|
|
45
|
+
action_card: {
|
|
46
|
+
id: 'ACTIVITY_CARD-EU-PROGRAMME-1',
|
|
47
|
+
project: 'ACTION-EU-PROGRAMME-1',
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
expect(r.valid).toBe(true);
|
|
27
51
|
});
|
|
28
52
|
|
|
29
53
|
it('rejects non-object input', () => {
|
|
@@ -104,16 +104,17 @@ function collectByNotation(
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
/**
|
|
107
|
-
* GOAL ids reached by an *active* `
|
|
108
|
-
* `fromId`. A relation with a non-null `valid_to` has
|
|
109
|
-
* re-aimed) and is excluded
|
|
107
|
+
* GOAL ids reached by an *active* `action_goal` (or legacy `activity_goal`)
|
|
108
|
+
* relation originating at `fromId`. A relation with a non-null `valid_to` has
|
|
109
|
+
* ended (the activity was re-aimed) and is excluded.
|
|
110
110
|
*/
|
|
111
111
|
function activeActivityGoals(relations: unknown[], fromId: string): string[] {
|
|
112
112
|
const out: string[] = [];
|
|
113
113
|
for (const doc of relations) {
|
|
114
114
|
if (!isObject(doc)) continue;
|
|
115
115
|
if (str(doc['notation']) !== 'relation') continue;
|
|
116
|
-
|
|
116
|
+
const type = str(doc['type']);
|
|
117
|
+
if (type !== 'action_goal' && type !== 'activity_goal') continue;
|
|
117
118
|
if (str(doc['from']) !== fromId) continue;
|
|
118
119
|
const to = str(doc['to']);
|
|
119
120
|
if (!to) continue;
|
|
@@ -125,11 +126,8 @@ function activeActivityGoals(relations: unknown[], fromId: string): string[] {
|
|
|
125
126
|
}
|
|
126
127
|
|
|
127
128
|
/**
|
|
128
|
-
* STAKEHOLDER ids + roles reached by an *active* `
|
|
129
|
-
* relation originating at `fromId`.
|
|
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).
|
|
129
|
+
* STAKEHOLDER ids + roles reached by an *active* `action_stakeholder` (or
|
|
130
|
+
* legacy `activity_stakeholder`) relation originating at `fromId`.
|
|
133
131
|
*/
|
|
134
132
|
function activeActivityStakeholders(
|
|
135
133
|
relations: unknown[],
|
|
@@ -139,7 +137,8 @@ function activeActivityStakeholders(
|
|
|
139
137
|
for (const doc of relations) {
|
|
140
138
|
if (!isObject(doc)) continue;
|
|
141
139
|
if (str(doc['notation']) !== 'relation') continue;
|
|
142
|
-
|
|
140
|
+
const type = str(doc['type']);
|
|
141
|
+
if (type !== 'action_stakeholder' && type !== 'activity_stakeholder') continue;
|
|
143
142
|
if (str(doc['from']) !== fromId) continue;
|
|
144
143
|
const to = str(doc['to']);
|
|
145
144
|
if (!to) continue;
|
|
@@ -165,20 +164,22 @@ export function resolveActivityCard(
|
|
|
165
164
|
return { valid: false, errors, warnings };
|
|
166
165
|
}
|
|
167
166
|
|
|
168
|
-
|
|
169
|
-
const
|
|
167
|
+
// Look up by canonical `notation: action` first, then legacy `notation: activity`.
|
|
168
|
+
const actionElements = collectByNotation(sources.elements, 'action');
|
|
169
|
+
const activityElements = collectByNotation(sources.elements, 'activity');
|
|
170
|
+
const allActionElements = new Map([...activityElements, ...actionElements]); // action wins on duplicate id
|
|
171
|
+
const projectRec = allActionElements.get(projectId);
|
|
170
172
|
|
|
171
|
-
// PC-001 — project must resolve to an admitted
|
|
172
|
-
// exhausting the canonical resolution scope (§6.1)
|
|
173
|
-
// recursively first, then canon/views/activities/** as secondary fallback.
|
|
173
|
+
// PC-001 — project must resolve to an admitted ACTION (or legacy ACTIVITY)
|
|
174
|
+
// element after exhausting the canonical resolution scope (§6.1).
|
|
174
175
|
if (!projectRec) {
|
|
175
176
|
errors.push({
|
|
176
177
|
code: 'PC-001',
|
|
177
178
|
message:
|
|
178
|
-
`
|
|
179
|
+
`action_card.project "${projectId}" not found after searching ` +
|
|
179
180
|
`canon/elements/** and canon/views/activities/** — ` +
|
|
180
|
-
`add a YAML file with notation:
|
|
181
|
-
`(e.g. canon/elements/05_implementation/
|
|
181
|
+
`add a YAML file with notation: action and id: "${projectId}" ` +
|
|
182
|
+
`(e.g. canon/elements/05_implementation/actions/${projectId}.yaml)`,
|
|
182
183
|
});
|
|
183
184
|
return { valid: false, errors, warnings };
|
|
184
185
|
}
|
|
@@ -349,13 +350,14 @@ export function resolveActivityCard(
|
|
|
349
350
|
});
|
|
350
351
|
|
|
351
352
|
// ── Child activities — parent = project id ──────────────────────────────────
|
|
353
|
+
// Look in both `notation: action` and legacy `notation: activity` elements.
|
|
352
354
|
const childActivities: ResolvedChildActivity[] = [];
|
|
353
|
-
for (const [id, rec] of
|
|
355
|
+
for (const [id, rec] of allActionElements) {
|
|
354
356
|
if (str(rec['parent']) !== projectId) continue;
|
|
355
357
|
childActivities.push({
|
|
356
358
|
id,
|
|
357
359
|
name: str(rec['name']) ?? id,
|
|
358
|
-
activity_type: str(rec['activity_type']),
|
|
360
|
+
activity_type: str(rec['action_type']) ?? str(rec['activity_type']),
|
|
359
361
|
start_date: str(rec['start_date']),
|
|
360
362
|
end_date: str(rec['end_date']),
|
|
361
363
|
owner: str(rec['owner']),
|
|
@@ -401,7 +403,7 @@ export function resolveActivityCard(
|
|
|
401
403
|
valid_to: validToOk ? (validToRaw as string) : undefined,
|
|
402
404
|
start_date: str(projectRec['start_date']),
|
|
403
405
|
end_date: str(projectRec['end_date']),
|
|
404
|
-
activity_type: str(projectRec['activity_type']),
|
|
406
|
+
activity_type: str(projectRec['action_type']) ?? str(projectRec['activity_type']),
|
|
405
407
|
status: str(projectRec['status']),
|
|
406
408
|
};
|
|
407
409
|
|
|
@@ -26,8 +26,8 @@ import type { ValidationError, ValidationWarning, ValidationResult } from '../va
|
|
|
26
26
|
|
|
27
27
|
export type { ValidationError, ValidationWarning, ValidationResult };
|
|
28
28
|
|
|
29
|
-
const ACTIVITY_CARD_ID_RE = /^ACTIVITY_CARD(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
|
|
30
|
-
const ACTIVITY_ID_RE = /^ACTIVITY(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
|
|
29
|
+
const ACTIVITY_CARD_ID_RE = /^(ACTIVITY_CARD|ACTION_CARD)(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
|
|
30
|
+
const ACTIVITY_ID_RE = /^(ACTIVITY|ACTION)(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
|
|
31
31
|
const MILESTONE_ID_RE = /^MILESTONE(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
|
|
32
32
|
const CHANGE_ID_RE = /^CHANGE(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
|
|
33
33
|
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
@@ -49,37 +49,46 @@ export function validateActivityCard(input: unknown): ValidationResult {
|
|
|
49
49
|
// ── Header (CONTRACT.md §2) ────────────────────────────────────────────────
|
|
50
50
|
if (!('notation' in raw)) {
|
|
51
51
|
errors.push({ code: 'HDR-001', message: 'notation field is required' });
|
|
52
|
-
} else if (raw['notation'] !== 'activity-card') {
|
|
52
|
+
} else if (raw['notation'] !== 'action-card' && raw['notation'] !== 'activity-card') {
|
|
53
53
|
errors.push({
|
|
54
54
|
code: 'HDR-002',
|
|
55
|
-
message: `notation must be "
|
|
55
|
+
message: `notation must be "action-card", got "${String(raw['notation'])}"`,
|
|
56
56
|
});
|
|
57
|
+
} else if (raw['notation'] === 'activity-card') {
|
|
58
|
+
warnings.push({ code: 'DEPRECATED_NOTATION', message: 'notation: "activity-card" is deprecated — migrate to notation: "action-card"' });
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
// ── activity_card block
|
|
60
|
-
|
|
61
|
+
// ── action_card / activity_card block ───────────────────────────────────────
|
|
62
|
+
// "action_card:" is canonical; "activity_card:" is accepted for compat.
|
|
63
|
+
const rawBlock = raw['action_card'] ?? raw['activity_card'];
|
|
64
|
+
if (raw['activity_card'] !== undefined && raw['action_card'] === undefined) {
|
|
65
|
+
warnings.push({ code: 'DEPRECATED_FIELD', message: 'Root key "activity_card:" is deprecated — rename to "action_card:"' });
|
|
66
|
+
}
|
|
67
|
+
const block = rawBlock;
|
|
61
68
|
if (!block || typeof block !== 'object' || Array.isArray(block)) {
|
|
62
|
-
errors.push({ code: 'AC-001', message: 'Missing required root key:
|
|
69
|
+
errors.push({ code: 'AC-001', message: 'Missing required root key: action_card (object)' });
|
|
63
70
|
return { valid: false, errors, warnings };
|
|
64
71
|
}
|
|
72
|
+
// Normalise onto activity_card for downstream (resolver reads doc.activity_card)
|
|
73
|
+
if (raw['activity_card'] === undefined) raw['activity_card'] = rawBlock;
|
|
65
74
|
const card = block as Record<string, unknown>;
|
|
66
75
|
|
|
67
76
|
if (!isNonEmptyString(card['id'])) {
|
|
68
|
-
errors.push({ code: 'AC-002', message: '
|
|
77
|
+
errors.push({ code: 'AC-002', message: 'action_card.id is required' });
|
|
69
78
|
} else if (!ACTIVITY_CARD_ID_RE.test(card['id'])) {
|
|
70
79
|
errors.push({
|
|
71
80
|
code: 'AC-002',
|
|
72
|
-
message: `
|
|
81
|
+
message: `action_card.id "${card['id']}" must match ACTION_CARD-[<middle>-]<INTEGER> (or legacy ACTIVITY_CARD-…)`,
|
|
73
82
|
});
|
|
74
83
|
}
|
|
75
84
|
|
|
76
85
|
// PC-001 (document-local half): project present + well-formed.
|
|
77
86
|
if (!isNonEmptyString(card['project'])) {
|
|
78
|
-
errors.push({ code: 'PC-001', message: '
|
|
87
|
+
errors.push({ code: 'PC-001', message: 'action_card.project is required (an ACTION-… id)' });
|
|
79
88
|
} else if (!ACTIVITY_ID_RE.test(card['project'])) {
|
|
80
89
|
errors.push({
|
|
81
90
|
code: 'PC-001',
|
|
82
|
-
message: `
|
|
91
|
+
message: `action_card.project "${card['project']}" is malformed; must match ACTION-[<middle>-]<INTEGER> (or legacy ACTIVITY-…)`,
|
|
83
92
|
});
|
|
84
93
|
}
|
|
85
94
|
|
|
@@ -6,3 +6,14 @@ export type {
|
|
|
6
6
|
} from './types.js';
|
|
7
7
|
|
|
8
8
|
export { validateCapabilityMap } from './validate.js';
|
|
9
|
+
|
|
10
|
+
export type {
|
|
11
|
+
CapabilityTreeNode,
|
|
12
|
+
CapabilityTreeEdge,
|
|
13
|
+
CapabilityTreeLevelCounts,
|
|
14
|
+
CapabilityTreeLayout,
|
|
15
|
+
} from './layout-tree.js';
|
|
16
|
+
export { layoutCapabilityTree, TREE_NODE_WIDTH, TREE_NODE_HEIGHT, TREE_RANK_SEP, TREE_NODE_SEP } from './layout-tree.js';
|
|
17
|
+
|
|
18
|
+
export type { RenderCapabilityTreeOptions } from './render-capability-tree.js';
|
|
19
|
+
export { renderCapabilityTreeSvg } from './render-capability-tree.js';
|