@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
package/src/fgca/validate.ts
CHANGED
|
@@ -35,28 +35,20 @@ export function validateFGCADoc(input: unknown): FGCAValidationResult {
|
|
|
35
35
|
errors.push({ code: 'MISSING_NOTATION', message: 'notation field is required' });
|
|
36
36
|
return { valid: false, errors, warnings };
|
|
37
37
|
}
|
|
38
|
-
if (raw.notation !== '
|
|
39
|
-
errors.push({ code: 'WRONG_NOTATION', message: `notation must be "dgca"
|
|
38
|
+
if (raw.notation !== 'dgca') {
|
|
39
|
+
errors.push({ code: 'WRONG_NOTATION', message: `notation must be "dgca", got "${String(raw.notation)}"` });
|
|
40
40
|
return { valid: false, errors, warnings };
|
|
41
41
|
}
|
|
42
|
-
if (raw.notation === 'fgca') {
|
|
43
|
-
warnings.push({ code: 'DEPRECATED_NOTATION', message: 'notation "fgca" is deprecated — rename to "dgca"' });
|
|
44
|
-
}
|
|
45
42
|
|
|
46
43
|
if (!Array.isArray(raw.factors)) errors.push({ code: 'SCHEMA_INVALID', message: 'factors must be an array', path: 'factors' });
|
|
47
44
|
if (!Array.isArray(raw.goals)) errors.push({ code: 'SCHEMA_INVALID', message: 'goals must be an array', path: 'goals' });
|
|
48
45
|
if (!Array.isArray(raw.changes)) errors.push({ code: 'SCHEMA_INVALID', message: 'changes must be an array', path: 'changes' });
|
|
49
|
-
|
|
50
|
-
const hasActions = Array.isArray(raw.actions);
|
|
51
|
-
const hasActivities = Array.isArray(raw.activities);
|
|
52
|
-
if (!hasActions && !hasActivities) {
|
|
46
|
+
if (!Array.isArray(raw.actions)) {
|
|
53
47
|
errors.push({ code: 'SCHEMA_INVALID', message: 'actions must be an array', path: 'actions' });
|
|
54
|
-
} else if ('activities' in raw) {
|
|
55
|
-
warnings.push({ code: 'DEPRECATED_NOTATION', message: '"activities" key is deprecated — rename to "actions"' });
|
|
56
48
|
}
|
|
57
49
|
if (errors.length > 0) return { valid: false, errors, warnings };
|
|
58
50
|
|
|
59
|
-
const actionsArr =
|
|
51
|
+
const actionsArr = raw.actions as unknown[];
|
|
60
52
|
const doc = raw as unknown as FGCADoc;
|
|
61
53
|
const collectIds = (arr: unknown[]): Set<number | string> => {
|
|
62
54
|
const out = new Set<number | string>();
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from "./activities/index";
|
|
2
2
|
export * from "./activity-card/index";
|
|
3
3
|
export * from "./actor/index";
|
|
4
|
+
export * from "./business-service/index";
|
|
4
5
|
export * from "./geometry";
|
|
5
6
|
export * from "./applications/index";
|
|
6
7
|
export * from "./assertion/index";
|
|
@@ -13,6 +14,9 @@ export * from "./confidence/index";
|
|
|
13
14
|
export * from "./factor/index";
|
|
14
15
|
export * from "./fgca/index";
|
|
15
16
|
export * from "./goals/index";
|
|
17
|
+
export * from "./integration/index";
|
|
18
|
+
export * from "./location/index";
|
|
19
|
+
export * from "./node/index";
|
|
16
20
|
export * from "./process-blueprint/index";
|
|
17
21
|
export * from "./process-map/index";
|
|
18
22
|
export * from "./products/index";
|
|
@@ -21,6 +25,7 @@ export * from "./requirement/index";
|
|
|
21
25
|
export * from "./scenarios/index";
|
|
22
26
|
export * from "./stakeholder/index";
|
|
23
27
|
export * from "./target-state/index";
|
|
28
|
+
export * from "./technology-service/index";
|
|
24
29
|
export { SCHEMA_VERSION } from "./schema-version";
|
|
25
30
|
export * from "./theme/index";
|
|
26
31
|
export * from "./typed-id";
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { validateIntegration } from '../validate.js';
|
|
3
|
+
|
|
4
|
+
function valid(): Record<string, unknown> {
|
|
5
|
+
return {
|
|
6
|
+
notation: 'integration',
|
|
7
|
+
id: 'INTEGRATION-OMS-CRM-1',
|
|
8
|
+
source: 'APPLICATION-OMS-1',
|
|
9
|
+
target: 'APPLICATION-CRM-1',
|
|
10
|
+
direction: 'outbound',
|
|
11
|
+
protocol: 'REST',
|
|
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
|
+
|
|
21
|
+
function validWithInterface(): Record<string, unknown> {
|
|
22
|
+
return {
|
|
23
|
+
...valid(),
|
|
24
|
+
id: 'INTEGRATION-OMS-EVENTS-1',
|
|
25
|
+
interface_semantics: true,
|
|
26
|
+
payload_class: 'domain_event',
|
|
27
|
+
sensitivity: 'internal',
|
|
28
|
+
directionality: 'producer',
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const codes = (input: unknown): string[] =>
|
|
33
|
+
validateIntegration(input).errors.map(e => e.code);
|
|
34
|
+
|
|
35
|
+
describe('validateIntegration — positive', () => {
|
|
36
|
+
it('accepts a well-formed integration without interface_semantics', () => {
|
|
37
|
+
const r = validateIntegration(valid());
|
|
38
|
+
expect(r.valid, JSON.stringify(r.errors)).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('accepts integration with no optional fields except required ones', () => {
|
|
42
|
+
const a = valid();
|
|
43
|
+
delete a.direction;
|
|
44
|
+
delete a.protocol;
|
|
45
|
+
expect(validateIntegration(a).valid).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('accepts a valid integration with interface_semantics: true and all required fields', () => {
|
|
49
|
+
const r = validateIntegration(validWithInterface());
|
|
50
|
+
expect(r.valid, JSON.stringify(r.errors)).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('accepts valid_to as a date string', () => {
|
|
54
|
+
expect(validateIntegration({ ...valid(), valid_to: '2026-12-31' }).valid).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('accepts interface_semantics: false without requiring conditional fields', () => {
|
|
58
|
+
expect(validateIntegration({ ...valid(), interface_semantics: false }).valid).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('accepts all four directionality values', () => {
|
|
62
|
+
for (const d of ['producer', 'consumer', 'request_reply', 'bidirectional_stream']) {
|
|
63
|
+
expect(validateIntegration({ ...validWithInterface(), directionality: d }).valid).toBe(true);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('accepts all four sensitivity values', () => {
|
|
68
|
+
for (const s of ['public', 'internal', 'confidential', 'restricted']) {
|
|
69
|
+
expect(validateIntegration({ ...validWithInterface(), sensitivity: s }).valid).toBe(true);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe('validateIntegration — INT-001 (shape / id grammar)', () => {
|
|
75
|
+
it('flags a non-object input', () => {
|
|
76
|
+
expect(codes(null)).toEqual(['INT-001']);
|
|
77
|
+
expect(codes('string')).toEqual(['INT-001']);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('flags an id that violates the grammar', () => {
|
|
81
|
+
expect(codes({ ...valid(), id: 'INTEGRATION-001' })).toContain('INT-001');
|
|
82
|
+
expect(codes({ ...valid(), id: 'INT-1' })).toContain('INT-001');
|
|
83
|
+
expect(codes({ ...valid(), id: 'integration-oms-1' })).toContain('INT-001');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('flags a wrong notation tag', () => {
|
|
87
|
+
expect(codes({ ...valid(), notation: 'integrations' })).toContain('INT-001');
|
|
88
|
+
expect(codes({ ...valid(), notation: 'application' })).toContain('INT-001');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('flags a non-canon zone', () => {
|
|
92
|
+
expect(codes({ ...valid(), zone: 'sandbox' })).toContain('INT-001');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('flags a missing zone', () => {
|
|
96
|
+
const a = valid();
|
|
97
|
+
delete a.zone;
|
|
98
|
+
expect(codes(a)).toContain('INT-001');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('flags a missing source', () => {
|
|
102
|
+
const a = valid();
|
|
103
|
+
delete a.source;
|
|
104
|
+
expect(codes(a)).toContain('INT-001');
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('flags a missing target', () => {
|
|
108
|
+
const a = valid();
|
|
109
|
+
delete a.target;
|
|
110
|
+
expect(codes(a)).toContain('INT-001');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('flags a missing admitted_at', () => {
|
|
114
|
+
const a = valid();
|
|
115
|
+
delete a.admitted_at;
|
|
116
|
+
expect(codes(a)).toContain('INT-001');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('flags a missing valid_to key', () => {
|
|
120
|
+
const a = valid();
|
|
121
|
+
delete a.valid_to;
|
|
122
|
+
expect(codes(a)).toContain('INT-001');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('flags missing gate_checks', () => {
|
|
126
|
+
const a = valid();
|
|
127
|
+
delete a.gate_checks;
|
|
128
|
+
expect(codes(a)).toContain('INT-001');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('flags an invalid direction value', () => {
|
|
132
|
+
expect(codes({ ...valid(), direction: 'both' })).toContain('INT-001');
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('validateIntegration — INT-001 (interface_semantics conditional fields)', () => {
|
|
137
|
+
it('flags missing protocol when interface_semantics is true', () => {
|
|
138
|
+
const a = validWithInterface();
|
|
139
|
+
delete a.protocol;
|
|
140
|
+
expect(codes(a)).toContain('INT-001');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('flags missing payload_class when interface_semantics is true', () => {
|
|
144
|
+
const a = validWithInterface();
|
|
145
|
+
delete a.payload_class;
|
|
146
|
+
expect(codes(a)).toContain('INT-001');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('flags missing sensitivity when interface_semantics is true', () => {
|
|
150
|
+
const a = validWithInterface();
|
|
151
|
+
delete a.sensitivity;
|
|
152
|
+
expect(codes(a)).toContain('INT-001');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('flags missing directionality when interface_semantics is true', () => {
|
|
156
|
+
const a = validWithInterface();
|
|
157
|
+
delete a.directionality;
|
|
158
|
+
expect(codes(a)).toContain('INT-001');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('flags all four missing at once', () => {
|
|
162
|
+
const a = valid();
|
|
163
|
+
a.interface_semantics = true;
|
|
164
|
+
delete a.protocol;
|
|
165
|
+
const errs = validateIntegration(a).errors.filter(e => e.code === 'INT-001');
|
|
166
|
+
expect(errs.length).toBeGreaterThanOrEqual(4);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('flags an invalid sensitivity value when interface_semantics is true', () => {
|
|
170
|
+
expect(codes({ ...validWithInterface(), sensitivity: 'secret' })).toContain('INT-001');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('flags an invalid directionality value when interface_semantics is true', () => {
|
|
174
|
+
expect(codes({ ...validWithInterface(), directionality: 'both_ways' })).toContain('INT-001');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('does not fire INT-001 conditional checks when interface_semantics is absent', () => {
|
|
178
|
+
const a = valid();
|
|
179
|
+
const errs = validateIntegration(a).errors.filter(e => e.code === 'INT-001' && e.path && ['protocol', 'payload_class', 'sensitivity', 'directionality'].includes(e.path));
|
|
180
|
+
expect(errs).toHaveLength(0);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('validateIntegration — INT-002 (endpoint APPLICATION type)', () => {
|
|
185
|
+
it('flags a source that is not an APPLICATION-… id when interface_semantics is true', () => {
|
|
186
|
+
expect(codes({ ...validWithInterface(), source: 'INTEGRATION-OTHER-1' })).toContain('INT-002');
|
|
187
|
+
expect(codes({ ...validWithInterface(), source: 'ACTOR-SALES-1' })).toContain('INT-002');
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('flags a target that is not an APPLICATION-… id when interface_semantics is true', () => {
|
|
191
|
+
expect(codes({ ...validWithInterface(), target: 'TECHNOLOGY_SERVICE-KAFKA-1' })).toContain('INT-002');
|
|
192
|
+
expect(codes({ ...validWithInterface(), target: 'NODE-HOST-1' })).toContain('INT-002');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('does not fire INT-002 when interface_semantics is absent', () => {
|
|
196
|
+
const a = { ...valid(), source: 'ACTOR-OPS-1', target: 'NODE-HOST-1' };
|
|
197
|
+
expect(codes(a)).not.toContain('INT-002');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('does not fire INT-002 for valid APPLICATION-… endpoints', () => {
|
|
201
|
+
expect(codes(validWithInterface())).not.toContain('INT-002');
|
|
202
|
+
});
|
|
203
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// INTEGRATION — application-layer point-to-point exchange (ArchiMate Application Interface).
|
|
2
|
+
// Schema: methodology notations/ELEMENT_PRIMITIVES.md §7.8
|
|
3
|
+
|
|
4
|
+
import type { GateChecks } from '../requirement/types.js';
|
|
5
|
+
|
|
6
|
+
export const INTEGRATION_DIRECTIONS = ['inbound', 'outbound', 'bidirectional'] as const;
|
|
7
|
+
export type IntegrationDirectionType = typeof INTEGRATION_DIRECTIONS[number];
|
|
8
|
+
|
|
9
|
+
export const SENSITIVITY_VALUES = ['public', 'internal', 'confidential', 'restricted'] as const;
|
|
10
|
+
export type SensitivityValue = typeof SENSITIVITY_VALUES[number];
|
|
11
|
+
|
|
12
|
+
export const DIRECTIONALITY_VALUES = ['producer', 'consumer', 'request_reply', 'bidirectional_stream'] as const;
|
|
13
|
+
export type DirectionalityValue = typeof DIRECTIONALITY_VALUES[number];
|
|
14
|
+
|
|
15
|
+
export interface Integration {
|
|
16
|
+
notation: 'integration';
|
|
17
|
+
id: string;
|
|
18
|
+
name?: string;
|
|
19
|
+
source: string;
|
|
20
|
+
target: string;
|
|
21
|
+
direction?: IntegrationDirectionType;
|
|
22
|
+
protocol?: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
interface_semantics?: boolean;
|
|
25
|
+
payload_class?: string;
|
|
26
|
+
sensitivity?: SensitivityValue;
|
|
27
|
+
directionality?: DirectionalityValue;
|
|
28
|
+
|
|
29
|
+
// Admission record (CONTRACT.md §6).
|
|
30
|
+
zone: 'canon';
|
|
31
|
+
admitted_at: string;
|
|
32
|
+
admitted_by: string;
|
|
33
|
+
gate_checks: GateChecks;
|
|
34
|
+
|
|
35
|
+
// Primitive lifecycle (CONTRACT.md §7).
|
|
36
|
+
valid_from: string;
|
|
37
|
+
valid_to: string | null;
|
|
38
|
+
}
|
|
@@ -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
|
+
}
|