@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.
Files changed (140) hide show
  1. package/dist/__tests__/yaml-normalize.test.js +9 -9
  2. package/dist/activities/__tests__/validate.test.js +77 -74
  3. package/dist/activities/validate.js +5 -12
  4. package/dist/activity-card/__tests__/example.test.js +3 -5
  5. package/dist/activity-card/__tests__/layout.test.js +2 -9
  6. package/dist/activity-card/__tests__/resolver.test.js +25 -0
  7. package/dist/activity-card/__tests__/validate.test.js +29 -18
  8. package/dist/activity-card/layout.js +1 -7
  9. package/dist/activity-card/resolver.js +11 -2
  10. package/dist/activity-card/types.d.ts +1 -1
  11. package/dist/activity-card/validate.js +3 -10
  12. package/dist/actor/__tests__/validate.test.js +6 -0
  13. package/dist/actor/types.d.ts +1 -1
  14. package/dist/actor/types.js +2 -0
  15. package/dist/applications/types.d.ts +7 -0
  16. package/dist/applications/validate.js +18 -0
  17. package/dist/blocks/__tests__/layout.test.js +1 -1
  18. package/dist/blocks/layout.js +1 -1
  19. package/dist/business-service/__tests__/validate.test.d.ts +1 -0
  20. package/dist/business-service/__tests__/validate.test.js +135 -0
  21. package/dist/business-service/index.d.ts +3 -0
  22. package/dist/business-service/index.js +2 -0
  23. package/dist/business-service/types.d.ts +20 -0
  24. package/dist/business-service/types.js +3 -0
  25. package/dist/business-service/validate.d.ts +2 -0
  26. package/dist/business-service/validate.js +74 -0
  27. package/dist/fgca/__tests__/parse-canonical.test.js +24 -11
  28. package/dist/fgca/__tests__/resolver.test.js +7 -7
  29. package/dist/fgca/__tests__/validate.test.js +11 -11
  30. package/dist/fgca/parse-canonical.js +18 -32
  31. package/dist/fgca/preview-layout.js +50 -1
  32. package/dist/fgca/resolver.d.ts +2 -2
  33. package/dist/fgca/resolver.js +5 -5
  34. package/dist/fgca/validate.js +4 -13
  35. package/dist/index.d.ts +5 -0
  36. package/dist/index.js +5 -0
  37. package/dist/integration/__tests__/validate.test.d.ts +1 -0
  38. package/dist/integration/__tests__/validate.test.js +169 -0
  39. package/dist/integration/index.d.ts +2 -0
  40. package/dist/integration/index.js +1 -0
  41. package/dist/integration/types.d.ts +27 -0
  42. package/dist/integration/types.js +5 -0
  43. package/dist/integration/validate.d.ts +2 -0
  44. package/dist/integration/validate.js +94 -0
  45. package/dist/location/__tests__/validate.test.d.ts +1 -0
  46. package/dist/location/__tests__/validate.test.js +107 -0
  47. package/dist/location/index.d.ts +3 -0
  48. package/dist/location/index.js +2 -0
  49. package/dist/location/types.d.ts +20 -0
  50. package/dist/location/types.js +3 -0
  51. package/dist/location/validate.d.ts +2 -0
  52. package/dist/location/validate.js +63 -0
  53. package/dist/node/__tests__/validate.test.d.ts +1 -0
  54. package/dist/node/__tests__/validate.test.js +100 -0
  55. package/dist/node/index.d.ts +2 -0
  56. package/dist/node/index.js +1 -0
  57. package/dist/node/types.d.ts +18 -0
  58. package/dist/node/types.js +3 -0
  59. package/dist/node/validate.d.ts +2 -0
  60. package/dist/node/validate.js +53 -0
  61. package/dist/repo-validate/__tests__/validate-repo.test.js +444 -0
  62. package/dist/repo-validate/validate-repo.js +224 -10
  63. package/dist/technology-service/__tests__/validate.test.d.ts +1 -0
  64. package/dist/technology-service/__tests__/validate.test.js +121 -0
  65. package/dist/technology-service/index.d.ts +2 -0
  66. package/dist/technology-service/index.js +1 -0
  67. package/dist/technology-service/types.d.ts +18 -0
  68. package/dist/technology-service/types.js +3 -0
  69. package/dist/technology-service/validate.d.ts +2 -0
  70. package/dist/technology-service/validate.js +64 -0
  71. package/dist/webview/__tests__/entry-notations.test.js +2 -4
  72. package/dist/webview/__tests__/render-activities.test.d.ts +1 -0
  73. package/dist/webview/__tests__/render-activities.test.js +91 -0
  74. package/dist/webview/__tests__/render-blocks.test.d.ts +1 -0
  75. package/dist/webview/__tests__/render-blocks.test.js +130 -0
  76. package/dist/webview/entry.d.ts +1 -1
  77. package/dist/webview/entry.js +8 -15
  78. package/dist/webview/render-activities.d.ts +12 -0
  79. package/dist/webview/render-activities.js +9 -3
  80. package/dist/webview/render-activity-card.js +2 -3
  81. package/dist/webview/render-blocks.js +97 -18
  82. package/dist/webview/render-fgca.js +1 -1
  83. package/dist/webview/render-goals.js +28 -5
  84. package/package.json +1 -1
  85. package/src/__tests__/yaml-normalize.test.ts +11 -11
  86. package/src/activities/__tests__/validate.test.ts +77 -74
  87. package/src/activities/validate.ts +5 -12
  88. package/src/activity-card/__tests__/example.test.ts +3 -5
  89. package/src/activity-card/__tests__/layout.test.ts +2 -9
  90. package/src/activity-card/__tests__/resolver.test.ts +27 -0
  91. package/src/activity-card/__tests__/validate.test.ts +30 -18
  92. package/src/activity-card/layout.ts +1 -8
  93. package/src/activity-card/resolver.ts +11 -2
  94. package/src/activity-card/types.ts +1 -1
  95. package/src/activity-card/validate.ts +3 -9
  96. package/src/actor/__tests__/validate.test.ts +8 -0
  97. package/src/actor/types.ts +2 -0
  98. package/src/applications/types.ts +8 -0
  99. package/src/applications/validate.ts +20 -0
  100. package/src/blocks/__tests__/layout.test.ts +1 -1
  101. package/src/blocks/layout.ts +1 -1
  102. package/src/business-service/__tests__/validate.test.ts +160 -0
  103. package/src/business-service/index.ts +3 -0
  104. package/src/business-service/types.ts +29 -0
  105. package/src/business-service/validate.ts +82 -0
  106. package/src/fgca/__tests__/parse-canonical.test.ts +25 -11
  107. package/src/fgca/__tests__/resolver.test.ts +7 -7
  108. package/src/fgca/__tests__/validate.test.ts +11 -11
  109. package/src/fgca/parse-canonical.ts +18 -38
  110. package/src/fgca/preview-layout.ts +53 -1
  111. package/src/fgca/resolver.ts +5 -5
  112. package/src/fgca/validate.ts +4 -12
  113. package/src/index.ts +5 -0
  114. package/src/integration/__tests__/validate.test.ts +203 -0
  115. package/src/integration/index.ts +2 -0
  116. package/src/integration/types.ts +38 -0
  117. package/src/integration/validate.ts +104 -0
  118. package/src/location/__tests__/validate.test.ts +127 -0
  119. package/src/location/index.ts +3 -0
  120. package/src/location/types.ts +29 -0
  121. package/src/location/validate.ts +70 -0
  122. package/src/node/__tests__/validate.test.ts +118 -0
  123. package/src/node/index.ts +2 -0
  124. package/src/node/types.ts +27 -0
  125. package/src/node/validate.ts +59 -0
  126. package/src/repo-validate/__tests__/validate-repo.test.ts +550 -0
  127. package/src/repo-validate/validate-repo.ts +233 -10
  128. package/src/technology-service/__tests__/validate.test.ts +143 -0
  129. package/src/technology-service/index.ts +2 -0
  130. package/src/technology-service/types.ts +27 -0
  131. package/src/technology-service/validate.ts +71 -0
  132. package/src/webview/__tests__/entry-notations.test.ts +2 -4
  133. package/src/webview/__tests__/render-activities.test.ts +100 -0
  134. package/src/webview/__tests__/render-blocks.test.ts +155 -0
  135. package/src/webview/entry.ts +10 -19
  136. package/src/webview/render-activities.ts +29 -4
  137. package/src/webview/render-activity-card.ts +2 -3
  138. package/src/webview/render-blocks.ts +101 -20
  139. package/src/webview/render-fgca.ts +1 -1
  140. 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: 'fgca',
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
- activities: [{ id: 1, name: 'Market research', goal_id: 1 }],
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: 'fgca' });
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: 'fgca' });
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: 'fgca' });
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 { activities: _, ...without } = VALID_DOC;
61
- const result = validateFGCADoc({ ...without, notation: 'fgca' });
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: 'fgca',
107
+ notation: 'dgca',
108
108
  factors: [],
