@transitrix/diagrams 1.3.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 +2 -2
- package/dist/activity-card/__tests__/validate.test.js +29 -18
- 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 +10 -10
- package/dist/fgca/__tests__/resolver.test.js +7 -7
- package/dist/fgca/__tests__/validate.test.js +11 -11
- package/dist/fgca/parse-canonical.js +7 -11
- 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-blocks.js +97 -18
- 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 +2 -2
- package/src/activity-card/__tests__/validate.test.ts +30 -18
- 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 +10 -10
- package/src/fgca/__tests__/resolver.test.ts +7 -7
- package/src/fgca/__tests__/validate.test.ts +11 -11
- package/src/fgca/parse-canonical.ts +7 -11
- 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-blocks.ts +101 -20
|
@@ -54,8 +54,8 @@ describe('coerceDatesToIsoStrings', () => {
|
|
|
54
54
|
describe('coerceDatesToIsoStrings — js-yaml integration', () => {
|
|
55
55
|
it('quoted ISO dates are preserved untouched', () => {
|
|
56
56
|
const text = [
|
|
57
|
-
'notation:
|
|
58
|
-
'
|
|
57
|
+
'notation: action',
|
|
58
|
+
'actions:',
|
|
59
59
|
' - id: "A-001"',
|
|
60
60
|
' name: "Quoted dates"',
|
|
61
61
|
' duration: 3',
|
|
@@ -65,15 +65,15 @@ describe('coerceDatesToIsoStrings — js-yaml integration', () => {
|
|
|
65
65
|
].join('\n');
|
|
66
66
|
const parsed = yaml.load(text);
|
|
67
67
|
coerceDatesToIsoStrings(parsed);
|
|
68
|
-
expect(parsed.
|
|
69
|
-
expect(parsed.
|
|
68
|
+
expect(parsed.actions[0].start_date).toBe('2026-06-01');
|
|
69
|
+
expect(parsed.actions[0].end_date).toBe('2026-06-03');
|
|
70
70
|
const v = validateActivities(parsed);
|
|
71
71
|
expect(v.valid).toBe(true);
|
|
72
72
|
});
|
|
73
73
|
it('bare ISO dates are coerced to ISO strings and pass validation', () => {
|
|
74
74
|
const text = [
|
|
75
|
-
'notation:
|
|
76
|
-
'
|
|
75
|
+
'notation: action',
|
|
76
|
+
'actions:',
|
|
77
77
|
' - id: "A-001"',
|
|
78
78
|
' name: "Bare dates"',
|
|
79
79
|
' duration: 3',
|
|
@@ -85,10 +85,10 @@ describe('coerceDatesToIsoStrings — js-yaml integration', () => {
|
|
|
85
85
|
// Without coercion, js-yaml returns Date instances and the validator
|
|
86
86
|
// would emit ACT-008 because typeof !== 'string'. Sanity-check that
|
|
87
87
|
// assumption before normalisation.
|
|
88
|
-
expect(parsed.
|
|
88
|
+
expect(parsed.actions[0].start_date).toBeInstanceOf(Date);
|
|
89
89
|
coerceDatesToIsoStrings(parsed);
|
|
90
|
-
expect(parsed.
|
|
91
|
-
expect(parsed.
|
|
90
|
+
expect(parsed.actions[0].start_date).toBe('2026-06-01');
|
|
91
|
+
expect(parsed.actions[0].end_date).toBe('2026-06-03');
|
|
92
92
|
const v = validateActivities(parsed);
|
|
93
93
|
expect(v.valid).toBe(true);
|
|
94
94
|
});
|
|
@@ -3,39 +3,37 @@ import * as fs from 'node:fs';
|
|
|
3
3
|
import * as path from 'node:path';
|
|
4
4
|
import yaml from 'js-yaml';
|
|
5
5
|
import { validateActivities } from '../validate.js';
|
|
6
|
-
const EXAMPLES_DIR = path.resolve(process.cwd(), '..', '..', 'tests', 'fixtures', 'notation-corpus', '
|
|
6
|
+
const EXAMPLES_DIR = path.resolve(process.cwd(), '..', '..', 'tests', 'fixtures', 'notation-corpus', 'action');
|
|
7
7
|
const minimalValid = {
|
|
8
|
-
notation: '
|
|
9
|
-
|
|
8
|
+
notation: 'action',
|
|
9
|
+
actions: [
|
|
10
10
|
{ id: 'A-001', name: 'Start', duration: 3 },
|
|
11
11
|
{ id: 'A-002', name: 'End', duration: 2, predecessors: ['A-001'] },
|
|
12
12
|
],
|
|
13
13
|
};
|
|
14
14
|
describe('validateActivities', () => {
|
|
15
|
-
it('ACT-001 — accepts notation:
|
|
15
|
+
it('ACT-001 — accepts notation: action (canonical)', () => {
|
|
16
16
|
const r = validateActivities(minimalValid);
|
|
17
17
|
expect(r.valid).toBe(true);
|
|
18
|
-
|
|
19
|
-
expect(r.warnings.some(w => w.code === 'DEPRECATED_NOTATION')).toBe(true);
|
|
18
|
+
expect(r.warnings.some(w => w.code === 'DEPRECATED_NOTATION')).toBe(false);
|
|
20
19
|
});
|
|
21
|
-
it('ACT-001 —
|
|
20
|
+
it('ACT-001 — rejects notation: activities (no longer accepted)', () => {
|
|
22
21
|
const r = validateActivities({
|
|
23
|
-
notation: '
|
|
24
|
-
|
|
25
|
-
{ id: '
|
|
26
|
-
{ id: 'ACTION-002', name: 'End', duration: 2, predecessors: ['ACTION-001'] },
|
|
22
|
+
notation: 'activities',
|
|
23
|
+
activities: [
|
|
24
|
+
{ id: 'A-001', name: 'Start', duration: 3 },
|
|
27
25
|
],
|
|
28
26
|
});
|
|
29
|
-
expect(r.valid).toBe(
|
|
30
|
-
expect(r.
|
|
27
|
+
expect(r.valid).toBe(false);
|
|
28
|
+
expect(r.errors.some(e => e.code === 'ACT-001')).toBe(true);
|
|
31
29
|
});
|
|
32
|
-
it('ACT-001 —
|
|
30
|
+
it('ACT-001 — rejects activities: root key (only actions: accepted)', () => {
|
|
33
31
|
const r = validateActivities({
|
|
34
32
|
notation: 'action',
|
|
35
33
|
activities: [{ id: 'ACTION-001', name: 'Foo', duration: 1 }],
|
|
36
34
|
});
|
|
37
|
-
expect(r.valid).toBe(
|
|
38
|
-
expect(r.
|
|
35
|
+
expect(r.valid).toBe(false);
|
|
36
|
+
expect(r.errors.some(e => e.code === 'SCHEMA_INVALID')).toBe(true);
|
|
39
37
|
});
|
|
40
38
|
it('ACT-001 — normalises action_type to activity_type for downstream layout', () => {
|
|
41
39
|
const input = {
|
|
@@ -60,32 +58,32 @@ describe('validateActivities', () => {
|
|
|
60
58
|
});
|
|
61
59
|
it('ACT-002 — rejects activity with missing id', () => {
|
|
62
60
|
const r = validateActivities({
|
|
63
|
-
notation: '
|
|
64
|
-
|
|
61
|
+
notation: 'action',
|
|
62
|
+
actions: [{ name: 'No ID', duration: 1 }],
|
|
65
63
|
});
|
|
66
64
|
expect(r.valid).toBe(false);
|
|
67
65
|
expect(r.errors.some(e => e.code === 'ACT-002')).toBe(true);
|
|
68
66
|
});
|
|
69
67
|
it('ACT-002 — rejects activity with empty id', () => {
|
|
70
68
|
const r = validateActivities({
|
|
71
|
-
notation: '
|
|
72
|
-
|
|
69
|
+
notation: 'action',
|
|
70
|
+
actions: [{ id: ' ', name: 'Empty ID', duration: 1 }],
|
|
73
71
|
});
|
|
74
72
|
expect(r.valid).toBe(false);
|
|
75
73
|
expect(r.errors.some(e => e.code === 'ACT-002')).toBe(true);
|
|
76
74
|
});
|
|
77
75
|
it('ACT-003 — rejects activity with missing name', () => {
|
|
78
76
|
const r = validateActivities({
|
|
79
|
-
notation: '
|
|
80
|
-
|
|
77
|
+
notation: 'action',
|
|
78
|
+
actions: [{ id: 'A-001', duration: 1 }],
|
|
81
79
|
});
|
|
82
80
|
expect(r.valid).toBe(false);
|
|
83
81
|
expect(r.errors.some(e => e.code === 'ACT-003')).toBe(true);
|
|
84
82
|
});
|
|
85
83
|
it('ACT-004 — rejects duplicate ids', () => {
|
|
86
84
|
const r = validateActivities({
|
|
87
|
-
notation: '
|
|
88
|
-
|
|
85
|
+
notation: 'action',
|
|
86
|
+
actions: [
|
|
89
87
|
{ id: 'A-001', name: 'First', duration: 1 },
|
|
90
88
|
{ id: 'A-001', name: 'Duplicate', duration: 2 },
|
|
91
89
|
],
|
|
@@ -95,8 +93,8 @@ describe('validateActivities', () => {
|
|
|
95
93
|
});
|
|
96
94
|
it('ACT-005 — rejects unknown predecessor reference', () => {
|
|
97
95
|
const r = validateActivities({
|
|
98
|
-
notation: '
|
|
99
|
-
|
|
96
|
+
notation: 'action',
|
|
97
|
+
actions: [{ id: 'A-001', name: 'Task', duration: 1, predecessors: ['MISSING'] }],
|
|
100
98
|
});
|
|
101
99
|
expect(r.valid).toBe(false);
|
|
102
100
|
expect(r.errors.some(e => e.code === 'ACT-005')).toBe(true);
|
|
@@ -108,8 +106,8 @@ describe('validateActivities', () => {
|
|
|
108
106
|
});
|
|
109
107
|
it('ACT-006 — rejects cyclic dependency', () => {
|
|
110
108
|
const r = validateActivities({
|
|
111
|
-
notation: '
|
|
112
|
-
|
|
109
|
+
notation: 'action',
|
|
110
|
+
actions: [
|
|
113
111
|
{ id: 'A-001', name: 'Task A', duration: 1, predecessors: ['A-002'] },
|
|
114
112
|
{ id: 'A-002', name: 'Task B', duration: 1, predecessors: ['A-001'] },
|
|
115
113
|
],
|
|
@@ -119,16 +117,16 @@ describe('validateActivities', () => {
|
|
|
119
117
|
});
|
|
120
118
|
it('ACT-007 — rejects self-loop', () => {
|
|
121
119
|
const r = validateActivities({
|
|
122
|
-
notation: '
|
|
123
|
-
|
|
120
|
+
notation: 'action',
|
|
121
|
+
actions: [{ id: 'A-001', name: 'Task', duration: 1, predecessors: ['A-001'] }],
|
|
124
122
|
});
|
|
125
123
|
expect(r.valid).toBe(false);
|
|
126
124
|
expect(r.errors.some(e => e.code === 'ACT-007')).toBe(true);
|
|
127
125
|
});
|
|
128
126
|
it('ACT-008 — rejects end_date before start_date', () => {
|
|
129
127
|
const r = validateActivities({
|
|
130
|
-
notation: '
|
|
131
|
-
|
|
128
|
+
notation: 'action',
|
|
129
|
+
actions: [
|
|
132
130
|
{ id: 'A-001', name: 'Task', duration: 5, start_date: '2026-06-10', end_date: '2026-06-01' },
|
|
133
131
|
],
|
|
134
132
|
});
|
|
@@ -137,8 +135,8 @@ describe('validateActivities', () => {
|
|
|
137
135
|
});
|
|
138
136
|
it('ACT-008 — accepts end_date equal to start_date', () => {
|
|
139
137
|
const r = validateActivities({
|
|
140
|
-
notation: '
|
|
141
|
-
|
|
138
|
+
notation: 'action',
|
|
139
|
+
actions: [
|
|
142
140
|
{ id: 'A-001', name: 'Task', duration: 0, start_date: '2026-06-01', end_date: '2026-06-01' },
|
|
143
141
|
],
|
|
144
142
|
});
|
|
@@ -146,8 +144,8 @@ describe('validateActivities', () => {
|
|
|
146
144
|
});
|
|
147
145
|
it('ACT-008 — rejects non-ISO start_date format', () => {
|
|
148
146
|
const r = validateActivities({
|
|
149
|
-
notation: '
|
|
150
|
-
|
|
147
|
+
notation: 'action',
|
|
148
|
+
actions: [
|
|
151
149
|
{ id: 'A-001', name: 'Task', duration: 5, start_date: 'June 1 2026' },
|
|
152
150
|
],
|
|
153
151
|
});
|
|
@@ -156,8 +154,8 @@ describe('validateActivities', () => {
|
|
|
156
154
|
});
|
|
157
155
|
it('ACT-008 — rejects non-ISO end_date format', () => {
|
|
158
156
|
const r = validateActivities({
|
|
159
|
-
notation: '
|
|
160
|
-
|
|
157
|
+
notation: 'action',
|
|
158
|
+
actions: [
|
|
161
159
|
{ id: 'A-001', name: 'Task', duration: 5, end_date: '2026/06/30' },
|
|
162
160
|
],
|
|
163
161
|
});
|
|
@@ -170,8 +168,8 @@ describe('validateActivities', () => {
|
|
|
170
168
|
// on top would be misleading because the lexicographic compare is
|
|
171
169
|
// meaningless on non-ISO strings.
|
|
172
170
|
const r = validateActivities({
|
|
173
|
-
notation: '
|
|
174
|
-
|
|
171
|
+
notation: 'action',
|
|
172
|
+
actions: [
|
|
175
173
|
{ id: 'A-001', name: 'Task', duration: 5, start_date: 'tomorrow', end_date: 'yesterday' },
|
|
176
174
|
],
|
|
177
175
|
});
|
|
@@ -180,31 +178,31 @@ describe('validateActivities', () => {
|
|
|
180
178
|
});
|
|
181
179
|
it('ACT-009 — rejects negative duration', () => {
|
|
182
180
|
const r = validateActivities({
|
|
183
|
-
notation: '
|
|
184
|
-
|
|
181
|
+
notation: 'action',
|
|
182
|
+
actions: [{ id: 'A-001', name: 'Task', duration: -1 }],
|
|
185
183
|
});
|
|
186
184
|
expect(r.valid).toBe(false);
|
|
187
185
|
expect(r.errors.some(e => e.code === 'ACT-009')).toBe(true);
|
|
188
186
|
});
|
|
189
187
|
it('ACT-009 — accepts zero duration', () => {
|
|
190
188
|
const r = validateActivities({
|
|
191
|
-
notation: '
|
|
192
|
-
|
|
189
|
+
notation: 'action',
|
|
190
|
+
actions: [{ id: 'A-001', name: 'Milestone', duration: 0 }],
|
|
193
191
|
});
|
|
194
192
|
expect(r.errors.some(e => e.code === 'ACT-009')).toBe(false);
|
|
195
193
|
});
|
|
196
194
|
it('ACT-010 — rejects singular "goal:" field', () => {
|
|
197
195
|
const r = validateActivities({
|
|
198
|
-
notation: '
|
|
199
|
-
|
|
196
|
+
notation: 'action',
|
|
197
|
+
actions: [{ id: 'A-001', name: 'Task', duration: 1, goal: 'GOAL-001' }],
|
|
200
198
|
});
|
|
201
199
|
expect(r.valid).toBe(false);
|
|
202
200
|
expect(r.errors.some(e => e.code === 'ACT-010')).toBe(true);
|
|
203
201
|
});
|
|
204
202
|
it('ACT-010 — rejects singular "predecessor:" field', () => {
|
|
205
203
|
const r = validateActivities({
|
|
206
|
-
notation: '
|
|
207
|
-
|
|
204
|
+
notation: 'action',
|
|
205
|
+
actions: [
|
|
208
206
|
{ id: 'A-001', name: 'First', duration: 1 },
|
|
209
207
|
{ id: 'A-002', name: 'Task', duration: 1, predecessor: 'A-001' },
|
|
210
208
|
],
|
|
@@ -214,24 +212,24 @@ describe('validateActivities', () => {
|
|
|
214
212
|
});
|
|
215
213
|
it('ACT-010 — rejects singular "tag:" field', () => {
|
|
216
214
|
const r = validateActivities({
|
|
217
|
-
notation: '
|
|
218
|
-
|
|
215
|
+
notation: 'action',
|
|
216
|
+
actions: [{ id: 'A-001', name: 'Task', duration: 1, tag: 'q3' }],
|
|
219
217
|
});
|
|
220
218
|
expect(r.valid).toBe(false);
|
|
221
219
|
expect(r.errors.some(e => e.code === 'ACT-010')).toBe(true);
|
|
222
220
|
});
|
|
223
221
|
it('ACT-011 — warns when duration is absent', () => {
|
|
224
222
|
const r = validateActivities({
|
|
225
|
-
notation: '
|
|
226
|
-
|
|
223
|
+
notation: 'action',
|
|
224
|
+
actions: [{ id: 'A-001', name: 'No duration' }],
|
|
227
225
|
});
|
|
228
226
|
expect(r.valid).toBe(true);
|
|
229
227
|
expect(r.warnings.some(w => w.code === 'ACT-011')).toBe(true);
|
|
230
228
|
});
|
|
231
229
|
it('ACT-013 — warns on structurally orphan activity', () => {
|
|
232
230
|
const r = validateActivities({
|
|
233
|
-
notation: '
|
|
234
|
-
|
|
231
|
+
notation: 'action',
|
|
232
|
+
actions: [
|
|
235
233
|
{ id: 'A-001', name: 'Connected', duration: 1 },
|
|
236
234
|
{ id: 'A-002', name: 'Orphan', duration: 1 },
|
|
237
235
|
{ id: 'A-003', name: 'End', duration: 1, predecessors: ['A-001'] },
|
|
@@ -243,9 +241,9 @@ describe('validateActivities', () => {
|
|
|
243
241
|
});
|
|
244
242
|
it('full valid example passes with no errors', () => {
|
|
245
243
|
const r = validateActivities({
|
|
246
|
-
notation: '
|
|
244
|
+
notation: 'action',
|
|
247
245
|
title: 'Test Project',
|
|
248
|
-
|
|
246
|
+
actions: [
|
|
249
247
|
{ id: 'A', name: 'Start', duration: 3 },
|
|
250
248
|
{ id: 'B', name: 'Middle', duration: 5, predecessors: ['A'] },
|
|
251
249
|
{ id: 'C', name: 'End', duration: 2, predecessors: ['B'] },
|
|
@@ -296,8 +294,8 @@ describe('validateActivities', () => {
|
|
|
296
294
|
// ── Milestone date equality (ACT-016) ─────────────────────────────────────
|
|
297
295
|
it('ACT-016 — rejects milestone with mismatched start/end dates', () => {
|
|
298
296
|
const r = validateActivities({
|
|
299
|
-
notation: '
|
|
300
|
-
|
|
297
|
+
notation: 'action',
|
|
298
|
+
actions: [
|
|
301
299
|
{ id: 'M-1', name: 'Launch', duration: 0, start_date: '2026-06-01', end_date: '2026-06-02' },
|
|
302
300
|
],
|
|
303
301
|
});
|
|
@@ -306,8 +304,8 @@ describe('validateActivities', () => {
|
|
|
306
304
|
});
|
|
307
305
|
it('ACT-016 — accepts milestone with equal start/end dates', () => {
|
|
308
306
|
const r = validateActivities({
|
|
309
|
-
notation: '
|
|
310
|
-
|
|
307
|
+
notation: 'action',
|
|
308
|
+
actions: [
|
|
311
309
|
{ id: 'M-1', name: 'Launch', duration: 0, start_date: '2026-06-01', end_date: '2026-06-01' },
|
|
312
310
|
],
|
|
313
311
|
});
|
|
@@ -316,8 +314,8 @@ describe('validateActivities', () => {
|
|
|
316
314
|
// ── Phase warnings (ACT-017, ACT-018) ─────────────────────────────────────
|
|
317
315
|
it('ACT-017 — warns when a referenced-as-parent activity carries its own duration', () => {
|
|
318
316
|
const r = validateActivities({
|
|
319
|
-
notation: '
|
|
320
|
-
|
|
317
|
+
notation: 'action',
|
|
318
|
+
actions: [
|
|
321
319
|
{ id: 'PHASE-1', name: 'Design', duration: 5 },
|
|
322
320
|
{ id: 'A-1', name: 'Child', duration: 3, parent: 'PHASE-1' },
|
|
323
321
|
],
|
|
@@ -327,8 +325,8 @@ describe('validateActivities', () => {
|
|
|
327
325
|
});
|
|
328
326
|
it('ACT-017 — no warning when phase omits duration/dates and rolls up from children', () => {
|
|
329
327
|
const r = validateActivities({
|
|
330
|
-
notation: '
|
|
331
|
-
|
|
328
|
+
notation: 'action',
|
|
329
|
+
actions: [
|
|
332
330
|
{ id: 'PHASE-1', name: 'Design' },
|
|
333
331
|
{ id: 'A-1', name: 'Child', duration: 3, parent: 'PHASE-1' },
|
|
334
332
|
],
|
|
@@ -337,8 +335,8 @@ describe('validateActivities', () => {
|
|
|
337
335
|
});
|
|
338
336
|
it('ACT-018 — warns when an activity with no duration is not referenced as parent', () => {
|
|
339
337
|
const r = validateActivities({
|
|
340
|
-
notation: '
|
|
341
|
-
|
|
338
|
+
notation: 'action',
|
|
339
|
+
actions: [
|
|
342
340
|
{ id: 'A-1', name: 'Has duration', duration: 3 },
|
|
343
341
|
{ id: 'P-EMPTY', name: 'Looks like phase but no children' },
|
|
344
342
|
],
|
|
@@ -347,8 +345,8 @@ describe('validateActivities', () => {
|
|
|
347
345
|
});
|
|
348
346
|
it('ACT-018 — no warning when the no-duration activity has at least one child', () => {
|
|
349
347
|
const r = validateActivities({
|
|
350
|
-
notation: '
|
|
351
|
-
|
|
348
|
+
notation: 'action',
|
|
349
|
+
actions: [
|
|
352
350
|
{ id: 'PHASE-1', name: 'Phase' },
|
|
353
351
|
{ id: 'A-1', name: 'Child', duration: 3, parent: 'PHASE-1' },
|
|
354
352
|
],
|
|
@@ -366,24 +364,29 @@ describe('validateActivities', () => {
|
|
|
366
364
|
});
|
|
367
365
|
it('ACT-019 — no warning when every activity has pinned dates', () => {
|
|
368
366
|
const r = validateActivities({
|
|
369
|
-
notation: '
|
|
370
|
-
|
|
367
|
+
notation: 'action',
|
|
368
|
+
actions: [
|
|
371
369
|
{ id: 'A-1', name: 'A', duration: 3, start_date: '2026-06-01', end_date: '2026-06-04' },
|
|
372
370
|
],
|
|
373
371
|
});
|
|
374
372
|
expect(r.warnings.some(w => w.code === 'ACT-019')).toBe(false);
|
|
375
373
|
});
|
|
376
|
-
it('[blocker] tolerates a null element in
|
|
377
|
-
|
|
374
|
+
it('[blocker] tolerates a null element in actions[] without throwing', () => {
|
|
375
|
+
// Do NOT spread minimalValid here: validateActivities mutates its input
|
|
376
|
+
// to normalise raw.activities, so a previously-called test that passed
|
|
377
|
+
// minimalValid directly would have set minimalValid.activities. Spreading
|
|
378
|
+
// that mutated object would cause the actions:[null] override to be masked
|
|
379
|
+
// by the pre-existing activities array.
|
|
380
|
+
const r = validateActivities({ notation: 'action', actions: [null] });
|
|
378
381
|
expect(r.valid).toBe(false);
|
|
379
382
|
expect(r.errors.some(e => e.code === 'SCHEMA_INVALID')).toBe(true);
|
|
380
383
|
});
|
|
381
384
|
});
|
|
382
|
-
describe('
|
|
385
|
+
describe('action examples (regression)', () => {
|
|
383
386
|
const files = fs.readdirSync(EXAMPLES_DIR).filter(f => f.endsWith('.yaml'));
|
|
384
387
|
expect(files.length).toBeGreaterThan(0);
|
|
385
388
|
for (const file of files) {
|
|
386
|
-
it(`validates tests/fixtures/notation-corpus/
|
|
389
|
+
it(`validates tests/fixtures/notation-corpus/action/${file}`, () => {
|
|
387
390
|
const text = fs.readFileSync(path.join(EXAMPLES_DIR, file), 'utf8');
|
|
388
391
|
const parsed = yaml.load(text);
|
|
389
392
|
const r = validateActivities(parsed);
|
|
@@ -7,26 +7,19 @@ export function validateActivities(input) {
|
|
|
7
7
|
return { valid: false, errors: [{ code: 'ACT-001', message: 'Input must be an object' }], warnings };
|
|
8
8
|
}
|
|
9
9
|
const raw = input;
|
|
10
|
-
// ACT-001: notation must be "action"
|
|
10
|
+
// ACT-001: notation must be "action"
|
|
11
11
|
if (raw.notation === undefined) {
|
|
12
12
|
errors.push({ code: 'ACT-001', message: 'notation field is required' });
|
|
13
13
|
return { valid: false, errors, warnings };
|
|
14
14
|
}
|
|
15
|
-
if (raw.notation !== 'action'
|
|
15
|
+
if (raw.notation !== 'action') {
|
|
16
16
|
errors.push({ code: 'ACT-001', message: `notation must be "action", got "${String(raw.notation)}"` });
|
|
17
17
|
return { valid: false, errors, warnings };
|
|
18
18
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
// Accept "actions:" (canonical) or "activities:" (deprecated)
|
|
23
|
-
const rawArray = Array.isArray(raw.actions) ? raw.actions : (Array.isArray(raw.activities) ? raw.activities : undefined);
|
|
24
|
-
if (Array.isArray(raw.activities) && !Array.isArray(raw.actions)) {
|
|
25
|
-
warnings.push({ code: 'DEPRECATED_FIELD', message: 'Root key "activities:" is deprecated — rename to "actions:"' });
|
|
26
|
-
}
|
|
19
|
+
// Only "actions:" root key is accepted.
|
|
20
|
+
const rawArray = Array.isArray(raw.actions) ? raw.actions : undefined;
|
|
27
21
|
if (!rawArray) {
|
|
28
|
-
|
|
29
|
-
errors.push({ code: 'SCHEMA_INVALID', message: `${canonKey} must be an array`, path: canonKey });
|
|
22
|
+
errors.push({ code: 'SCHEMA_INVALID', message: 'actions must be an array', path: 'actions' });
|
|
30
23
|
return { valid: false, errors, warnings };
|
|
31
24
|
}
|
|
32
25
|
// Normalise onto raw.activities for the rest of this function (avoids touching downstream code)
|
|
@@ -10,7 +10,7 @@ import yaml from 'js-yaml';
|
|
|
10
10
|
import { validateActivityCard } from '../validate.js';
|
|
11
11
|
import { resolveActivityCard } from '../resolver.js';
|
|
12
12
|
import { layoutActivityCard } from '../layout.js';
|
|
13
|
-
const dir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../../../tests/fixtures/notation-corpus/
|
|
13
|
+
const dir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../../../tests/fixtures/notation-corpus/action-card');
|
|
14
14
|
const load = (f) => yaml.load(readFileSync(path.join(dir, f), 'utf-8'));
|
|
15
15
|
/** Recursively load every `*.yaml` doc under a canon subtree (mirrors the
|
|
16
16
|
* extension's element/relation walk). */
|
|
@@ -27,7 +27,7 @@ function loadYamlTree(root) {
|
|
|
27
27
|
}
|
|
28
28
|
describe('activity-card worked example (eu-programme)', () => {
|
|
29
29
|
it('validates, resolves, and lays out the success-signal card', () => {
|
|
30
|
-
const card = load('eu-programme.
|
|
30
|
+
const card = load('eu-programme.action-card.transitrix.yaml');
|
|
31
31
|
const elements = loadYamlTree(path.join(dir, 'canon', 'elements'));
|
|
32
32
|
const relations = loadYamlTree(path.join(dir, 'canon', 'relations'));
|
|
33
33
|
const v = validateActivityCard(card);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { validateActivityCard } from '../validate.js';
|
|
3
3
|
const VALID = {
|
|
4
|
-
notation: '
|
|
4
|
+
notation: 'action-card',
|
|
5
5
|
spec_version: '0.1',
|
|
6
|
-
|
|
6
|
+
action_card: {
|
|
7
7
|
id: 'ACTIVITY_CARD-EU-PROGRAMME-1',
|
|
8
8
|
project: 'ACTIVITY-EU-PROGRAMME-1',
|
|
9
9
|
description: 'Executive summary.',
|
|
@@ -18,11 +18,22 @@ const VALID = {
|
|
|
18
18
|
},
|
|
19
19
|
};
|
|
20
20
|
describe('validateActivityCard', () => {
|
|
21
|
-
it('accepts a well-formed card (
|
|
21
|
+
it('accepts a well-formed card (notation: action-card)', () => {
|
|
22
22
|
const r = validateActivityCard(VALID);
|
|
23
23
|
expect(r.valid).toBe(true);
|
|
24
24
|
expect(r.errors).toHaveLength(0);
|
|
25
|
-
expect(r.warnings.some(w => w.code === 'DEPRECATED_NOTATION')).toBe(
|
|
25
|
+
expect(r.warnings.some(w => w.code === 'DEPRECATED_NOTATION')).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
it('rejects deprecated notation: activity-card', () => {
|
|
28
|
+
const r = validateActivityCard({
|
|
29
|
+
notation: 'activity-card',
|
|
30
|
+
activity_card: {
|
|
31
|
+
id: 'ACTIVITY_CARD-EU-PROGRAMME-1',
|
|
32
|
+
project: 'ACTIVITY-EU-PROGRAMME-1',
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
expect(r.valid).toBe(false);
|
|
36
|
+
expect(r.errors.some(e => e.code === 'HDR-002')).toBe(true);
|
|
26
37
|
});
|
|
27
38
|
it('accepts notation: action-card with action_card block (canonical)', () => {
|
|
28
39
|
const r = validateActivityCard({
|
|
@@ -59,33 +70,33 @@ describe('validateActivityCard', () => {
|
|
|
59
70
|
const r = validateActivityCard({ ...VALID, notation: 'activities' });
|
|
60
71
|
expect(r.errors.some((e) => e.code === 'HDR-002')).toBe(true);
|
|
61
72
|
});
|
|
62
|
-
it('AC-001 on missing
|
|
63
|
-
const r = validateActivityCard({ notation: '
|
|
73
|
+
it('AC-001 on missing action_card block', () => {
|
|
74
|
+
const r = validateActivityCard({ notation: 'action-card' });
|
|
64
75
|
expect(r.valid).toBe(false);
|
|
65
76
|
expect(r.errors.some((e) => e.code === 'AC-001')).toBe(true);
|
|
66
77
|
});
|
|
67
78
|
it('AC-002 on malformed card id', () => {
|
|
68
|
-
const r = validateActivityCard({ ...VALID,
|
|
79
|
+
const r = validateActivityCard({ ...VALID, action_card: { ...VALID.action_card, id: 'CARD-1' } });
|
|
69
80
|
expect(r.errors.some((e) => e.code === 'AC-002')).toBe(true);
|
|
70
81
|
});
|
|
71
82
|
it('PC-001 on missing project', () => {
|
|
72
83
|
const r = validateActivityCard({
|
|
73
84
|
...VALID,
|
|
74
|
-
|
|
85
|
+
action_card: { ...VALID.action_card, project: undefined },
|
|
75
86
|
});
|
|
76
87
|
expect(r.errors.some((e) => e.code === 'PC-001')).toBe(true);
|
|
77
88
|
});
|
|
78
89
|
it('PC-001 on malformed project id', () => {
|
|
79
90
|
const r = validateActivityCard({
|
|
80
91
|
...VALID,
|
|
81
|
-
|
|
92
|
+
action_card: { ...VALID.action_card, project: 'GOAL-1' },
|
|
82
93
|
});
|
|
83
94
|
expect(r.errors.some((e) => e.code === 'PC-001')).toBe(true);
|
|
84
95
|
});
|
|
85
96
|
it('AC-003 on milestone missing required fields', () => {
|
|
86
97
|
const r = validateActivityCard({
|
|
87
98
|
...VALID,
|
|
88
|
-
|
|
99
|
+
action_card: { ...VALID.action_card, milestones: [{ id: 'MILESTONE-X-1' }] },
|
|
89
100
|
});
|
|
90
101
|
expect(r.valid).toBe(false);
|
|
91
102
|
expect(r.errors.some((e) => e.code === 'AC-003')).toBe(true);
|
|
@@ -93,8 +104,8 @@ describe('validateActivityCard', () => {
|
|
|
93
104
|
it('AC-004 on malformed milestone id', () => {
|
|
94
105
|
const r = validateActivityCard({
|
|
95
106
|
...VALID,
|
|
96
|
-
|
|
97
|
-
...VALID.
|
|
107
|
+
action_card: {
|
|
108
|
+
...VALID.action_card,
|
|
98
109
|
milestones: [{ id: 'MS-1', name: 'x', date: '2027-01-01' }],
|
|
99
110
|
},
|
|
100
111
|
});
|
|
@@ -104,15 +115,15 @@ describe('validateActivityCard', () => {
|
|
|
104
115
|
const m = { id: 'MILESTONE-X-1', name: 'x', date: '2027-01-01' };
|
|
105
116
|
const r = validateActivityCard({
|
|
106
117
|
...VALID,
|
|
107
|
-
|
|
118
|
+
action_card: { ...VALID.action_card, milestones: [m, { ...m }] },
|
|
108
119
|
});
|
|
109
120
|
expect(r.errors.some((e) => e.code === 'AC-004' && /Duplicate/.test(e.message))).toBe(true);
|
|
110
121
|
});
|
|
111
122
|
it('AC-005 on bad milestone date format', () => {
|
|
112
123
|
const r = validateActivityCard({
|
|
113
124
|
...VALID,
|
|
114
|
-
|
|
115
|
-
...VALID.
|
|
125
|
+
action_card: {
|
|
126
|
+
...VALID.action_card,
|
|
116
127
|
milestones: [{ id: 'MILESTONE-X-1', name: 'x', date: '31/01/2027' }],
|
|
117
128
|
},
|
|
118
129
|
});
|
|
@@ -121,8 +132,8 @@ describe('validateActivityCard', () => {
|
|
|
121
132
|
it('AC-006 on malformed delivers_changes entry', () => {
|
|
122
133
|
const r = validateActivityCard({
|
|
123
134
|
...VALID,
|
|
124
|
-
|
|
125
|
-
...VALID.
|
|
135
|
+
action_card: {
|
|
136
|
+
...VALID.action_card,
|
|
126
137
|
milestones: [{ id: 'MILESTONE-X-1', name: 'x', date: '2027-01-01', delivers_changes: ['GOAL-1'] }],
|
|
127
138
|
},
|
|
128
139
|
});
|
|
@@ -131,7 +142,7 @@ describe('validateActivityCard', () => {
|
|
|
131
142
|
it('accepts a card with no milestones', () => {
|
|
132
143
|
const r = validateActivityCard({
|
|
133
144
|
...VALID,
|
|
134
|
-
|
|
145
|
+
action_card: { id: 'ACTIVITY_CARD-X-1', project: 'ACTIVITY-X-1' },
|
|
135
146
|
});
|
|
136
147
|
expect(r.valid).toBe(true);
|
|
137
148
|
});
|
|
@@ -40,21 +40,14 @@ export function validateActivityCard(input) {
|
|
|
40
40
|
if (!('notation' in raw)) {
|
|
41
41
|
errors.push({ code: 'HDR-001', message: 'notation field is required' });
|
|
42
42
|
}
|
|
43
|
-
else if (raw['notation'] !== 'action-card'
|
|
43
|
+
else if (raw['notation'] !== 'action-card') {
|
|
44
44
|
errors.push({
|
|
45
45
|
code: 'HDR-002',
|
|
46
46
|
message: `notation must be "action-card", got "${String(raw['notation'])}"`,
|
|
47
47
|
});
|
|
48
48
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
// ── action_card / activity_card block ───────────────────────────────────────
|
|
53
|
-
// "action_card:" is canonical; "activity_card:" is accepted for compat.
|
|
54
|
-
const rawBlock = raw['action_card'] ?? raw['activity_card'];
|
|
55
|
-
if (raw['activity_card'] !== undefined && raw['action_card'] === undefined) {
|
|
56
|
-
warnings.push({ code: 'DEPRECATED_FIELD', message: 'Root key "activity_card:" is deprecated — rename to "action_card:"' });
|
|
57
|
-
}
|
|
49
|
+
// ── action_card block ────────────────────────────────────────────────────────
|
|
50
|
+
const rawBlock = raw['action_card'];
|
|
58
51
|
const block = rawBlock;
|
|
59
52
|
if (!block || typeof block !== 'object' || Array.isArray(block)) {
|
|
60
53
|
errors.push({ code: 'AC-001', message: 'Missing required root key: action_card (object)' });
|
|
@@ -86,4 +86,10 @@ describe('validateActor — ACTOR-003 (engagement fields forbidden inline)', ()
|
|
|
86
86
|
it('flags inline owner', () => {
|
|
87
87
|
expect(codes({ ...valid(), owner: 'ACTOR-OPS-1' })).toContain('ACTOR-003');
|
|
88
88
|
});
|
|
89
|
+
it('flags inline located_at (location belongs in a REL file)', () => {
|
|
90
|
+
expect(codes({ ...valid(), located_at: 'LOCATION-TBILISI-1' })).toContain('ACTOR-003');
|
|
91
|
+
});
|
|
92
|
+
it('flags inline unit_located_at (location belongs in a REL file)', () => {
|
|
93
|
+
expect(codes({ ...valid(), unit_located_at: 'LOCATION-TBILISI-1' })).toContain('ACTOR-003');
|
|
94
|
+
});
|
|
89
95
|
});
|
package/dist/actor/types.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export type ActorType = typeof ACTOR_TYPES[number];
|
|
|
6
6
|
* inline on an ACTOR file (§7.10, ACTOR-003). They belong in time-aware REL
|
|
7
7
|
* records, never on the identity primitive.
|
|
8
8
|
*/
|
|
9
|
-
export declare const ACTOR_FORBIDDEN_INLINE_FIELDS: readonly ["employment", "candidacy", "alumni_membership", "community_membership", "contracting", "unit_parent", "roles", "owner"];
|
|
9
|
+
export declare const ACTOR_FORBIDDEN_INLINE_FIELDS: readonly ["employment", "candidacy", "alumni_membership", "community_membership", "contracting", "unit_parent", "located_at", "unit_located_at", "roles", "owner"];
|
|
10
10
|
export interface Actor {
|
|
11
11
|
notation: 'actor';
|
|
12
12
|
id: string;
|