@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
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { validateActivityCard } from '../validate.js';
|
|
3
3
|
const VALID = {
|
|
4
|
-
notation: '
|
|
4
|
+
notation: 'action-card',
|
|
5
5
|
spec_version: '0.1',
|
|
6
|
-
|
|
6
|
+
action_card: {
|
|
7
7
|
id: 'ACTIVITY_CARD-EU-PROGRAMME-1',
|
|
8
8
|
project: 'ACTIVITY-EU-PROGRAMME-1',
|
|
9
9
|
description: 'Executive summary.',
|
|
@@ -18,11 +18,22 @@ const VALID = {
|
|
|
18
18
|
},
|
|
19
19
|
};
|
|
20
20
|
describe('validateActivityCard', () => {
|
|
21
|
-
it('accepts a well-formed card (
|
|
21
|
+
it('accepts a well-formed card (notation: action-card)', () => {
|
|
22
22
|
const r = validateActivityCard(VALID);
|
|
23
23
|
expect(r.valid).toBe(true);
|
|
24
24
|
expect(r.errors).toHaveLength(0);
|
|
25
|
-
expect(r.warnings.some(w => w.code === 'DEPRECATED_NOTATION')).toBe(
|
|
25
|
+
expect(r.warnings.some(w => w.code === 'DEPRECATED_NOTATION')).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
it('rejects deprecated notation: activity-card', () => {
|
|
28
|
+
const r = validateActivityCard({
|
|
29
|
+
notation: 'activity-card',
|
|
30
|
+
activity_card: {
|
|
31
|
+
id: 'ACTIVITY_CARD-EU-PROGRAMME-1',
|
|
32
|
+
project: 'ACTIVITY-EU-PROGRAMME-1',
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
expect(r.valid).toBe(false);
|
|
36
|
+
expect(r.errors.some(e => e.code === 'HDR-002')).toBe(true);
|
|
26
37
|
});
|
|
27
38
|
it('accepts notation: action-card with action_card block (canonical)', () => {
|
|
28
39
|
const r = validateActivityCard({
|
|
@@ -59,33 +70,33 @@ describe('validateActivityCard', () => {
|
|
|
59
70
|
const r = validateActivityCard({ ...VALID, notation: 'activities' });
|
|
60
71
|
expect(r.errors.some((e) => e.code === 'HDR-002')).toBe(true);
|
|
61
72
|
});
|
|
62
|
-
it('AC-001 on missing
|
|
63
|
-
const r = validateActivityCard({ notation: '
|
|
73
|
+
it('AC-001 on missing action_card block', () => {
|
|
74
|
+
const r = validateActivityCard({ notation: 'action-card' });
|
|
64
75
|
expect(r.valid).toBe(false);
|
|
65
76
|
expect(r.errors.some((e) => e.code === 'AC-001')).toBe(true);
|
|
66
77
|
});
|
|
67
78
|
it('AC-002 on malformed card id', () => {
|
|
68
|
-
const r = validateActivityCard({ ...VALID,
|
|
79
|
+
const r = validateActivityCard({ ...VALID, action_card: { ...VALID.action_card, id: 'CARD-1' } });
|
|
69
80
|
expect(r.errors.some((e) => e.code === 'AC-002')).toBe(true);
|
|
70
81
|
});
|
|
71
82
|
it('PC-001 on missing project', () => {
|
|
72
83
|
const r = validateActivityCard({
|
|
73
84
|
...VALID,
|
|
74
|
-
|
|
85
|
+
action_card: { ...VALID.action_card, project: undefined },
|
|
75
86
|
});
|
|
76
87
|
expect(r.errors.some((e) => e.code === 'PC-001')).toBe(true);
|
|
77
88
|
});
|
|
78
89
|
it('PC-001 on malformed project id', () => {
|
|
79
90
|
const r = validateActivityCard({
|
|
80
91
|
...VALID,
|
|
81
|
-
|
|
92
|
+
action_card: { ...VALID.action_card, project: 'GOAL-1' },
|
|
82
93
|
});
|
|
83
94
|
expect(r.errors.some((e) => e.code === 'PC-001')).toBe(true);
|
|
84
95
|
});
|
|
85
96
|
it('AC-003 on milestone missing required fields', () => {
|
|
86
97
|
const r = validateActivityCard({
|
|
87
98
|
...VALID,
|
|
88
|
-
|
|
99
|
+
action_card: { ...VALID.action_card, milestones: [{ id: 'MILESTONE-X-1' }] },
|
|
89
100
|
});
|
|
90
101
|
expect(r.valid).toBe(false);
|
|
91
102
|
expect(r.errors.some((e) => e.code === 'AC-003')).toBe(true);
|
|
@@ -93,8 +104,8 @@ describe('validateActivityCard', () => {
|
|
|
93
104
|
it('AC-004 on malformed milestone id', () => {
|
|
94
105
|
const r = validateActivityCard({
|
|
95
106
|
...VALID,
|
|
96
|
-
|
|
97
|
-
...VALID.
|
|
107
|
+
action_card: {
|
|
108
|
+
...VALID.action_card,
|
|
98
109
|
milestones: [{ id: 'MS-1', name: 'x', date: '2027-01-01' }],
|
|
99
110
|
},
|
|
100
111
|
});
|
|
@@ -104,15 +115,15 @@ describe('validateActivityCard', () => {
|
|
|
104
115
|
const m = { id: 'MILESTONE-X-1', name: 'x', date: '2027-01-01' };
|
|
105
116
|
const r = validateActivityCard({
|
|
106
117
|
...VALID,
|
|
107
|
-
|
|
118
|
+
action_card: { ...VALID.action_card, milestones: [m, { ...m }] },
|
|
108
119
|
});
|
|
109
120
|
expect(r.errors.some((e) => e.code === 'AC-004' && /Duplicate/.test(e.message))).toBe(true);
|
|
110
121
|
});
|
|
111
122
|
it('AC-005 on bad milestone date format', () => {
|
|
112
123
|
const r = validateActivityCard({
|
|
113
124
|
...VALID,
|
|
114
|
-
|
|
115
|
-
...VALID.
|
|
125
|
+
action_card: {
|
|
126
|
+
...VALID.action_card,
|
|
116
127
|
milestones: [{ id: 'MILESTONE-X-1', name: 'x', date: '31/01/2027' }],
|
|
117
128
|
},
|
|
118
129
|
});
|
|
@@ -121,8 +132,8 @@ describe('validateActivityCard', () => {
|
|
|
121
132
|
it('AC-006 on malformed delivers_changes entry', () => {
|
|
122
133
|
const r = validateActivityCard({
|
|
123
134
|
...VALID,
|
|
124
|
-
|
|
125
|
-
...VALID.
|
|
135
|
+
action_card: {
|
|
136
|
+
...VALID.action_card,
|
|
126
137
|
milestones: [{ id: 'MILESTONE-X-1', name: 'x', date: '2027-01-01', delivers_changes: ['GOAL-1'] }],
|
|
127
138
|
},
|
|
128
139
|
});
|
|
@@ -131,7 +142,7 @@ describe('validateActivityCard', () => {
|
|
|
131
142
|
it('accepts a card with no milestones', () => {
|
|
132
143
|
const r = validateActivityCard({
|
|
133
144
|
...VALID,
|
|
134
|
-
|
|
145
|
+
action_card: { id: 'ACTIVITY_CARD-X-1', project: 'ACTIVITY-X-1' },
|
|
135
146
|
});
|
|
136
147
|
expect(r.valid).toBe(true);
|
|
137
148
|
});
|
|
@@ -164,7 +164,7 @@ export function layoutActivityCard(card, options) {
|
|
|
164
164
|
descriptionRow = { label: 'Description', valueLines: lines, x: contentX, y: cursorY, width: contentW, height };
|
|
165
165
|
cursorY += height + SECTION_GAP;
|
|
166
166
|
}
|
|
167
|
-
// ── 5. Chain: Drivers →
|
|
167
|
+
// ── 5. Chain: Drivers → Goals → Changes ──────────────────────────────────
|
|
168
168
|
// Each section is a full-width band with a header label and stacked node rows.
|
|
169
169
|
// An empty section shows a gap indicator ("— not on file") so the author
|
|
170
170
|
// sees which parts of the narrative are missing.
|
|
@@ -192,9 +192,7 @@ export function layoutActivityCard(card, options) {
|
|
|
192
192
|
cursorY += sectionH + CONNECTOR_H;
|
|
193
193
|
}
|
|
194
194
|
const { drivers, goals, changes } = card.motivation;
|
|
195
|
-
const assessments = card.assessments ?? [];
|
|
196
195
|
pushChainSection('drivers', 'Drivers', 'What prompted this initiative?', drivers.map((d) => ({ id: d.id, name: d.name })));
|
|
197
|
-
pushChainSection('assessments', 'Assessments', 'What is the current gap?', assessments.map((a) => ({ id: a.id, name: a.name, meta: a.observed_at })));
|
|
198
196
|
pushChainSection('goals', 'Goals', 'What outcome do we target?', goals.map((g) => ({ id: g.id, name: g.name })));
|
|
199
197
|
pushChainSection('changes', 'Changes', 'What will change?', changes.map((c) => ({ id: c.id, name: c.name })));
|
|
200
198
|
// Remove trailing CONNECTOR_H — last section connects to milestones with SECTION_GAP.
|
|
@@ -202,10 +200,6 @@ export function layoutActivityCard(card, options) {
|
|
|
202
200
|
// Node-level chain edges.
|
|
203
201
|
const driverIdSet = new Set(drivers.map((d) => d.id));
|
|
204
202
|
const goalIdSet = new Set(goals.map((g) => g.id));
|
|
205
|
-
for (const a of assessments) {
|
|
206
|
-
if (driverIdSet.has(a.driverId))
|
|
207
|
-
chainEdges.push({ sourceId: a.driverId, targetId: a.id });
|
|
208
|
-
}
|
|
209
203
|
for (const g of goals) {
|
|
210
204
|
for (const did of g.driverIds) {
|
|
211
205
|
if (driverIdSet.has(did))
|
|
@@ -350,10 +350,19 @@ export function resolveActivityCard(doc, sources) {
|
|
|
350
350
|
// goals referenced only by in-scope changes). Resolve each id to its GOAL
|
|
351
351
|
// element name, falling back to the id when the element is absent.
|
|
352
352
|
const goalNames = projectGoalIds.map((gid) => str(goalElems.get(gid)?.['name']) ?? gid);
|
|
353
|
-
// ── Stakeholders —
|
|
353
|
+
// ── Stakeholders — inline `stakeholders: []` on the Action element PLUS active
|
|
354
|
+
// `action_stakeholder` / `activity_stakeholder` REL files. Both sources are
|
|
355
|
+
// merged; REL entries take precedence (they carry the role field).
|
|
354
356
|
const stakeholderElems = collectByNotation(sources.elements, 'stakeholder');
|
|
357
|
+
const relStakeholderRefs = activeActivityStakeholders(sources.relations, projectId);
|
|
358
|
+
const relStakeholderIds = new Set(relStakeholderRefs.map((s) => s.id));
|
|
359
|
+
const inlineStakeholderIds = strArray(projectRec['stakeholders']).filter((id) => !relStakeholderIds.has(id));
|
|
360
|
+
const allStakeholderRefs = [
|
|
361
|
+
...relStakeholderRefs,
|
|
362
|
+
...inlineStakeholderIds.map((id) => ({ id })),
|
|
363
|
+
];
|
|
355
364
|
const stakeholders = [];
|
|
356
|
-
for (const { id: sid, role } of
|
|
365
|
+
for (const { id: sid, role } of allStakeholderRefs) {
|
|
357
366
|
const rec = stakeholderElems.get(sid);
|
|
358
367
|
if (!rec) {
|
|
359
368
|
warnings.push({
|
|
@@ -214,7 +214,7 @@ export interface ChainEdge {
|
|
|
214
214
|
* so the author sees the missing data.
|
|
215
215
|
*/
|
|
216
216
|
export interface ChainSectionLayout {
|
|
217
|
-
type: 'drivers' | '
|
|
217
|
+
type: 'drivers' | 'goals' | 'changes';
|
|
218
218
|
/** Section header label. */
|
|
219
219
|
label: string;
|
|
220
220
|
/** Short question answered by this section, shown in grey after the label. */
|
|
@@ -40,21 +40,14 @@ export function validateActivityCard(input) {
|
|
|
40
40
|
if (!('notation' in raw)) {
|
|
41
41
|
errors.push({ code: 'HDR-001', message: 'notation field is required' });
|
|
42
42
|
}
|
|
43
|
-
else if (raw['notation'] !== 'action-card'
|
|
43
|
+
else if (raw['notation'] !== 'action-card') {
|
|
44
44
|
errors.push({
|
|
45
45
|
code: 'HDR-002',
|
|
46
46
|
message: `notation must be "action-card", got "${String(raw['notation'])}"`,
|
|
47
47
|
});
|
|
48
48
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
// ── action_card / activity_card block ───────────────────────────────────────
|
|
53
|
-
// "action_card:" is canonical; "activity_card:" is accepted for compat.
|
|
54
|
-
const rawBlock = raw['action_card'] ?? raw['activity_card'];
|
|
55
|
-
if (raw['activity_card'] !== undefined && raw['action_card'] === undefined) {
|
|
56
|
-
warnings.push({ code: 'DEPRECATED_FIELD', message: 'Root key "activity_card:" is deprecated — rename to "action_card:"' });
|
|
57
|
-
}
|
|
49
|
+
// ── action_card block ────────────────────────────────────────────────────────
|
|
50
|
+
const rawBlock = raw['action_card'];
|
|
58
51
|
const block = rawBlock;
|
|
59
52
|
if (!block || typeof block !== 'object' || Array.isArray(block)) {
|
|
60
53
|
errors.push({ code: 'AC-001', message: 'Missing required root key: action_card (object)' });
|
|
@@ -86,4 +86,10 @@ describe('validateActor — ACTOR-003 (engagement fields forbidden inline)', ()
|
|
|
86
86
|
it('flags inline owner', () => {
|
|
87
87
|
expect(codes({ ...valid(), owner: 'ACTOR-OPS-1' })).toContain('ACTOR-003');
|
|
88
88
|
});
|
|
89
|
+
it('flags inline located_at (location belongs in a REL file)', () => {
|
|
90
|
+
expect(codes({ ...valid(), located_at: 'LOCATION-TBILISI-1' })).toContain('ACTOR-003');
|
|
91
|
+
});
|
|
92
|
+
it('flags inline unit_located_at (location belongs in a REL file)', () => {
|
|
93
|
+
expect(codes({ ...valid(), unit_located_at: 'LOCATION-TBILISI-1' })).toContain('ACTOR-003');
|
|
94
|
+
});
|
|
89
95
|
});
|
package/dist/actor/types.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export type ActorType = typeof ACTOR_TYPES[number];
|
|
|
6
6
|
* inline on an ACTOR file (§7.10, ACTOR-003). They belong in time-aware REL
|
|
7
7
|
* records, never on the identity primitive.
|
|
8
8
|
*/
|
|
9
|
-
export declare const ACTOR_FORBIDDEN_INLINE_FIELDS: readonly ["employment", "candidacy", "alumni_membership", "community_membership", "contracting", "unit_parent", "roles", "owner"];
|
|
9
|
+
export declare const ACTOR_FORBIDDEN_INLINE_FIELDS: readonly ["employment", "candidacy", "alumni_membership", "community_membership", "contracting", "unit_parent", "located_at", "unit_located_at", "roles", "owner"];
|
|
10
10
|
export interface Actor {
|
|
11
11
|
notation: 'actor';
|
|
12
12
|
id: string;
|
package/dist/actor/types.js
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
export type ApplicationType = 'application' | 'integration' | 'platform' | 'data_store';
|
|
2
2
|
export type ApplicationStatus = 'Draft' | 'Active' | 'Deprecated' | 'Decommissioning';
|
|
3
3
|
export type IntegrationDirection = 'inbound' | 'outbound' | 'bidirectional';
|
|
4
|
+
export type IntegrationSensitivity = 'public' | 'internal' | 'confidential' | 'restricted';
|
|
5
|
+
export type IntegrationDirectionality = 'producer' | 'consumer' | 'request_reply' | 'bidirectional_stream';
|
|
4
6
|
export interface ApplicationIntegration {
|
|
5
7
|
target?: string;
|
|
8
|
+
source?: string;
|
|
6
9
|
direction?: IntegrationDirection;
|
|
7
10
|
protocol?: string;
|
|
8
11
|
description?: string;
|
|
12
|
+
interface_semantics?: boolean;
|
|
13
|
+
payload_class?: string;
|
|
14
|
+
sensitivity?: IntegrationSensitivity;
|
|
15
|
+
directionality?: IntegrationDirectionality;
|
|
9
16
|
}
|
|
10
17
|
export interface Application {
|
|
11
18
|
app_id: string;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
const VALID_TYPES = new Set(['application', 'integration', 'platform', 'data_store']);
|
|
2
2
|
const VALID_STATUSES = new Set(['Draft', 'Active', 'Deprecated', 'Decommissioning']);
|
|
3
3
|
const VALID_DIRECTIONS = new Set(['inbound', 'outbound', 'bidirectional']);
|
|
4
|
+
const VALID_SENSITIVITY = new Set(['public', 'internal', 'confidential', 'restricted']);
|
|
5
|
+
const VALID_DIRECTIONALITY = new Set(['producer', 'consumer', 'request_reply', 'bidirectional_stream']);
|
|
4
6
|
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
7
|
+
const IFACE_CONDITIONAL = ['protocol', 'payload_class', 'sensitivity', 'directionality'];
|
|
5
8
|
export function validateApplicationsCatalogue(input) {
|
|
6
9
|
const errors = [];
|
|
7
10
|
const warnings = [];
|
|
@@ -102,6 +105,21 @@ export function validateApplicationsCatalogue(input) {
|
|
|
102
105
|
if (intg['direction'] !== undefined && !VALID_DIRECTIONS.has(intg['direction'])) {
|
|
103
106
|
errors.push({ code: 'APP-009', message: `${idx}.integrations[${j}]: direction "${intg['direction']}" must be one of: inbound, outbound, bidirectional` });
|
|
104
107
|
}
|
|
108
|
+
// INT-001 — when interface_semantics: true, four fields become required.
|
|
109
|
+
if (intg['interface_semantics'] === true) {
|
|
110
|
+
for (const field of IFACE_CONDITIONAL) {
|
|
111
|
+
const v = intg[field];
|
|
112
|
+
if (typeof v !== 'string' || v.trim() === '') {
|
|
113
|
+
errors.push({ code: 'INT-001', message: `${idx}.integrations[${j}]: ${field} is required when interface_semantics is true.`, path: `${idx}.integrations[${j}].${field}` });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (typeof intg['sensitivity'] === 'string' && !VALID_SENSITIVITY.has(intg['sensitivity'])) {
|
|
117
|
+
errors.push({ code: 'INT-001', message: `${idx}.integrations[${j}]: sensitivity "${intg['sensitivity']}" must be one of public, internal, confidential, restricted.`, path: `${idx}.integrations[${j}].sensitivity` });
|
|
118
|
+
}
|
|
119
|
+
if (typeof intg['directionality'] === 'string' && !VALID_DIRECTIONALITY.has(intg['directionality'])) {
|
|
120
|
+
errors.push({ code: 'INT-001', message: `${idx}.integrations[${j}]: directionality "${intg['directionality']}" must be one of producer, consumer, request_reply, bidirectional_stream.`, path: `${idx}.integrations[${j}].directionality` });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
105
123
|
}
|
|
106
124
|
}
|
|
107
125
|
}
|
|
@@ -109,7 +109,7 @@ describe('layoutNestedBlocks', () => {
|
|
|
109
109
|
});
|
|
110
110
|
const a = layout.blocks[0];
|
|
111
111
|
expect(a.width).toBe(160);
|
|
112
|
-
expect(a.height).toBe(
|
|
112
|
+
expect(a.height).toBe(72);
|
|
113
113
|
});
|
|
114
114
|
it('honours custom layout options', () => {
|
|
115
115
|
const layout = layoutNestedBlocks({
|
package/dist/blocks/layout.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { validateBusinessService } from '../validate.js';
|
|
3
|
+
function valid() {
|
|
4
|
+
return {
|
|
5
|
+
notation: 'business-service',
|
|
6
|
+
id: 'BUSINESS_SERVICE-CRM-1',
|
|
7
|
+
name: 'CRM Service',
|
|
8
|
+
type: 'internal',
|
|
9
|
+
description: 'Customer relationship management service offering contact management and deal tracking.',
|
|
10
|
+
offering_unit: 'ACTOR-SALES-OPS-1',
|
|
11
|
+
capability: 'CAPABILITY-V2.1',
|
|
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) => validateBusinessService(input).errors.map(e => e.code);
|
|
21
|
+
describe('validateBusinessService — positive', () => {
|
|
22
|
+
it('accepts a well-formed internal service with all optional fields', () => {
|
|
23
|
+
const r = validateBusinessService(valid());
|
|
24
|
+
expect(r.valid, JSON.stringify(r.errors)).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
it('accepts an external service with no optional fields', () => {
|
|
27
|
+
const a = valid();
|
|
28
|
+
a.id = 'BUSINESS_SERVICE-ONBOARDING-1';
|
|
29
|
+
a.name = 'Onboarding Service';
|
|
30
|
+
a.type = 'external';
|
|
31
|
+
delete a.description;
|
|
32
|
+
delete a.offering_unit;
|
|
33
|
+
delete a.capability;
|
|
34
|
+
expect(validateBusinessService(a).valid).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
it('accepts a shared service offered by a ROLE', () => {
|
|
37
|
+
expect(validateBusinessService({ ...valid(), type: 'shared', offering_unit: 'ROLE-PLATFORM-OWNER-1' }).valid).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
it('accepts valid_to as a date string', () => {
|
|
40
|
+
expect(validateBusinessService({ ...valid(), valid_to: '2026-12-31' }).valid).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
it('accepts a service with no offering_unit or capability', () => {
|
|
43
|
+
const a = valid();
|
|
44
|
+
delete a.offering_unit;
|
|
45
|
+
delete a.capability;
|
|
46
|
+
expect(validateBusinessService(a).valid).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
describe('validateBusinessService — BSV-001 (shape / id grammar)', () => {
|
|
50
|
+
it('flags a missing required envelope field (name)', () => {
|
|
51
|
+
const a = valid();
|
|
52
|
+
delete a.name;
|
|
53
|
+
expect(codes(a)).toContain('BSV-001');
|
|
54
|
+
});
|
|
55
|
+
it('flags an id that violates the grammar', () => {
|
|
56
|
+
expect(codes({ ...valid(), id: 'BUSINESS_SERVICE-001' })).toContain('BSV-001');
|
|
57
|
+
expect(codes({ ...valid(), id: 'BSV-1' })).toContain('BSV-001');
|
|
58
|
+
expect(codes({ ...valid(), id: 'business-service-1' })).toContain('BSV-001');
|
|
59
|
+
});
|
|
60
|
+
it('flags a wrong notation tag', () => {
|
|
61
|
+
expect(codes({ ...valid(), notation: 'service' })).toContain('BSV-001');
|
|
62
|
+
expect(codes({ ...valid(), notation: 'business_service' })).toContain('BSV-001');
|
|
63
|
+
});
|
|
64
|
+
it('flags a non-canon zone', () => {
|
|
65
|
+
expect(codes({ ...valid(), zone: 'sandbox' })).toContain('BSV-001');
|
|
66
|
+
});
|
|
67
|
+
it('flags a missing zone', () => {
|
|
68
|
+
const a = valid();
|
|
69
|
+
delete a.zone;
|
|
70
|
+
expect(codes(a)).toContain('BSV-001');
|
|
71
|
+
});
|
|
72
|
+
it('flags a missing type', () => {
|
|
73
|
+
const a = valid();
|
|
74
|
+
delete a.type;
|
|
75
|
+
expect(codes(a)).toContain('BSV-001');
|
|
76
|
+
});
|
|
77
|
+
it('flags a missing valid_to key', () => {
|
|
78
|
+
const a = valid();
|
|
79
|
+
delete a.valid_to;
|
|
80
|
+
expect(codes(a)).toContain('BSV-001');
|
|
81
|
+
});
|
|
82
|
+
it('flags missing gate_checks', () => {
|
|
83
|
+
const a = valid();
|
|
84
|
+
delete a.gate_checks;
|
|
85
|
+
expect(codes(a)).toContain('BSV-001');
|
|
86
|
+
});
|
|
87
|
+
it('rejects a non-object', () => {
|
|
88
|
+
expect(codes(null)).toEqual(['BSV-001']);
|
|
89
|
+
expect(codes('string')).toEqual(['BSV-001']);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
describe('validateBusinessService — BSV-002 (type enum)', () => {
|
|
93
|
+
it('flags a type outside the allowed enum', () => {
|
|
94
|
+
expect(codes({ ...valid(), type: 'public' })).toContain('BSV-002');
|
|
95
|
+
expect(codes({ ...valid(), type: 'platform' })).toContain('BSV-002');
|
|
96
|
+
});
|
|
97
|
+
it('accepts all valid type values', () => {
|
|
98
|
+
for (const t of ['internal', 'external', 'shared']) {
|
|
99
|
+
expect(codes({ ...valid(), type: t })).not.toContain('BSV-002');
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
describe('validateBusinessService — BSV-003 (offering_unit grammar)', () => {
|
|
104
|
+
it('flags an offering_unit that is not an ACTOR-… or ROLE-… id', () => {
|
|
105
|
+
expect(codes({ ...valid(), offering_unit: 'PRODUCT-PLATFORM-1' })).toContain('BSV-003');
|
|
106
|
+
expect(codes({ ...valid(), offering_unit: 'not-an-id' })).toContain('BSV-003');
|
|
107
|
+
expect(codes({ ...valid(), offering_unit: 'BUSINESS_SERVICE-CRM-1' })).toContain('BSV-003');
|
|
108
|
+
});
|
|
109
|
+
it('accepts a null or absent offering_unit without error', () => {
|
|
110
|
+
expect(codes({ ...valid(), offering_unit: undefined })).not.toContain('BSV-003');
|
|
111
|
+
const a = valid();
|
|
112
|
+
delete a.offering_unit;
|
|
113
|
+
expect(codes(a)).not.toContain('BSV-003');
|
|
114
|
+
});
|
|
115
|
+
it('accepts ACTOR-… and ROLE-… ids', () => {
|
|
116
|
+
expect(codes({ ...valid(), offering_unit: 'ACTOR-OPS-1' })).not.toContain('BSV-003');
|
|
117
|
+
expect(codes({ ...valid(), offering_unit: 'ROLE-SERVICE-OWNER-1' })).not.toContain('BSV-003');
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
describe('validateBusinessService — BSV-004 (capability grammar)', () => {
|
|
121
|
+
it('flags a capability that is not a CAPABILITY-… id', () => {
|
|
122
|
+
expect(codes({ ...valid(), capability: 'PRODUCT-MOBILE-1' })).toContain('BSV-004');
|
|
123
|
+
expect(codes({ ...valid(), capability: 'not-a-capability' })).toContain('BSV-004');
|
|
124
|
+
});
|
|
125
|
+
it('accepts a null or absent capability without error', () => {
|
|
126
|
+
expect(codes({ ...valid(), capability: undefined })).not.toContain('BSV-004');
|
|
127
|
+
const a = valid();
|
|
128
|
+
delete a.capability;
|
|
129
|
+
expect(codes(a)).not.toContain('BSV-004');
|
|
130
|
+
});
|
|
131
|
+
it('accepts a CAPABILITY-… id', () => {
|
|
132
|
+
expect(codes({ ...valid(), capability: 'CAPABILITY-V2.1' })).not.toContain('BSV-004');
|
|
133
|
+
expect(codes({ ...valid(), capability: 'CAPABILITY-H3.1' })).not.toContain('BSV-004');
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { GateChecks } from '../requirement/types.js';
|
|
2
|
+
export declare const BUSINESS_SERVICE_TYPES: readonly ["internal", "external", "shared"];
|
|
3
|
+
export type BusinessServiceType = typeof BUSINESS_SERVICE_TYPES[number];
|
|
4
|
+
export interface BusinessService {
|
|
5
|
+
notation: 'business-service';
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
type: BusinessServiceType;
|
|
9
|
+
description?: string;
|
|
10
|
+
offering_unit?: string;
|
|
11
|
+
capability?: string;
|
|
12
|
+
owner_role?: string;
|
|
13
|
+
status?: 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
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// BUSINESS_SERVICE validator — methodology notations/elements/25-business-services.md §5.
|
|
2
|
+
//
|
|
3
|
+
// Implements BSV-001..004. The shared HDR-/LIFECYCLE- rules are
|
|
4
|
+
// cross-cutting concerns (CONTRACT.md §8) and out of scope for the
|
|
5
|
+
// single-artefact validator.
|
|
6
|
+
import { isCanonicalIdOfType, typeOfId } from '../typed-id.js';
|
|
7
|
+
import { BUSINESS_SERVICE_TYPES } from './types.js';
|
|
8
|
+
const REQUIRED_STRING_FIELDS = [
|
|
9
|
+
'name', 'admitted_at', 'admitted_by', 'valid_from',
|
|
10
|
+
];
|
|
11
|
+
const TYPES = BUSINESS_SERVICE_TYPES;
|
|
12
|
+
export function validateBusinessService(input) {
|
|
13
|
+
const errors = [];
|
|
14
|
+
const warnings = [];
|
|
15
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
|
16
|
+
errors.push({ code: 'BSV-001', message: 'Business service must be a YAML mapping.' });
|
|
17
|
+
return { valid: false, errors, warnings };
|
|
18
|
+
}
|
|
19
|
+
const a = input;
|
|
20
|
+
// BSV-001 — id grammar + notation tag + envelope.
|
|
21
|
+
if (!isCanonicalIdOfType(a.id, 'BUSINESS_SERVICE')) {
|
|
22
|
+
errors.push({ code: 'BSV-001', message: `id "${String(a.id)}" must match BUSINESS_SERVICE-[<middle>-]<INTEGER>.`, path: 'id' });
|
|
23
|
+
}
|
|
24
|
+
if (a.notation !== 'business-service') {
|
|
25
|
+
errors.push({ code: 'BSV-001', message: 'notation must be the fixed value "business-service".', path: 'notation' });
|
|
26
|
+
}
|
|
27
|
+
if (a.zone !== undefined && a.zone !== 'canon') {
|
|
28
|
+
errors.push({ code: 'BSV-001', message: 'zone must be "canon" for a BUSINESS_SERVICE.', path: 'zone' });
|
|
29
|
+
}
|
|
30
|
+
else if (a.zone === undefined) {
|
|
31
|
+
errors.push({ code: 'BSV-001', message: 'zone is required.', path: 'zone' });
|
|
32
|
+
}
|
|
33
|
+
for (const field of REQUIRED_STRING_FIELDS) {
|
|
34
|
+
const v = a[field];
|
|
35
|
+
if (typeof v !== 'string' || v.trim() === '') {
|
|
36
|
+
errors.push({ code: 'BSV-001', message: `${field} is required.`, path: field });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (a.gate_checks === null || typeof a.gate_checks !== 'object') {
|
|
40
|
+
errors.push({ code: 'BSV-001', message: 'gate_checks is required and must be a mapping.', path: 'gate_checks' });
|
|
41
|
+
}
|
|
42
|
+
if (!('valid_to' in a) || !(typeof a.valid_to === 'string' || a.valid_to === null)) {
|
|
43
|
+
errors.push({ code: 'BSV-001', message: 'valid_to is required (an ISO date string or null).', path: 'valid_to' });
|
|
44
|
+
}
|
|
45
|
+
if (typeof a.type !== 'string' || a.type.trim() === '') {
|
|
46
|
+
errors.push({ code: 'BSV-001', message: 'type is required.', path: 'type' });
|
|
47
|
+
}
|
|
48
|
+
else if (!TYPES.includes(a.type)) {
|
|
49
|
+
// BSV-002 — type enum.
|
|
50
|
+
errors.push({ code: 'BSV-002', message: `type "${a.type}" must be one of ${TYPES.join(', ')}.`, path: 'type' });
|
|
51
|
+
}
|
|
52
|
+
// BSV-003 — offering_unit must resolve to ACTOR(business_unit) or ROLE when present.
|
|
53
|
+
if ('offering_unit' in a && a.offering_unit !== undefined && a.offering_unit !== null) {
|
|
54
|
+
const t = typeOfId(a.offering_unit);
|
|
55
|
+
if (t !== 'ACTOR' && t !== 'ROLE') {
|
|
56
|
+
errors.push({
|
|
57
|
+
code: 'BSV-003',
|
|
58
|
+
message: `offering_unit "${String(a.offering_unit)}" must be an ACTOR-… or ROLE-… canonical id.`,
|
|
59
|
+
path: 'offering_unit',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// BSV-004 — capability must resolve to CAPABILITY when present.
|
|
64
|
+
if ('capability' in a && a.capability !== undefined && a.capability !== null) {
|
|
65
|
+
if (typeOfId(a.capability) !== 'CAPABILITY') {
|
|
66
|
+
errors.push({
|
|
67
|
+
code: 'BSV-004',
|
|
68
|
+
message: `capability "${String(a.capability)}" must be a CAPABILITY-… canonical id.`,
|
|
69
|
+
path: 'capability',
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
74
|
+
}
|