109
109
  goals: [],
110
110
  changes: [],
111
- activities: [],
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 activities[] without throwing', () => {
138
- const r = validateFGCADoc({ ...VALID_DOC, activities: [null] });
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'] !== 'fgca' && raw['notation'] !== 'dgca') {
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" (or legacy "fgca"), got "${String(raw['notation'])}"`,
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
- // Accept canonical `actions:` or deprecated `activities:`; warn on the latter.
126
- const activitiesRaw = asObjectArray(raw['actions']) ?? asObjectArray(raw['activities']);
127
- if ('activities' in raw && !('actions' in raw)) {
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
- // Build internal id-maps (canonical string ID → internal sequential number).
227
- const idMap: InternalIdMap = { toInternal: new Map() };
228
- const allCanonicalIds = [...factorIds, ...goalIds, ...changeIds, ...activityIds];
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: intId(String(el!['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: intId(String(el!['id'])),
229
+ id: String(el!['id']),
250
230
  name: String(el!['name'] ?? ''),
251
- factor: refIds.map((r) => ({ id: intId(r) })),
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: intId(canonicalChangeId),
267
+ id: canonicalChangeId,
288
268
  name: String(el!['name'] ?? ''),
289
- goal_id: goalRefs.length > 0 ? intId(goalRefs[0]) : 0,
290
- activity_ids: linkedActivityIds.map((aid) => intId(aid)),
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: intId(String(el!['id'])),
278
+ id: String(el!['id']),
299
279
  name: String(el!['name'] ?? ''),
300
- goal_id: goalRefs.length > 0 ? intId(goalRefs[0]) : null,
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'] !== 'fga' && raw['notation'] !== 'dga') {
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" (or legacy "fga"), got "${String(raw['notation'])}"` }],
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 colItems[col]) {
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
  }
@@ -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[]`, `activities[]` with typed
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
- * `activity.changes[]`) are carried from the canon elements unchanged; the
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: 'fgca', factors: [], goals: [], changes: [], activities: [] };
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: 'fgca',
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
- activities: selectedActivities,
157
+ actions: selectedActivities,
158
158
  };
159
159
  }
@@ -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 !== 'fgca' && raw.notation !== 'dgca') {
39
- errors.push({ code: 'WRONG_NOTATION', message: `notation must be "dgca" (or legacy "fgca"), got "${String(raw.notation)}"` });
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
- // Accept canonical `actions:` or deprecated `activities:`; warn on the latter.
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 = (hasActions ? raw.actions : raw.activities) as unknown[];
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,2 @@
1
+ export { validateIntegration } from './validate.js';
2
+ export type { Integration, IntegrationDirectionType, SensitivityValue, DirectionalityValue } from './types.js';
@@ -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
+ }