@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
|
@@ -2,11 +2,11 @@ import { describe, it, expect } from 'vitest';
|
|
|
2
2
|
import { validateFGCADoc } from '../validate.js';
|
|
3
3
|
|
|
4
4
|
const VALID_DOC = {
|
|
5
|
-
notation: '
|
|
5
|
+
notation: 'dgca',
|
|
6
6
|
factors: [{ id: 1, name: 'Market pressure' }],
|
|
7
7
|
goals: [{ id: 1, name: 'Grow revenue', factor: [{ id: 1 }] }],
|
|
8
8
|
changes: [{ id: 1, name: 'Launch product', goal_id: 1, activity_ids: [1] }],
|
|
9
|
-
|
|
9
|
+
actions: [{ id: 1, name: 'Market research', goal_id: 1 }],
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
describe('validateFGCADoc', () => {
|
|
@@ -37,28 +37,28 @@ describe('validateFGCADoc', () => {
|
|
|
37
37
|
|
|
38
38
|
it('errors when factors array is missing', () => {
|
|
39
39
|
const { factors: _, ...without } = VALID_DOC;
|
|
40
|
-
const result = validateFGCADoc({ ...without, notation: '
|
|
40
|
+
const result = validateFGCADoc({ ...without, notation: 'dgca' });
|
|
41
41
|
expect(result.valid).toBe(false);
|
|
42
42
|
expect(result.errors.some(e => e.path === 'factors')).toBe(true);
|
|
43
43
|
});
|
|
44
44
|
|
|
45
45
|
it('errors when goals array is missing', () => {
|
|
46
46
|
const { goals: _, ...without } = VALID_DOC;
|
|
47
|
-
const result = validateFGCADoc({ ...without, notation: '
|
|
47
|
+
const result = validateFGCADoc({ ...without, notation: 'dgca' });
|
|
48
48
|
expect(result.valid).toBe(false);
|
|
49
49
|
expect(result.errors.some(e => e.path === 'goals')).toBe(true);
|
|
50
50
|
});
|
|
51
51
|
|
|
52
52
|
it('errors when changes array is missing', () => {
|
|
53
53
|
const { changes: _, ...without } = VALID_DOC;
|
|
54
|
-
const result = validateFGCADoc({ ...without, notation: '
|
|
54
|
+
const result = validateFGCADoc({ ...without, notation: 'dgca' });
|
|
55
55
|
expect(result.valid).toBe(false);
|
|
56
56
|
expect(result.errors.some(e => e.path === 'changes')).toBe(true);
|
|
57
57
|
});
|
|
58
58
|
|
|
59
59
|
it('errors when actions array is missing', () => {
|
|
60
|
-
const {
|
|
61
|
-
const result = validateFGCADoc({ ...without, notation: '
|
|
60
|
+
const { actions: _, ...without } = VALID_DOC;
|
|
61
|
+
const result = validateFGCADoc({ ...without, notation: 'dgca' });
|
|
62
62
|
expect(result.valid).toBe(false);
|
|
63
63
|
expect(result.errors.some(e => e.path === 'actions')).toBe(true);
|
|
64
64
|
});
|
|
@@ -104,11 +104,11 @@ describe('validateFGCADoc', () => {
|
|
|
104
104
|
|
|
105
105
|
it('accepts a document with empty arrays', () => {
|
|
106
106
|
const result = validateFGCADoc({
|
|
107
|
-
notation: '
|
|
107
|
+
notation: 'dgca',
|
|
108
108
|
factors: [],
|
|
109
109
|
goals: [],
|
|
110
110
|
changes: [],
|
|
111
|
-
|
|
111
|
+
actions: [],
|
|
112
112
|
});
|
|
113
113
|
expect(result.valid).toBe(true);
|
|
114
114
|
});
|
|
@@ -134,8 +134,8 @@ describe('validateFGCADoc', () => {
|
|
|
134
134
|
expect(r.valid).toBe(false);
|
|
135
135
|
expect(r.errors.some(e => e.code === 'SCHEMA_INVALID')).toBe(true);
|
|
136
136
|
});
|
|
137
|
-
it('[blocker] tolerates a null element in
|
|
138
|
-
const r = validateFGCADoc({ ...VALID_DOC,
|
|
137
|
+
it('[blocker] tolerates a null element in actions[] without throwing', () => {
|
|
138
|
+
const r = validateFGCADoc({ ...VALID_DOC, actions: [null] });
|
|
139
139
|
expect(r.valid).toBe(false);
|
|
140
140
|
expect(r.errors.some(e => e.code === 'SCHEMA_INVALID')).toBe(true);
|
|
141
141
|
});
|
|
@@ -63,10 +63,6 @@ function asObjectArray(v: unknown): Array<Record<string, unknown>> | null {
|
|
|
63
63
|
return v.map((el) => (el && typeof el === 'object' ? (el as Record<string, unknown>) : null) as Record<string, unknown>);
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
interface InternalIdMap {
|
|
67
|
-
/** Canonical typed-string ID → internal numeric ID for layout consumption. */
|
|
68
|
-
toInternal: Map<string, number>;
|
|
69
|
-
}
|
|
70
66
|
|
|
71
67
|
/**
|
|
72
68
|
* Validate canonical FGCA YAML and, on success, return an internal-form
|
|
@@ -86,21 +82,18 @@ export function parseCanonicalFGCA(input: unknown): CanonicalFGCAResult {
|
|
|
86
82
|
|
|
87
83
|
const raw = input as Record<string, unknown>;
|
|
88
84
|
|
|
89
|
-
if ('notation' in raw && raw['notation'] !== '
|
|
85
|
+
if ('notation' in raw && raw['notation'] !== 'dgca') {
|
|
90
86
|
return {
|
|
91
87
|
valid: false,
|
|
92
88
|
errors: [
|
|
93
89
|
{
|
|
94
90
|
code: 'FGCA-001',
|
|
95
|
-
message: `notation must be "dgca"
|
|
91
|
+
message: `notation must be "dgca", got "${String(raw['notation'])}"`,
|
|
96
92
|
},
|
|
97
93
|
],
|
|
98
94
|
warnings,
|
|
99
95
|
};
|
|
100
96
|
}
|
|
101
|
-
if ('notation' in raw && raw['notation'] === 'fgca') {
|
|
102
|
-
warnings.push({ code: 'FGCA-001', message: 'notation "fgca" is deprecated — rename to "dgca"' });
|
|
103
|
-
}
|
|
104
97
|
|
|
105
98
|
// FGCA-002 — document id (optional in v1.4 per the canonical spec? — spec
|
|
106
99
|
// says required; absence is FGCA-002).
|
|
@@ -122,10 +115,9 @@ export function parseCanonicalFGCA(input: unknown): CanonicalFGCAResult {
|
|
|
122
115
|
const factorsRaw = asObjectArray(raw['factors']);
|
|
123
116
|
const goalsRaw = asObjectArray(raw['goals']);
|
|
124
117
|
const changesRaw = asObjectArray(raw['changes']);
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
warnings.push({ code: 'FGCA-001', message: '"activities" key is deprecated — rename to "actions"' });
|
|
118
|
+
const activitiesRaw = asObjectArray(raw['actions']);
|
|
119
|
+
if (activitiesRaw === null && 'activities' in raw) {
|
|
120
|
+
errors.push({ code: 'FGCA-004', message: '"activities" key is not accepted — rename to "actions"', path: 'actions' });
|
|
129
121
|
}
|
|
130
122
|
|
|
131
123
|
if (factorsRaw === null) errors.push({ code: 'FGCA-004', message: 'factors must be an array', path: 'factors' });
|
|
@@ -223,32 +215,20 @@ export function parseCanonicalFGCA(input: unknown): CanonicalFGCAResult {
|
|
|
223
215
|
return out;
|
|
224
216
|
}
|
|
225
217
|
|
|
226
|
-
//
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
allCanonicalIds.forEach((id, i) => idMap.toInternal.set(id, i + 1));
|
|
230
|
-
|
|
231
|
-
function intId(canonical: string): number {
|
|
232
|
-
const n = idMap.toInternal.get(canonical);
|
|
233
|
-
if (n === undefined) {
|
|
234
|
-
// Shouldn't happen — we built the map from the same sets.
|
|
235
|
-
throw new Error(`internal: canonical id "${canonical}" not mapped`);
|
|
236
|
-
}
|
|
237
|
-
return n;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Layer entries (internal form).
|
|
218
|
+
// Layer entries (internal form). Canonical string IDs are passed through
|
|
219
|
+
// directly — no conversion to sequential numbers. The layout and renderer
|
|
220
|
+
// treat IDs as opaque keys and also display them verbatim to the user.
|
|
241
221
|
const internalFactors: DriverItem[] = factors.map((el) => ({
|
|
242
|
-
id:
|
|
222
|
+
id: String(el!['id']),
|
|
243
223
|
name: String(el!['name'] ?? ''),
|
|
244
224
|
}));
|
|
245
225
|
|
|
246
226
|
const internalGoals: GoalItem[] = goals.map((el, i) => {
|
|
247
227
|
const refIds = checkRefArray(el!, 'factors', factorIds, FACTOR_OR_DRIVER_ID_RE, 'FGCA-008', `goals[${i}]`);
|
|
248
228
|
return {
|
|
249
|
-
id:
|
|
229
|
+
id: String(el!['id']),
|
|
250
230
|
name: String(el!['name'] ?? ''),
|
|
251
|
-
factor: refIds.map((r) => ({ id:
|
|
231
|
+
factor: refIds.map((r) => ({ id: r })),
|
|
252
232
|
};
|
|
253
233
|
});
|
|
254
234
|
|
|
@@ -284,10 +264,10 @@ export function parseCanonicalFGCA(input: unknown): CanonicalFGCAResult {
|
|
|
284
264
|
const canonicalChangeId = String(el!['id']);
|
|
285
265
|
const linkedActivityIds = activitiesForChange.get(canonicalChangeId) ?? [];
|
|
286
266
|
return {
|
|
287
|
-
id:
|
|
267
|
+
id: canonicalChangeId,
|
|
288
268
|
name: String(el!['name'] ?? ''),
|
|
289
|
-
goal_id: goalRefs.length > 0 ?
|
|
290
|
-
activity_ids: linkedActivityIds
|
|
269
|
+
goal_id: goalRefs.length > 0 ? goalRefs[0] : '',
|
|
270
|
+
activity_ids: linkedActivityIds,
|
|
291
271
|
};
|
|
292
272
|
});
|
|
293
273
|
|
|
@@ -295,9 +275,9 @@ export function parseCanonicalFGCA(input: unknown): CanonicalFGCAResult {
|
|
|
295
275
|
const goalRefs = checkRefArray(el!, 'goals', goalIds, GOAL_ID_RE, 'FGCA-011', `actions[${i}]`);
|
|
296
276
|
const typeVal = typeof el!['type'] === 'string' ? el!['type'] : undefined;
|
|
297
277
|
return {
|
|
298
|
-
id:
|
|
278
|
+
id: String(el!['id']),
|
|
299
279
|
name: String(el!['name'] ?? ''),
|
|
300
|
-
goal_id: goalRefs.length > 0 ?
|
|
280
|
+
goal_id: goalRefs.length > 0 ? goalRefs[0] : null,
|
|
301
281
|
...(typeVal !== undefined ? { type: typeVal } : {}),
|
|
302
282
|
};
|
|
303
283
|
});
|
|
@@ -364,10 +344,10 @@ export function parseCanonicalFGA(input: unknown): CanonicalFGAResult {
|
|
|
364
344
|
return { valid: false, errors: [{ code: 'FGA-001', message: 'document root is not an object' }], warnings: [] };
|
|
365
345
|
}
|
|
366
346
|
const raw = input as Record<string, unknown>;
|
|
367
|
-
if ('notation' in raw && raw['notation'] !== '
|
|
347
|
+
if ('notation' in raw && raw['notation'] !== 'dga') {
|
|
368
348
|
return {
|
|
369
349
|
valid: false,
|
|
370
|
-
errors: [{ code: 'FGA-001', message: `notation must be "dga"
|
|
350
|
+
errors: [{ code: 'FGA-001', message: `notation must be "dga", got "${String(raw['notation'])}"` }],
|
|
371
351
|
warnings: [],
|
|
372
352
|
};
|
|
373
353
|
}
|
|
@@ -154,19 +154,71 @@ export function layoutFGCAPreview(
|
|
|
154
154
|
activity: doc.activities.map(a => ({ id: `activity_${a.id}`, label: a.name })),
|
|
155
155
|
};
|
|
156
156
|
|
|
157
|
+
// Build predecessor map: for each node, which node IDs in the previous column
|
|
158
|
+
// connect to it from the left. Used for barycenter crossing minimization.
|
|
159
|
+
const predecessors = new Map<string, string[]>();
|
|
160
|
+
for (const g of doc.goals) {
|
|
161
|
+
predecessors.set(`goal_${g.id}`, (g.factor ?? []).map(f => `driver_${f.id}`));
|
|
162
|
+
}
|
|
163
|
+
if (!hideChanges) {
|
|
164
|
+
for (const c of changes) {
|
|
165
|
+
predecessors.set(`change_${c.id}`, [`goal_${c.goal_id}`]);
|
|
166
|
+
}
|
|
167
|
+
const coveredActs = new Set(changes.flatMap(c => c.activity_ids.map(String)));
|
|
168
|
+
for (const a of doc.activities) {
|
|
169
|
+
const preds = changes
|
|
170
|
+
.filter(c => c.activity_ids.map(String).includes(String(a.id)))
|
|
171
|
+
.map(c => `change_${c.id}`);
|
|
172
|
+
if (a.goal_id != null && !coveredActs.has(String(a.id))) preds.push(`goal_${a.goal_id}`);
|
|
173
|
+
predecessors.set(`activity_${a.id}`, preds);
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
const connectedViaChange = new Set<string>();
|
|
177
|
+
for (const c of changes) {
|
|
178
|
+
for (const aid of c.activity_ids) {
|
|
179
|
+
predecessors.set(`activity_${aid}`, [`goal_${c.goal_id}`]);
|
|
180
|
+
connectedViaChange.add(String(aid));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
for (const a of doc.activities) {
|
|
184
|
+
if (a.goal_id != null && !connectedViaChange.has(String(a.id))) {
|
|
185
|
+
predecessors.set(`activity_${a.id}`, [`goal_${a.goal_id}`]);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Sort a column's items by the barycenter of their predecessors' y-centres.
|
|
191
|
+
// Nodes with no predecessors sort last (Infinity barycenter) so they don't
|
|
192
|
+
// displace connected nodes.
|
|
193
|
+
function barycentricSort(
|
|
194
|
+
items: Array<{ id: string; label: string }>,
|
|
195
|
+
yCenters: Map<string, number>,
|
|
196
|
+
): Array<{ id: string; label: string }> {
|
|
197
|
+
return [...items].sort((a, b) => {
|
|
198
|
+
const pA = (predecessors.get(a.id) ?? []).map(p => yCenters.get(p) ?? 0).filter(v => v > 0);
|
|
199
|
+
const pB = (predecessors.get(b.id) ?? []).map(p => yCenters.get(p) ?? 0).filter(v => v > 0);
|
|
200
|
+
const bcA = pA.length > 0 ? pA.reduce((s, v) => s + v, 0) / pA.length : Infinity;
|
|
201
|
+
const bcB = pB.length > 0 ? pB.reduce((s, v) => s + v, 0) / pB.length : Infinity;
|
|
202
|
+
return bcA - bcB;
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
157
206
|
const nodeMap = new Map<string, FGCAPreviewNode>();
|
|
158
207
|
const nodes: FGCAPreviewNode[] = [];
|
|
159
208
|
const columns: FGCAPreviewColumnPos[] = [];
|
|
209
|
+
const yCenters = new Map<string, number>(); // nodeId → y + FGCA_NODE_H / 2
|
|
160
210
|
|
|
161
211
|
for (let ci = 0; ci < cols.length; ci++) {
|
|
162
212
|
const col = cols[ci];
|
|
163
213
|
const x = FGCA_PAD + ci * colStride;
|
|
164
214
|
columns.push({ col, x });
|
|
215
|
+
const items = ci === 0 ? colItems[col] : barycentricSort(colItems[col], yCenters);
|
|
165
216
|
let y = FGCA_PAD + FGCA_HEADER_H + rowGap;
|
|
166
|
-
for (const item of
|
|
217
|
+
for (const item of items) {
|
|
167
218
|
const node: FGCAPreviewNode = { id: item.id, x, y, label: item.label, col };
|
|
168
219
|
nodes.push(node);
|
|
169
220
|
nodeMap.set(item.id, node);
|
|
221
|
+
yCenters.set(item.id, y + FGCA_NODE_H / 2);
|
|
170
222
|
y += FGCA_NODE_H + rowGap;
|
|
171
223
|
}
|
|
172
224
|
}
|
package/src/fgca/resolver.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
|
|
|
@@ -69,7 +69,7 @@ export function isFGCAViewDoc(parsed: unknown): boolean {
|
|
|
69
69
|
* to pass to `parseCanonicalFGCA`.
|
|
70
70
|
*
|
|
71
71
|
* Cross-reference fields (`goal.factors[]`, `change.goals[]`,
|
|
72
|
-
* `
|
|
72
|
+
* `action.changes[]`) are carried from the canon elements unchanged; the
|
|
73
73
|
* resolver decides which elements to include, not how they are shaped.
|
|
74
74
|
*/
|
|
75
75
|
export function resolveFGCA(
|
|
@@ -77,7 +77,7 @@ export function resolveFGCA(
|
|
|
77
77
|
sources: FGCACanonSources,
|
|
78
78
|
): Record<string, unknown> {
|
|
79
79
|
if (!isObject(viewDoc)) {
|
|
80
|
-
return { notation: '
|
|
80
|
+
return { notation: 'dgca', factors: [], goals: [], changes: [], actions: [] };
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
const vc = isObject(viewDoc['view_config']) ? viewDoc['view_config'] : {};
|
|
@@ -147,13 +147,13 @@ export function resolveFGCA(
|
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
return {
|
|
150
|
-
notation: '
|
|
150
|
+
notation: 'dgca',
|
|
151
151
|
id: viewDoc['id'],
|
|
152
152
|
name: viewDoc['name'],
|
|
153
153
|
spec_version: viewDoc['spec_version'],
|
|
154
154
|
factors: selectedFactors,
|
|
155
155
|
goals: selectedGoals,
|
|
156
156
|
changes: selectedChanges,
|
|
157
|
-
|
|
157
|
+
actions: selectedActivities,
|
|
158
158
|
};
|
|
159
159
|
}
|
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
|
+
}
|