@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.
Files changed (140) hide show
  1. package/dist/__tests__/yaml-normalize.test.js +9 -9
  2. package/dist/activities/__tests__/validate.test.js +77 -74
  3. package/dist/activities/validate.js +5 -12
  4. package/dist/activity-card/__tests__/example.test.js +3 -5
  5. package/dist/activity-card/__tests__/layout.test.js +2 -9
  6. package/dist/activity-card/__tests__/resolver.test.js +25 -0
  7. package/dist/activity-card/__tests__/validate.test.js +29 -18
  8. package/dist/activity-card/layout.js +1 -7
  9. package/dist/activity-card/resolver.js +11 -2
  10. package/dist/activity-card/types.d.ts +1 -1
  11. package/dist/activity-card/validate.js +3 -10
  12. package/dist/actor/__tests__/validate.test.js +6 -0
  13. package/dist/actor/types.d.ts +1 -1
  14. package/dist/actor/types.js +2 -0
  15. package/dist/applications/types.d.ts +7 -0
  16. package/dist/applications/validate.js +18 -0
  17. package/dist/blocks/__tests__/layout.test.js +1 -1
  18. package/dist/blocks/layout.js +1 -1
  19. package/dist/business-service/__tests__/validate.test.d.ts +1 -0
  20. package/dist/business-service/__tests__/validate.test.js +135 -0
  21. package/dist/business-service/index.d.ts +3 -0
  22. package/dist/business-service/index.js +2 -0
  23. package/dist/business-service/types.d.ts +20 -0
  24. package/dist/business-service/types.js +3 -0
  25. package/dist/business-service/validate.d.ts +2 -0
  26. package/dist/business-service/validate.js +74 -0
  27. package/dist/fgca/__tests__/parse-canonical.test.js +24 -11
  28. package/dist/fgca/__tests__/resolver.test.js +7 -7
  29. package/dist/fgca/__tests__/validate.test.js +11 -11
  30. package/dist/fgca/parse-canonical.js +18 -32
  31. package/dist/fgca/preview-layout.js +50 -1
  32. package/dist/fgca/resolver.d.ts +2 -2
  33. package/dist/fgca/resolver.js +5 -5
  34. package/dist/fgca/validate.js +4 -13
  35. package/dist/index.d.ts +5 -0
  36. package/dist/index.js +5 -0
  37. package/dist/integration/__tests__/validate.test.d.ts +1 -0
  38. package/dist/integration/__tests__/validate.test.js +169 -0
  39. package/dist/integration/index.d.ts +2 -0
  40. package/dist/integration/index.js +1 -0
  41. package/dist/integration/types.d.ts +27 -0
  42. package/dist/integration/types.js +5 -0
  43. package/dist/integration/validate.d.ts +2 -0
  44. package/dist/integration/validate.js +94 -0
  45. package/dist/location/__tests__/validate.test.d.ts +1 -0
  46. package/dist/location/__tests__/validate.test.js +107 -0
  47. package/dist/location/index.d.ts +3 -0
  48. package/dist/location/index.js +2 -0
  49. package/dist/location/types.d.ts +20 -0
  50. package/dist/location/types.js +3 -0
  51. package/dist/location/validate.d.ts +2 -0
  52. package/dist/location/validate.js +63 -0
  53. package/dist/node/__tests__/validate.test.d.ts +1 -0
  54. package/dist/node/__tests__/validate.test.js +100 -0
  55. package/dist/node/index.d.ts +2 -0
  56. package/dist/node/index.js +1 -0
  57. package/dist/node/types.d.ts +18 -0
  58. package/dist/node/types.js +3 -0
  59. package/dist/node/validate.d.ts +2 -0
  60. package/dist/node/validate.js +53 -0
  61. package/dist/repo-validate/__tests__/validate-repo.test.js +444 -0
  62. package/dist/repo-validate/validate-repo.js +224 -10
  63. package/dist/technology-service/__tests__/validate.test.d.ts +1 -0
  64. package/dist/technology-service/__tests__/validate.test.js +121 -0
  65. package/dist/technology-service/index.d.ts +2 -0
  66. package/dist/technology-service/index.js +1 -0
  67. package/dist/technology-service/types.d.ts +18 -0
  68. package/dist/technology-service/types.js +3 -0
  69. package/dist/technology-service/validate.d.ts +2 -0
  70. package/dist/technology-service/validate.js +64 -0
  71. package/dist/webview/__tests__/entry-notations.test.js +2 -4
  72. package/dist/webview/__tests__/render-activities.test.d.ts +1 -0
  73. package/dist/webview/__tests__/render-activities.test.js +91 -0
  74. package/dist/webview/__tests__/render-blocks.test.d.ts +1 -0
  75. package/dist/webview/__tests__/render-blocks.test.js +130 -0
  76. package/dist/webview/entry.d.ts +1 -1
  77. package/dist/webview/entry.js +8 -15
  78. package/dist/webview/render-activities.d.ts +12 -0
  79. package/dist/webview/render-activities.js +9 -3
  80. package/dist/webview/render-activity-card.js +2 -3
  81. package/dist/webview/render-blocks.js +97 -18
  82. package/dist/webview/render-fgca.js +1 -1
  83. package/dist/webview/render-goals.js +28 -5
  84. package/package.json +1 -1
  85. package/src/__tests__/yaml-normalize.test.ts +11 -11
  86. package/src/activities/__tests__/validate.test.ts +77 -74
  87. package/src/activities/validate.ts +5 -12
  88. package/src/activity-card/__tests__/example.test.ts +3 -5
  89. package/src/activity-card/__tests__/layout.test.ts +2 -9
  90. package/src/activity-card/__tests__/resolver.test.ts +27 -0
  91. package/src/activity-card/__tests__/validate.test.ts +30 -18
  92. package/src/activity-card/layout.ts +1 -8
  93. package/src/activity-card/resolver.ts +11 -2
  94. package/src/activity-card/types.ts +1 -1
  95. package/src/activity-card/validate.ts +3 -9
  96. package/src/actor/__tests__/validate.test.ts +8 -0
  97. package/src/actor/types.ts +2 -0
  98. package/src/applications/types.ts +8 -0
  99. package/src/applications/validate.ts +20 -0
  100. package/src/blocks/__tests__/layout.test.ts +1 -1
  101. package/src/blocks/layout.ts +1 -1
  102. package/src/business-service/__tests__/validate.test.ts +160 -0
  103. package/src/business-service/index.ts +3 -0
  104. package/src/business-service/types.ts +29 -0
  105. package/src/business-service/validate.ts +82 -0
  106. package/src/fgca/__tests__/parse-canonical.test.ts +25 -11
  107. package/src/fgca/__tests__/resolver.test.ts +7 -7
  108. package/src/fgca/__tests__/validate.test.ts +11 -11
  109. package/src/fgca/parse-canonical.ts +18 -38
  110. package/src/fgca/preview-layout.ts +53 -1
  111. package/src/fgca/resolver.ts +5 -5
  112. package/src/fgca/validate.ts +4 -12
  113. package/src/index.ts +5 -0
  114. package/src/integration/__tests__/validate.test.ts +203 -0
  115. package/src/integration/index.ts +2 -0
  116. package/src/integration/types.ts +38 -0
  117. package/src/integration/validate.ts +104 -0
  118. package/src/location/__tests__/validate.test.ts +127 -0
  119. package/src/location/index.ts +3 -0
  120. package/src/location/types.ts +29 -0
  121. package/src/location/validate.ts +70 -0
  122. package/src/node/__tests__/validate.test.ts +118 -0
  123. package/src/node/index.ts +2 -0
  124. package/src/node/types.ts +27 -0
  125. package/src/node/validate.ts +59 -0
  126. package/src/repo-validate/__tests__/validate-repo.test.ts +550 -0
  127. package/src/repo-validate/validate-repo.ts +233 -10
  128. package/src/technology-service/__tests__/validate.test.ts +143 -0
  129. package/src/technology-service/index.ts +2 -0
  130. package/src/technology-service/types.ts +27 -0
  131. package/src/technology-service/validate.ts +71 -0
  132. package/src/webview/__tests__/entry-notations.test.ts +2 -4
  133. package/src/webview/__tests__/render-activities.test.ts +100 -0
  134. package/src/webview/__tests__/render-blocks.test.ts +155 -0
  135. package/src/webview/entry.ts +10 -19
  136. package/src/webview/render-activities.ts +29 -4
  137. package/src/webview/render-activity-card.ts +2 -3
  138. package/src/webview/render-blocks.ts +101 -20
  139. package/src/webview/render-fgca.ts +1 -1
  140. package/src/webview/render-goals.ts +26 -5
