@transitrix/diagrams 1.2.0 → 1.4.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/__tests__/yaml-normalize.test.js +9 -9
- package/dist/activities/__tests__/validate.test.js +77 -74
- package/dist/activities/validate.js +5 -12
- package/dist/activity-card/__tests__/example.test.js +3 -5
- package/dist/activity-card/__tests__/layout.test.js +2 -9
- package/dist/activity-card/__tests__/resolver.test.js +25 -0
- package/dist/activity-card/__tests__/validate.test.js +29 -18
- package/dist/activity-card/layout.js +1 -7
- package/dist/activity-card/resolver.js +11 -2
- package/dist/activity-card/types.d.ts +1 -1
- package/dist/activity-card/validate.js +3 -10
- package/dist/actor/__tests__/validate.test.js +6 -0
- package/dist/actor/types.d.ts +1 -1
- package/dist/actor/types.js +2 -0
- package/dist/applications/types.d.ts +7 -0
- package/dist/applications/validate.js +18 -0
- package/dist/blocks/__tests__/layout.test.js +1 -1
- package/dist/blocks/layout.js +1 -1
- package/dist/business-service/__tests__/validate.test.d.ts +1 -0
- package/dist/business-service/__tests__/validate.test.js +135 -0
- package/dist/business-service/index.d.ts +3 -0
- package/dist/business-service/index.js +2 -0
- package/dist/business-service/types.d.ts +20 -0
- package/dist/business-service/types.js +3 -0
- package/dist/business-service/validate.d.ts +2 -0
- package/dist/business-service/validate.js +74 -0
- package/dist/fgca/__tests__/parse-canonical.test.js +24 -11
- package/dist/fgca/__tests__/resolver.test.js +7 -7
- package/dist/fgca/__tests__/validate.test.js +11 -11
- package/dist/fgca/parse-canonical.js +18 -32
- package/dist/fgca/preview-layout.js +50 -1
- package/dist/fgca/resolver.d.ts +2 -2
- package/dist/fgca/resolver.js +5 -5
- package/dist/fgca/validate.js +4 -13
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/integration/__tests__/validate.test.d.ts +1 -0
- package/dist/integration/__tests__/validate.test.js +169 -0
- package/dist/integration/index.d.ts +2 -0
- package/dist/integration/index.js +1 -0
- package/dist/integration/types.d.ts +27 -0
- package/dist/integration/types.js +5 -0
- package/dist/integration/validate.d.ts +2 -0
- package/dist/integration/validate.js +94 -0
- package/dist/location/__tests__/validate.test.d.ts +1 -0
- package/dist/location/__tests__/validate.test.js +107 -0
- package/dist/location/index.d.ts +3 -0
- package/dist/location/index.js +2 -0
- package/dist/location/types.d.ts +20 -0
- package/dist/location/types.js +3 -0
- package/dist/location/validate.d.ts +2 -0
- package/dist/location/validate.js +63 -0
- package/dist/node/__tests__/validate.test.d.ts +1 -0
- package/dist/node/__tests__/validate.test.js +100 -0
- package/dist/node/index.d.ts +2 -0
- package/dist/node/index.js +1 -0
- package/dist/node/types.d.ts +18 -0
- package/dist/node/types.js +3 -0
- package/dist/node/validate.d.ts +2 -0
- package/dist/node/validate.js +53 -0
- package/dist/repo-validate/__tests__/validate-repo.test.js +444 -0
- package/dist/repo-validate/validate-repo.js +224 -10
- package/dist/technology-service/__tests__/validate.test.d.ts +1 -0
- package/dist/technology-service/__tests__/validate.test.js +121 -0
- package/dist/technology-service/index.d.ts +2 -0
- package/dist/technology-service/index.js +1 -0
- package/dist/technology-service/types.d.ts +18 -0
- package/dist/technology-service/types.js +3 -0
- package/dist/technology-service/validate.d.ts +2 -0
- package/dist/technology-service/validate.js +64 -0
- package/dist/webview/__tests__/entry-notations.test.js +2 -4
- package/dist/webview/__tests__/render-activities.test.d.ts +1 -0
- package/dist/webview/__tests__/render-activities.test.js +91 -0
- package/dist/webview/__tests__/render-blocks.test.d.ts +1 -0
- package/dist/webview/__tests__/render-blocks.test.js +130 -0
- package/dist/webview/entry.d.ts +1 -1
- package/dist/webview/entry.js +8 -15
- package/dist/webview/render-activities.d.ts +12 -0
- package/dist/webview/render-activities.js +9 -3
- package/dist/webview/render-activity-card.js +2 -3
- package/dist/webview/render-blocks.js +97 -18
- package/dist/webview/render-fgca.js +1 -1
- package/dist/webview/render-goals.js +28 -5
- package/package.json +1 -1
- package/src/__tests__/yaml-normalize.test.ts +11 -11
- package/src/activities/__tests__/validate.test.ts +77 -74
- package/src/activities/validate.ts +5 -12
- package/src/activity-card/__tests__/example.test.ts +3 -5
- package/src/activity-card/__tests__/layout.test.ts +2 -9
- package/src/activity-card/__tests__/resolver.test.ts +27 -0
- package/src/activity-card/__tests__/validate.test.ts +30 -18
- package/src/activity-card/layout.ts +1 -8
- package/src/activity-card/resolver.ts +11 -2
- package/src/activity-card/types.ts +1 -1
- package/src/activity-card/validate.ts +3 -9
- package/src/actor/__tests__/validate.test.ts +8 -0
- package/src/actor/types.ts +2 -0
- package/src/applications/types.ts +8 -0
- package/src/applications/validate.ts +20 -0
- package/src/blocks/__tests__/layout.test.ts +1 -1
- package/src/blocks/layout.ts +1 -1
- package/src/business-service/__tests__/validate.test.ts +160 -0
- package/src/business-service/index.ts +3 -0
- package/src/business-service/types.ts +29 -0
- package/src/business-service/validate.ts +82 -0
- package/src/fgca/__tests__/parse-canonical.test.ts +25 -11
- package/src/fgca/__tests__/resolver.test.ts +7 -7
- package/src/fgca/__tests__/validate.test.ts +11 -11
- package/src/fgca/parse-canonical.ts +18 -38
- package/src/fgca/preview-layout.ts +53 -1
- package/src/fgca/resolver.ts +5 -5
- package/src/fgca/validate.ts +4 -12
- package/src/index.ts +5 -0
- package/src/integration/__tests__/validate.test.ts +203 -0
- package/src/integration/index.ts +2 -0
- package/src/integration/types.ts +38 -0
- package/src/integration/validate.ts +104 -0
- package/src/location/__tests__/validate.test.ts +127 -0
- package/src/location/index.ts +3 -0
- package/src/location/types.ts +29 -0
- package/src/location/validate.ts +70 -0
- package/src/node/__tests__/validate.test.ts +118 -0
- package/src/node/index.ts +2 -0
- package/src/node/types.ts +27 -0
- package/src/node/validate.ts +59 -0
- package/src/repo-validate/__tests__/validate-repo.test.ts +550 -0
- package/src/repo-validate/validate-repo.ts +233 -10
- package/src/technology-service/__tests__/validate.test.ts +143 -0
- package/src/technology-service/index.ts +2 -0
- package/src/technology-service/types.ts +27 -0
- package/src/technology-service/validate.ts +71 -0
- package/src/webview/__tests__/entry-notations.test.ts +2 -4
- package/src/webview/__tests__/render-activities.test.ts +100 -0
- package/src/webview/__tests__/render-blocks.test.ts +155 -0
- package/src/webview/entry.ts +10 -19
- package/src/webview/render-activities.ts +29 -4
- package/src/webview/render-activity-card.ts +2 -3
- package/src/webview/render-blocks.ts +101 -20
- package/src/webview/render-fgca.ts +1 -1
- package/src/webview/render-goals.ts +26 -5
|
@@ -4,43 +4,41 @@ import * as path from 'node:path';
|
|
|
4
4
|
import yaml from 'js-yaml';
|
|
5
5
|
import { validateActivities } from '../validate.js';
|
|
6
6
|
|
|
7
|
-
const EXAMPLES_DIR = path.resolve(process.cwd(), '..', '..', 'tests', 'fixtures', 'notation-corpus', '
|
|
7
|
+
const EXAMPLES_DIR = path.resolve(process.cwd(), '..', '..', 'tests', 'fixtures', 'notation-corpus', 'action');
|
|
8
8
|
|
|
9
9
|
const minimalValid = {
|
|
10
|
-
notation: '
|
|
11
|
-
|
|
10
|
+
notation: 'action',
|
|
11
|
+
actions: [
|
|
12
12
|
{ id: 'A-001', name: 'Start', duration: 3 },
|
|
13
13
|
{ id: 'A-002', name: 'End', duration: 2, predecessors: ['A-001'] },
|
|
14
14
|
],
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
describe('validateActivities', () => {
|
|
18
|
-
it('ACT-001 — accepts notation:
|
|
18
|
+
it('ACT-001 — accepts notation: action (canonical)', () => {
|
|
19
19
|
const r = validateActivities(minimalValid);
|
|
20
20
|
expect(r.valid).toBe(true);
|
|
21
|
-
|
|
22
|
-
expect(r.warnings.some(w => w.code === 'DEPRECATED_NOTATION')).toBe(true);
|
|
21
|
+
expect(r.warnings.some(w => w.code === 'DEPRECATED_NOTATION')).toBe(false);
|
|
23
22
|
});
|
|
24
23
|
|
|
25
|
-
it('ACT-001 —
|
|
24
|
+
it('ACT-001 — rejects notation: activities (no longer accepted)', () => {
|
|
26
25
|
const r = validateActivities({
|
|
27
|
-
notation: '
|
|
28
|
-
|
|
29
|
-
{ id: '
|
|
30
|
-
{ id: 'ACTION-002', name: 'End', duration: 2, predecessors: ['ACTION-001'] },
|
|
26
|
+
notation: 'activities',
|
|
27
|
+
activities: [
|
|
28
|
+
{ id: 'A-001', name: 'Start', duration: 3 },
|
|
31
29
|
],
|
|
32
30
|
});
|
|
33
|
-
expect(r.valid).toBe(
|
|
34
|
-
expect(r.
|
|
31
|
+
expect(r.valid).toBe(false);
|
|
32
|
+
expect(r.errors.some(e => e.code === 'ACT-001')).toBe(true);
|
|
35
33
|
});
|
|
36
34
|
|
|
37
|
-
it('ACT-001 —
|
|
35
|
+
it('ACT-001 — rejects activities: root key (only actions: accepted)', () => {
|
|
38
36
|
const r = validateActivities({
|
|
39
37
|
notation: 'action',
|
|
40
38
|
activities: [{ id: 'ACTION-001', name: 'Foo', duration: 1 }],
|
|
41
39
|
});
|
|
42
|
-
expect(r.valid).toBe(
|
|
43
|
-
expect(r.
|
|
40
|
+
expect(r.valid).toBe(false);
|
|
41
|
+
expect(r.errors.some(e => e.code === 'SCHEMA_INVALID')).toBe(true);
|
|
44
42
|
});
|
|
45
43
|
|
|
46
44
|
it('ACT-001 — normalises action_type to activity_type for downstream layout', () => {
|
|
@@ -69,8 +67,8 @@ describe('validateActivities', () => {
|
|
|
69
67
|
|
|
70
68
|
it('ACT-002 — rejects activity with missing id', () => {
|
|
71
69
|
const r = validateActivities({
|
|
72
|
-
notation: '
|
|
73
|
-
|
|
70
|
+
notation: 'action',
|
|
71
|
+
actions: [{ name: 'No ID', duration: 1 }],
|
|
74
72
|
});
|
|
75
73
|
expect(r.valid).toBe(false);
|
|
76
74
|
expect(r.errors.some(e => e.code === 'ACT-002')).toBe(true);
|
|
@@ -78,8 +76,8 @@ describe('validateActivities', () => {
|
|
|
78
76
|
|
|
79
77
|
it('ACT-002 — rejects activity with empty id', () => {
|
|
80
78
|
const r = validateActivities({
|
|
81
|
-
notation: '
|
|
82
|
-
|
|
79
|
+
notation: 'action',
|
|
80
|
+
actions: [{ id: ' ', name: 'Empty ID', duration: 1 }],
|
|
83
81
|
});
|
|
84
82
|
expect(r.valid).toBe(false);
|
|
85
83
|
expect(r.errors.some(e => e.code === 'ACT-002')).toBe(true);
|
|
@@ -87,8 +85,8 @@ describe('validateActivities', () => {
|
|
|
87
85
|
|
|
88
86
|
it('ACT-003 — rejects activity with missing name', () => {
|
|
89
87
|
const r = validateActivities({
|
|
90
|
-
notation: '
|
|
91
|
-
|
|
88
|
+
notation: 'action',
|
|
89
|
+
actions: [{ id: 'A-001', duration: 1 }],
|
|
92
90
|
});
|
|
93
91
|
expect(r.valid).toBe(false);
|
|
94
92
|
expect(r.errors.some(e => e.code === 'ACT-003')).toBe(true);
|
|
@@ -96,8 +94,8 @@ describe('validateActivities', () => {
|
|
|
96
94
|
|
|
97
95
|
it('ACT-004 — rejects duplicate ids', () => {
|
|
98
96
|
const r = validateActivities({
|
|
99
|
-
notation: '
|
|
100
|
-
|
|
97
|
+
notation: 'action',
|
|
98
|
+
actions: [
|
|
101
99
|
{ id: 'A-001', name: 'First', duration: 1 },
|
|
102
100
|
{ id: 'A-001', name: 'Duplicate', duration: 2 },
|
|
103
101
|
],
|
|
@@ -108,8 +106,8 @@ describe('validateActivities', () => {
|
|
|
108
106
|
|
|
109
107
|
it('ACT-005 — rejects unknown predecessor reference', () => {
|
|
110
108
|
const r = validateActivities({
|
|
111
|
-
notation: '
|
|
112
|
-
|
|
109
|
+
notation: 'action',
|
|
110
|
+
actions: [{ id: 'A-001', name: 'Task', duration: 1, predecessors: ['MISSING'] }],
|
|
113
111
|
});
|
|
114
112
|
expect(r.valid).toBe(false);
|
|
115
113
|
expect(r.errors.some(e => e.code === 'ACT-005')).toBe(true);
|
|
@@ -123,8 +121,8 @@ describe('validateActivities', () => {
|
|
|
123
121
|
|
|
124
122
|
it('ACT-006 — rejects cyclic dependency', () => {
|
|
125
123
|
const r = validateActivities({
|
|
126
|
-
notation: '
|
|
127
|
-
|
|
124
|
+
notation: 'action',
|
|
125
|
+
actions: [
|
|
128
126
|
{ id: 'A-001', name: 'Task A', duration: 1, predecessors: ['A-002'] },
|
|
129
127
|
{ id: 'A-002', name: 'Task B', duration: 1, predecessors: ['A-001'] },
|
|
130
128
|
],
|
|
@@ -135,8 +133,8 @@ describe('validateActivities', () => {
|
|
|
135
133
|
|
|
136
134
|
it('ACT-007 — rejects self-loop', () => {
|
|
137
135
|
const r = validateActivities({
|
|
138
|
-
notation: '
|
|
139
|
-
|
|
136
|
+
notation: 'action',
|
|
137
|
+
actions: [{ id: 'A-001', name: 'Task', duration: 1, predecessors: ['A-001'] }],
|
|
140
138
|
});
|
|
141
139
|
expect(r.valid).toBe(false);
|
|
142
140
|
expect(r.errors.some(e => e.code === 'ACT-007')).toBe(true);
|
|
@@ -144,8 +142,8 @@ describe('validateActivities', () => {
|
|
|
144
142
|
|
|
145
143
|
it('ACT-008 — rejects end_date before start_date', () => {
|
|
146
144
|
const r = validateActivities({
|
|
147
|
-
notation: '
|
|
148
|
-
|
|
145
|
+
notation: 'action',
|
|
146
|
+
actions: [
|
|
149
147
|
{ id: 'A-001', name: 'Task', duration: 5, start_date: '2026-06-10', end_date: '2026-06-01' },
|
|
150
148
|
],
|
|
151
149
|
});
|
|
@@ -155,8 +153,8 @@ describe('validateActivities', () => {
|
|
|
155
153
|
|
|
156
154
|
it('ACT-008 — accepts end_date equal to start_date', () => {
|
|
157
155
|
const r = validateActivities({
|
|
158
|
-
notation: '
|
|
159
|
-
|
|
156
|
+
notation: 'action',
|
|
157
|
+
actions: [
|
|
160
158
|
{ id: 'A-001', name: 'Task', duration: 0, start_date: '2026-06-01', end_date: '2026-06-01' },
|
|
161
159
|
],
|
|
162
160
|
});
|
|
@@ -165,8 +163,8 @@ describe('validateActivities', () => {
|
|
|
165
163
|
|
|
166
164
|
it('ACT-008 — rejects non-ISO start_date format', () => {
|
|
167
165
|
const r = validateActivities({
|
|
168
|
-
notation: '
|
|
169
|
-
|
|
166
|
+
notation: 'action',
|
|
167
|
+
actions: [
|
|
170
168
|
{ id: 'A-001', name: 'Task', duration: 5, start_date: 'June 1 2026' },
|
|
171
169
|
],
|
|
172
170
|
});
|
|
@@ -176,8 +174,8 @@ describe('validateActivities', () => {
|
|
|
176
174
|
|
|
177
175
|
it('ACT-008 — rejects non-ISO end_date format', () => {
|
|
178
176
|
const r = validateActivities({
|
|
179
|
-
notation: '
|
|
180
|
-
|
|
177
|
+
notation: 'action',
|
|
178
|
+
actions: [
|
|
181
179
|
{ id: 'A-001', name: 'Task', duration: 5, end_date: '2026/06/30' },
|
|
182
180
|
],
|
|
183
181
|
});
|
|
@@ -191,8 +189,8 @@ describe('validateActivities', () => {
|
|
|
191
189
|
// on top would be misleading because the lexicographic compare is
|
|
192
190
|
// meaningless on non-ISO strings.
|
|
193
191
|
const r = validateActivities({
|
|
194
|
-
notation: '
|
|
195
|
-
|
|
192
|
+
notation: 'action',
|
|
193
|
+
actions: [
|
|
196
194
|
{ id: 'A-001', name: 'Task', duration: 5, start_date: 'tomorrow', end_date: 'yesterday' },
|
|
197
195
|
],
|
|
198
196
|
});
|
|
@@ -202,8 +200,8 @@ describe('validateActivities', () => {
|
|
|
202
200
|
|
|
203
201
|
it('ACT-009 — rejects negative duration', () => {
|
|
204
202
|
const r = validateActivities({
|
|
205
|
-
notation: '
|
|
206
|
-
|
|
203
|
+
notation: 'action',
|
|
204
|
+
actions: [{ id: 'A-001', name: 'Task', duration: -1 }],
|
|
207
205
|
});
|
|
208
206
|
expect(r.valid).toBe(false);
|
|
209
207
|
expect(r.errors.some(e => e.code === 'ACT-009')).toBe(true);
|
|
@@ -211,16 +209,16 @@ describe('validateActivities', () => {
|
|
|
211
209
|
|
|
212
210
|
it('ACT-009 — accepts zero duration', () => {
|
|
213
211
|
const r = validateActivities({
|
|
214
|
-
notation: '
|
|
215
|
-
|
|
212
|
+
notation: 'action',
|
|
213
|
+
actions: [{ id: 'A-001', name: 'Milestone', duration: 0 }],
|
|
216
214
|
});
|
|
217
215
|
expect(r.errors.some(e => e.code === 'ACT-009')).toBe(false);
|
|
218
216
|
});
|
|
219
217
|
|
|
220
218
|
it('ACT-010 — rejects singular "goal:" field', () => {
|
|
221
219
|
const r = validateActivities({
|
|
222
|
-
notation: '
|
|
223
|
-
|
|
220
|
+
notation: 'action',
|
|
221
|
+
actions: [{ id: 'A-001', name: 'Task', duration: 1, goal: 'GOAL-001' } as any],
|
|
224
222
|
});
|
|
225
223
|
expect(r.valid).toBe(false);
|
|
226
224
|
expect(r.errors.some(e => e.code === 'ACT-010')).toBe(true);
|
|
@@ -228,8 +226,8 @@ describe('validateActivities', () => {
|
|
|
228
226
|
|
|
229
227
|
it('ACT-010 — rejects singular "predecessor:" field', () => {
|
|
230
228
|
const r = validateActivities({
|
|
231
|
-
notation: '
|
|
232
|
-
|
|
229
|
+
notation: 'action',
|
|
230
|
+
actions: [
|
|
233
231
|
{ id: 'A-001', name: 'First', duration: 1 },
|
|
234
232
|
{ id: 'A-002', name: 'Task', duration: 1, predecessor: 'A-001' } as any,
|
|
235
233
|
],
|
|
@@ -240,8 +238,8 @@ describe('validateActivities', () => {
|
|
|
240
238
|
|
|
241
239
|
it('ACT-010 — rejects singular "tag:" field', () => {
|
|
242
240
|
const r = validateActivities({
|
|
243
|
-
notation: '
|
|
244
|
-
|
|
241
|
+
notation: 'action',
|
|
242
|
+
actions: [{ id: 'A-001', name: 'Task', duration: 1, tag: 'q3' } as any],
|
|
245
243
|
});
|
|
246
244
|
expect(r.valid).toBe(false);
|
|
247
245
|
expect(r.errors.some(e => e.code === 'ACT-010')).toBe(true);
|
|
@@ -249,8 +247,8 @@ describe('validateActivities', () => {
|
|
|
249
247
|
|
|
250
248
|
it('ACT-011 — warns when duration is absent', () => {
|
|
251
249
|
const r = validateActivities({
|
|
252
|
-
notation: '
|
|
253
|
-
|
|
250
|
+
notation: 'action',
|
|
251
|
+
actions: [{ id: 'A-001', name: 'No duration' }],
|
|
254
252
|
});
|
|
255
253
|
expect(r.valid).toBe(true);
|
|
256
254
|
expect(r.warnings.some(w => w.code === 'ACT-011')).toBe(true);
|
|
@@ -258,8 +256,8 @@ describe('validateActivities', () => {
|
|
|
258
256
|
|
|
259
257
|
it('ACT-013 — warns on structurally orphan activity', () => {
|
|
260
258
|
const r = validateActivities({
|
|
261
|
-
notation: '
|
|
262
|
-
|
|
259
|
+
notation: 'action',
|
|
260
|
+
actions: [
|
|
263
261
|
{ id: 'A-001', name: 'Connected', duration: 1 },
|
|
264
262
|
{ id: 'A-002', name: 'Orphan', duration: 1 },
|
|
265
263
|
{ id: 'A-003', name: 'End', duration: 1, predecessors: ['A-001'] },
|
|
@@ -272,9 +270,9 @@ describe('validateActivities', () => {
|
|
|
272
270
|
|
|
273
271
|
it('full valid example passes with no errors', () => {
|
|
274
272
|
const r = validateActivities({
|
|
275
|
-
notation: '
|
|
273
|
+
notation: 'action',
|
|
276
274
|
title: 'Test Project',
|
|
277
|
-
|
|
275
|
+
actions: [
|
|
278
276
|
{ id: 'A', name: 'Start', duration: 3 },
|
|
279
277
|
{ id: 'B', name: 'Middle', duration: 5, predecessors: ['A'] },
|
|
280
278
|
{ id: 'C', name: 'End', duration: 2, predecessors: ['B'] },
|
|
@@ -333,8 +331,8 @@ describe('validateActivities', () => {
|
|
|
333
331
|
|
|
334
332
|
it('ACT-016 — rejects milestone with mismatched start/end dates', () => {
|
|
335
333
|
const r = validateActivities({
|
|
336
|
-
notation: '
|
|
337
|
-
|
|
334
|
+
notation: 'action',
|
|
335
|
+
actions: [
|
|
338
336
|
{ id: 'M-1', name: 'Launch', duration: 0, start_date: '2026-06-01', end_date: '2026-06-02' },
|
|
339
337
|
],
|
|
340
338
|
});
|
|
@@ -344,8 +342,8 @@ describe('validateActivities', () => {
|
|
|
344
342
|
|
|
345
343
|
it('ACT-016 — accepts milestone with equal start/end dates', () => {
|
|
346
344
|
const r = validateActivities({
|
|
347
|
-
notation: '
|
|
348
|
-
|
|
345
|
+
notation: 'action',
|
|
346
|
+
actions: [
|
|
349
347
|
{ id: 'M-1', name: 'Launch', duration: 0, start_date: '2026-06-01', end_date: '2026-06-01' },
|
|
350
348
|
],
|
|
351
349
|
});
|
|
@@ -356,8 +354,8 @@ describe('validateActivities', () => {
|
|
|
356
354
|
|
|
357
355
|
it('ACT-017 — warns when a referenced-as-parent activity carries its own duration', () => {
|
|
358
356
|
const r = validateActivities({
|
|
359
|
-
notation: '
|
|
360
|
-
|
|
357
|
+
notation: 'action',
|
|
358
|
+
actions: [
|
|
361
359
|
{ id: 'PHASE-1', name: 'Design', duration: 5 },
|
|
362
360
|
{ id: 'A-1', name: 'Child', duration: 3, parent: 'PHASE-1' },
|
|
363
361
|
],
|
|
@@ -368,8 +366,8 @@ describe('validateActivities', () => {
|
|
|
368
366
|
|
|
369
367
|
it('ACT-017 — no warning when phase omits duration/dates and rolls up from children', () => {
|
|
370
368
|
const r = validateActivities({
|
|
371
|
-
notation: '
|
|
372
|
-
|
|
369
|
+
notation: 'action',
|
|
370
|
+
actions: [
|
|
373
371
|
{ id: 'PHASE-1', name: 'Design' },
|
|
374
372
|
{ id: 'A-1', name: 'Child', duration: 3, parent: 'PHASE-1' },
|
|
375
373
|
],
|
|
@@ -379,8 +377,8 @@ describe('validateActivities', () => {
|
|
|
379
377
|
|
|
380
378
|
it('ACT-018 — warns when an activity with no duration is not referenced as parent', () => {
|
|
381
379
|
const r = validateActivities({
|
|
382
|
-
notation: '
|
|
383
|
-
|
|
380
|
+
notation: 'action',
|
|
381
|
+
actions: [
|
|
384
382
|
{ id: 'A-1', name: 'Has duration', duration: 3 },
|
|
385
383
|
{ id: 'P-EMPTY', name: 'Looks like phase but no children' },
|
|
386
384
|
],
|
|
@@ -390,8 +388,8 @@ describe('validateActivities', () => {
|
|
|
390
388
|
|
|
391
389
|
it('ACT-018 — no warning when the no-duration activity has at least one child', () => {
|
|
392
390
|
const r = validateActivities({
|
|
393
|
-
notation: '
|
|
394
|
-
|
|
391
|
+
notation: 'action',
|
|
392
|
+
actions: [
|
|
395
393
|
{ id: 'PHASE-1', name: 'Phase' },
|
|
396
394
|
{ id: 'A-1', name: 'Child', duration: 3, parent: 'PHASE-1' },
|
|
397
395
|
],
|
|
@@ -413,26 +411,31 @@ describe('validateActivities', () => {
|
|
|
413
411
|
|
|
414
412
|
it('ACT-019 — no warning when every activity has pinned dates', () => {
|
|
415
413
|
const r = validateActivities({
|
|
416
|
-
notation: '
|
|
417
|
-
|
|
414
|
+
notation: 'action',
|
|
415
|
+
actions: [
|
|
418
416
|
{ id: 'A-1', name: 'A', duration: 3, start_date: '2026-06-01', end_date: '2026-06-04' },
|
|
419
417
|
],
|
|
420
418
|
});
|
|
421
419
|
expect(r.warnings.some(w => w.code === 'ACT-019')).toBe(false);
|
|
422
420
|
});
|
|
423
421
|
|
|
424
|
-
it('[blocker] tolerates a null element in
|
|
425
|
-
|
|
422
|
+
it('[blocker] tolerates a null element in actions[] without throwing', () => {
|
|
423
|
+
// Do NOT spread minimalValid here: validateActivities mutates its input
|
|
424
|
+
// to normalise raw.activities, so a previously-called test that passed
|
|
425
|
+
// minimalValid directly would have set minimalValid.activities. Spreading
|
|
426
|
+
// that mutated object would cause the actions:[null] override to be masked
|
|
427
|
+
// by the pre-existing activities array.
|
|
428
|
+
const r = validateActivities({ notation: 'action', actions: [null] });
|
|
426
429
|
expect(r.valid).toBe(false);
|
|
427
430
|
expect(r.errors.some(e => e.code === 'SCHEMA_INVALID')).toBe(true);
|
|
428
431
|
});
|
|
429
432
|
});
|
|
430
433
|
|
|
431
|
-
describe('
|
|
434
|
+
describe('action examples (regression)', () => {
|
|
432
435
|
const files = fs.readdirSync(EXAMPLES_DIR).filter(f => f.endsWith('.yaml'));
|
|
433
436
|
expect(files.length).toBeGreaterThan(0);
|
|
434
437
|
for (const file of files) {
|
|
435
|
-
it(`validates tests/fixtures/notation-corpus/
|
|
438
|
+
it(`validates tests/fixtures/notation-corpus/action/${file}`, () => {
|
|
436
439
|
const text = fs.readFileSync(path.join(EXAMPLES_DIR, file), 'utf8');
|
|
437
440
|
const parsed = yaml.load(text);
|
|
438
441
|
const r = validateActivities(parsed);
|
|
@@ -19,27 +19,20 @@ export function validateActivities(input: unknown): ActivityValidationResult {
|
|
|
19
19
|
|
|
20
20
|
const raw = input as Record<string, unknown>;
|
|
21
21
|
|
|
22
|
-
// ACT-001: notation must be "action"
|
|
22
|
+
// ACT-001: notation must be "action"
|
|
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 !== 'action'
|
|
27
|
+
if (raw.notation !== 'action') {
|
|
28
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
|
-
}
|
|
34
31
|
|
|
35
|
-
//
|
|
36
|
-
const rawArray = Array.isArray(raw.actions) ? raw.actions :
|
|
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
|
-
}
|
|
32
|
+
// Only "actions:" root key is accepted.
|
|
33
|
+
const rawArray = Array.isArray(raw.actions) ? raw.actions : undefined;
|
|
40
34
|
if (!rawArray) {
|
|
41
|
-
|
|
42
|
-
errors.push({ code: 'SCHEMA_INVALID', message: `${canonKey} must be an array`, path: canonKey });
|
|
35
|
+
errors.push({ code: 'SCHEMA_INVALID', message: 'actions must be an array', path: 'actions' });
|
|
43
36
|
return { valid: false, errors, warnings };
|
|
44
37
|
}
|
|
45
38
|
// Normalise onto raw.activities for the rest of this function (avoids touching downstream code)
|
|
@@ -15,7 +15,7 @@ import type { ActivityCardDoc } from '../types.js';
|
|
|
15
15
|
|
|
16
16
|
const dir = path.resolve(
|
|
17
17
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
18
|
-
'../../../../../tests/fixtures/notation-corpus/
|
|
18
|
+
'../../../../../tests/fixtures/notation-corpus/action-card',
|
|
19
19
|
);
|
|
20
20
|
const load = (f: string): unknown => yaml.load(readFileSync(path.join(dir, f), 'utf-8'));
|
|
21
21
|
|
|
@@ -33,7 +33,7 @@ function loadYamlTree(root: string): unknown[] {
|
|
|
33
33
|
|
|
34
34
|
describe('activity-card worked example (eu-programme)', () => {
|
|
35
35
|
it('validates, resolves, and lays out the success-signal card', () => {
|
|
36
|
-
const card = load('eu-programme.
|
|
36
|
+
const card = load('eu-programme.action-card.transitrix.yaml');
|
|
37
37
|
const elements = loadYamlTree(path.join(dir, 'canon', 'elements'));
|
|
38
38
|
const relations = loadYamlTree(path.join(dir, 'canon', 'relations'));
|
|
39
39
|
|
|
@@ -71,13 +71,11 @@ describe('activity-card worked example (eu-programme)', () => {
|
|
|
71
71
|
// Description row
|
|
72
72
|
expect(layout.descriptionRow?.label).toBe('Description');
|
|
73
73
|
// Chain sections
|
|
74
|
-
expect(layout.chainSections.map((s) => s.type)).toEqual(['drivers', '
|
|
74
|
+
expect(layout.chainSections.map((s) => s.type)).toEqual(['drivers', 'goals', 'changes']);
|
|
75
75
|
const driversSection = layout.chainSections.find((s) => s.type === 'drivers')!;
|
|
76
76
|
const goalsSection = layout.chainSections.find((s) => s.type === 'goals')!;
|
|
77
|
-
const assessmentsSection = layout.chainSections.find((s) => s.type === 'assessments')!;
|
|
78
77
|
expect(driversSection.nodes.map((n) => n.id)).toContain('FACTOR-EU-MDR-1');
|
|
79
78
|
expect(goalsSection.nodes.map((n) => n.id)).toContain('GOAL-EU-MARKET-1');
|
|
80
|
-
expect(assessmentsSection.isEmpty).toBe(true); // no assessment in this fixture
|
|
81
79
|
// Chain edges
|
|
82
80
|
expect(layout.chainEdges).toContainEqual({ sourceId: 'FACTOR-EU-MDR-1', targetId: 'GOAL-EU-MARKET-1' });
|
|
83
81
|
expect(layout.chainEdges).toContainEqual({ sourceId: 'GOAL-EU-MARKET-1', targetId: 'CHANGE-EU-COMPLIANCE-1' });
|
|
@@ -94,20 +94,17 @@ describe('layoutActivityCard', () => {
|
|
|
94
94
|
expect(l.descriptionRow).toBeUndefined();
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
-
it('
|
|
97
|
+
it('three chain sections in order: drivers, goals, changes', () => {
|
|
98
98
|
const l = layoutActivityCard(RESOLVED);
|
|
99
|
-
expect(l.chainSections.map((s) => s.type)).toEqual(['drivers', '
|
|
99
|
+
expect(l.chainSections.map((s) => s.type)).toEqual(['drivers', 'goals', 'changes']);
|
|
100
100
|
});
|
|
101
101
|
|
|
102
102
|
it('chain sections carry the resolved nodes', () => {
|
|
103
103
|
const l = layoutActivityCard(RESOLVED);
|
|
104
104
|
const drivers = l.chainSections.find((s) => s.type === 'drivers')!;
|
|
105
|
-
const assessments = l.chainSections.find((s) => s.type === 'assessments')!;
|
|
106
105
|
const goals = l.chainSections.find((s) => s.type === 'goals')!;
|
|
107
106
|
const changes = l.chainSections.find((s) => s.type === 'changes')!;
|
|
108
107
|
expect(drivers.nodes.map((n) => n.id)).toEqual(['FACTOR-EU-MDR-1']);
|
|
109
|
-
expect(assessments.nodes.map((n) => n.id)).toEqual(['ASSESSMENT-MDR-1']);
|
|
110
|
-
expect(assessments.nodes[0].meta).toBe('2026-03-01');
|
|
111
108
|
expect(goals.nodes.map((n) => n.id)).toEqual(['GOAL-EU-MARKET-1']);
|
|
112
109
|
expect(changes.nodes.map((n) => n.id)).toEqual(['CHANGE-EU-COMPLIANCE-1']);
|
|
113
110
|
});
|
|
@@ -127,10 +124,6 @@ describe('layoutActivityCard', () => {
|
|
|
127
124
|
expect(l.chainEdges).toContainEqual({ sourceId: 'GOAL-EU-MARKET-1', targetId: 'CHANGE-EU-COMPLIANCE-1' });
|
|
128
125
|
});
|
|
129
126
|
|
|
130
|
-
it('builds driver→assessment edge when assessment is linked', () => {
|
|
131
|
-
const l = layoutActivityCard(RESOLVED);
|
|
132
|
-
expect(l.chainEdges).toContainEqual({ sourceId: 'FACTOR-EU-MDR-1', targetId: 'ASSESSMENT-MDR-1' });
|
|
133
|
-
});
|
|
134
127
|
|
|
135
128
|
it('milestone ArchiMate class is set', () => {
|
|
136
129
|
const l = layoutActivityCard(RESOLVED);
|
|
@@ -217,6 +217,33 @@ describe('resolveActivityCard', () => {
|
|
|
217
217
|
expect(r.warnings.some((w) => w.code === 'PC-001')).toBe(true);
|
|
218
218
|
});
|
|
219
219
|
|
|
220
|
+
it('resolves stakeholders from inline stakeholders: [] on the Action element', () => {
|
|
221
|
+
const projectWithInlineStakeholders = { ...PROJECT, stakeholders: ['STAKEHOLDER-OPS-1'] };
|
|
222
|
+
const r = resolveActivityCard(CARD, {
|
|
223
|
+
elements: [projectWithInlineStakeholders, CHILD, FACTOR, GOAL, CHANGE, STAKEHOLDER],
|
|
224
|
+
relations: [REL_GOAL],
|
|
225
|
+
});
|
|
226
|
+
expect(r.valid).toBe(true);
|
|
227
|
+
const sh = r.resolved!.stakeholders!;
|
|
228
|
+
expect(sh).toHaveLength(1);
|
|
229
|
+
expect(sh[0].id).toBe('STAKEHOLDER-OPS-1');
|
|
230
|
+
expect(sh[0].name).toBe('Operations');
|
|
231
|
+
expect(sh[0].role).toBeUndefined();
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('REL stakeholder takes precedence over inline duplicate (carries role)', () => {
|
|
235
|
+
const projectWithInlineStakeholders = { ...PROJECT, stakeholders: ['STAKEHOLDER-OPS-1'] };
|
|
236
|
+
const r = resolveActivityCard(CARD, {
|
|
237
|
+
elements: [projectWithInlineStakeholders, CHILD, FACTOR, GOAL, CHANGE, STAKEHOLDER],
|
|
238
|
+
relations: [REL_GOAL, REL_STAKEHOLDER],
|
|
239
|
+
});
|
|
240
|
+
expect(r.valid).toBe(true);
|
|
241
|
+
const sh = r.resolved!.stakeholders!;
|
|
242
|
+
// deduplicated — REL entry wins, inline duplicate is dropped
|
|
243
|
+
expect(sh).toHaveLength(1);
|
|
244
|
+
expect(sh[0].role).toBe('sponsor');
|
|
245
|
+
});
|
|
246
|
+
|
|
220
247
|
it('falls back to the inline goals field when no activity_goal relation exists', () => {
|
|
221
248
|
const projectWithInline = { ...PROJECT, goals: ['GOAL-EU-MARKET-1'] };
|
|
222
249
|
const r = resolveActivityCard(CARD, {
|
|
@@ -2,9 +2,9 @@ import { describe, it, expect } from 'vitest';
|
|
|
2
2
|
import { validateActivityCard } from '../validate.js';
|
|
3
3
|
|
|
4
4
|
const VALID = {
|
|
5
|
-
notation: '
|
|
5
|
+
notation: 'action-card',
|
|
6
6
|
spec_version: '0.1',
|
|
7
|
-
|
|
7
|
+
action_card: {
|
|
8
8
|
id: 'ACTIVITY_CARD-EU-PROGRAMME-1',
|
|
9
9
|
project: 'ACTIVITY-EU-PROGRAMME-1',
|
|
10
10
|
description: 'Executive summary.',
|
|
@@ -20,11 +20,23 @@ const VALID = {
|
|
|
20
20
|
};
|
|
21
21
|
|
|
22
22
|
describe('validateActivityCard', () => {
|
|
23
|
-
it('accepts a well-formed card (
|
|
23
|
+
it('accepts a well-formed card (notation: action-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(
|
|
27
|
+
expect(r.warnings.some(w => w.code === 'DEPRECATED_NOTATION')).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('rejects deprecated notation: activity-card', () => {
|
|
31
|
+
const r = validateActivityCard({
|
|
32
|
+
notation: 'activity-card',
|
|
33
|
+
activity_card: {
|
|
34
|
+
id: 'ACTIVITY_CARD-EU-PROGRAMME-1',
|
|
35
|
+
project: 'ACTIVITY-EU-PROGRAMME-1',
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
expect(r.valid).toBe(false);
|
|
39
|
+
expect(r.errors.some(e => e.code === 'HDR-002')).toBe(true);
|
|
28
40
|
});
|
|
29
41
|
|
|
30
42
|
it('accepts notation: action-card with action_card block (canonical)', () => {
|
|
@@ -67,21 +79,21 @@ describe('validateActivityCard', () => {
|
|
|
67
79
|
expect(r.errors.some((e) => e.code === 'HDR-002')).toBe(true);
|
|
68
80
|
});
|
|
69
81
|
|
|
70
|
-
it('AC-001 on missing
|
|
71
|
-
const r = validateActivityCard({ notation: '
|
|
82
|
+
it('AC-001 on missing action_card block', () => {
|
|
83
|
+
const r = validateActivityCard({ notation: 'action-card' });
|
|
72
84
|
expect(r.valid).toBe(false);
|
|
73
85
|
expect(r.errors.some((e) => e.code === 'AC-001')).toBe(true);
|
|
74
86
|
});
|
|
75
87
|
|
|
76
88
|
it('AC-002 on malformed card id', () => {
|
|
77
|
-
const r = validateActivityCard({ ...VALID,
|
|
89
|
+
const r = validateActivityCard({ ...VALID, action_card: { ...VALID.action_card, id: 'CARD-1' } });
|
|
78
90
|
expect(r.errors.some((e) => e.code === 'AC-002')).toBe(true);
|
|
79
91
|
});
|
|
80
92
|
|
|
81
93
|
it('PC-001 on missing project', () => {
|
|
82
94
|
const r = validateActivityCard({
|
|
83
95
|
...VALID,
|
|
84
|
-
|
|
96
|
+
action_card: { ...VALID.action_card, project: undefined },
|
|
85
97
|
});
|
|
86
98
|
expect(r.errors.some((e) => e.code === 'PC-001')).toBe(true);
|
|
87
99
|
});
|
|
@@ -89,7 +101,7 @@ describe('validateActivityCard', () => {
|
|
|
89
101
|
it('PC-001 on malformed project id', () => {
|
|
90
102
|
const r = validateActivityCard({
|
|
91
103
|
...VALID,
|
|
92
|
-
|
|
104
|
+
action_card: { ...VALID.action_card, project: 'GOAL-1' },
|
|
93
105
|
});
|
|
94
106
|
expect(r.errors.some((e) => e.code === 'PC-001')).toBe(true);
|
|
95
107
|
});
|
|
@@ -97,7 +109,7 @@ describe('validateActivityCard', () => {
|
|
|
97
109
|
it('AC-003 on milestone missing required fields', () => {
|
|
98
110
|
const r = validateActivityCard({
|
|
99
111
|
...VALID,
|
|
100
|
-
|
|
112
|
+
action_card: { ...VALID.action_card, milestones: [{ id: 'MILESTONE-X-1' }] },
|
|
101
113
|
});
|
|
102
114
|
expect(r.valid).toBe(false);
|
|
103
115
|
expect(r.errors.some((e) => e.code === 'AC-003')).toBe(true);
|
|
@@ -106,8 +118,8 @@ describe('validateActivityCard', () => {
|
|
|
106
118
|
it('AC-004 on malformed milestone id', () => {
|
|
107
119
|
const r = validateActivityCard({
|
|
108
120
|
...VALID,
|
|
109
|
-
|
|
110
|
-
...VALID.
|
|
121
|
+
action_card: {
|
|
122
|
+
...VALID.action_card,
|
|
111
123
|
milestones: [{ id: 'MS-1', name: 'x', date: '2027-01-01' }],
|
|
112
124
|
},
|
|
113
125
|
});
|
|
@@ -118,7 +130,7 @@ describe('validateActivityCard', () => {
|
|
|
118
130
|
const m = { id: 'MILESTONE-X-1', name: 'x', date: '2027-01-01' };
|
|
119
131
|
const r = validateActivityCard({
|
|
120
132
|
...VALID,
|
|
121
|
-
|
|
133
|
+
action_card: { ...VALID.action_card, milestones: [m, { ...m }] },
|
|
122
134
|
});
|
|
123
135
|
expect(r.errors.some((e) => e.code === 'AC-004' && /Duplicate/.test(e.message))).toBe(true);
|
|
124
136
|
});
|
|
@@ -126,8 +138,8 @@ describe('validateActivityCard', () => {
|
|
|
126
138
|
it('AC-005 on bad milestone date format', () => {
|
|
127
139
|
const r = validateActivityCard({
|
|
128
140
|
...VALID,
|
|
129
|
-
|
|
130
|
-
...VALID.
|
|
141
|
+
action_card: {
|
|
142
|
+
...VALID.action_card,
|
|
131
143
|
milestones: [{ id: 'MILESTONE-X-1', name: 'x', date: '31/01/2027' }],
|
|
132
144
|
},
|
|
133
145
|
});
|
|
@@ -137,8 +149,8 @@ describe('validateActivityCard', () => {
|
|
|
137
149
|
it('AC-006 on malformed delivers_changes entry', () => {
|
|
138
150
|
const r = validateActivityCard({
|
|
139
151
|
...VALID,
|
|
140
|
-
|
|
141
|
-
...VALID.
|
|
152
|
+
action_card: {
|
|
153
|
+
...VALID.action_card,
|
|
142
154
|
milestones: [{ id: 'MILESTONE-X-1', name: 'x', date: '2027-01-01', delivers_changes: ['GOAL-1'] }],
|
|
143
155
|
},
|
|
144
156
|
});
|
|
@@ -148,7 +160,7 @@ describe('validateActivityCard', () => {
|
|
|
148
160
|
it('accepts a card with no milestones', () => {
|
|
149
161
|
const r = validateActivityCard({
|
|
150
162
|
...VALID,
|
|
151
|
-
|
|
163
|
+
action_card: { id: 'ACTIVITY_CARD-X-1', project: 'ACTIVITY-X-1' },
|
|
152
164
|
});
|
|
153
165
|
expect(r.valid).toBe(true);
|
|
154
166
|
});
|