@transitrix/diagrams 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/yaml-normalize.test.js +9 -9
- package/dist/activities/__tests__/validate.test.js +77 -74
- package/dist/activities/validate.js +5 -12
- package/dist/activity-card/__tests__/example.test.js +3 -5
- package/dist/activity-card/__tests__/layout.test.js +2 -9
- package/dist/activity-card/__tests__/resolver.test.js +25 -0
- package/dist/activity-card/__tests__/validate.test.js +29 -18
- package/dist/activity-card/layout.js +1 -7
- package/dist/activity-card/resolver.js +11 -2
- package/dist/activity-card/types.d.ts +1 -1
- package/dist/activity-card/validate.js +3 -10
- package/dist/actor/__tests__/validate.test.js +6 -0
- package/dist/actor/types.d.ts +1 -1
- package/dist/actor/types.js +2 -0
- package/dist/applications/types.d.ts +7 -0
- package/dist/applications/validate.js +18 -0
- package/dist/blocks/__tests__/layout.test.js +1 -1
- package/dist/blocks/layout.js +1 -1
- package/dist/business-service/__tests__/validate.test.d.ts +1 -0
- package/dist/business-service/__tests__/validate.test.js +135 -0
- package/dist/business-service/index.d.ts +3 -0
- package/dist/business-service/index.js +2 -0
- package/dist/business-service/types.d.ts +20 -0
- package/dist/business-service/types.js +3 -0
- package/dist/business-service/validate.d.ts +2 -0
- package/dist/business-service/validate.js +74 -0
- package/dist/fgca/__tests__/parse-canonical.test.js +24 -11
- package/dist/fgca/__tests__/resolver.test.js +7 -7
- package/dist/fgca/__tests__/validate.test.js +11 -11
- package/dist/fgca/parse-canonical.js +18 -32
- package/dist/fgca/preview-layout.js +50 -1
- package/dist/fgca/resolver.d.ts +2 -2
- package/dist/fgca/resolver.js +5 -5
- package/dist/fgca/validate.js +4 -13
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/integration/__tests__/validate.test.d.ts +1 -0
- package/dist/integration/__tests__/validate.test.js +169 -0
- package/dist/integration/index.d.ts +2 -0
- package/dist/integration/index.js +1 -0
- package/dist/integration/types.d.ts +27 -0
- package/dist/integration/types.js +5 -0
- package/dist/integration/validate.d.ts +2 -0
- package/dist/integration/validate.js +94 -0
- package/dist/location/__tests__/validate.test.d.ts +1 -0
- package/dist/location/__tests__/validate.test.js +107 -0
- package/dist/location/index.d.ts +3 -0
- package/dist/location/index.js +2 -0
- package/dist/location/types.d.ts +20 -0
- package/dist/location/types.js +3 -0
- package/dist/location/validate.d.ts +2 -0
- package/dist/location/validate.js +63 -0
- package/dist/node/__tests__/validate.test.d.ts +1 -0
- package/dist/node/__tests__/validate.test.js +100 -0
- package/dist/node/index.d.ts +2 -0
- package/dist/node/index.js +1 -0
- package/dist/node/types.d.ts +18 -0
- package/dist/node/types.js +3 -0
- package/dist/node/validate.d.ts +2 -0
- package/dist/node/validate.js +53 -0
- package/dist/repo-validate/__tests__/validate-repo.test.js +444 -0
- package/dist/repo-validate/validate-repo.js +224 -10
- package/dist/technology-service/__tests__/validate.test.d.ts +1 -0
- package/dist/technology-service/__tests__/validate.test.js +121 -0
- package/dist/technology-service/index.d.ts +2 -0
- package/dist/technology-service/index.js +1 -0
- package/dist/technology-service/types.d.ts +18 -0
- package/dist/technology-service/types.js +3 -0
- package/dist/technology-service/validate.d.ts +2 -0
- package/dist/technology-service/validate.js +64 -0
- package/dist/webview/__tests__/entry-notations.test.js +2 -4
- package/dist/webview/__tests__/render-activities.test.d.ts +1 -0
- package/dist/webview/__tests__/render-activities.test.js +91 -0
- package/dist/webview/__tests__/render-blocks.test.d.ts +1 -0
- package/dist/webview/__tests__/render-blocks.test.js +130 -0
- package/dist/webview/entry.d.ts +1 -1
- package/dist/webview/entry.js +8 -15
- package/dist/webview/render-activities.d.ts +12 -0
- package/dist/webview/render-activities.js +9 -3
- package/dist/webview/render-activity-card.js +2 -3
- package/dist/webview/render-blocks.js +97 -18
- package/dist/webview/render-fgca.js +1 -1
- package/dist/webview/render-goals.js +28 -5
- package/package.json +1 -1
- package/src/__tests__/yaml-normalize.test.ts +11 -11
- package/src/activities/__tests__/validate.test.ts +77 -74
- package/src/activities/validate.ts +5 -12
- package/src/activity-card/__tests__/example.test.ts +3 -5
- package/src/activity-card/__tests__/layout.test.ts +2 -9
- package/src/activity-card/__tests__/resolver.test.ts +27 -0
- package/src/activity-card/__tests__/validate.test.ts +30 -18
- package/src/activity-card/layout.ts +1 -8
- package/src/activity-card/resolver.ts +11 -2
- package/src/activity-card/types.ts +1 -1
- package/src/activity-card/validate.ts +3 -9
- package/src/actor/__tests__/validate.test.ts +8 -0
- package/src/actor/types.ts +2 -0
- package/src/applications/types.ts +8 -0
- package/src/applications/validate.ts +20 -0
- package/src/blocks/__tests__/layout.test.ts +1 -1
- package/src/blocks/layout.ts +1 -1
- package/src/business-service/__tests__/validate.test.ts +160 -0
- package/src/business-service/index.ts +3 -0
- package/src/business-service/types.ts +29 -0
- package/src/business-service/validate.ts +82 -0
- package/src/fgca/__tests__/parse-canonical.test.ts +25 -11
- package/src/fgca/__tests__/resolver.test.ts +7 -7
- package/src/fgca/__tests__/validate.test.ts +11 -11
- package/src/fgca/parse-canonical.ts +18 -38
- package/src/fgca/preview-layout.ts +53 -1
- package/src/fgca/resolver.ts +5 -5
- package/src/fgca/validate.ts +4 -12
- package/src/index.ts +5 -0
- package/src/integration/__tests__/validate.test.ts +203 -0
- package/src/integration/index.ts +2 -0
- package/src/integration/types.ts +38 -0
- package/src/integration/validate.ts +104 -0
- package/src/location/__tests__/validate.test.ts +127 -0
- package/src/location/index.ts +3 -0
- package/src/location/types.ts +29 -0
- package/src/location/validate.ts +70 -0
- package/src/node/__tests__/validate.test.ts +118 -0
- package/src/node/index.ts +2 -0
- package/src/node/types.ts +27 -0
- package/src/node/validate.ts +59 -0
- package/src/repo-validate/__tests__/validate-repo.test.ts +550 -0
- package/src/repo-validate/validate-repo.ts +233 -10
- package/src/technology-service/__tests__/validate.test.ts +143 -0
- package/src/technology-service/index.ts +2 -0
- package/src/technology-service/types.ts +27 -0
- package/src/technology-service/validate.ts +71 -0
- package/src/webview/__tests__/entry-notations.test.ts +2 -4
- package/src/webview/__tests__/render-activities.test.ts +100 -0
- package/src/webview/__tests__/render-blocks.test.ts +155 -0
- package/src/webview/entry.ts +10 -19
- package/src/webview/render-activities.ts +29 -4
- package/src/webview/render-activity-card.ts +2 -3
- package/src/webview/render-blocks.ts +101 -20
- package/src/webview/render-fgca.ts +1 -1
- package/src/webview/render-goals.ts +26 -5
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// INTEGRATION validator — methodology notations/ELEMENT_PRIMITIVES.md §7.8.
|
|
2
|
+
//
|
|
3
|
+
// Implements INT-001 (conditional field enforcement when interface_semantics: true)
|
|
4
|
+
// and INT-002 (endpoint APPLICATION type check). The shared HDR-/LIFECYCLE- rules
|
|
5
|
+
// are cross-cutting concerns (CONTRACT.md §8) and out of scope for the
|
|
6
|
+
// single-artefact validator.
|
|
7
|
+
|
|
8
|
+
import type { ValidationError, ValidationWarning, ValidationResult } from '../validation-types.js';
|
|
9
|
+
import { isCanonicalIdOfType } from '../typed-id.js';
|
|
10
|
+
|
|
11
|
+
const REQUIRED_STRING_FIELDS = [
|
|
12
|
+
'source', 'target', 'admitted_at', 'admitted_by', 'valid_from',
|
|
13
|
+
] as const;
|
|
14
|
+
|
|
15
|
+
const VALID_DIRECTIONS = new Set(['inbound', 'outbound', 'bidirectional']);
|
|
16
|
+
const VALID_SENSITIVITY = new Set(['public', 'internal', 'confidential', 'restricted']);
|
|
17
|
+
const VALID_DIRECTIONALITY = new Set(['producer', 'consumer', 'request_reply', 'bidirectional_stream']);
|
|
18
|
+
|
|
19
|
+
const IFACE_CONDITIONAL = ['protocol', 'payload_class', 'sensitivity', 'directionality'] as const;
|
|
20
|
+
|
|
21
|
+
export function validateIntegration(input: unknown): ValidationResult {
|
|
22
|
+
const errors: ValidationError[] = [];
|
|
23
|
+
const warnings: ValidationWarning[] = [];
|
|
24
|
+
|
|
25
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
|
26
|
+
errors.push({ code: 'INT-001', message: 'Integration must be a YAML mapping.' });
|
|
27
|
+
return { valid: false, errors, warnings };
|
|
28
|
+
}
|
|
29
|
+
const a = input as Record<string, unknown>;
|
|
30
|
+
|
|
31
|
+
// Shape — id grammar + notation tag + envelope.
|
|
32
|
+
if (!isCanonicalIdOfType(a.id, 'INTEGRATION')) {
|
|
33
|
+
errors.push({ code: 'INT-001', message: `id "${String(a.id)}" must match INTEGRATION-[<middle>-]<INTEGER>.`, path: 'id' });
|
|
34
|
+
}
|
|
35
|
+
if (a.notation !== 'integration') {
|
|
36
|
+
errors.push({ code: 'INT-001', message: 'notation must be the fixed value "integration".', path: 'notation' });
|
|
37
|
+
}
|
|
38
|
+
if (a.zone !== undefined && a.zone !== 'canon') {
|
|
39
|
+
errors.push({ code: 'INT-001', message: 'zone must be "canon" for a standalone INTEGRATION.', path: 'zone' });
|
|
40
|
+
} else if (a.zone === undefined) {
|
|
41
|
+
errors.push({ code: 'INT-001', message: 'zone is required.', path: 'zone' });
|
|
42
|
+
}
|
|
43
|
+
for (const field of REQUIRED_STRING_FIELDS) {
|
|
44
|
+
const v = a[field];
|
|
45
|
+
if (typeof v !== 'string' || v.trim() === '') {
|
|
46
|
+
errors.push({ code: 'INT-001', message: `${field} is required.`, path: field });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (a.gate_checks === null || typeof a.gate_checks !== 'object') {
|
|
50
|
+
errors.push({ code: 'INT-001', message: 'gate_checks is required and must be a mapping.', path: 'gate_checks' });
|
|
51
|
+
}
|
|
52
|
+
if (!('valid_to' in a) || !(typeof a.valid_to === 'string' || a.valid_to === null)) {
|
|
53
|
+
errors.push({ code: 'INT-001', message: 'valid_to is required (an ISO date string or null).', path: 'valid_to' });
|
|
54
|
+
}
|
|
55
|
+
if (a.direction !== undefined && !VALID_DIRECTIONS.has(a.direction as string)) {
|
|
56
|
+
errors.push({ code: 'INT-001', message: `direction "${a.direction}" must be one of inbound, outbound, bidirectional.`, path: 'direction' });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// INT-001 — interface_semantics: true triggers conditional field enforcement.
|
|
60
|
+
if (a.interface_semantics === true) {
|
|
61
|
+
for (const field of IFACE_CONDITIONAL) {
|
|
62
|
+
const v = a[field];
|
|
63
|
+
if (typeof v !== 'string' || v.trim() === '') {
|
|
64
|
+
errors.push({
|
|
65
|
+
code: 'INT-001',
|
|
66
|
+
message: `${field} is required when interface_semantics is true.`,
|
|
67
|
+
path: field,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (typeof a.sensitivity === 'string' && !VALID_SENSITIVITY.has(a.sensitivity)) {
|
|
72
|
+
errors.push({
|
|
73
|
+
code: 'INT-001',
|
|
74
|
+
message: `sensitivity "${a.sensitivity}" must be one of public, internal, confidential, restricted.`,
|
|
75
|
+
path: 'sensitivity',
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
if (typeof a.directionality === 'string' && !VALID_DIRECTIONALITY.has(a.directionality)) {
|
|
79
|
+
errors.push({
|
|
80
|
+
code: 'INT-001',
|
|
81
|
+
message: `directionality "${a.directionality}" must be one of producer, consumer, request_reply, bidirectional_stream.`,
|
|
82
|
+
path: 'directionality',
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// INT-002 — source and target must be APPLICATION-… IDs.
|
|
87
|
+
if (typeof a.source === 'string' && a.source.trim() !== '' && !isCanonicalIdOfType(a.source, 'APPLICATION')) {
|
|
88
|
+
errors.push({
|
|
89
|
+
code: 'INT-002',
|
|
90
|
+
message: `source "${a.source}" must be an APPLICATION-… canonical id when interface_semantics is true.`,
|
|
91
|
+
path: 'source',
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
if (typeof a.target === 'string' && a.target.trim() !== '' && !isCanonicalIdOfType(a.target, 'APPLICATION')) {
|
|
95
|
+
errors.push({
|
|
96
|
+
code: 'INT-002',
|
|
97
|
+
message: `target "${a.target}" must be an APPLICATION-… canonical id when interface_semantics is true.`,
|
|
98
|
+
path: 'target',
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
104
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { validateLocation } from '../validate.js';
|
|
3
|
+
|
|
4
|
+
function valid(): Record<string, unknown> {
|
|
5
|
+
return {
|
|
6
|
+
notation: 'location',
|
|
7
|
+
id: 'LOCATION-TBILISI-1',
|
|
8
|
+
name: 'Tbilisi Office',
|
|
9
|
+
type: 'office',
|
|
10
|
+
address: '14 Rustaveli Ave, Tbilisi 0108, Georgia',
|
|
11
|
+
country_code: 'GE',
|
|
12
|
+
timezone: 'Asia/Tbilisi',
|
|
13
|
+
zone: 'canon',
|
|
14
|
+
admitted_at: '2026-06-28',
|
|
15
|
+
admitted_by: 'v.korobeinikov',
|
|
16
|
+
gate_checks: { uniqueness: 'pass', consistency: 'pass', completeness: 'pass' },
|
|
17
|
+
valid_from: '2024-01-01',
|
|
18
|
+
valid_to: null,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const codes = (input: unknown): string[] =>
|
|
23
|
+
validateLocation(input).errors.map(e => e.code);
|
|
24
|
+
|
|
25
|
+
describe('validateLocation — positive', () => {
|
|
26
|
+
it('accepts a well-formed office location', () => {
|
|
27
|
+
const r = validateLocation(valid());
|
|
28
|
+
expect(r.valid, JSON.stringify(r.errors)).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('accepts a country with no optional fields', () => {
|
|
32
|
+
const a = valid();
|
|
33
|
+
a.id = 'LOCATION-GE-1';
|
|
34
|
+
a.name = 'Georgia';
|
|
35
|
+
a.type = 'country';
|
|
36
|
+
delete a.address;
|
|
37
|
+
delete a.country_code;
|
|
38
|
+
delete a.timezone;
|
|
39
|
+
expect(validateLocation(a).valid).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('accepts a virtual location', () => {
|
|
43
|
+
const a = valid();
|
|
44
|
+
a.id = 'LOCATION-VIRTUAL-1';
|
|
45
|
+
a.name = 'Remote';
|
|
46
|
+
a.type = 'virtual';
|
|
47
|
+
expect(validateLocation(a).valid).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('accepts a location with a valid parent reference', () => {
|
|
51
|
+
expect(validateLocation({ ...valid(), parent: 'LOCATION-GE-1' }).valid).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('accepts valid_to as a date string', () => {
|
|
55
|
+
expect(validateLocation({ ...valid(), valid_to: '2026-12-31' }).valid).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('validateLocation — LOC-001 (shape / id grammar)', () => {
|
|
60
|
+
it('flags a missing required envelope field (name)', () => {
|
|
61
|
+
const a = valid();
|
|
62
|
+
delete a.name;
|
|
63
|
+
expect(codes(a)).toContain('LOC-001');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('flags an id that violates the grammar', () => {
|
|
67
|
+
expect(codes({ ...valid(), id: 'LOCATION-001' })).toContain('LOC-001');
|
|
68
|
+
expect(codes({ ...valid(), id: 'LOC-1' })).toContain('LOC-001');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('flags a wrong notation tag', () => {
|
|
72
|
+
expect(codes({ ...valid(), notation: 'place' })).toContain('LOC-001');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('flags a non-canon zone', () => {
|
|
76
|
+
expect(codes({ ...valid(), zone: 'sandbox' })).toContain('LOC-001');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('flags a missing zone', () => {
|
|
80
|
+
const a = valid();
|
|
81
|
+
delete a.zone;
|
|
82
|
+
expect(codes(a)).toContain('LOC-001');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('flags a missing type', () => {
|
|
86
|
+
const a = valid();
|
|
87
|
+
delete a.type;
|
|
88
|
+
expect(codes(a)).toContain('LOC-001');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('flags a missing valid_to key', () => {
|
|
92
|
+
const a = valid();
|
|
93
|
+
delete a.valid_to;
|
|
94
|
+
expect(codes(a)).toContain('LOC-001');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('flags missing gate_checks', () => {
|
|
98
|
+
const a = valid();
|
|
99
|
+
delete a.gate_checks;
|
|
100
|
+
expect(codes(a)).toContain('LOC-001');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('rejects a non-object', () => {
|
|
104
|
+
expect(codes(null)).toEqual(['LOC-001']);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('validateLocation — LOC-002 (type enum)', () => {
|
|
109
|
+
it('flags a type outside the allowed enum', () => {
|
|
110
|
+
expect(codes({ ...valid(), type: 'building' })).toContain('LOC-002');
|
|
111
|
+
expect(codes({ ...valid(), type: 'facility' })).toContain('LOC-002');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('validateLocation — LOC-003 (parent grammar)', () => {
|
|
116
|
+
it('flags a parent that is not a LOCATION-… id', () => {
|
|
117
|
+
expect(codes({ ...valid(), parent: 'ACTOR-OPS-1' })).toContain('LOC-003');
|
|
118
|
+
expect(codes({ ...valid(), parent: 'not-an-id' })).toContain('LOC-003');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('accepts a null or absent parent without error', () => {
|
|
122
|
+
expect(codes({ ...valid(), parent: undefined })).not.toContain('LOC-003');
|
|
123
|
+
const a = valid();
|
|
124
|
+
delete a.parent;
|
|
125
|
+
expect(codes(a)).not.toContain('LOC-003');
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// LOCATION — physical and virtual place primitive (ArchiMate Location/Facility).
|
|
2
|
+
// Schema: methodology notations/elements/21-locations.md
|
|
3
|
+
|
|
4
|
+
import type { GateChecks } from '../requirement/types.js';
|
|
5
|
+
|
|
6
|
+
export const LOCATION_TYPES = ['country', 'region', 'city', 'site', 'office', 'virtual'] as const;
|
|
7
|
+
export type LocationType = typeof LOCATION_TYPES[number];
|
|
8
|
+
|
|
9
|
+
export interface Location {
|
|
10
|
+
notation: 'location';
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
type: LocationType;
|
|
14
|
+
address?: string;
|
|
15
|
+
country_code?: string;
|
|
16
|
+
timezone?: string;
|
|
17
|
+
parent?: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
|
|
20
|
+
// Admission record (CONTRACT.md §6).
|
|
21
|
+
zone: 'canon';
|
|
22
|
+
admitted_at: string;
|
|
23
|
+
admitted_by: string;
|
|
24
|
+
gate_checks: GateChecks;
|
|
25
|
+
|
|
26
|
+
// Primitive lifecycle (CONTRACT.md §7).
|
|
27
|
+
valid_from: string;
|
|
28
|
+
valid_to: string | null;
|
|
29
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// LOCATION validator — methodology notations/elements/21-locations.md §6.
|
|
2
|
+
//
|
|
3
|
+
// Implements LOC-001..003. The shared HDR-/LIFECYCLE- rules are
|
|
4
|
+
// cross-cutting concerns (CONTRACT.md §8) and out of scope for the
|
|
5
|
+
// single-artefact validator.
|
|
6
|
+
|
|
7
|
+
import type { ValidationError, ValidationWarning, ValidationResult } from '../validation-types.js';
|
|
8
|
+
import { isCanonicalIdOfType, typeOfId } from '../typed-id.js';
|
|
9
|
+
import { LOCATION_TYPES } from './types.js';
|
|
10
|
+
|
|
11
|
+
const REQUIRED_STRING_FIELDS = [
|
|
12
|
+
'name', 'admitted_at', 'admitted_by', 'valid_from',
|
|
13
|
+
] as const;
|
|
14
|
+
|
|
15
|
+
const TYPES: readonly string[] = LOCATION_TYPES;
|
|
16
|
+
|
|
17
|
+
export function validateLocation(input: unknown): ValidationResult {
|
|
18
|
+
const errors: ValidationError[] = [];
|
|
19
|
+
const warnings: ValidationWarning[] = [];
|
|
20
|
+
|
|
21
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
|
22
|
+
errors.push({ code: 'LOC-001', message: 'Location must be a YAML mapping.' });
|
|
23
|
+
return { valid: false, errors, warnings };
|
|
24
|
+
}
|
|
25
|
+
const a = input as Record<string, unknown>;
|
|
26
|
+
|
|
27
|
+
// LOC-001 — id grammar + notation tag + envelope.
|
|
28
|
+
if (!isCanonicalIdOfType(a.id, 'LOCATION')) {
|
|
29
|
+
errors.push({ code: 'LOC-001', message: `id "${String(a.id)}" must match LOCATION-[<middle>-]<INTEGER>.`, path: 'id' });
|
|
30
|
+
}
|
|
31
|
+
if (a.notation !== 'location') {
|
|
32
|
+
errors.push({ code: 'LOC-001', message: 'notation must be the fixed value "location".', path: 'notation' });
|
|
33
|
+
}
|
|
34
|
+
if (a.zone !== undefined && a.zone !== 'canon') {
|
|
35
|
+
errors.push({ code: 'LOC-001', message: 'zone must be "canon" for a LOCATION.', path: 'zone' });
|
|
36
|
+
} else if (a.zone === undefined) {
|
|
37
|
+
errors.push({ code: 'LOC-001', message: 'zone is required.', path: 'zone' });
|
|
38
|
+
}
|
|
39
|
+
for (const field of REQUIRED_STRING_FIELDS) {
|
|
40
|
+
const v = a[field];
|
|
41
|
+
if (typeof v !== 'string' || v.trim() === '') {
|
|
42
|
+
errors.push({ code: 'LOC-001', message: `${field} is required.`, path: field });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (a.gate_checks === null || typeof a.gate_checks !== 'object') {
|
|
46
|
+
errors.push({ code: 'LOC-001', message: 'gate_checks is required and must be a mapping.', path: 'gate_checks' });
|
|
47
|
+
}
|
|
48
|
+
if (!('valid_to' in a) || !(typeof a.valid_to === 'string' || a.valid_to === null)) {
|
|
49
|
+
errors.push({ code: 'LOC-001', message: 'valid_to is required (an ISO date string or null).', path: 'valid_to' });
|
|
50
|
+
}
|
|
51
|
+
if (typeof a.type !== 'string' || a.type.trim() === '') {
|
|
52
|
+
errors.push({ code: 'LOC-001', message: 'type is required.', path: 'type' });
|
|
53
|
+
} else if (!TYPES.includes(a.type)) {
|
|
54
|
+
// LOC-002 — type enum.
|
|
55
|
+
errors.push({ code: 'LOC-002', message: `type "${a.type}" must be one of ${TYPES.join(', ')}.`, path: 'type' });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// LOC-003 — parent must resolve to a LOCATION when present.
|
|
59
|
+
if ('parent' in a && a.parent !== undefined && a.parent !== null) {
|
|
60
|
+
if (typeof a.parent !== 'string' || !isCanonicalIdOfType(a.parent, 'LOCATION')) {
|
|
61
|
+
errors.push({
|
|
62
|
+
code: 'LOC-003',
|
|
63
|
+
message: `parent "${String(a.parent)}" must be a canonical LOCATION-… id.`,
|
|
64
|
+
path: 'parent',
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
70
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { validateNode } from '../validate.js';
|
|
3
|
+
|
|
4
|
+
function valid(): Record<string, unknown> {
|
|
5
|
+
return {
|
|
6
|
+
notation: 'node',
|
|
7
|
+
id: 'NODE-KAFKA-HOST-1',
|
|
8
|
+
name: 'Kafka Cluster Host',
|
|
9
|
+
type: 'cloud_instance',
|
|
10
|
+
description: 'AWS EC2 auto-scaling group hosting the Kafka broker cluster.',
|
|
11
|
+
provider: 'AWS',
|
|
12
|
+
region: 'eu-central-1',
|
|
13
|
+
zone: 'canon',
|
|
14
|
+
admitted_at: '2026-06-28',
|
|
15
|
+
admitted_by: 'v.korobeinikov',
|
|
16
|
+
gate_checks: { uniqueness: 'pass', consistency: 'pass', completeness: 'pass' },
|
|
17
|
+
valid_from: '2024-01-01',
|
|
18
|
+
valid_to: null,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const codes = (input: unknown): string[] =>
|
|
23
|
+
validateNode(input).errors.map(e => e.code);
|
|
24
|
+
|
|
25
|
+
describe('validateNode — positive', () => {
|
|
26
|
+
it('accepts a well-formed cloud_instance node with all optional fields', () => {
|
|
27
|
+
const r = validateNode(valid());
|
|
28
|
+
expect(r.valid, JSON.stringify(r.errors)).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('accepts a server node with no optional fields', () => {
|
|
32
|
+
const a = valid();
|
|
33
|
+
a.id = 'NODE-DB-1';
|
|
34
|
+
a.name = 'Primary DB Server';
|
|
35
|
+
a.type = 'server';
|
|
36
|
+
delete a.description;
|
|
37
|
+
delete a.provider;
|
|
38
|
+
delete a.region;
|
|
39
|
+
expect(validateNode(a).valid).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('accepts all five type values', () => {
|
|
43
|
+
for (const t of ['server', 'cloud_instance', 'container_platform', 'database_server', 'network_device']) {
|
|
44
|
+
expect(validateNode({ ...valid(), type: t }).valid).toBe(true);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('accepts valid_to as a date string', () => {
|
|
49
|
+
expect(validateNode({ ...valid(), valid_to: '2026-12-31' }).valid).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('validateNode — NOD-001 (shape / id grammar)', () => {
|
|
54
|
+
it('flags a non-object input', () => {
|
|
55
|
+
expect(codes(null)).toEqual(['NOD-001']);
|
|
56
|
+
expect(codes('string')).toEqual(['NOD-001']);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('flags an id that violates the grammar', () => {
|
|
60
|
+
expect(codes({ ...valid(), id: 'NODE-001' })).toContain('NOD-001');
|
|
61
|
+
expect(codes({ ...valid(), id: 'NOD-1' })).toContain('NOD-001');
|
|
62
|
+
expect(codes({ ...valid(), id: 'node-kafka-1' })).toContain('NOD-001');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('flags a wrong notation tag', () => {
|
|
66
|
+
expect(codes({ ...valid(), notation: 'nodes' })).toContain('NOD-001');
|
|
67
|
+
expect(codes({ ...valid(), notation: 'infrastructure' })).toContain('NOD-001');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('flags a non-canon zone', () => {
|
|
71
|
+
expect(codes({ ...valid(), zone: 'sandbox' })).toContain('NOD-001');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('flags a missing zone', () => {
|
|
75
|
+
const a = valid();
|
|
76
|
+
delete a.zone;
|
|
77
|
+
expect(codes(a)).toContain('NOD-001');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('flags a missing name', () => {
|
|
81
|
+
const a = valid();
|
|
82
|
+
delete a.name;
|
|
83
|
+
expect(codes(a)).toContain('NOD-001');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('flags a missing valid_to key', () => {
|
|
87
|
+
const a = valid();
|
|
88
|
+
delete a.valid_to;
|
|
89
|
+
expect(codes(a)).toContain('NOD-001');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('flags missing gate_checks', () => {
|
|
93
|
+
const a = valid();
|
|
94
|
+
delete a.gate_checks;
|
|
95
|
+
expect(codes(a)).toContain('NOD-001');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('flags a missing type', () => {
|
|
99
|
+
const a = valid();
|
|
100
|
+
delete a.type;
|
|
101
|
+
expect(codes(a)).toContain('NOD-001');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('validateNode — NOD-002 (type enum)', () => {
|
|
106
|
+
it('flags a type outside the allowed enum', () => {
|
|
107
|
+
expect(codes({ ...valid(), type: 'virtual_machine' })).toContain('NOD-002');
|
|
108
|
+
expect(codes({ ...valid(), type: 'storage' })).toContain('NOD-002');
|
|
109
|
+
expect(codes({ ...valid(), type: 'cluster' })).toContain('NOD-002');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('does not flag a missing type with NOD-002 (NOD-001 fires instead)', () => {
|
|
113
|
+
const a = valid();
|
|
114
|
+
delete a.type;
|
|
115
|
+
expect(codes(a)).not.toContain('NOD-002');
|
|
116
|
+
expect(codes(a)).toContain('NOD-001');
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// NODE — technology-layer infrastructure node primitive (ArchiMate Technology Node).
|
|
2
|
+
// Schema: methodology notations/elements/25-nodes.md
|
|
3
|
+
|
|
4
|
+
import type { GateChecks } from '../requirement/types.js';
|
|
5
|
+
|
|
6
|
+
export const NODE_TYPES = ['server', 'cloud_instance', 'container_platform', 'database_server', 'network_device'] as const;
|
|
7
|
+
export type NodeType = typeof NODE_TYPES[number];
|
|
8
|
+
|
|
9
|
+
export interface Node {
|
|
10
|
+
notation: 'node';
|
|
11
|
+
id: string;
|
|
12
|
+
name: string;
|
|
13
|
+
type: NodeType;
|
|
14
|
+
description?: string;
|
|
15
|
+
provider?: string;
|
|
16
|
+
region?: string;
|
|
17
|
+
|
|
18
|
+
// Admission record (CONTRACT.md §6).
|
|
19
|
+
zone: 'canon';
|
|
20
|
+
admitted_at: string;
|
|
21
|
+
admitted_by: string;
|
|
22
|
+
gate_checks: GateChecks;
|
|
23
|
+
|
|
24
|
+
// Primitive lifecycle (CONTRACT.md §7).
|
|
25
|
+
valid_from: string;
|
|
26
|
+
valid_to: string | null;
|
|
27
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// NODE validator — methodology notations/elements/25-nodes.md §5.
|
|
2
|
+
//
|
|
3
|
+
// Implements NOD-001, NOD-002. The shared HDR-/LIFECYCLE- rules are
|
|
4
|
+
// cross-cutting concerns (CONTRACT.md §8) and out of scope for the
|
|
5
|
+
// single-artefact validator.
|
|
6
|
+
|
|
7
|
+
import type { ValidationError, ValidationWarning, ValidationResult } from '../validation-types.js';
|
|
8
|
+
import { isCanonicalIdOfType } from '../typed-id.js';
|
|
9
|
+
import { NODE_TYPES } from './types.js';
|
|
10
|
+
|
|
11
|
+
const REQUIRED_STRING_FIELDS = [
|
|
12
|
+
'name', 'admitted_at', 'admitted_by', 'valid_from',
|
|
13
|
+
] as const;
|
|
14
|
+
|
|
15
|
+
const TYPES: readonly string[] = NODE_TYPES;
|
|
16
|
+
|
|
17
|
+
export function validateNode(input: unknown): ValidationResult {
|
|
18
|
+
const errors: ValidationError[] = [];
|
|
19
|
+
const warnings: ValidationWarning[] = [];
|
|
20
|
+
|
|
21
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
|
22
|
+
errors.push({ code: 'NOD-001', message: 'Node must be a YAML mapping.' });
|
|
23
|
+
return { valid: false, errors, warnings };
|
|
24
|
+
}
|
|
25
|
+
const a = input as Record<string, unknown>;
|
|
26
|
+
|
|
27
|
+
// NOD-001 — id grammar + notation tag + envelope.
|
|
28
|
+
if (!isCanonicalIdOfType(a.id, 'NODE')) {
|
|
29
|
+
errors.push({ code: 'NOD-001', message: `id "${String(a.id)}" must match NODE-[<middle>-]<INTEGER>.`, path: 'id' });
|
|
30
|
+
}
|
|
31
|
+
if (a.notation !== 'node') {
|
|
32
|
+
errors.push({ code: 'NOD-001', message: 'notation must be the fixed value "node".', path: 'notation' });
|
|
33
|
+
}
|
|
34
|
+
if (a.zone !== undefined && a.zone !== 'canon') {
|
|
35
|
+
errors.push({ code: 'NOD-001', message: 'zone must be "canon" for a NODE.', path: 'zone' });
|
|
36
|
+
} else if (a.zone === undefined) {
|
|
37
|
+
errors.push({ code: 'NOD-001', message: 'zone is required.', path: 'zone' });
|
|
38
|
+
}
|
|
39
|
+
for (const field of REQUIRED_STRING_FIELDS) {
|
|
40
|
+
const v = a[field];
|
|
41
|
+
if (typeof v !== 'string' || v.trim() === '') {
|
|
42
|
+
errors.push({ code: 'NOD-001', message: `${field} is required.`, path: field });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (a.gate_checks === null || typeof a.gate_checks !== 'object') {
|
|
46
|
+
errors.push({ code: 'NOD-001', message: 'gate_checks is required and must be a mapping.', path: 'gate_checks' });
|
|
47
|
+
}
|
|
48
|
+
if (!('valid_to' in a) || !(typeof a.valid_to === 'string' || a.valid_to === null)) {
|
|
49
|
+
errors.push({ code: 'NOD-001', message: 'valid_to is required (an ISO date string or null).', path: 'valid_to' });
|
|
50
|
+
}
|
|
51
|
+
if (typeof a.type !== 'string' || a.type.trim() === '') {
|
|
52
|
+
errors.push({ code: 'NOD-001', message: 'type is required.', path: 'type' });
|
|
53
|
+
} else if (!TYPES.includes(a.type)) {
|
|
54
|
+
// NOD-002 — type enum.
|
|
55
|
+
errors.push({ code: 'NOD-002', message: `type "${a.type}" must be one of ${TYPES.join(', ')}.`, path: 'type' });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
59
|
+
}
|