@transitrix/diagrams 1.3.0 → 1.5.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__/gantt.test.js +39 -0
- 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 +178 -0
- package/dist/webview/entry.d.ts +1 -1
- package/dist/webview/entry.js +8 -15
- package/dist/webview/notation-style.d.ts +11 -0
- package/dist/webview/notation-style.js +11 -0
- package/dist/webview/render-activities.d.ts +12 -0
- package/dist/webview/render-activities.js +9 -3
- package/dist/webview/render-blocks.js +102 -19
- package/dist/webview/render-goals.js +2 -1
- package/package.json +1 -1
- package/src/__tests__/yaml-normalize.test.ts +11 -11
- package/src/activities/__tests__/gantt.test.ts +45 -0
- 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 +209 -0
- package/src/webview/entry.ts +10 -19
- package/src/webview/notation-style.ts +12 -0
- package/src/webview/render-activities.ts +29 -4
- package/src/webview/render-blocks.ts +106 -21
- package/src/webview/render-goals.ts +2 -1
|
@@ -134,17 +134,231 @@ function checkReferentialIntegrity(input, findings) {
|
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
|
-
/** Phase 5 — ArchiMate layer-semantics on relations.
|
|
137
|
+
/** Phase 5 — ArchiMate layer-semantics on relations and elements.
|
|
138
138
|
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
139
|
+
* Enforces endpoint-type constraints for relation kinds whose methodology
|
|
140
|
+
* semantics are formally defined. Rules added here must have a corresponding
|
|
141
|
+
* entry in methodology `notations/elements/17-relations.md`; do not add
|
|
142
|
+
* speculative rules.
|
|
143
|
+
*
|
|
144
|
+
* Implemented relation kinds:
|
|
145
|
+
* unit_located_at — ACTOR(business_unit) → LOCATION (21-locations.md)
|
|
146
|
+
* located_at — ACTOR(person|business_unit) → LOCATION (canonical form)
|
|
147
|
+
* offers — ACTOR(business_unit)|ROLE → BUSINESS_SERVICE (25-business-services.md)
|
|
148
|
+
* realizes — BUSINESS_SERVICE → CAPABILITY (25-business-services.md)
|
|
149
|
+
* hosts — NODE → TECHNOLOGY_SERVICE (25-nodes.md / 26-technology-services.md)
|
|
150
|
+
* uses — APPLICATION → TECHNOLOGY_SERVICE (26-technology-services.md)
|
|
151
|
+
*
|
|
152
|
+
* Implemented element checks:
|
|
153
|
+
* TSVC-003 — TECHNOLOGY_SERVICE.node must resolve to NODE notation
|
|
154
|
+
* (26-technology-services.md §5)
|
|
155
|
+
*
|
|
156
|
+
* Implemented element checks:
|
|
157
|
+
* INT-002 — INTEGRATION(interface_semantics: true) source/target → APPLICATION
|
|
158
|
+
* (ELEMENT_PRIMITIVES.md §7.8.1)
|
|
159
|
+
*
|
|
160
|
+
* lint.py ships this as a no-op stub; Studio is ahead of the Python tool here.
|
|
161
|
+
* These findings are non-blocking for cross-tool parity — they surface only in
|
|
162
|
+
* the TypeScript validator path (CLI `validate --scope=repo`). */
|
|
163
|
+
function checkLayerSemantics(input, findings) {
|
|
164
|
+
const elementById = new Map();
|
|
165
|
+
for (const doc of input.elements) {
|
|
166
|
+
const id = docId(doc);
|
|
167
|
+
if (id && doc.data)
|
|
168
|
+
elementById.set(id, doc.data);
|
|
169
|
+
}
|
|
170
|
+
for (const doc of input.relations) {
|
|
171
|
+
if (!doc.data)
|
|
172
|
+
continue;
|
|
173
|
+
const relId = docId(doc) ?? '';
|
|
174
|
+
const relType = doc.data['type'];
|
|
175
|
+
if (typeof relType !== 'string')
|
|
176
|
+
continue;
|
|
177
|
+
const fromId = endpointId(doc.data['from']) ?? endpointId(doc.data['source']);
|
|
178
|
+
const toId = endpointId(doc.data['to']) ?? endpointId(doc.data['target']);
|
|
179
|
+
if (relType === 'unit_located_at') {
|
|
180
|
+
if (fromId) {
|
|
181
|
+
const from = elementById.get(fromId);
|
|
182
|
+
if (from && (from['notation'] !== 'actor' || from['type'] !== 'business_unit')) {
|
|
183
|
+
findings.push({
|
|
184
|
+
scope: PScope,
|
|
185
|
+
id: relId,
|
|
186
|
+
message: `Layer-semantics: '${relId}' type 'unit_located_at' requires from to be ` +
|
|
187
|
+
`ACTOR(business_unit); got notation='${from['notation']}', type='${from['type']}'.`,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (toId) {
|
|
192
|
+
const to = elementById.get(toId);
|
|
193
|
+
if (to && to['notation'] !== 'location') {
|
|
194
|
+
findings.push({
|
|
195
|
+
scope: PScope,
|
|
196
|
+
id: relId,
|
|
197
|
+
message: `Layer-semantics: '${relId}' type 'unit_located_at' requires to to be a LOCATION; ` +
|
|
198
|
+
`got notation='${to['notation']}'.`,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
else if (relType === 'located_at') {
|
|
204
|
+
if (fromId) {
|
|
205
|
+
const from = elementById.get(fromId);
|
|
206
|
+
if (from &&
|
|
207
|
+
(from['notation'] !== 'actor' ||
|
|
208
|
+
(from['type'] !== 'person' && from['type'] !== 'business_unit'))) {
|
|
209
|
+
findings.push({
|
|
210
|
+
scope: PScope,
|
|
211
|
+
id: relId,
|
|
212
|
+
message: `Layer-semantics: '${relId}' type 'located_at' requires from to be ` +
|
|
213
|
+
`ACTOR(person|business_unit); got notation='${from['notation']}', type='${from['type']}'.`,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (toId) {
|
|
218
|
+
const to = elementById.get(toId);
|
|
219
|
+
if (to && to['notation'] !== 'location') {
|
|
220
|
+
findings.push({
|
|
221
|
+
scope: PScope,
|
|
222
|
+
id: relId,
|
|
223
|
+
message: `Layer-semantics: '${relId}' type 'located_at' requires to to be a LOCATION; ` +
|
|
224
|
+
`got notation='${to['notation']}'.`,
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
else if (relType === 'offers') {
|
|
230
|
+
if (fromId) {
|
|
231
|
+
const from = elementById.get(fromId);
|
|
232
|
+
if (from &&
|
|
233
|
+
from['notation'] !== 'actor' &&
|
|
234
|
+
from['notation'] !== 'role') {
|
|
235
|
+
findings.push({
|
|
236
|
+
scope: PScope,
|
|
237
|
+
id: relId,
|
|
238
|
+
message: `Layer-semantics: '${relId}' type 'offers' requires from to be ` +
|
|
239
|
+
`ACTOR(business_unit) or ROLE; got notation='${from['notation']}'.`,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (toId) {
|
|
244
|
+
const to = elementById.get(toId);
|
|
245
|
+
if (to && to['notation'] !== 'business-service') {
|
|
246
|
+
findings.push({
|
|
247
|
+
scope: PScope,
|
|
248
|
+
id: relId,
|
|
249
|
+
message: `Layer-semantics: '${relId}' type 'offers' requires to to be a BUSINESS_SERVICE; ` +
|
|
250
|
+
`got notation='${to['notation']}'.`,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
else if (relType === 'realizes') {
|
|
256
|
+
if (fromId) {
|
|
257
|
+
const from = elementById.get(fromId);
|
|
258
|
+
if (from && from['notation'] !== 'business-service') {
|
|
259
|
+
findings.push({
|
|
260
|
+
scope: PScope,
|
|
261
|
+
id: relId,
|
|
262
|
+
message: `Layer-semantics: '${relId}' type 'realizes' requires from to be ` +
|
|
263
|
+
`BUSINESS_SERVICE; got notation='${from['notation']}'.`,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
else if (relType === 'hosts') {
|
|
269
|
+
if (fromId) {
|
|
270
|
+
const from = elementById.get(fromId);
|
|
271
|
+
if (from && from['notation'] !== 'node') {
|
|
272
|
+
findings.push({
|
|
273
|
+
scope: PScope,
|
|
274
|
+
id: relId,
|
|
275
|
+
message: `Layer-semantics: '${relId}' type 'hosts' requires from to be a NODE; ` +
|
|
276
|
+
`got notation='${from['notation']}'.`,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (toId) {
|
|
281
|
+
const to = elementById.get(toId);
|
|
282
|
+
if (to && to['notation'] !== 'technology-service') {
|
|
283
|
+
findings.push({
|
|
284
|
+
scope: PScope,
|
|
285
|
+
id: relId,
|
|
286
|
+
message: `Layer-semantics: '${relId}' type 'hosts' requires to to be a TECHNOLOGY_SERVICE; ` +
|
|
287
|
+
`got notation='${to['notation']}'.`,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
else if (relType === 'uses') {
|
|
293
|
+
if (fromId) {
|
|
294
|
+
const from = elementById.get(fromId);
|
|
295
|
+
if (from && from['notation'] !== 'application') {
|
|
296
|
+
findings.push({
|
|
297
|
+
scope: PScope,
|
|
298
|
+
id: relId,
|
|
299
|
+
message: `Layer-semantics: '${relId}' type 'uses' requires from to be an APPLICATION; ` +
|
|
300
|
+
`got notation='${from['notation']}'.`,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (toId) {
|
|
305
|
+
const to = elementById.get(toId);
|
|
306
|
+
if (to && to['notation'] !== 'technology-service') {
|
|
307
|
+
findings.push({
|
|
308
|
+
scope: PScope,
|
|
309
|
+
id: relId,
|
|
310
|
+
message: `Layer-semantics: '${relId}' type 'uses' requires to to be a TECHNOLOGY_SERVICE; ` +
|
|
311
|
+
`got notation='${to['notation']}'.`,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
// TSVC-003 — TECHNOLOGY_SERVICE.node must resolve to a NODE-notation element.
|
|
318
|
+
for (const doc of input.elements) {
|
|
319
|
+
if (!doc.data)
|
|
320
|
+
continue;
|
|
321
|
+
if (doc.data['notation'] !== 'technology-service')
|
|
322
|
+
continue;
|
|
323
|
+
const nodeRef = doc.data['node'];
|
|
324
|
+
if (typeof nodeRef !== 'string' || nodeRef.trim() === '')
|
|
325
|
+
continue;
|
|
326
|
+
const svcId = docId(doc) ?? '';
|
|
327
|
+
const resolved = elementById.get(nodeRef);
|
|
328
|
+
if (resolved && resolved['notation'] !== 'node') {
|
|
329
|
+
findings.push({
|
|
330
|
+
scope: PScope,
|
|
331
|
+
id: svcId,
|
|
332
|
+
message: `TSVC-003: TECHNOLOGY_SERVICE '${svcId}' node '${nodeRef}' ` +
|
|
333
|
+
`must resolve to a NODE element; got notation='${resolved['notation']}'.`,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// INT-002 — INTEGRATION elements with interface_semantics: true must have
|
|
338
|
+
// source and target that resolve to APPLICATION-notation elements.
|
|
339
|
+
for (const doc of input.elements) {
|
|
340
|
+
if (!doc.data)
|
|
341
|
+
continue;
|
|
342
|
+
if (doc.data['notation'] !== 'integration')
|
|
343
|
+
continue;
|
|
344
|
+
if (doc.data['interface_semantics'] !== true)
|
|
345
|
+
continue;
|
|
346
|
+
const intId = docId(doc) ?? '';
|
|
347
|
+
for (const field of ['source', 'target']) {
|
|
348
|
+
const endpointId = doc.data[field];
|
|
349
|
+
if (typeof endpointId !== 'string' || endpointId.trim() === '')
|
|
350
|
+
continue;
|
|
351
|
+
const resolved = elementById.get(endpointId);
|
|
352
|
+
if (resolved && resolved['notation'] !== 'application') {
|
|
353
|
+
findings.push({
|
|
354
|
+
scope: PScope,
|
|
355
|
+
id: intId,
|
|
356
|
+
message: `INT-002: INTEGRATION '${intId}' interface_semantics endpoint '${endpointId}' ` +
|
|
357
|
+
`(${field}) must resolve to an APPLICATION element; got notation='${resolved['notation']}'.`,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
148
362
|
}
|
|
149
363
|
/** Phase 6 — an element that is Active/Production must declare an owner. */
|
|
150
364
|
function checkPolicy(input, findings) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { validateTechnologyService } from '../validate.js';
|
|
3
|
+
function valid() {
|
|
4
|
+
return {
|
|
5
|
+
notation: 'technology-service',
|
|
6
|
+
id: 'TECHNOLOGY_SERVICE-KAFKA-1',
|
|
7
|
+
name: 'Kafka Event Bus',
|
|
8
|
+
type: 'messaging',
|
|
9
|
+
description: 'Managed Kafka cluster for async event streaming.',
|
|
10
|
+
node: 'NODE-KAFKA-HOST-1',
|
|
11
|
+
endpoint: 'kafka.internal:9092',
|
|
12
|
+
zone: 'canon',
|
|
13
|
+
admitted_at: '2026-06-28',
|
|
14
|
+
admitted_by: 'v.korobeinikov',
|
|
15
|
+
gate_checks: { uniqueness: 'pass', consistency: 'pass', completeness: 'pass' },
|
|
16
|
+
valid_from: '2024-01-01',
|
|
17
|
+
valid_to: null,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const codes = (input) => validateTechnologyService(input).errors.map(e => e.code);
|
|
21
|
+
describe('validateTechnologyService — positive', () => {
|
|
22
|
+
it('accepts a well-formed messaging service with all optional fields', () => {
|
|
23
|
+
const r = validateTechnologyService(valid());
|
|
24
|
+
expect(r.valid, JSON.stringify(r.errors)).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
it('accepts a storage service with no optional fields', () => {
|
|
27
|
+
const a = valid();
|
|
28
|
+
a.id = 'TECHNOLOGY_SERVICE-S3-1';
|
|
29
|
+
a.name = 'Object Storage';
|
|
30
|
+
a.type = 'storage';
|
|
31
|
+
delete a.description;
|
|
32
|
+
delete a.node;
|
|
33
|
+
delete a.endpoint;
|
|
34
|
+
expect(validateTechnologyService(a).valid).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
it('accepts all five type values', () => {
|
|
37
|
+
for (const t of ['messaging', 'storage', 'api_gateway', 'database', 'compute']) {
|
|
38
|
+
expect(validateTechnologyService({ ...valid(), type: t }).valid).toBe(true);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
it('accepts valid_to as a date string', () => {
|
|
42
|
+
expect(validateTechnologyService({ ...valid(), valid_to: '2026-12-31' }).valid).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
it('accepts absent node without error', () => {
|
|
45
|
+
const a = valid();
|
|
46
|
+
delete a.node;
|
|
47
|
+
expect(validateTechnologyService(a).valid).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
describe('validateTechnologyService — TSVC-001 (shape / id grammar)', () => {
|
|
51
|
+
it('flags a non-object input', () => {
|
|
52
|
+
expect(codes(null)).toEqual(['TSVC-001']);
|
|
53
|
+
});
|
|
54
|
+
it('flags an id that violates the grammar', () => {
|
|
55
|
+
expect(codes({ ...valid(), id: 'TECHNOLOGY_SERVICE-001' })).toContain('TSVC-001');
|
|
56
|
+
expect(codes({ ...valid(), id: 'TSVC-1' })).toContain('TSVC-001');
|
|
57
|
+
expect(codes({ ...valid(), id: 'technology-service-kafka-1' })).toContain('TSVC-001');
|
|
58
|
+
});
|
|
59
|
+
it('flags a wrong notation tag', () => {
|
|
60
|
+
expect(codes({ ...valid(), notation: 'technology_service' })).toContain('TSVC-001');
|
|
61
|
+
expect(codes({ ...valid(), notation: 'service' })).toContain('TSVC-001');
|
|
62
|
+
});
|
|
63
|
+
it('flags a non-canon zone', () => {
|
|
64
|
+
expect(codes({ ...valid(), zone: 'sandbox' })).toContain('TSVC-001');
|
|
65
|
+
});
|
|
66
|
+
it('flags a missing zone', () => {
|
|
67
|
+
const a = valid();
|
|
68
|
+
delete a.zone;
|
|
69
|
+
expect(codes(a)).toContain('TSVC-001');
|
|
70
|
+
});
|
|
71
|
+
it('flags a missing name', () => {
|
|
72
|
+
const a = valid();
|
|
73
|
+
delete a.name;
|
|
74
|
+
expect(codes(a)).toContain('TSVC-001');
|
|
75
|
+
});
|
|
76
|
+
it('flags a missing valid_to key', () => {
|
|
77
|
+
const a = valid();
|
|
78
|
+
delete a.valid_to;
|
|
79
|
+
expect(codes(a)).toContain('TSVC-001');
|
|
80
|
+
});
|
|
81
|
+
it('flags missing gate_checks', () => {
|
|
82
|
+
const a = valid();
|
|
83
|
+
delete a.gate_checks;
|
|
84
|
+
expect(codes(a)).toContain('TSVC-001');
|
|
85
|
+
});
|
|
86
|
+
it('flags a missing type', () => {
|
|
87
|
+
const a = valid();
|
|
88
|
+
delete a.type;
|
|
89
|
+
expect(codes(a)).toContain('TSVC-001');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
describe('validateTechnologyService — TSVC-002 (type enum)', () => {
|
|
93
|
+
it('flags a type outside the allowed enum', () => {
|
|
94
|
+
expect(codes({ ...valid(), type: 'queue' })).toContain('TSVC-002');
|
|
95
|
+
expect(codes({ ...valid(), type: 'event_streaming' })).toContain('TSVC-002');
|
|
96
|
+
expect(codes({ ...valid(), type: 'cache' })).toContain('TSVC-002');
|
|
97
|
+
});
|
|
98
|
+
it('does not flag a missing type with TSVC-002 (TSVC-001 fires instead)', () => {
|
|
99
|
+
const a = valid();
|
|
100
|
+
delete a.type;
|
|
101
|
+
expect(codes(a)).not.toContain('TSVC-002');
|
|
102
|
+
expect(codes(a)).toContain('TSVC-001');
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
describe('validateTechnologyService — TSVC-003 (node grammar)', () => {
|
|
106
|
+
it('flags a node reference that is not a NODE-… id', () => {
|
|
107
|
+
expect(codes({ ...valid(), node: 'ACTOR-OPS-1' })).toContain('TSVC-003');
|
|
108
|
+
expect(codes({ ...valid(), node: 'not-an-id' })).toContain('TSVC-003');
|
|
109
|
+
expect(codes({ ...valid(), node: 'NODE-001' })).toContain('TSVC-003');
|
|
110
|
+
});
|
|
111
|
+
it('accepts a null or absent node without error', () => {
|
|
112
|
+
expect(codes({ ...valid(), node: undefined })).not.toContain('TSVC-003');
|
|
113
|
+
const a = valid();
|
|
114
|
+
delete a.node;
|
|
115
|
+
expect(codes(a)).not.toContain('TSVC-003');
|
|
116
|
+
});
|
|
117
|
+
it('accepts a well-formed NODE-… id', () => {
|
|
118
|
+
expect(codes({ ...valid(), node: 'NODE-KAFKA-HOST-1' })).not.toContain('TSVC-003');
|
|
119
|
+
expect(codes({ ...valid(), node: 'NODE-K8S-CLUSTER-1' })).not.toContain('TSVC-003');
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { validateTechnologyService } from './validate.js';
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { GateChecks } from '../requirement/types.js';
|
|
2
|
+
export declare const TECHNOLOGY_SERVICE_TYPES: readonly ["messaging", "storage", "api_gateway", "database", "compute"];
|
|
3
|
+
export type TechnologyServiceType = typeof TECHNOLOGY_SERVICE_TYPES[number];
|
|
4
|
+
export interface TechnologyService {
|
|
5
|
+
notation: 'technology-service';
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
type: TechnologyServiceType;
|
|
9
|
+
description?: string;
|
|
10
|
+
node?: string;
|
|
11
|
+
endpoint?: string;
|
|
12
|
+
zone: 'canon';
|
|
13
|
+
admitted_at: string;
|
|
14
|
+
admitted_by: string;
|
|
15
|
+
gate_checks: GateChecks;
|
|
16
|
+
valid_from: string;
|
|
17
|
+
valid_to: string | null;
|
|
18
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// TECHNOLOGY_SERVICE validator — methodology notations/elements/26-technology-services.md §5.
|
|
2
|
+
//
|
|
3
|
+
// Implements TSVC-001, TSVC-002, TSVC-003. The shared HDR-/LIFECYCLE- rules are
|
|
4
|
+
// cross-cutting concerns (CONTRACT.md §8) and out of scope for the
|
|
5
|
+
// single-artefact validator. TSVC-003 (node resolves to admitted NODE) is also
|
|
6
|
+
// enforced at repo scope in validate-repo.ts checkLayerSemantics.
|
|
7
|
+
import { isCanonicalIdOfType } from '../typed-id.js';
|
|
8
|
+
import { TECHNOLOGY_SERVICE_TYPES } from './types.js';
|
|
9
|
+
const REQUIRED_STRING_FIELDS = [
|
|
10
|
+
'name', 'admitted_at', 'admitted_by', 'valid_from',
|
|
11
|
+
];
|
|
12
|
+
const TYPES = TECHNOLOGY_SERVICE_TYPES;
|
|
13
|
+
export function validateTechnologyService(input) {
|
|
14
|
+
const errors = [];
|
|
15
|
+
const warnings = [];
|
|
16
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
|
17
|
+
errors.push({ code: 'TSVC-001', message: 'Technology service must be a YAML mapping.' });
|
|
18
|
+
return { valid: false, errors, warnings };
|
|
19
|
+
}
|
|
20
|
+
const a = input;
|
|
21
|
+
// TSVC-001 — id grammar + notation tag + envelope.
|
|
22
|
+
if (!isCanonicalIdOfType(a.id, 'TECHNOLOGY_SERVICE')) {
|
|
23
|
+
errors.push({ code: 'TSVC-001', message: `id "${String(a.id)}" must match TECHNOLOGY_SERVICE-[<middle>-]<INTEGER>.`, path: 'id' });
|
|
24
|
+
}
|
|
25
|
+
if (a.notation !== 'technology-service') {
|
|
26
|
+
errors.push({ code: 'TSVC-001', message: 'notation must be the fixed value "technology-service".', path: 'notation' });
|
|
27
|
+
}
|
|
28
|
+
if (a.zone !== undefined && a.zone !== 'canon') {
|
|
29
|
+
errors.push({ code: 'TSVC-001', message: 'zone must be "canon" for a TECHNOLOGY_SERVICE.', path: 'zone' });
|
|
30
|
+
}
|
|
31
|
+
else if (a.zone === undefined) {
|
|
32
|
+
errors.push({ code: 'TSVC-001', message: 'zone is required.', path: 'zone' });
|
|
33
|
+
}
|
|
34
|
+
for (const field of REQUIRED_STRING_FIELDS) {
|
|
35
|
+
const v = a[field];
|
|
36
|
+
if (typeof v !== 'string' || v.trim() === '') {
|
|
37
|
+
errors.push({ code: 'TSVC-001', message: `${field} is required.`, path: field });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (a.gate_checks === null || typeof a.gate_checks !== 'object') {
|
|
41
|
+
errors.push({ code: 'TSVC-001', message: 'gate_checks is required and must be a mapping.', path: 'gate_checks' });
|
|
42
|
+
}
|
|
43
|
+
if (!('valid_to' in a) || !(typeof a.valid_to === 'string' || a.valid_to === null)) {
|
|
44
|
+
errors.push({ code: 'TSVC-001', message: 'valid_to is required (an ISO date string or null).', path: 'valid_to' });
|
|
45
|
+
}
|
|
46
|
+
if (typeof a.type !== 'string' || a.type.trim() === '') {
|
|
47
|
+
errors.push({ code: 'TSVC-001', message: 'type is required.', path: 'type' });
|
|
48
|
+
}
|
|
49
|
+
else if (!TYPES.includes(a.type)) {
|
|
50
|
+
// TSVC-002 — type enum.
|
|
51
|
+
errors.push({ code: 'TSVC-002', message: `type "${a.type}" must be one of ${TYPES.join(', ')}.`, path: 'type' });
|
|
52
|
+
}
|
|
53
|
+
// TSVC-003 — node must be a NODE-… id when present.
|
|
54
|
+
if ('node' in a && a.node !== undefined && a.node !== null) {
|
|
55
|
+
if (typeof a.node !== 'string' || !isCanonicalIdOfType(a.node, 'NODE')) {
|
|
56
|
+
errors.push({
|
|
57
|
+
code: 'TSVC-003',
|
|
58
|
+
message: `node "${String(a.node)}" must be a canonical NODE-… id.`,
|
|
59
|
+
path: 'node',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
64
|
+
}
|
|
@@ -17,10 +17,8 @@ const FIXTURES = [
|
|
|
17
17
|
{ kind: 'goals', file: 'goals/strategy-2026.goals.transitrix.yaml', markup: 'svg' },
|
|
18
18
|
{ kind: 'dgca', file: 'dgca/strategy-2026.dgca.transitrix.yaml', markup: 'svg' },
|
|
19
19
|
{ kind: 'dga', file: 'dga/strategy-2026.dga.transitrix.yaml', markup: 'svg' },
|
|
20
|
-
{ kind: '
|
|
21
|
-
{ kind: '
|
|
22
|
-
{ kind: 'activities', file: 'activities/platform-launch.activities.transitrix.yaml', markup: 'svg' },
|
|
23
|
-
{ kind: 'activity-card', file: 'activity-card/eu-programme.activity-card.transitrix.yaml', markup: 'svg' },
|
|
20
|
+
{ kind: 'action', file: 'action/platform-launch.action.transitrix.yaml', markup: 'svg' },
|
|
21
|
+
{ kind: 'action-card', file: 'action-card/eu-programme.action-card.transitrix.yaml', markup: 'svg' },
|
|
24
22
|
{ kind: 'process-blueprint', file: 'process-blueprint/order-fulfilment.process-blueprint.transitrix.yaml', markup: 'svg' },
|
|
25
23
|
{ kind: 'blocks', file: 'blocks/architecture.blocks.transitrix.yaml', markup: 'svg' },
|
|
26
24
|
{ kind: 'applications', file: 'applications/portfolio-2026.applications.transitrix.yaml', markup: 'section' },
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for renderActivitiesSvg — project-node suppression (#421).
|
|
3
|
+
*
|
|
4
|
+
* Convention: activities with activity_type === 'project' are omitted from
|
|
5
|
+
* the Network/PSND layout by default (suppressProjectNodes: true). Text/Tree
|
|
6
|
+
* views keep them via the caller; data is never mutated.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, expect } from 'vitest';
|
|
9
|
+
import { renderActivitiesSvg } from '../render-activities.js';
|
|
10
|
+
const BASE_DOC = {
|
|
11
|
+
notation: 'action',
|
|
12
|
+
activities: [
|
|
13
|
+
{
|
|
14
|
+
id: 'PROJ-1',
|
|
15
|
+
name: 'Platform Launch',
|
|
16
|
+
activity_type: 'project',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: 'TASK-1',
|
|
20
|
+
name: 'Design',
|
|
21
|
+
activity_type: 'task',
|
|
22
|
+
parent: 'PROJ-1',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: 'TASK-2',
|
|
26
|
+
name: 'Implement',
|
|
27
|
+
activity_type: 'task',
|
|
28
|
+
parent: 'PROJ-1',
|
|
29
|
+
predecessors: ['TASK-1'],
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
};
|
|
33
|
+
describe('renderActivitiesSvg — project-node suppression (#421)', () => {
|
|
34
|
+
it('suppresses project-type nodes in the network SVG by default', () => {
|
|
35
|
+
const svg = renderActivitiesSvg(BASE_DOC);
|
|
36
|
+
// TASK-1 and TASK-2 should appear as rendered nodes.
|
|
37
|
+
expect(svg).toContain('TASK-1');
|
|
38
|
+
expect(svg).toContain('TASK-2');
|
|
39
|
+
// The project container should not appear as a rendered node.
|
|
40
|
+
expect(svg).not.toContain('PROJ-1');
|
|
41
|
+
});
|
|
42
|
+
it('suppresses project nodes when suppressProjectNodes is explicitly true', () => {
|
|
43
|
+
const svg = renderActivitiesSvg(BASE_DOC, { suppressProjectNodes: true });
|
|
44
|
+
expect(svg).not.toContain('PROJ-1');
|
|
45
|
+
expect(svg).toContain('TASK-1');
|
|
46
|
+
});
|
|
47
|
+
it('renders project nodes when suppressProjectNodes is false', () => {
|
|
48
|
+
const svg = renderActivitiesSvg(BASE_DOC, { suppressProjectNodes: false });
|
|
49
|
+
expect(svg).toContain('PROJ-1');
|
|
50
|
+
expect(svg).toContain('TASK-1');
|
|
51
|
+
expect(svg).toContain('TASK-2');
|
|
52
|
+
});
|
|
53
|
+
it('does not mutate the original doc', () => {
|
|
54
|
+
const original = BASE_DOC.activities.length;
|
|
55
|
+
renderActivitiesSvg(BASE_DOC);
|
|
56
|
+
expect(BASE_DOC.activities).toHaveLength(original);
|
|
57
|
+
});
|
|
58
|
+
it('suppresses project_type matching case-insensitively', () => {
|
|
59
|
+
const doc = {
|
|
60
|
+
notation: 'action',
|
|
61
|
+
activities: [
|
|
62
|
+
{ id: 'P1', name: 'Container', activity_type: 'Project' },
|
|
63
|
+
{ id: 'T1', name: 'Leaf', activity_type: 'task' },
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
const svg = renderActivitiesSvg(doc);
|
|
67
|
+
expect(svg).not.toContain('P1');
|
|
68
|
+
expect(svg).toContain('T1');
|
|
69
|
+
});
|
|
70
|
+
it('renders a valid SVG when all activities are project-type (graceful degrade)', () => {
|
|
71
|
+
const doc = {
|
|
72
|
+
notation: 'action',
|
|
73
|
+
activities: [{ id: 'P1', name: 'Only project', activity_type: 'project' }],
|
|
74
|
+
};
|
|
75
|
+
const svg = renderActivitiesSvg(doc);
|
|
76
|
+
// When all activities are filtered out, the renderer returns an empty SVG.
|
|
77
|
+
expect(svg).toContain('<svg');
|
|
78
|
+
});
|
|
79
|
+
it('renders activities without activity_type unchanged', () => {
|
|
80
|
+
const doc = {
|
|
81
|
+
notation: 'action',
|
|
82
|
+
activities: [
|
|
83
|
+
{ id: 'A1', name: 'Analysis', duration: 5 },
|
|
84
|
+
{ id: 'A2', name: 'Design', duration: 3, predecessors: ['A1'] },
|
|
85
|
+
],
|
|
86
|
+
};
|
|
87
|
+
const svg = renderActivitiesSvg(doc);
|
|
88
|
+
expect(svg).toContain('A1');
|
|
89
|
+
expect(svg).toContain('A2');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|