@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
@@ -4,7 +4,7 @@ import * as path from 'node:path';
4
4
  import yaml from 'js-yaml';
5
5
  import { parseCanonicalFGCA, parseCanonicalFGA } from '../parse-canonical.js';
6
6
  const VALID = {
7
- notation: 'fgca',
7
+ notation: 'dgca',
8
8
  spec_version: '0.1',
9
9
  id: 'FGCA-SAMPLE-1',
10
10
  name: 'Sample FGCA chain',
@@ -17,7 +17,7 @@ const VALID = {
17
17
  changes: [
18
18
  { id: 'CHANGE-1', name: 'Transformation', goals: ['GOAL-1'] },
19
19
  ],
20
- activities: [
20
+ actions: [
21
21
  { id: 'ACTIVITY-1', name: 'Workstream', changes: ['CHANGE-1'] },
22
22
  ],
23
23
  };
@@ -96,7 +96,7 @@ describe('parseCanonicalFGCA', () => {
96
96
  it('FGCA-010: rejects activity.changes[] referencing undefined change', () => {
97
97
  const r = parseCanonicalFGCA({
98
98
  ...VALID,
99
- activities: [{ id: 'ACTIVITY-1', name: 'A', changes: ['CHANGE-99'] }],
99
+ actions: [{ id: 'ACTIVITY-1', name: 'A', changes: ['CHANGE-99'] }],
100
100
  });
101
101
  expect(r.valid).toBe(false);
102
102
  expect(r.errors.some((e) => e.code === 'FGCA-010')).toBe(true);
@@ -120,7 +120,7 @@ describe('parseCanonicalFGCA', () => {
120
120
  const r = parseCanonicalFGCA({
121
121
  ...VALID,
122
122
  changes: [{ id: 'CHANGE-1', name: 'C', goals: ['GOAL-1'] }],
123
- activities: [
123
+ actions: [
124
124
  { id: 'ACTIVITY-1', name: 'A1', changes: ['CHANGE-1'] },
125
125
  { id: 'ACTIVITY-2', name: 'A2', changes: ['CHANGE-1'] },
126
126
  ],
@@ -143,19 +143,32 @@ describe('parseCanonicalFGCA — edge-driving fields populated', () => {
143
143
  expect(parsed.goals[0].factor).toHaveLength(1);
144
144
  // goal → change edge
145
145
  expect(parsed.changes[0].goal_id).not.toBe(0);
146
+ expect(parsed.changes[0].goal_id).not.toBe('');
146
147
  // change → activity edge
147
148
  expect(parsed.changes[0].activity_ids).toHaveLength(1);
148
149
  });
150
+ it('preserves canonical string IDs — not converted to sequential numbers', () => {
151
+ const r = parseCanonicalFGCA(VALID);
152
+ expect(r.valid).toBe(true);
153
+ const parsed = r.parsed;
154
+ expect(parsed.factors[0].id).toBe('FACTOR-1');
155
+ expect(parsed.goals[0].id).toBe('GOAL-1');
156
+ expect(parsed.goals[0].factor[0].id).toBe('FACTOR-1');
157
+ expect(parsed.changes[0].id).toBe('CHANGE-1');
158
+ expect(parsed.changes[0].goal_id).toBe('GOAL-1');
159
+ expect(parsed.changes[0].activity_ids[0]).toBe('ACTIVITY-1');
160
+ expect(parsed.activities[0].id).toBe('ACTIVITY-1');
161
+ });
149
162
  });
150
163
  describe('parseCanonicalFGA', () => {
151
164
  const VALID_FGA = {
152
- notation: 'fga',
165
+ notation: 'dga',
153
166
  spec_version: '0.1',
154
167
  id: 'FGA-SAMPLE-1',
155
168
  name: 'Sample FGA chain',
156
169
  factors: [{ id: 'FACTOR-1', name: 'Driver' }],
157
170
  goals: [{ id: 'GOAL-1', name: 'Outcome', factors: ['FACTOR-1'] }],
158
- activities: [{ id: 'ACTIVITY-1', name: 'Workstream', goals: ['GOAL-1'] }],
171
+ actions: [{ id: 'ACTIVITY-1', name: 'Workstream', goals: ['GOAL-1'] }],
159
172
  };
160
173
  it('accepts a valid canonical FGA document (no changes layer)', () => {
161
174
  const r = parseCanonicalFGA(VALID_FGA);
@@ -192,12 +205,12 @@ describe('parseCanonicalFGA', () => {
192
205
  });
193
206
  });
194
207
  describe('parseCanonicalFGCA — example file regression', () => {
195
- const EXAMPLES_DIR = path.resolve(process.cwd(), '..', '..', 'tests', 'fixtures', 'notation-corpus', 'fgca');
208
+ const EXAMPLES_DIR = path.resolve(process.cwd(), '..', '..', 'tests', 'fixtures', 'notation-corpus', 'dgca');
196
209
  const files = fs.existsSync(EXAMPLES_DIR)
197
210
  ? fs.readdirSync(EXAMPLES_DIR).filter((f) => f.endsWith('.transitrix.yaml'))
198
211
  : [];
199
212
  for (const file of files) {
200
- it(`accepts tests/fixtures/notation-corpus/fgca/${file}`, () => {
213
+ it(`accepts tests/fixtures/notation-corpus/dgca/${file}`, () => {
201
214
  const text = fs.readFileSync(path.join(EXAMPLES_DIR, file), 'utf8');
202
215
  const parsedYaml = yaml.load(text);
203
216
  const r = parseCanonicalFGCA(parsedYaml);
@@ -207,18 +220,18 @@ describe('parseCanonicalFGCA — example file regression', () => {
207
220
  }
208
221
  });
209
222
  describe('parseCanonicalFGA — example file regression', () => {
210
- const EXAMPLES_DIR = path.resolve(process.cwd(), '..', '..', 'tests', 'fixtures', 'notation-corpus', 'fga');
223
+ const EXAMPLES_DIR = path.resolve(process.cwd(), '..', '..', 'tests', 'fixtures', 'notation-corpus', 'dga');
211
224
  const files = fs.existsSync(EXAMPLES_DIR)
212
225
  ? fs.readdirSync(EXAMPLES_DIR).filter((f) => f.endsWith('.transitrix.yaml'))
213
226
  : [];
214
227
  for (const file of files) {
215
- it(`accepts tests/fixtures/notation-corpus/fga/${file} and resolves every activity to a goal`, () => {
228
+ it(`accepts tests/fixtures/notation-corpus/dga/${file} and resolves every activity to a goal`, () => {
216
229
  const text = fs.readFileSync(path.join(EXAMPLES_DIR, file), 'utf8');
217
230
  const r = parseCanonicalFGA(yaml.load(text));
218
231
  expect(r.errors).toEqual([]);
219
232
  expect(r.valid).toBe(true);
220
233
  // Every activity must carry a goal_id, or the FGA preview draws no edges.
221
- expect(r.parsed.activities.every((a) => a.goal_id != null && a.goal_id !== 0)).toBe(true);
234
+ expect(r.parsed.activities.every((a) => a.goal_id != null && a.goal_id !== 0 && a.goal_id !== '')).toBe(true);
222
235
  });
223
236
  }
224
237
  });
@@ -22,7 +22,7 @@ const ELEMENTS = [
22
22
  ];
23
23
  const SOURCES = { elements: ELEMENTS, relations: [] };
24
24
  const VIEW_DOC = {
25
- notation: 'fgca',
25
+ notation: 'dgca',
26
26
  id: 'FGCA-STRAT-1',
27
27
  name: 'Strategy 2026 — FGCA chain',
28
28
  spec_version: '0.1',
@@ -40,7 +40,7 @@ describe('isFGCAViewDoc', () => {
40
40
  });
41
41
  it('returns false for an inline doc with factors/goals arrays', () => {
42
42
  expect(isFGCAViewDoc({
43
- notation: 'fgca', id: 'FGCA-1', name: 'x',
43
+ notation: 'dgca', id: 'FGCA-1', name: 'x',
44
44
  factors: [], goals: [], changes: [], activities: [],
45
45
  })).toBe(false);
46
46
  });
@@ -56,7 +56,7 @@ describe('resolveFGCA — goals.filter=all', () => {
56
56
  const doc = resolveFGCA(VIEW_DOC, SOURCES);
57
57
  expect(doc['goals'].length).toBe(3);
58
58
  expect(doc['changes'].length).toBe(3);
59
- expect(doc['activities'].length).toBe(5);
59
+ expect(doc['actions'].length).toBe(5);
60
60
  });
61
61
  it('derives factors from all goal.factors[] refs', () => {
62
62
  const doc = resolveFGCA(VIEW_DOC, SOURCES);
@@ -77,7 +77,7 @@ describe('resolveFGCA — goals.filter=all', () => {
77
77
  });
78
78
  it('carries view doc metadata (id, name, spec_version)', () => {
79
79
  const doc = resolveFGCA(VIEW_DOC, SOURCES);
80
- expect(doc['notation']).toBe('fgca');
80
+ expect(doc['notation']).toBe('dgca');
81
81
  expect(doc['id']).toBe('FGCA-STRAT-1');
82
82
  expect(doc['name']).toBe('Strategy 2026 — FGCA chain');
83
83
  expect(doc['spec_version']).toBe('0.1');
@@ -95,7 +95,7 @@ describe('resolveFGCA — goals.filter=ids', () => {
95
95
  expect(goalIds).toEqual(['GOAL-1']);
96
96
  const changeIds = doc['changes'].map((c) => c.id);
97
97
  expect(changeIds).toEqual(['CHANGE-1']);
98
- const actIds = doc['activities'].map((a) => a.id).sort();
98
+ const actIds = doc['actions'].map((a) => a.id).sort();
99
99
  expect(actIds).toEqual(['ACTIVITY-1', 'ACTIVITY-2']);
100
100
  const r = parseCanonicalFGCA(doc);
101
101
  expect(r.valid).toBe(true);
@@ -138,7 +138,7 @@ describe('resolveFGCA — empty sources', () => {
138
138
  expect(doc['goals'].length).toBe(0);
139
139
  expect(doc['factors'].length).toBe(0);
140
140
  expect(doc['changes'].length).toBe(0);
141
- expect(doc['activities'].length).toBe(0);
141
+ expect(doc['actions'].length).toBe(0);
142
142
  });
143
143
  it('returns empty arrays for a non-object viewDoc', () => {
144
144
  const doc = resolveFGCA(null, SOURCES);
@@ -147,7 +147,7 @@ describe('resolveFGCA — empty sources', () => {
147
147
  });
148
148
  // ── File-based regression — resolver + canon fixture + parseCanonicalFGCA ─────
149
149
  describe('resolveFGCA — file-based regression against canon fixture', () => {
150
- const CANON_ROOT = path.resolve(process.cwd(), '..', '..', 'tests', 'fixtures', 'notation-corpus', 'fgca', 'canon');
150
+ const CANON_ROOT = path.resolve(process.cwd(), '..', '..', 'tests', 'fixtures', 'notation-corpus', 'dgca', 'canon');
151
151
  const VIEW_DIR = path.join(CANON_ROOT, 'views');
152
152
  const ELEM_DIR = path.join(CANON_ROOT, 'elements');
153
153
  function loadYamlsUnder(dir) {
@@ -1,11 +1,11 @@
1
1
  import { describe, it, expect } from 'vitest';
2
2
  import { validateFGCADoc } from '../validate.js';
3
3
  const VALID_DOC = {
4
- notation: 'fgca',
4
+ notation: 'dgca',
5
5
  factors: [{ id: 1, name: 'Market pressure' }],
6
6
  goals: [{ id: 1, name: 'Grow revenue', factor: [{ id: 1 }] }],
7
7
  changes: [{ id: 1, name: 'Launch product', goal_id: 1, activity_ids: [1] }],
8
- activities: [{ id: 1, name: 'Market research', goal_id: 1 }],
8
+ actions: [{ id: 1, name: 'Market research', goal_id: 1 }],
9
9
  };
10
10
  describe('validateFGCADoc', () => {
11
11
  it('accepts a valid document', () => {
@@ -31,25 +31,25 @@ describe('validateFGCADoc', () => {
31
31
  });
32
32
  it('errors when factors array is missing', () => {
33
33
  const { factors: _, ...without } = VALID_DOC;
34
- const result = validateFGCADoc({ ...without, notation: 'fgca' });
34
+ const result = validateFGCADoc({ ...without, notation: 'dgca' });
35
35
  expect(result.valid).toBe(false);
36
36
  expect(result.errors.some(e => e.path === 'factors')).toBe(true);
37
37
  });
38
38
  it('errors when goals array is missing', () => {
39
39
  const { goals: _, ...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 === 'goals')).toBe(true);
43
43
  });
44
44
  it('errors when changes array is missing', () => {
45
45
  const { changes: _, ...without } = VALID_DOC;
46
- const result = validateFGCADoc({ ...without, notation: 'fgca' });
46
+ const result = validateFGCADoc({ ...without, notation: 'dgca' });
47
47
  expect(result.valid).toBe(false);
48
48
  expect(result.errors.some(e => e.path === 'changes')).toBe(true);
49
49
  });
50
50
  it('errors when actions array is missing', () => {
51
- const { activities: _, ...without } = VALID_DOC;
52
- const result = validateFGCADoc({ ...without, notation: 'fgca' });
51
+ const { actions: _, ...without } = VALID_DOC;
52
+ const result = validateFGCADoc({ ...without, notation: 'dgca' });
53
53
  expect(result.valid).toBe(false);
54
54
  expect(result.errors.some(e => e.path === 'actions')).toBe(true);
55
55
  });
@@ -89,11 +89,11 @@ describe('validateFGCADoc', () => {
89
89
  });
90
90
  it('accepts a document with empty arrays', () => {
91
91
  const result = validateFGCADoc({
92
- notation: 'fgca',
92
+ notation: 'dgca',
93
93
  factors: [],
94
94
  goals: [],
95
95
  changes: [],
96
- activities: [],
96
+ actions: [],
97
97
  });
98
98
  expect(result.valid).toBe(true);
99
99
  });
@@ -117,8 +117,8 @@ describe('validateFGCADoc', () => {
117
117
  expect(r.valid).toBe(false);
118
118
  expect(r.errors.some(e => e.code === 'SCHEMA_INVALID')).toBe(true);
119
119
  });
120
- it('[blocker] tolerates a null element in activities[] without throwing', () => {
121
- const r = validateFGCADoc({ ...VALID_DOC, activities: [null] });
120
+ it('[blocker] tolerates a null element in actions[] without throwing', () => {
121
+ const r = validateFGCADoc({ ...VALID_DOC, actions: [null] });
122
122
  expect(r.valid).toBe(false);
123
123
  expect(r.errors.some(e => e.code === 'SCHEMA_INVALID')).toBe(true);
124
124
  });
@@ -57,21 +57,18 @@ export function parseCanonicalFGCA(input) {
57
57
  };
58
58
  }
59
59
  const raw = input;
60
- if ('notation' in raw && raw['notation'] !== 'fgca' && raw['notation'] !== 'dgca') {
60
+ if ('notation' in raw && raw['notation'] !== 'dgca') {
61
61
  return {
62
62
  valid: false,
63
63
  errors: [
64
64
  {
65
65
  code: 'FGCA-001',
66
- message: `notation must be "dgca" (or legacy "fgca"), got "${String(raw['notation'])}"`,
66
+ message: `notation must be "dgca", got "${String(raw['notation'])}"`,
67
67
  },
68
68
  ],
69
69
  warnings,
70
70
  };
71
71
  }
72
- if ('notation' in raw && raw['notation'] === 'fgca') {
73
- warnings.push({ code: 'FGCA-001', message: 'notation "fgca" is deprecated — rename to "dgca"' });
74
- }
75
72
  // FGCA-002 — document id (optional in v1.4 per the canonical spec? — spec
76
73
  // says required; absence is FGCA-002).
77
74
  if (raw['id'] !== undefined) {
@@ -91,10 +88,9 @@ export function parseCanonicalFGCA(input) {
91
88
  const factorsRaw = asObjectArray(raw['factors']);
92
89
  const goalsRaw = asObjectArray(raw['goals']);
93
90
  const changesRaw = asObjectArray(raw['changes']);
94
- // Accept canonical `actions:` or deprecated `activities:`; warn on the latter.
95
- const activitiesRaw = asObjectArray(raw['actions']) ?? asObjectArray(raw['activities']);
96
- if ('activities' in raw && !('actions' in raw)) {
97
- warnings.push({ code: 'FGCA-001', message: '"activities" key is deprecated — rename to "actions"' });
91
+ const activitiesRaw = asObjectArray(raw['actions']);
92
+ if (activitiesRaw === null && 'activities' in raw) {
93
+ errors.push({ code: 'FGCA-004', message: '"activities" key is not accepted — rename to "actions"', path: 'actions' });
98
94
  }
99
95
  if (factorsRaw === null)
100
96
  errors.push({ code: 'FGCA-004', message: 'factors must be an array', path: 'factors' });
@@ -179,29 +175,19 @@ export function parseCanonicalFGCA(input) {
179
175
  }
180
176
  return out;
181
177
  }
182
- // Build internal id-maps (canonical string ID → internal sequential number).
183
- const idMap = { toInternal: new Map() };
184
- const allCanonicalIds = [...factorIds, ...goalIds, ...changeIds, ...activityIds];
185
- allCanonicalIds.forEach((id, i) => idMap.toInternal.set(id, i + 1));
186
- function intId(canonical) {
187
- const n = idMap.toInternal.get(canonical);
188
- if (n === undefined) {
189
- // Shouldn't happen — we built the map from the same sets.
190
- throw new Error(`internal: canonical id "${canonical}" not mapped`);
191
- }
192
- return n;
193
- }
194
- // Layer entries (internal form).
178
+ // Layer entries (internal form). Canonical string IDs are passed through
179
+ // directly — no conversion to sequential numbers. The layout and renderer
180
+ // treat IDs as opaque keys and also display them verbatim to the user.
195
181
  const internalFactors = factors.map((el) => ({
196
- id: intId(String(el['id'])),
182
+ id: String(el['id']),
197
183
  name: String(el['name'] ?? ''),
198
184
  }));
199
185
  const internalGoals = goals.map((el, i) => {
200
186
  const refIds = checkRefArray(el, 'factors', factorIds, FACTOR_OR_DRIVER_ID_RE, 'FGCA-008', `goals[${i}]`);
201
187
  return {
202
- id: intId(String(el['id'])),
188
+ id: String(el['id']),
203
189
  name: String(el['name'] ?? ''),
204
- factor: refIds.map((r) => ({ id: intId(r) })),
190
+ factor: refIds.map((r) => ({ id: r })),
205
191
  };
206
192
  });
207
193
  // Build a reverse mapping: which changes does each activity reference (via
@@ -234,19 +220,19 @@ export function parseCanonicalFGCA(input) {
234
220
  const canonicalChangeId = String(el['id']);
235
221
  const linkedActivityIds = activitiesForChange.get(canonicalChangeId) ?? [];
236
222
  return {
237
- id: intId(canonicalChangeId),
223
+ id: canonicalChangeId,
238
224
  name: String(el['name'] ?? ''),
239
- goal_id: goalRefs.length > 0 ? intId(goalRefs[0]) : 0,
240
- activity_ids: linkedActivityIds.map((aid) => intId(aid)),
225
+ goal_id: goalRefs.length > 0 ? goalRefs[0] : '',
226
+ activity_ids: linkedActivityIds,
241
227
  };
242
228
  });
243
229
  const internalActivities = activities.map((el, i) => {
244
230
  const goalRefs = checkRefArray(el, 'goals', goalIds, GOAL_ID_RE, 'FGCA-011', `actions[${i}]`);
245
231
  const typeVal = typeof el['type'] === 'string' ? el['type'] : undefined;
246
232
  return {
247
- id: intId(String(el['id'])),
233
+ id: String(el['id']),
248
234
  name: String(el['name'] ?? ''),
249
- goal_id: goalRefs.length > 0 ? intId(goalRefs[0]) : null,
235
+ goal_id: goalRefs.length > 0 ? goalRefs[0] : null,
250
236
  ...(typeVal !== undefined ? { type: typeVal } : {}),
251
237
  };
252
238
  });
@@ -304,10 +290,10 @@ export function parseCanonicalFGA(input) {
304
290
  return { valid: false, errors: [{ code: 'FGA-001', message: 'document root is not an object' }], warnings: [] };
305
291
  }
306
292
  const raw = input;
307
- if ('notation' in raw && raw['notation'] !== 'fga' && raw['notation'] !== 'dga') {
293
+ if ('notation' in raw && raw['notation'] !== 'dga') {
308
294
  return {
309
295
  valid: false,
310
- errors: [{ code: 'FGA-001', message: `notation must be "dga" (or legacy "fga"), got "${String(raw['notation'])}"` }],
296
+ errors: [{ code: 'FGA-001', message: `notation must be "dga", got "${String(raw['notation'])}"` }],
311
297
  warnings: [],
312
298
  };
313
299
  }
@@ -61,18 +61,67 @@ export function layoutFGCAPreview(inputDoc, options = {}) {
61
61
  change: changes.map(c => ({ id: `change_${c.id}`, label: c.name })),
62
62
  activity: doc.activities.map(a => ({ id: `activity_${a.id}`, label: a.name })),
63
63
  };
64
+ // Build predecessor map: for each node, which node IDs in the previous column
65
+ // connect to it from the left. Used for barycenter crossing minimization.
66
+ const predecessors = new Map();
67
+ for (const g of doc.goals) {
68
+ predecessors.set(`goal_${g.id}`, (g.factor ?? []).map(f => `driver_${f.id}`));
69
+ }
70
+ if (!hideChanges) {
71
+ for (const c of changes) {
72
+ predecessors.set(`change_${c.id}`, [`goal_${c.goal_id}`]);
73
+ }
74
+ const coveredActs = new Set(changes.flatMap(c => c.activity_ids.map(String)));
75
+ for (const a of doc.activities) {
76
+ const preds = changes
77
+ .filter(c => c.activity_ids.map(String).includes(String(a.id)))
78
+ .map(c => `change_${c.id}`);
79
+ if (a.goal_id != null && !coveredActs.has(String(a.id)))
80
+ preds.push(`goal_${a.goal_id}`);
81
+ predecessors.set(`activity_${a.id}`, preds);
82
+ }
83
+ }
84
+ else {
85
+ const connectedViaChange = new Set();
86
+ for (const c of changes) {
87
+ for (const aid of c.activity_ids) {
88
+ predecessors.set(`activity_${aid}`, [`goal_${c.goal_id}`]);
89
+ connectedViaChange.add(String(aid));
90
+ }
91
+ }
92
+ for (const a of doc.activities) {
93
+ if (a.goal_id != null && !connectedViaChange.has(String(a.id))) {
94
+ predecessors.set(`activity_${a.id}`, [`goal_${a.goal_id}`]);
95
+ }
96
+ }
97
+ }
98
+ // Sort a column's items by the barycenter of their predecessors' y-centres.
99
+ // Nodes with no predecessors sort last (Infinity barycenter) so they don't
100
+ // displace connected nodes.
101
+ function barycentricSort(items, yCenters) {
102
+ return [...items].sort((a, b) => {
103
+ const pA = (predecessors.get(a.id) ?? []).map(p => yCenters.get(p) ?? 0).filter(v => v > 0);
104
+ const pB = (predecessors.get(b.id) ?? []).map(p => yCenters.get(p) ?? 0).filter(v => v > 0);
105
+ const bcA = pA.length > 0 ? pA.reduce((s, v) => s + v, 0) / pA.length : Infinity;
106
+ const bcB = pB.length > 0 ? pB.reduce((s, v) => s + v, 0) / pB.length : Infinity;
107
+ return bcA - bcB;
108
+ });
109
+ }
64
110
  const nodeMap = new Map();
65
111
  const nodes = [];
66
112
  const columns = [];
113
+ const yCenters = new Map(); // nodeId → y + FGCA_NODE_H / 2
67
114
  for (let ci = 0; ci < cols.length; ci++) {
68
115
  const col = cols[ci];
69
116
  const x = FGCA_PAD + ci * colStride;
70
117
  columns.push({ col, x });
118
+ const items = ci === 0 ? colItems[col] : barycentricSort(colItems[col], yCenters);
71
119
  let y = FGCA_PAD + FGCA_HEADER_H + rowGap;
72
- for (const item of colItems[col]) {
120
+ for (const item of items) {
73
121
  const node = { id: item.id, x, y, label: item.label, col };
74
122
  nodes.push(node);
75
123
  nodeMap.set(item.id, node);
124
+ yCenters.set(item.id, y + FGCA_NODE_H / 2);
76
125
  y += FGCA_NODE_H + rowGap;
77
126
  }
78
127
  }
@@ -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
  export interface FGCAViewConfig {
@@ -43,7 +43,7 @@ export declare function isFGCAViewDoc(parsed: unknown): boolean;
43
43
  * to pass to `parseCanonicalFGCA`.
44
44
  *
45
45
  * Cross-reference fields (`goal.factors[]`, `change.goals[]`,
46
- * `activity.changes[]`) are carried from the canon elements unchanged; the
46
+ * `action.changes[]`) are carried from the canon elements unchanged; the
47
47
  * resolver decides which elements to include, not how they are shaped.
48
48
  */
49
49
  export declare function resolveFGCA(viewDoc: unknown, sources: FGCACanonSources): Record<string, unknown>;
@@ -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
  function isObject(v) {
@@ -49,12 +49,12 @@ export function isFGCAViewDoc(parsed) {
49
49
  * to pass to `parseCanonicalFGCA`.
50
50
  *
51
51
  * Cross-reference fields (`goal.factors[]`, `change.goals[]`,
52
- * `activity.changes[]`) are carried from the canon elements unchanged; the
52
+ * `action.changes[]`) are carried from the canon elements unchanged; the
53
53
  * resolver decides which elements to include, not how they are shaped.
54
54
  */
55
55
  export function resolveFGCA(viewDoc, sources) {
56
56
  if (!isObject(viewDoc)) {
57
- return { notation: 'fgca', factors: [], goals: [], changes: [], activities: [] };
57
+ return { notation: 'dgca', factors: [], goals: [], changes: [], actions: [] };
58
58
  }
59
59
  const vc = isObject(viewDoc['view_config']) ? viewDoc['view_config'] : {};
60
60
  const goalsConf = isObject(vc['goals']) ? vc['goals'] : {};
@@ -114,13 +114,13 @@ export function resolveFGCA(viewDoc, sources) {
114
114
  strArray(a['goals']).some((gid) => selectedGoalIds.has(gid)));
115
115
  }
116
116
  return {
117
- notation: 'fgca',
117
+ notation: 'dgca',
118
118
  id: viewDoc['id'],
119
119
  name: viewDoc['name'],
120
120
  spec_version: viewDoc['spec_version'],
121
121
  factors: selectedFactors,
122
122
  goals: selectedGoals,
123
123
  changes: selectedChanges,
124
- activities: selectedActivities,
124
+ actions: selectedActivities,
125
125
  };
126
126
  }
@@ -9,31 +9,22 @@ export function validateFGCADoc(input) {
9
9
  errors.push({ code: 'MISSING_NOTATION', message: 'notation field is required' });
10
10
  return { valid: false, errors, warnings };
11
11
  }
12
- if (raw.notation !== 'fgca' && raw.notation !== 'dgca') {
13
- errors.push({ code: 'WRONG_NOTATION', message: `notation must be "dgca" (or legacy "fgca"), got "${String(raw.notation)}"` });
12
+ if (raw.notation !== 'dgca') {
13
+ errors.push({ code: 'WRONG_NOTATION', message: `notation must be "dgca", got "${String(raw.notation)}"` });
14
14
  return { valid: false, errors, warnings };
15
15
  }
16
- if (raw.notation === 'fgca') {
17
- warnings.push({ code: 'DEPRECATED_NOTATION', message: 'notation "fgca" is deprecated — rename to "dgca"' });
18
- }
19
16
  if (!Array.isArray(raw.factors))
20
17
  errors.push({ code: 'SCHEMA_INVALID', message: 'factors must be an array', path: 'factors' });
21
18
  if (!Array.isArray(raw.goals))
22
19
  errors.push({ code: 'SCHEMA_INVALID', message: 'goals must be an array', path: 'goals' });
23
20
  if (!Array.isArray(raw.changes))
24
21
  errors.push({ code: 'SCHEMA_INVALID', message: 'changes must be an array', path: 'changes' });
25
- // Accept canonical `actions:` or deprecated `activities:`; warn on the latter.
26
- const hasActions = Array.isArray(raw.actions);
27
- const hasActivities = Array.isArray(raw.activities);
28
- if (!hasActions && !hasActivities) {
22
+ if (!Array.isArray(raw.actions)) {
29
23
  errors.push({ code: 'SCHEMA_INVALID', message: 'actions must be an array', path: 'actions' });
30
24
  }
31
- else if ('activities' in raw) {
32
- warnings.push({ code: 'DEPRECATED_NOTATION', message: '"activities" key is deprecated — rename to "actions"' });
33
- }
34
25
  if (errors.length > 0)
35
26
  return { valid: false, errors, warnings };
36
- const actionsArr = (hasActions ? raw.actions : raw.activities);
27
+ const actionsArr = raw.actions;
37
28
  const doc = raw;
38
29
  const collectIds = (arr) => {
39
30
  const out = new Set();
package/dist/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from "./activities/index";
2
2
  export * from "./activity-card/index";
3
3
  export * from "./actor/index";
4
+ export * from "./business-service/index";
4
5
  export * from "./geometry";
5
6
  export * from "./applications/index";
6
7
  export * from "./assertion/index";
@@ -13,6 +14,9 @@ export * from "./confidence/index";
13
14
  export * from "./factor/index";
14
15
  export * from "./fgca/index";
15
16
  export * from "./goals/index";
17
+ export * from "./integration/index";
18
+ export * from "./location/index";
19
+ export * from "./node/index";
16
20
  export * from "./process-blueprint/index";
17
21
  export * from "./process-map/index";
18
22
  export * from "./products/index";
@@ -21,6 +25,7 @@ export * from "./requirement/index";
21
25
  export * from "./scenarios/index";
22
26
  export * from "./stakeholder/index";
23
27
  export * from "./target-state/index";
28
+ export * from "./technology-service/index";
24
29
  export { SCHEMA_VERSION } from "./schema-version";
25
30
  export * from "./theme/index";
26
31
  export * from "./typed-id";
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export * from "./activities/index";
2
2
  export * from "./activity-card/index";
3
3
  export * from "./actor/index";
4
+ export * from "./business-service/index";
4
5
  export * from "./geometry";
5
6
  export * from "./applications/index";
6
7
  export * from "./assertion/index";
@@ -13,6 +14,9 @@ export * from "./confidence/index";
13
14
  export * from "./factor/index";
14
15
  export * from "./fgca/index";
15
16
  export * from "./goals/index";
17
+ export * from "./integration/index";
18
+ export * from "./location/index";
19
+ export * from "./node/index";
16
20
  export * from "./process-blueprint/index";
17
21
  export * from "./process-map/index";
18
22
  export * from "./products/index";
@@ -21,6 +25,7 @@ export * from "./requirement/index";
21
25
  export * from "./scenarios/index";
22
26
  export * from "./stakeholder/index";
23
27
  export * from "./target-state/index";
28
+ export * from "./technology-service/index";
24
29
  export { SCHEMA_VERSION } from "./schema-version";
25
30
  export * from "./theme/index";
26
31
  export * from "./typed-id";
@@ -0,0 +1 @@
1
+ export {};