@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
|
@@ -57,21 +57,18 @@ export function parseCanonicalFGCA(input) {
|
|
|
57
57
|
};
|
|
58
58
|
}
|
|
59
59
|
const raw = input;
|
|
60
|
-
if ('notation' in raw && raw['notation'] !== '
|
|
60
|
+
if ('notation' in raw && raw['notation'] !== 'dgca') {
|
|
61
61
|
return {
|
|
62
62
|
valid: false,
|
|
63
63
|
errors: [
|
|
64
64
|
{
|
|
65
65
|
code: 'FGCA-001',
|
|
66
|
-
message: `notation must be "dgca"
|
|
66
|
+
message: `notation must be "dgca", got "${String(raw['notation'])}"`,
|
|
67
67
|
},
|
|
68
68
|
],
|
|
69
69
|
warnings,
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
|
-
if ('notation' in raw && raw['notation'] === 'fgca') {
|
|
73
|
-
warnings.push({ code: 'FGCA-001', message: 'notation "fgca" is deprecated — rename to "dgca"' });
|
|
74
|
-
}
|
|
75
72
|
// FGCA-002 — document id (optional in v1.4 per the canonical spec? — spec
|
|
76
73
|
// says required; absence is FGCA-002).
|
|
77
74
|
if (raw['id'] !== undefined) {
|
|
@@ -91,10 +88,9 @@ export function parseCanonicalFGCA(input) {
|
|
|
91
88
|
const factorsRaw = asObjectArray(raw['factors']);
|
|
92
89
|
const goalsRaw = asObjectArray(raw['goals']);
|
|
93
90
|
const changesRaw = asObjectArray(raw['changes']);
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
warnings.push({ code: 'FGCA-001', message: '"activities" key is deprecated — rename to "actions"' });
|
|
91
|
+
const activitiesRaw = asObjectArray(raw['actions']);
|
|
92
|
+
if (activitiesRaw === null && 'activities' in raw) {
|
|
93
|
+
errors.push({ code: 'FGCA-004', message: '"activities" key is not accepted — rename to "actions"', path: 'actions' });
|
|
98
94
|
}
|
|
99
95
|
if (factorsRaw === null)
|
|
100
96
|
errors.push({ code: 'FGCA-004', message: 'factors must be an array', path: 'factors' });
|
|
@@ -294,10 +290,10 @@ export function parseCanonicalFGA(input) {
|
|
|
294
290
|
return { valid: false, errors: [{ code: 'FGA-001', message: 'document root is not an object' }], warnings: [] };
|
|
295
291
|
}
|
|
296
292
|
const raw = input;
|
|
297
|
-
if ('notation' in raw && raw['notation'] !== '
|
|
293
|
+
if ('notation' in raw && raw['notation'] !== 'dga') {
|
|
298
294
|
return {
|
|
299
295
|
valid: false,
|
|
300
|
-
errors: [{ code: 'FGA-001', message: `notation must be "dga"
|
|
296
|
+
errors: [{ code: 'FGA-001', message: `notation must be "dga", got "${String(raw['notation'])}"` }],
|
|
301
297
|
warnings: [],
|
|
302
298
|
};
|
|
303
299
|
}
|
package/dist/fgca/resolver.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* view_config projection and a canon element/relation store (VP-3).
|
|
4
4
|
*
|
|
5
5
|
* Output shape matches the flat form that `parseCanonicalFGCA` expects:
|
|
6
|
-
* top-level `factors[]`, `goals[]`, `changes[]`, `
|
|
6
|
+
* top-level `factors[]`, `goals[]`, `changes[]`, `actions[]` with typed
|
|
7
7
|
* string IDs. Filesystem-free — callable from unit tests without vscode.
|
|
8
8
|
*/
|
|
9
9
|
export interface FGCAViewConfig {
|
|
@@ -43,7 +43,7 @@ export declare function isFGCAViewDoc(parsed: unknown): boolean;
|
|
|
43
43
|
* to pass to `parseCanonicalFGCA`.
|
|
44
44
|
*
|
|
45
45
|
* Cross-reference fields (`goal.factors[]`, `change.goals[]`,
|
|
46
|
-
* `
|
|
46
|
+
* `action.changes[]`) are carried from the canon elements unchanged; the
|
|
47
47
|
* resolver decides which elements to include, not how they are shaped.
|
|
48
48
|
*/
|
|
49
49
|
export declare function resolveFGCA(viewDoc: unknown, sources: FGCACanonSources): Record<string, unknown>;
|
package/dist/fgca/resolver.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* view_config projection and a canon element/relation store (VP-3).
|
|
4
4
|
*
|
|
5
5
|
* Output shape matches the flat form that `parseCanonicalFGCA` expects:
|
|
6
|
-
* top-level `factors[]`, `goals[]`, `changes[]`, `
|
|
6
|
+
* top-level `factors[]`, `goals[]`, `changes[]`, `actions[]` with typed
|
|
7
7
|
* string IDs. Filesystem-free — callable from unit tests without vscode.
|
|
8
8
|
*/
|
|
9
9
|
function isObject(v) {
|
|
@@ -49,12 +49,12 @@ export function isFGCAViewDoc(parsed) {
|
|
|
49
49
|
* to pass to `parseCanonicalFGCA`.
|
|
50
50
|
*
|
|
51
51
|
* Cross-reference fields (`goal.factors[]`, `change.goals[]`,
|
|
52
|
-
* `
|
|
52
|
+
* `action.changes[]`) are carried from the canon elements unchanged; the
|
|
53
53
|
* resolver decides which elements to include, not how they are shaped.
|
|
54
54
|
*/
|
|
55
55
|
export function resolveFGCA(viewDoc, sources) {
|
|
56
56
|
if (!isObject(viewDoc)) {
|
|
57
|
-
return { notation: '
|
|
57
|
+
return { notation: 'dgca', factors: [], goals: [], changes: [], actions: [] };
|
|
58
58
|
}
|
|
59
59
|
const vc = isObject(viewDoc['view_config']) ? viewDoc['view_config'] : {};
|
|
60
60
|
const goalsConf = isObject(vc['goals']) ? vc['goals'] : {};
|
|
@@ -114,13 +114,13 @@ export function resolveFGCA(viewDoc, sources) {
|
|
|
114
114
|
strArray(a['goals']).some((gid) => selectedGoalIds.has(gid)));
|
|
115
115
|
}
|
|
116
116
|
return {
|
|
117
|
-
notation: '
|
|
117
|
+
notation: 'dgca',
|
|
118
118
|
id: viewDoc['id'],
|
|
119
119
|
name: viewDoc['name'],
|
|
120
120
|
spec_version: viewDoc['spec_version'],
|
|
121
121
|
factors: selectedFactors,
|
|
122
122
|
goals: selectedGoals,
|
|
123
123
|
changes: selectedChanges,
|
|
124
|
-
|
|
124
|
+
actions: selectedActivities,
|
|
125
125
|
};
|
|
126
126
|
}
|
package/dist/fgca/validate.js
CHANGED
|
@@ -9,31 +9,22 @@ export function validateFGCADoc(input) {
|
|
|
9
9
|
errors.push({ code: 'MISSING_NOTATION', message: 'notation field is required' });
|
|
10
10
|
return { valid: false, errors, warnings };
|
|
11
11
|
}
|
|
12
|
-
if (raw.notation !== '
|
|
13
|
-
errors.push({ code: 'WRONG_NOTATION', message: `notation must be "dgca"
|
|
12
|
+
if (raw.notation !== 'dgca') {
|
|
13
|
+
errors.push({ code: 'WRONG_NOTATION', message: `notation must be "dgca", got "${String(raw.notation)}"` });
|
|
14
14
|
return { valid: false, errors, warnings };
|
|
15
15
|
}
|
|
16
|
-
if (raw.notation === 'fgca') {
|
|
17
|
-
warnings.push({ code: 'DEPRECATED_NOTATION', message: 'notation "fgca" is deprecated — rename to "dgca"' });
|
|
18
|
-
}
|
|
19
16
|
if (!Array.isArray(raw.factors))
|
|
20
17
|
errors.push({ code: 'SCHEMA_INVALID', message: 'factors must be an array', path: 'factors' });
|
|
21
18
|
if (!Array.isArray(raw.goals))
|
|
22
19
|
errors.push({ code: 'SCHEMA_INVALID', message: 'goals must be an array', path: 'goals' });
|
|
23
20
|
if (!Array.isArray(raw.changes))
|
|
24
21
|
errors.push({ code: 'SCHEMA_INVALID', message: 'changes must be an array', path: 'changes' });
|
|
25
|
-
|
|
26
|
-
const hasActions = Array.isArray(raw.actions);
|
|
27
|
-
const hasActivities = Array.isArray(raw.activities);
|
|
28
|
-
if (!hasActions && !hasActivities) {
|
|
22
|
+
if (!Array.isArray(raw.actions)) {
|
|
29
23
|
errors.push({ code: 'SCHEMA_INVALID', message: 'actions must be an array', path: 'actions' });
|
|
30
24
|
}
|
|
31
|
-
else if ('activities' in raw) {
|
|
32
|
-
warnings.push({ code: 'DEPRECATED_NOTATION', message: '"activities" key is deprecated — rename to "actions"' });
|
|
33
|
-
}
|
|
34
25
|
if (errors.length > 0)
|
|
35
26
|
return { valid: false, errors, warnings };
|
|
36
|
-
const actionsArr =
|
|
27
|
+
const actionsArr = raw.actions;
|
|
37
28
|
const doc = raw;
|
|
38
29
|
const collectIds = (arr) => {
|
|
39
30
|
const out = new Set();
|
package/dist/index.d.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";
|
package/dist/index.js
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 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { validateIntegration } from '../validate.js';
|
|
3
|
+
function valid() {
|
|
4
|
+
return {
|
|
5
|
+
notation: 'integration',
|
|
6
|
+
id: 'INTEGRATION-OMS-CRM-1',
|
|
7
|
+
source: 'APPLICATION-OMS-1',
|
|
8
|
+
target: 'APPLICATION-CRM-1',
|
|
9
|
+
direction: 'outbound',
|
|
10
|
+
protocol: 'REST',
|
|
11
|
+
zone: 'canon',
|
|
12
|
+
admitted_at: '2026-06-28',
|
|
13
|
+
admitted_by: 'v.korobeinikov',
|
|
14
|
+
gate_checks: { uniqueness: 'pass', consistency: 'pass', completeness: 'pass' },
|
|
15
|
+
valid_from: '2024-01-01',
|
|
16
|
+
valid_to: null,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function validWithInterface() {
|
|
20
|
+
return {
|
|
21
|
+
...valid(),
|
|
22
|
+
id: 'INTEGRATION-OMS-EVENTS-1',
|
|
23
|
+
interface_semantics: true,
|
|
24
|
+
payload_class: 'domain_event',
|
|
25
|
+
sensitivity: 'internal',
|
|
26
|
+
directionality: 'producer',
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const codes = (input) => validateIntegration(input).errors.map(e => e.code);
|
|
30
|
+
describe('validateIntegration — positive', () => {
|
|
31
|
+
it('accepts a well-formed integration without interface_semantics', () => {
|
|
32
|
+
const r = validateIntegration(valid());
|
|
33
|
+
expect(r.valid, JSON.stringify(r.errors)).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
it('accepts integration with no optional fields except required ones', () => {
|
|
36
|
+
const a = valid();
|
|
37
|
+
delete a.direction;
|
|
38
|
+
delete a.protocol;
|
|
39
|
+
expect(validateIntegration(a).valid).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
it('accepts a valid integration with interface_semantics: true and all required fields', () => {
|
|
42
|
+
const r = validateIntegration(validWithInterface());
|
|
43
|
+
expect(r.valid, JSON.stringify(r.errors)).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
it('accepts valid_to as a date string', () => {
|
|
46
|
+
expect(validateIntegration({ ...valid(), valid_to: '2026-12-31' }).valid).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
it('accepts interface_semantics: false without requiring conditional fields', () => {
|
|
49
|
+
expect(validateIntegration({ ...valid(), interface_semantics: false }).valid).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
it('accepts all four directionality values', () => {
|
|
52
|
+
for (const d of ['producer', 'consumer', 'request_reply', 'bidirectional_stream']) {
|
|
53
|
+
expect(validateIntegration({ ...validWithInterface(), directionality: d }).valid).toBe(true);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
it('accepts all four sensitivity values', () => {
|
|
57
|
+
for (const s of ['public', 'internal', 'confidential', 'restricted']) {
|
|
58
|
+
expect(validateIntegration({ ...validWithInterface(), sensitivity: s }).valid).toBe(true);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
describe('validateIntegration — INT-001 (shape / id grammar)', () => {
|
|
63
|
+
it('flags a non-object input', () => {
|
|
64
|
+
expect(codes(null)).toEqual(['INT-001']);
|
|
65
|
+
expect(codes('string')).toEqual(['INT-001']);
|
|
66
|
+
});
|
|
67
|
+
it('flags an id that violates the grammar', () => {
|
|
68
|
+
expect(codes({ ...valid(), id: 'INTEGRATION-001' })).toContain('INT-001');
|
|
69
|
+
expect(codes({ ...valid(), id: 'INT-1' })).toContain('INT-001');
|
|
70
|
+
expect(codes({ ...valid(), id: 'integration-oms-1' })).toContain('INT-001');
|
|
71
|
+
});
|
|
72
|
+
it('flags a wrong notation tag', () => {
|
|
73
|
+
expect(codes({ ...valid(), notation: 'integrations' })).toContain('INT-001');
|
|
74
|
+
expect(codes({ ...valid(), notation: 'application' })).toContain('INT-001');
|
|
75
|
+
});
|
|
76
|
+
it('flags a non-canon zone', () => {
|
|
77
|
+
expect(codes({ ...valid(), zone: 'sandbox' })).toContain('INT-001');
|
|
78
|
+
});
|
|
79
|
+
it('flags a missing zone', () => {
|
|
80
|
+
const a = valid();
|
|
81
|
+
delete a.zone;
|
|
82
|
+
expect(codes(a)).toContain('INT-001');
|
|
83
|
+
});
|
|
84
|
+
it('flags a missing source', () => {
|
|
85
|
+
const a = valid();
|
|
86
|
+
delete a.source;
|
|
87
|
+
expect(codes(a)).toContain('INT-001');
|
|
88
|
+
});
|
|
89
|
+
it('flags a missing target', () => {
|
|
90
|
+
const a = valid();
|
|
91
|
+
delete a.target;
|
|
92
|
+
expect(codes(a)).toContain('INT-001');
|
|
93
|
+
});
|
|
94
|
+
it('flags a missing admitted_at', () => {
|
|
95
|
+
const a = valid();
|
|
96
|
+
delete a.admitted_at;
|
|
97
|
+
expect(codes(a)).toContain('INT-001');
|
|
98
|
+
});
|
|
99
|
+
it('flags a missing valid_to key', () => {
|
|
100
|
+
const a = valid();
|
|
101
|
+
delete a.valid_to;
|
|
102
|
+
expect(codes(a)).toContain('INT-001');
|
|
103
|
+
});
|
|
104
|
+
it('flags missing gate_checks', () => {
|
|
105
|
+
const a = valid();
|
|
106
|
+
delete a.gate_checks;
|
|
107
|
+
expect(codes(a)).toContain('INT-001');
|
|
108
|
+
});
|
|
109
|
+
it('flags an invalid direction value', () => {
|
|
110
|
+
expect(codes({ ...valid(), direction: 'both' })).toContain('INT-001');
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
describe('validateIntegration — INT-001 (interface_semantics conditional fields)', () => {
|
|
114
|
+
it('flags missing protocol when interface_semantics is true', () => {
|
|
115
|
+
const a = validWithInterface();
|
|
116
|
+
delete a.protocol;
|
|
117
|
+
expect(codes(a)).toContain('INT-001');
|
|
118
|
+
});
|
|
119
|
+
it('flags missing payload_class when interface_semantics is true', () => {
|
|
120
|
+
const a = validWithInterface();
|
|
121
|
+
delete a.payload_class;
|
|
122
|
+
expect(codes(a)).toContain('INT-001');
|
|
123
|
+
});
|
|
124
|
+
it('flags missing sensitivity when interface_semantics is true', () => {
|
|
125
|
+
const a = validWithInterface();
|
|
126
|
+
delete a.sensitivity;
|
|
127
|
+
expect(codes(a)).toContain('INT-001');
|
|
128
|
+
});
|
|
129
|
+
it('flags missing directionality when interface_semantics is true', () => {
|
|
130
|
+
const a = validWithInterface();
|
|
131
|
+
delete a.directionality;
|
|
132
|
+
expect(codes(a)).toContain('INT-001');
|
|
133
|
+
});
|
|
134
|
+
it('flags all four missing at once', () => {
|
|
135
|
+
const a = valid();
|
|
136
|
+
a.interface_semantics = true;
|
|
137
|
+
delete a.protocol;
|
|
138
|
+
const errs = validateIntegration(a).errors.filter(e => e.code === 'INT-001');
|
|
139
|
+
expect(errs.length).toBeGreaterThanOrEqual(4);
|
|
140
|
+
});
|
|
141
|
+
it('flags an invalid sensitivity value when interface_semantics is true', () => {
|
|
142
|
+
expect(codes({ ...validWithInterface(), sensitivity: 'secret' })).toContain('INT-001');
|
|
143
|
+
});
|
|
144
|
+
it('flags an invalid directionality value when interface_semantics is true', () => {
|
|
145
|
+
expect(codes({ ...validWithInterface(), directionality: 'both_ways' })).toContain('INT-001');
|
|
146
|
+
});
|
|
147
|
+
it('does not fire INT-001 conditional checks when interface_semantics is absent', () => {
|
|
148
|
+
const a = valid();
|
|
149
|
+
const errs = validateIntegration(a).errors.filter(e => e.code === 'INT-001' && e.path && ['protocol', 'payload_class', 'sensitivity', 'directionality'].includes(e.path));
|
|
150
|
+
expect(errs).toHaveLength(0);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
describe('validateIntegration — INT-002 (endpoint APPLICATION type)', () => {
|
|
154
|
+
it('flags a source that is not an APPLICATION-… id when interface_semantics is true', () => {
|
|
155
|
+
expect(codes({ ...validWithInterface(), source: 'INTEGRATION-OTHER-1' })).toContain('INT-002');
|
|
156
|
+
expect(codes({ ...validWithInterface(), source: 'ACTOR-SALES-1' })).toContain('INT-002');
|
|
157
|
+
});
|
|
158
|
+
it('flags a target that is not an APPLICATION-… id when interface_semantics is true', () => {
|
|
159
|
+
expect(codes({ ...validWithInterface(), target: 'TECHNOLOGY_SERVICE-KAFKA-1' })).toContain('INT-002');
|
|
160
|
+
expect(codes({ ...validWithInterface(), target: 'NODE-HOST-1' })).toContain('INT-002');
|
|
161
|
+
});
|
|
162
|
+
it('does not fire INT-002 when interface_semantics is absent', () => {
|
|
163
|
+
const a = { ...valid(), source: 'ACTOR-OPS-1', target: 'NODE-HOST-1' };
|
|
164
|
+
expect(codes(a)).not.toContain('INT-002');
|
|
165
|
+
});
|
|
166
|
+
it('does not fire INT-002 for valid APPLICATION-… endpoints', () => {
|
|
167
|
+
expect(codes(validWithInterface())).not.toContain('INT-002');
|
|
168
|
+
});
|
|
169
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { validateIntegration } from './validate.js';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { GateChecks } from '../requirement/types.js';
|
|
2
|
+
export declare const INTEGRATION_DIRECTIONS: readonly ["inbound", "outbound", "bidirectional"];
|
|
3
|
+
export type IntegrationDirectionType = typeof INTEGRATION_DIRECTIONS[number];
|
|
4
|
+
export declare const SENSITIVITY_VALUES: readonly ["public", "internal", "confidential", "restricted"];
|
|
5
|
+
export type SensitivityValue = typeof SENSITIVITY_VALUES[number];
|
|
6
|
+
export declare const DIRECTIONALITY_VALUES: readonly ["producer", "consumer", "request_reply", "bidirectional_stream"];
|
|
7
|
+
export type DirectionalityValue = typeof DIRECTIONALITY_VALUES[number];
|
|
8
|
+
export interface Integration {
|
|
9
|
+
notation: 'integration';
|
|
10
|
+
id: string;
|
|
11
|
+
name?: string;
|
|
12
|
+
source: string;
|
|
13
|
+
target: string;
|
|
14
|
+
direction?: IntegrationDirectionType;
|
|
15
|
+
protocol?: string;
|
|
16
|
+
description?: string;
|
|
17
|
+
interface_semantics?: boolean;
|
|
18
|
+
payload_class?: string;
|
|
19
|
+
sensitivity?: SensitivityValue;
|
|
20
|
+
directionality?: DirectionalityValue;
|
|
21
|
+
zone: 'canon';
|
|
22
|
+
admitted_at: string;
|
|
23
|
+
admitted_by: string;
|
|
24
|
+
gate_checks: GateChecks;
|
|
25
|
+
valid_from: string;
|
|
26
|
+
valid_to: string | null;
|
|
27
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// INTEGRATION — application-layer point-to-point exchange (ArchiMate Application Interface).
|
|
2
|
+
// Schema: methodology notations/ELEMENT_PRIMITIVES.md §7.8
|
|
3
|
+
export const INTEGRATION_DIRECTIONS = ['inbound', 'outbound', 'bidirectional'];
|
|
4
|
+
export const SENSITIVITY_VALUES = ['public', 'internal', 'confidential', 'restricted'];
|
|
5
|
+
export const DIRECTIONALITY_VALUES = ['producer', 'consumer', 'request_reply', 'bidirectional_stream'];
|
|
@@ -0,0 +1,94 @@
|
|
|
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
|
+
import { isCanonicalIdOfType } from '../typed-id.js';
|
|
8
|
+
const REQUIRED_STRING_FIELDS = [
|
|
9
|
+
'source', 'target', 'admitted_at', 'admitted_by', 'valid_from',
|
|
10
|
+
];
|
|
11
|
+
const VALID_DIRECTIONS = new Set(['inbound', 'outbound', 'bidirectional']);
|
|
12
|
+
const VALID_SENSITIVITY = new Set(['public', 'internal', 'confidential', 'restricted']);
|
|
13
|
+
const VALID_DIRECTIONALITY = new Set(['producer', 'consumer', 'request_reply', 'bidirectional_stream']);
|
|
14
|
+
const IFACE_CONDITIONAL = ['protocol', 'payload_class', 'sensitivity', 'directionality'];
|
|
15
|
+
export function validateIntegration(input) {
|
|
16
|
+
const errors = [];
|
|
17
|
+
const warnings = [];
|
|
18
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
|
19
|
+
errors.push({ code: 'INT-001', message: 'Integration must be a YAML mapping.' });
|
|
20
|
+
return { valid: false, errors, warnings };
|
|
21
|
+
}
|
|
22
|
+
const a = input;
|
|
23
|
+
// Shape — id grammar + notation tag + envelope.
|
|
24
|
+
if (!isCanonicalIdOfType(a.id, 'INTEGRATION')) {
|
|
25
|
+
errors.push({ code: 'INT-001', message: `id "${String(a.id)}" must match INTEGRATION-[<middle>-]<INTEGER>.`, path: 'id' });
|
|
26
|
+
}
|
|
27
|
+
if (a.notation !== 'integration') {
|
|
28
|
+
errors.push({ code: 'INT-001', message: 'notation must be the fixed value "integration".', path: 'notation' });
|
|
29
|
+
}
|
|
30
|
+
if (a.zone !== undefined && a.zone !== 'canon') {
|
|
31
|
+
errors.push({ code: 'INT-001', message: 'zone must be "canon" for a standalone INTEGRATION.', path: 'zone' });
|
|
32
|
+
}
|
|
33
|
+
else if (a.zone === undefined) {
|
|
34
|
+
errors.push({ code: 'INT-001', message: 'zone is required.', path: 'zone' });
|
|
35
|
+
}
|
|
36
|
+
for (const field of REQUIRED_STRING_FIELDS) {
|
|
37
|
+
const v = a[field];
|
|
38
|
+
if (typeof v !== 'string' || v.trim() === '') {
|
|
39
|
+
errors.push({ code: 'INT-001', message: `${field} is required.`, path: field });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (a.gate_checks === null || typeof a.gate_checks !== 'object') {
|
|
43
|
+
errors.push({ code: 'INT-001', message: 'gate_checks is required and must be a mapping.', path: 'gate_checks' });
|
|
44
|
+
}
|
|
45
|
+
if (!('valid_to' in a) || !(typeof a.valid_to === 'string' || a.valid_to === null)) {
|
|
46
|
+
errors.push({ code: 'INT-001', message: 'valid_to is required (an ISO date string or null).', path: 'valid_to' });
|
|
47
|
+
}
|
|
48
|
+
if (a.direction !== undefined && !VALID_DIRECTIONS.has(a.direction)) {
|
|
49
|
+
errors.push({ code: 'INT-001', message: `direction "${a.direction}" must be one of inbound, outbound, bidirectional.`, path: 'direction' });
|
|
50
|
+
}
|
|
51
|
+
// INT-001 — interface_semantics: true triggers conditional field enforcement.
|
|
52
|
+
if (a.interface_semantics === true) {
|
|
53
|
+
for (const field of IFACE_CONDITIONAL) {
|
|
54
|
+
const v = a[field];
|
|
55
|
+
if (typeof v !== 'string' || v.trim() === '') {
|
|
56
|
+
errors.push({
|
|
57
|
+
code: 'INT-001',
|
|
58
|
+
message: `${field} is required when interface_semantics is true.`,
|
|
59
|
+
path: field,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (typeof a.sensitivity === 'string' && !VALID_SENSITIVITY.has(a.sensitivity)) {
|
|
64
|
+
errors.push({
|
|
65
|
+
code: 'INT-001',
|
|
66
|
+
message: `sensitivity "${a.sensitivity}" must be one of public, internal, confidential, restricted.`,
|
|
67
|
+
path: 'sensitivity',
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
if (typeof a.directionality === 'string' && !VALID_DIRECTIONALITY.has(a.directionality)) {
|
|
71
|
+
errors.push({
|
|
72
|
+
code: 'INT-001',
|
|
73
|
+
message: `directionality "${a.directionality}" must be one of producer, consumer, request_reply, bidirectional_stream.`,
|
|
74
|
+
path: 'directionality',
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
// INT-002 — source and target must be APPLICATION-… IDs.
|
|
78
|
+
if (typeof a.source === 'string' && a.source.trim() !== '' && !isCanonicalIdOfType(a.source, 'APPLICATION')) {
|
|
79
|
+
errors.push({
|
|
80
|
+
code: 'INT-002',
|
|
81
|
+
message: `source "${a.source}" must be an APPLICATION-… canonical id when interface_semantics is true.`,
|
|
82
|
+
path: 'source',
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
if (typeof a.target === 'string' && a.target.trim() !== '' && !isCanonicalIdOfType(a.target, 'APPLICATION')) {
|
|
86
|
+
errors.push({
|
|
87
|
+
code: 'INT-002',
|
|
88
|
+
message: `target "${a.target}" must be an APPLICATION-… canonical id when interface_semantics is true.`,
|
|
89
|
+
path: 'target',
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
94
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { validateLocation } from '../validate.js';
|
|
3
|
+
function valid() {
|
|
4
|
+
return {
|
|
5
|
+
notation: 'location',
|
|
6
|
+
id: 'LOCATION-TBILISI-1',
|
|
7
|
+
name: 'Tbilisi Office',
|
|
8
|
+
type: 'office',
|
|
9
|
+
address: '14 Rustaveli Ave, Tbilisi 0108, Georgia',
|
|
10
|
+
country_code: 'GE',
|
|
11
|
+
timezone: 'Asia/Tbilisi',
|
|
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) => validateLocation(input).errors.map(e => e.code);
|
|
21
|
+
describe('validateLocation — positive', () => {
|
|
22
|
+
it('accepts a well-formed office location', () => {
|
|
23
|
+
const r = validateLocation(valid());
|
|
24
|
+
expect(r.valid, JSON.stringify(r.errors)).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
it('accepts a country with no optional fields', () => {
|
|
27
|
+
const a = valid();
|
|
28
|
+
a.id = 'LOCATION-GE-1';
|
|
29
|
+
a.name = 'Georgia';
|
|
30
|
+
a.type = 'country';
|
|
31
|
+
delete a.address;
|
|
32
|
+
delete a.country_code;
|
|
33
|
+
delete a.timezone;
|
|
34
|
+
expect(validateLocation(a).valid).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
it('accepts a virtual location', () => {
|
|
37
|
+
const a = valid();
|
|
38
|
+
a.id = 'LOCATION-VIRTUAL-1';
|
|
39
|
+
a.name = 'Remote';
|
|
40
|
+
a.type = 'virtual';
|
|
41
|
+
expect(validateLocation(a).valid).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
it('accepts a location with a valid parent reference', () => {
|
|
44
|
+
expect(validateLocation({ ...valid(), parent: 'LOCATION-GE-1' }).valid).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
it('accepts valid_to as a date string', () => {
|
|
47
|
+
expect(validateLocation({ ...valid(), valid_to: '2026-12-31' }).valid).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
describe('validateLocation — LOC-001 (shape / id grammar)', () => {
|
|
51
|
+
it('flags a missing required envelope field (name)', () => {
|
|
52
|
+
const a = valid();
|
|
53
|
+
delete a.name;
|
|
54
|
+
expect(codes(a)).toContain('LOC-001');
|
|
55
|
+
});
|
|
56
|
+
it('flags an id that violates the grammar', () => {
|
|
57
|
+
expect(codes({ ...valid(), id: 'LOCATION-001' })).toContain('LOC-001');
|
|
58
|
+
expect(codes({ ...valid(), id: 'LOC-1' })).toContain('LOC-001');
|
|
59
|
+
});
|
|
60
|
+
it('flags a wrong notation tag', () => {
|
|
61
|
+
expect(codes({ ...valid(), notation: 'place' })).toContain('LOC-001');
|
|
62
|
+
});
|
|
63
|
+
it('flags a non-canon zone', () => {
|
|
64
|
+
expect(codes({ ...valid(), zone: 'sandbox' })).toContain('LOC-001');
|
|
65
|
+
});
|
|
66
|
+
it('flags a missing zone', () => {
|
|
67
|
+
const a = valid();
|
|
68
|
+
delete a.zone;
|
|
69
|
+
expect(codes(a)).toContain('LOC-001');
|
|
70
|
+
});
|
|
71
|
+
it('flags a missing type', () => {
|
|
72
|
+
const a = valid();
|
|
73
|
+
delete a.type;
|
|
74
|
+
expect(codes(a)).toContain('LOC-001');
|
|
75
|
+
});
|
|
76
|
+
it('flags a missing valid_to key', () => {
|
|
77
|
+
const a = valid();
|
|
78
|
+
delete a.valid_to;
|
|
79
|
+
expect(codes(a)).toContain('LOC-001');
|
|
80
|
+
});
|
|
81
|
+
it('flags missing gate_checks', () => {
|
|
82
|
+
const a = valid();
|
|
83
|
+
delete a.gate_checks;
|
|
84
|
+
expect(codes(a)).toContain('LOC-001');
|
|
85
|
+
});
|
|
86
|
+
it('rejects a non-object', () => {
|
|
87
|
+
expect(codes(null)).toEqual(['LOC-001']);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
describe('validateLocation — LOC-002 (type enum)', () => {
|
|
91
|
+
it('flags a type outside the allowed enum', () => {
|
|
92
|
+
expect(codes({ ...valid(), type: 'building' })).toContain('LOC-002');
|
|
93
|
+
expect(codes({ ...valid(), type: 'facility' })).toContain('LOC-002');
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
describe('validateLocation — LOC-003 (parent grammar)', () => {
|
|
97
|
+
it('flags a parent that is not a LOCATION-… id', () => {
|
|
98
|
+
expect(codes({ ...valid(), parent: 'ACTOR-OPS-1' })).toContain('LOC-003');
|
|
99
|
+
expect(codes({ ...valid(), parent: 'not-an-id' })).toContain('LOC-003');
|
|
100
|
+
});
|
|
101
|
+
it('accepts a null or absent parent without error', () => {
|
|
102
|
+
expect(codes({ ...valid(), parent: undefined })).not.toContain('LOC-003');
|
|
103
|
+
const a = valid();
|
|
104
|
+
delete a.parent;
|
|
105
|
+
expect(codes(a)).not.toContain('LOC-003');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { GateChecks } from '../requirement/types.js';
|
|
2
|
+
export declare const LOCATION_TYPES: readonly ["country", "region", "city", "site", "office", "virtual"];
|
|
3
|
+
export type LocationType = typeof LOCATION_TYPES[number];
|
|
4
|
+
export interface Location {
|
|
5
|
+
notation: 'location';
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
type: LocationType;
|
|
9
|
+
address?: string;
|
|
10
|
+
country_code?: string;
|
|
11
|
+
timezone?: string;
|
|
12
|
+
parent?: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
zone: 'canon';
|
|
15
|
+
admitted_at: string;
|
|
16
|
+
admitted_by: string;
|
|
17
|
+
gate_checks: GateChecks;
|
|
18
|
+
valid_from: string;
|
|
19
|
+
valid_to: string | null;
|
|
20
|
+
}
|