@@ -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: activities',
58
- 'activities:',
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.activities[0].start_date).toBe('2026-06-01');
69
- expect(parsed.activities[0].end_date).toBe('2026-06-03');
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: activities',
76
- 'activities:',
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.activities[0].start_date).toBeInstanceOf(Date);
88
+ expect(parsed.actions[0].start_date).toBeInstanceOf(Date);
89
89
  coerceDatesToIsoStrings(parsed);
90
- expect(parsed.activities[0].start_date).toBe('2026-06-01');
91
- expect(parsed.activities[0].end_date).toBe('2026-06-03');
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', 'activities');
6
+ const EXAMPLES_DIR = path.resolve(process.cwd(), '..', '..', 'tests', 'fixtures', 'notation-corpus', 'action');
7
7
  const minimalValid = {
8
- notation: 'activities',
9
- activities: [
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: activities (deprecated)', () => {
15
+ it('ACT-001 — accepts notation: action (canonical)', () => {
16
16
  const r = validateActivities(minimalValid);
17
17
  expect(r.valid).toBe(true);
18
- // deprecated notation emits a warning, not an error
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 — accepts notation: action (canonical)', () => {
20
+ it('ACT-001 — rejects notation: activities (no longer accepted)', () => {
22
21
  const r = validateActivities({
23
- notation: 'action',
24
- actions: [
25
- { id: 'ACTION-001', name: 'Start', duration: 3 },
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(true);
30
- expect(r.warnings.some(w => w.code === 'DEPRECATED_NOTATION')).toBe(false);
27
+ expect(r.valid).toBe(false);
28
+ expect(r.errors.some(e => e.code === 'ACT-001')).toBe(true);
31
29
  });
32
- it('ACT-001 — accepts notation: action with deprecated activities: key (emits field warning)', () => {
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(true);
38
- expect(r.warnings.some(w => w.code === 'DEPRECATED_FIELD')).toBe(true);
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: 'activities',
64
- activities: [{ name: 'No ID', duration: 1 }],
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: 'activities',
72
- activities: [{ id: ' ', name: 'Empty ID', duration: 1 }],
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: 'activities',
80
- activities: [{ id: 'A-001', duration: 1 }],
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: 'activities',
88
- activities: [
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: 'activities',
99
- activities: [{ id: 'A-001', name: 'Task', duration: 1, predecessors: ['MISSING'] }],
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: 'activities',
112
- activities: [
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: 'activities',
123
- activities: [{ id: 'A-001', name: 'Task', duration: 1, predecessors: ['A-001'] }],
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: 'activities',
131
- activities: [
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: 'activities',
141
- activities: [
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: 'activities',
150
- activities: [
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: 'activities',
160
- activities: [
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: 'activities',
174
- activities: [
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: 'activities',
184
- activities: [{ id: 'A-001', name: 'Task', duration: -1 }],
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: 'activities',
192
- activities: [{ id: 'A-001', name: 'Milestone', duration: 0 }],
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: 'activities',
199
- activities: [{ id: 'A-001', name: 'Task', duration: 1, goal: 'GOAL-001' }],
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: 'activities',
207
- activities: [
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: 'activities',
218
- activities: [{ id: 'A-001', name: 'Task', duration: 1, tag: 'q3' }],
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: 'activities',
226
- activities: [{ id: 'A-001', name: 'No duration' }],
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: 'activities',
234
- activities: [
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: 'activities',
244
+ notation: 'action',
247
245
  title: 'Test Project',
248
- activities: [
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: 'activities',
300
- activities: [
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: 'activities',
310
- activities: [
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: 'activities',
320
- activities: [
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: 'activities',
331
- activities: [
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: 'activities',
341
- activities: [
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: 'activities',
351
- activities: [
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: 'activities',
370
- activities: [
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 activities[] without throwing', () => {
377
- const r = validateActivities({ ...minimalValid, activities: [null] });
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('activities examples (regression)', () => {
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/activities/${file}`, () => {
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" (canonical) or "activities" (deprecated)
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' && raw.notation !== 'activities') {
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
- if (raw.notation === 'activities') {
20
- warnings.push({ code: 'DEPRECATED_NOTATION', message: 'notation: "activities" is deprecated — migrate to notation: "action"' });
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
- const canonKey = raw.notation === 'action' ? 'actions' : 'activities';
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/activity-card');
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.activity-card.transitrix.yaml');
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);
@@ -61,13 +61,11 @@ describe('activity-card worked example (eu-programme)', () => {
61
61
  // Description row
62
62
  expect(layout.descriptionRow?.label).toBe('Description');
63
63
  // Chain sections
64
- expect(layout.chainSections.map((s) => s.type)).toEqual(['drivers', 'assessments', 'goals', 'changes']);
64
+ expect(layout.chainSections.map((s) => s.type)).toEqual(['drivers', 'goals', 'changes']);
65
65
  const driversSection = layout.chainSections.find((s) => s.type === 'drivers');
66
66
  const goalsSection = layout.chainSections.find((s) => s.type === 'goals');
67
- const assessmentsSection = layout.chainSections.find((s) => s.type === 'assessments');
68
67
  expect(driversSection.nodes.map((n) => n.id)).toContain('FACTOR-EU-MDR-1');
69
68
  expect(goalsSection.nodes.map((n) => n.id)).toContain('GOAL-EU-MARKET-1');
70
- expect(assessmentsSection.isEmpty).toBe(true); // no assessment in this fixture
71
69
  // Chain edges
72
70
  expect(layout.chainEdges).toContainEqual({ sourceId: 'FACTOR-EU-MDR-1', targetId: 'GOAL-EU-MARKET-1' });
73
71
  expect(layout.chainEdges).toContainEqual({ sourceId: 'GOAL-EU-MARKET-1', targetId: 'CHANGE-EU-COMPLIANCE-1' });
@@ -82,19 +82,16 @@ describe('layoutActivityCard', () => {
82
82
  const l = layoutActivityCard({ ...RESOLVED, cardDescription: undefined });
83
83
  expect(l.descriptionRow).toBeUndefined();
84
84
  });
85
- it('four chain sections in order: drivers, assessments, goals, changes', () => {
85
+ it('three chain sections in order: drivers, goals, changes', () => {
86
86
  const l = layoutActivityCard(RESOLVED);
87
- expect(l.chainSections.map((s) => s.type)).toEqual(['drivers', 'assessments', 'goals', 'changes']);
87
+ expect(l.chainSections.map((s) => s.type)).toEqual(['drivers', 'goals', 'changes']);
88
88
  });
89
89
  it('chain sections carry the resolved nodes', () => {
90
90
  const l = layoutActivityCard(RESOLVED);
91
91
  const drivers = l.chainSections.find((s) => s.type === 'drivers');
92
- const assessments = l.chainSections.find((s) => s.type === 'assessments');
93
92
  const goals = l.chainSections.find((s) => s.type === 'goals');
94
93
  const changes = l.chainSections.find((s) => s.type === 'changes');
95
94
  expect(drivers.nodes.map((n) => n.id)).toEqual(['FACTOR-EU-MDR-1']);
96
- expect(assessments.nodes.map((n) => n.id)).toEqual(['ASSESSMENT-MDR-1']);
97
- expect(assessments.nodes[0].meta).toBe('2026-03-01');
98
95
  expect(goals.nodes.map((n) => n.id)).toEqual(['GOAL-EU-MARKET-1']);
99
96
  expect(changes.nodes.map((n) => n.id)).toEqual(['CHANGE-EU-COMPLIANCE-1']);
100
97
  });
@@ -111,10 +108,6 @@ describe('layoutActivityCard', () => {
111
108
  expect(l.chainEdges).toContainEqual({ sourceId: 'FACTOR-EU-MDR-1', targetId: 'GOAL-EU-MARKET-1' });
112
109
  expect(l.chainEdges).toContainEqual({ sourceId: 'GOAL-EU-MARKET-1', targetId: 'CHANGE-EU-COMPLIANCE-1' });
113
110
  });
114
- it('builds driver→assessment edge when assessment is linked', () => {
115
- const l = layoutActivityCard(RESOLVED);
116
- expect(l.chainEdges).toContainEqual({ sourceId: 'FACTOR-EU-MDR-1', targetId: 'ASSESSMENT-MDR-1' });
117
- });
118
111
  it('milestone ArchiMate class is set', () => {
119
112
  const l = layoutActivityCard(RESOLVED);
120
113
  expect(l.milestones).toHaveLength(1);
@@ -198,6 +198,31 @@ describe('resolveActivityCard', () => {
198
198
  expect(r.resolved.stakeholders).toEqual([{ id: 'STAKEHOLDER-OPS-1', name: 'STAKEHOLDER-OPS-1', role: 'sponsor' }]);
199
199
  expect(r.warnings.some((w) => w.code === 'PC-001')).toBe(true);
200
200
  });
201
+ it('resolves stakeholders from inline stakeholders: [] on the Action element', () => {
202
+ const projectWithInlineStakeholders = { ...PROJECT, stakeholders: ['STAKEHOLDER-OPS-1'] };
203
+ const r = resolveActivityCard(CARD, {
204
+ elements: [projectWithInlineStakeholders, CHILD, FACTOR, GOAL, CHANGE, STAKEHOLDER],
205
+ relations: [REL_GOAL],
206
+ });
207
+ expect(r.valid).toBe(true);
208
+ const sh = r.resolved.stakeholders;
209
+ expect(sh).toHaveLength(1);
210
+ expect(sh[0].id).toBe('STAKEHOLDER-OPS-1');
211
+ expect(sh[0].name).toBe('Operations');
212
+ expect(sh[0].role).toBeUndefined();
213
+ });
214
+ it('REL stakeholder takes precedence over inline duplicate (carries role)', () => {
215
+ const projectWithInlineStakeholders = { ...PROJECT, stakeholders: ['STAKEHOLDER-OPS-1'] };
216
+ const r = resolveActivityCard(CARD, {
217
+ elements: [projectWithInlineStakeholders, CHILD, FACTOR, GOAL, CHANGE, STAKEHOLDER],
218
+ relations: [REL_GOAL, REL_STAKEHOLDER],
219
+ });
220
+ expect(r.valid).toBe(true);
221
+ const sh = r.resolved.stakeholders;
222
+ // deduplicated — REL entry wins, inline duplicate is dropped
223
+ expect(sh).toHaveLength(1);
224
+ expect(sh[0].role).toBe('sponsor');
225
+ });
201
226
  it('falls back to the inline goals field when no activity_goal relation exists', () => {
202
227
  const projectWithInline = { ...PROJECT, goals: ['GOAL-EU-MARKET-1'] };
203
228
  const r = resolveActivityCard(CARD, {