@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
|
@@ -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: '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: '
|
|
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
|
-
|
|
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', '
|
|
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/
|
|
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', '
|
|
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/
|
|
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: '
|
|
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: '
|
|
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['
|
|
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('
|
|
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['
|
|
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['
|
|
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', '
|
|
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: '
|
|
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
|
-
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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 {
|
|
52
|
-
const result = validateFGCADoc({ ...without, notation: '
|
|
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: '
|
|
92
|
+
notation: 'dgca',
|
|
93
93
|
factors: [],
|
|
94
94
|
goals: [],
|
|
95
95
|
changes: [],
|
|
96
|
-
|
|
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
|
|
121
|
-
const r = validateFGCADoc({ ...VALID_DOC,
|
|
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'] !== '
|
|
60
|
+
if ('notation' in raw && raw['notation'] !== 'dgca') {
|
|
61
61
|
return {
|
|
62
62
|
valid: false,
|
|
63
63
|
errors: [
|
|
64
64
|
{
|
|
65
65
|
code: 'FGCA-001',
|
|
66
|
-
message: `notation must be "dgca"
|
|
66
|
+
message: `notation must be "dgca", got "${String(raw['notation'])}"`,
|
|
67
67
|
},
|
|
68
68
|
],
|
|
69
69
|
warnings,
|
|
70
70
|
};
|
|
71
71
|
}
|
|
72
|
-
if ('notation' in raw && raw['notation'] === 'fgca') {
|
|
73
|
-
warnings.push({ code: 'FGCA-001', message: 'notation "fgca" is deprecated — rename to "dgca"' });
|
|
74
|
-
}
|
|
75
72
|
// FGCA-002 — document id (optional in v1.4 per the canonical spec? — spec
|
|
76
73
|
// says required; absence is FGCA-002).
|
|
77
74
|
if (raw['id'] !== undefined) {
|
|
@@ -91,10 +88,9 @@ export function parseCanonicalFGCA(input) {
|
|
|
91
88
|
const factorsRaw = asObjectArray(raw['factors']);
|
|
92
89
|
const goalsRaw = asObjectArray(raw['goals']);
|
|
93
90
|
const changesRaw = asObjectArray(raw['changes']);
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
warnings.push({ code: 'FGCA-001', message: '"activities" key is deprecated — rename to "actions"' });
|
|
91
|
+
const activitiesRaw = asObjectArray(raw['actions']);
|
|
92
|
+
if (activitiesRaw === null && 'activities' in raw) {
|
|
93
|
+
errors.push({ code: 'FGCA-004', message: '"activities" key is not accepted — rename to "actions"', path: 'actions' });
|
|
98
94
|
}
|
|
99
95
|
if (factorsRaw === null)
|
|
100
96
|
errors.push({ code: 'FGCA-004', message: 'factors must be an array', path: 'factors' });
|
|
@@ -179,29 +175,19 @@ export function parseCanonicalFGCA(input) {
|
|
|
179
175
|
}
|
|
180
176
|
return out;
|
|
181
177
|
}
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
|
|
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:
|
|
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:
|
|
188
|
+
id: String(el['id']),
|
|
203
189
|
name: String(el['name'] ?? ''),
|
|
204
|
-
factor: refIds.map((r) => ({ id:
|
|
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:
|
|
223
|
+
id: canonicalChangeId,
|
|
238
224
|
name: String(el['name'] ?? ''),
|
|
239
|
-
goal_id: goalRefs.length > 0 ?
|
|
240
|
-
activity_ids: linkedActivityIds
|
|
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:
|
|
233
|
+
id: String(el['id']),
|
|
248
234
|
name: String(el['name'] ?? ''),
|
|
249
|
-
goal_id: goalRefs.length > 0 ?
|
|
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'] !== '
|
|
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"
|
|
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
|
|
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
|
}
|
package/dist/fgca/resolver.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* view_config projection and a canon element/relation store (VP-3).
|
|
4
4
|
*
|
|
5
5
|
* Output shape matches the flat form that `parseCanonicalFGCA` expects:
|
|
6
|
-
* top-level `factors[]`, `goals[]`, `changes[]`, `
|
|
6
|
+
* top-level `factors[]`, `goals[]`, `changes[]`, `actions[]` with typed
|
|
7
7
|
* string IDs. Filesystem-free — callable from unit tests without vscode.
|
|
8
8
|
*/
|
|
9
9
|
export interface FGCAViewConfig {
|
|
@@ -43,7 +43,7 @@ export declare function isFGCAViewDoc(parsed: unknown): boolean;
|
|
|
43
43
|
* to pass to `parseCanonicalFGCA`.
|
|
44
44
|
*
|
|
45
45
|
* Cross-reference fields (`goal.factors[]`, `change.goals[]`,
|
|
46
|
-
* `
|
|
46
|
+
* `action.changes[]`) are carried from the canon elements unchanged; the
|
|
47
47
|
* resolver decides which elements to include, not how they are shaped.
|
|
48
48
|
*/
|
|
49
49
|
export declare function resolveFGCA(viewDoc: unknown, sources: FGCACanonSources): Record<string, unknown>;
|
package/dist/fgca/resolver.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* view_config projection and a canon element/relation store (VP-3).
|
|
4
4
|
*
|
|
5
5
|
* Output shape matches the flat form that `parseCanonicalFGCA` expects:
|
|
6
|
-
* top-level `factors[]`, `goals[]`, `changes[]`, `
|
|
6
|
+
* top-level `factors[]`, `goals[]`, `changes[]`, `actions[]` with typed
|
|
7
7
|
* string IDs. Filesystem-free — callable from unit tests without vscode.
|
|
8
8
|
*/
|
|
9
9
|
function isObject(v) {
|
|
@@ -49,12 +49,12 @@ export function isFGCAViewDoc(parsed) {
|
|
|
49
49
|
* to pass to `parseCanonicalFGCA`.
|
|
50
50
|
*
|
|
51
51
|
* Cross-reference fields (`goal.factors[]`, `change.goals[]`,
|
|
52
|
-
* `
|
|
52
|
+
* `action.changes[]`) are carried from the canon elements unchanged; the
|
|
53
53
|
* resolver decides which elements to include, not how they are shaped.
|
|
54
54
|
*/
|
|
55
55
|
export function resolveFGCA(viewDoc, sources) {
|
|
56
56
|
if (!isObject(viewDoc)) {
|
|
57
|
-
return { notation: '
|
|
57
|
+
return { notation: 'dgca', factors: [], goals: [], changes: [], actions: [] };
|
|
58
58
|
}
|
|
59
59
|
const vc = isObject(viewDoc['view_config']) ? viewDoc['view_config'] : {};
|
|
60
60
|
const goalsConf = isObject(vc['goals']) ? vc['goals'] : {};
|
|
@@ -114,13 +114,13 @@ export function resolveFGCA(viewDoc, sources) {
|
|
|
114
114
|
strArray(a['goals']).some((gid) => selectedGoalIds.has(gid)));
|
|
115
115
|
}
|
|
116
116
|
return {
|
|
117
|
-
notation: '
|
|
117
|
+
notation: 'dgca',
|
|
118
118
|
id: viewDoc['id'],
|
|
119
119
|
name: viewDoc['name'],
|
|
120
120
|
spec_version: viewDoc['spec_version'],
|
|
121
121
|
factors: selectedFactors,
|
|
122
122
|
goals: selectedGoals,
|
|
123
123
|
changes: selectedChanges,
|
|
124
|
-
|
|
124
|
+
actions: selectedActivities,
|
|
125
125
|
};
|
|
126
126
|
}
|
package/dist/fgca/validate.js
CHANGED
|
@@ -9,31 +9,22 @@ export function validateFGCADoc(input) {
|
|
|
9
9
|
errors.push({ code: 'MISSING_NOTATION', message: 'notation field is required' });
|
|
10
10
|
return { valid: false, errors, warnings };
|
|
11
11
|
}
|
|
12
|
-
if (raw.notation !== '
|
|
13
|
-
errors.push({ code: 'WRONG_NOTATION', message: `notation must be "dgca"
|
|
12
|
+
if (raw.notation !== 'dgca') {
|
|
13
|
+
errors.push({ code: 'WRONG_NOTATION', message: `notation must be "dgca", got "${String(raw.notation)}"` });
|
|
14
14
|
return { valid: false, errors, warnings };
|
|
15
15
|
}
|
|
16
|
-
if (raw.notation === 'fgca') {
|
|
17
|
-
warnings.push({ code: 'DEPRECATED_NOTATION', message: 'notation "fgca" is deprecated — rename to "dgca"' });
|
|
18
|
-
}
|
|
19
16
|
if (!Array.isArray(raw.factors))
|
|
20
17
|
errors.push({ code: 'SCHEMA_INVALID', message: 'factors must be an array', path: 'factors' });
|
|
21
18
|
if (!Array.isArray(raw.goals))
|
|
22
19
|
errors.push({ code: 'SCHEMA_INVALID', message: 'goals must be an array', path: 'goals' });
|
|
23
20
|
if (!Array.isArray(raw.changes))
|
|
24
21
|
errors.push({ code: 'SCHEMA_INVALID', message: 'changes must be an array', path: 'changes' });
|
|
25
|
-
|
|
26
|
-
const hasActions = Array.isArray(raw.actions);
|
|
27
|
-
const hasActivities = Array.isArray(raw.activities);
|
|
28
|
-
if (!hasActions && !hasActivities) {
|
|
22
|
+
if (!Array.isArray(raw.actions)) {
|
|
29
23
|
errors.push({ code: 'SCHEMA_INVALID', message: 'actions must be an array', path: 'actions' });
|
|
30
24
|
}
|
|
31
|
-
else if ('activities' in raw) {
|
|
32
|
-
warnings.push({ code: 'DEPRECATED_NOTATION', message: '"activities" key is deprecated — rename to "actions"' });
|
|
33
|
-
}
|
|
34
25
|
if (errors.length > 0)
|
|
35
26
|
return { valid: false, errors, warnings };
|
|
36
|
-
const actionsArr =
|
|
27
|
+
const actionsArr = raw.actions;
|
|
37
28
|
const doc = raw;
|
|
38
29
|
const collectIds = (arr) => {
|
|
39
30
|
const out = new Set();
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from "./activities/index";
|
|
2
2
|
export * from "./activity-card/index";
|
|
3
3
|
export * from "./actor/index";
|
|
4
|
+
export * from "./business-service/index";
|
|
4
5
|
export * from "./geometry";
|
|
5
6
|
export * from "./applications/index";
|
|
6
7
|
export * from "./assertion/index";
|
|
@@ -13,6 +14,9 @@ export * from "./confidence/index";
|
|
|
13
14
|
export * from "./factor/index";
|
|
14
15
|
export * from "./fgca/index";
|
|
15
16
|
export * from "./goals/index";
|
|
17
|
+
export * from "./integration/index";
|
|
18
|
+
export * from "./location/index";
|
|
19
|
+
export * from "./node/index";
|
|
16
20
|
export * from "./process-blueprint/index";
|
|
17
21
|
export * from "./process-map/index";
|
|
18
22
|
export * from "./products/index";
|
|
@@ -21,6 +25,7 @@ export * from "./requirement/index";
|
|
|
21
25
|
export * from "./scenarios/index";
|
|
22
26
|
export * from "./stakeholder/index";
|
|
23
27
|
export * from "./target-state/index";
|
|
28
|
+
export * from "./technology-service/index";
|
|
24
29
|
export { SCHEMA_VERSION } from "./schema-version";
|
|
25
30
|
export * from "./theme/index";
|
|
26
31
|
export * from "./typed-id";
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export * from "./activities/index";
|
|
2
2
|
export * from "./activity-card/index";
|
|
3
3
|
export * from "./actor/index";
|
|
4
|
+
export * from "./business-service/index";
|
|
4
5
|
export * from "./geometry";
|
|
5
6
|
export * from "./applications/index";
|
|
6
7
|
export * from "./assertion/index";
|
|
@@ -13,6 +14,9 @@ export * from "./confidence/index";
|
|
|
13
14
|
export * from "./factor/index";
|
|
14
15
|
export * from "./fgca/index";
|
|
15
16
|
export * from "./goals/index";
|
|
17
|
+
export * from "./integration/index";
|
|
18
|
+
export * from "./location/index";
|
|
19
|
+
export * from "./node/index";
|
|
16
20
|
export * from "./process-blueprint/index";
|
|
17
21
|
export * from "./process-map/index";
|
|
18
22
|
export * from "./products/index";
|
|
@@ -21,6 +25,7 @@ export * from "./requirement/index";
|
|
|
21
25
|
export * from "./scenarios/index";
|
|
22
26
|
export * from "./stakeholder/index";
|
|
23
27
|
export * from "./target-state/index";
|
|
28
|
+
export * from "./technology-service/index";
|
|
24
29
|
export { SCHEMA_VERSION } from "./schema-version";
|
|
25
30
|
export * from "./theme/index";
|
|
26
31
|
export * from "./typed-id";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|