@transitrix/diagrams 1.0.0 → 1.0.1
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/LICENSE +21 -21
- package/README.md +47 -47
- package/dist/__tests__/edge-path.test.js +8 -0
- package/dist/activity-card/__tests__/example.test.js +20 -1
- package/dist/activity-card/__tests__/layout.test.js +102 -11
- package/dist/activity-card/__tests__/resolver.test.js +139 -6
- package/dist/activity-card/index.d.ts +1 -1
- package/dist/activity-card/layout.d.ts +1 -1
- package/dist/activity-card/layout.js +220 -103
- package/dist/activity-card/resolver.js +107 -26
- package/dist/activity-card/types.d.ts +150 -27
- package/dist/activity-card/validate.js +3 -0
- package/dist/actor/__tests__/validate.test.js +63 -31
- package/dist/actor/index.d.ts +1 -1
- package/dist/actor/index.js +1 -1
- package/dist/actor/types.d.ts +7 -3
- package/dist/actor/types.js +17 -3
- package/dist/actor/validate.js +55 -26
- package/dist/assertion/types.d.ts +3 -1
- package/dist/assertion/types.js +1 -1
- package/dist/change/__tests__/validate.test.js +40 -34
- package/dist/change/index.d.ts +1 -0
- package/dist/change/types.d.ts +1 -1
- package/dist/change/types.js +2 -2
- package/dist/change/validate.d.ts +6 -1
- package/dist/change/validate.js +68 -41
- package/dist/compliance/__tests__/classify.test.js +34 -0
- package/dist/compliance/__tests__/coverage-metric.test.js +146 -46
- package/dist/compliance/__tests__/html.test.js +2 -2
- package/dist/compliance/__tests__/impact.test.js +311 -6
- package/dist/compliance/classify.d.ts +6 -0
- package/dist/compliance/classify.js +9 -1
- package/dist/compliance/coverage-metric.d.ts +31 -11
- package/dist/compliance/coverage-metric.js +149 -39
- package/dist/compliance/html.js +77 -74
- package/dist/compliance/impact.d.ts +77 -16
- package/dist/compliance/impact.js +193 -25
- package/dist/compliance/index.d.ts +2 -2
- package/dist/compliance/index.js +1 -1
- package/dist/compliance/markdown.js +1 -0
- package/dist/compliance/types.d.ts +25 -6
- package/dist/edge-path.d.ts +8 -4
- package/dist/edge-path.js +11 -6
- package/dist/factor/__tests__/validate.test.js +74 -45
- package/dist/factor/index.d.ts +3 -2
- package/dist/factor/index.js +1 -1
- package/dist/factor/types.d.ts +4 -7
- package/dist/factor/types.js +10 -5
- package/dist/factor/validate.d.ts +6 -1
- package/dist/factor/validate.js +68 -50
- package/dist/fgca/__tests__/resolver.test.js +185 -0
- package/dist/fgca/index.d.ts +2 -0
- package/dist/fgca/index.js +1 -0
- package/dist/fgca/resolver.d.ts +49 -0
- package/dist/fgca/resolver.js +124 -0
- package/dist/goals/index.d.ts +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/process-blueprint/__tests__/layout.test.js +37 -0
- package/dist/process-blueprint/layout.js +5 -1
- package/dist/process-blueprint/types.d.ts +22 -1
- package/dist/stakeholder/__tests__/validate.test.js +49 -47
- package/dist/stakeholder/index.d.ts +1 -0
- package/dist/stakeholder/types.d.ts +3 -4
- package/dist/stakeholder/types.js +2 -3
- package/dist/stakeholder/validate.d.ts +6 -1
- package/dist/stakeholder/validate.js +71 -50
- package/dist/target-state/__tests__/validate.test.js +43 -27
- package/dist/target-state/index.d.ts +2 -0
- package/dist/target-state/index.js +1 -0
- package/dist/target-state/types.d.ts +6 -1
- package/dist/target-state/types.js +8 -7
- package/dist/target-state/validate.d.ts +6 -1
- package/dist/target-state/validate.js +69 -45
- package/dist/theme/themes.js +69 -69
- package/dist/webview/__tests__/entry.test.js +11 -11
- package/dist/webview/render-activities.d.ts +3 -1
- package/dist/webview/render-activities.js +2 -2
- package/dist/webview/render-activity-card.js +104 -77
- package/dist/webview/render-fgca.d.ts +4 -0
- package/dist/webview/render-fgca.js +2 -5
- package/dist/webview/render-goals.d.ts +1 -0
- package/dist/webview/render-goals.js +15 -15
- package/dist/webview/render-process-blueprint.js +10 -10
- package/package.json +40 -40
- package/src/__tests__/edge-path.test.ts +58 -46
- package/src/__tests__/scope.test.ts +27 -27
- package/src/__tests__/yaml-normalize.test.ts +158 -158
- package/src/activities/__tests__/cpm.test.ts +164 -164
- package/src/activities/__tests__/gantt.test.ts +245 -245
- package/src/activities/__tests__/layout.test.ts +112 -112
- package/src/activities/__tests__/validate.test.ts +409 -409
- package/src/activities/cpm.ts +95 -95
- package/src/activities/gantt.ts +276 -276
- package/src/activities/index.ts +28 -28
- package/src/activities/layout.ts +112 -112
- package/src/activities/types.ts +148 -148
- package/src/activities/validate.ts +357 -357
- package/src/activity-card/__tests__/example.test.ts +85 -66
- package/src/activity-card/__tests__/layout.test.ts +169 -63
- package/src/activity-card/__tests__/resolver.test.ts +307 -161
- package/src/activity-card/__tests__/validate.test.ts +131 -131
- package/src/activity-card/index.ts +38 -32
- package/src/activity-card/layout.ts +324 -194
- package/src/activity-card/resolver.ts +415 -337
- package/src/activity-card/types.ts +376 -237
- package/src/activity-card/validate.ts +162 -158
- package/src/actor/__tests__/validate.test.ts +70 -31
- package/src/actor/index.ts +1 -1
- package/src/actor/types.ts +21 -7
- package/src/actor/validate.ts +59 -27
- package/src/applications/__tests__/validate.test.ts +256 -256
- package/src/applications/index.ts +11 -11
- package/src/applications/types.ts +43 -43
- package/src/applications/validate.ts +131 -131
- package/src/assertion/__tests__/example.test.ts +37 -37
- package/src/assertion/__tests__/validate.test.ts +142 -142
- package/src/assertion/index.ts +12 -12
- package/src/assertion/types.ts +60 -58
- package/src/assertion/validate.ts +145 -145
- package/src/blocks/__tests__/layout.test.ts +169 -169
- package/src/blocks/__tests__/validate.test.ts +260 -260
- package/src/blocks/index.ts +17 -17
- package/src/blocks/layout.ts +186 -186
- package/src/blocks/types.ts +62 -62
- package/src/blocks/validate.ts +206 -206
- package/src/capability-map/__tests__/validate.test.ts +238 -238
- package/src/capability-map/index.ts +8 -8
- package/src/capability-map/types.ts +29 -29
- package/src/capability-map/validate.ts +127 -127
- package/src/change/__tests__/validate.test.ts +43 -34
- package/src/change/index.ts +1 -0
- package/src/change/types.ts +5 -3
- package/src/change/validate.ts +75 -43
- package/src/compliance/__tests__/classify.test.ts +64 -27
- package/src/compliance/__tests__/confidence.test.ts +98 -98
- package/src/compliance/__tests__/coverage-metric.test.ts +300 -188
- package/src/compliance/__tests__/demo-scenario.test.ts +273 -273
- package/src/compliance/__tests__/example.test.ts +57 -57
- package/src/compliance/__tests__/gap-report.test.ts +128 -128
- package/src/compliance/__tests__/html.test.ts +141 -141
- package/src/compliance/__tests__/impact.test.ts +840 -481
- package/src/compliance/__tests__/markdown.test.ts +94 -94
- package/src/compliance/__tests__/views.test.ts +64 -64
- package/src/compliance/classify.ts +100 -84
- package/src/compliance/confidence.ts +69 -69
- package/src/compliance/coverage-metric.ts +404 -263
- package/src/compliance/gap-report.ts +94 -94
- package/src/compliance/html.ts +298 -295
- package/src/compliance/impact.ts +861 -677
- package/src/compliance/index.ts +57 -56
- package/src/compliance/markdown.ts +136 -135
- package/src/compliance/reverse-index.ts +33 -33
- package/src/compliance/types.ts +136 -120
- package/src/compliance/views.ts +46 -46
- package/src/compliance-matrix/__tests__/build.test.ts +200 -200
- package/src/compliance-matrix/__tests__/example.test.ts +57 -57
- package/src/compliance-matrix/index.ts +11 -11
- package/src/compliance-matrix/types.ts +102 -102
- package/src/edge-path.ts +50 -44
- package/src/factor/__tests__/validate.test.ts +80 -45
- package/src/factor/index.ts +3 -7
- package/src/factor/types.ts +14 -18
- package/src/factor/validate.ts +77 -51
- package/src/fgca/__tests__/chain-table.test.ts +172 -172
- package/src/fgca/__tests__/layout.test.ts +179 -179
- package/src/fgca/__tests__/parse-canonical.test.ts +248 -248
- package/src/fgca/__tests__/preview-layout.test.ts +124 -124
- package/src/fgca/__tests__/resolver.test.ts +209 -0
- package/src/fgca/__tests__/validate.test.ts +142 -142
- package/src/fgca/chain-table.ts +225 -225
- package/src/fgca/index.ts +47 -45
- package/src/fgca/layout.ts +204 -204
- package/src/fgca/nodeTypes.ts +11 -11
- package/src/fgca/nodes/FGCAActivityNode.tsx +65 -65
- package/src/fgca/nodes/FGCAChangeNode.tsx +64 -64
- package/src/fgca/nodes/FGCAFactorNode.tsx +64 -64
- package/src/fgca/nodes/FGCAGoalNode.tsx +72 -72
- package/src/fgca/parse-canonical.ts +400 -400
- package/src/fgca/preview-layout.ts +217 -217
- package/src/fgca/resolver.ts +157 -0
- package/src/fgca/types.ts +50 -50
- package/src/fgca/validate.ts +126 -126
- package/src/geometry.ts +9 -9
- package/src/goals/__tests__/layout.test.ts +175 -175
- package/src/goals/__tests__/mutations.test.ts +111 -111
- package/src/goals/__tests__/parse-canonical.test.ts +145 -145
- package/src/goals/__tests__/validate.test.ts +202 -202
- package/src/goals/index.ts +20 -21
- package/src/goals/layout.ts +172 -172
- package/src/goals/mutations.ts +123 -123
- package/src/goals/parse-canonical.ts +286 -286
- package/src/goals/types.ts +85 -85
- package/src/goals/validate.ts +136 -136
- package/src/index.ts +28 -23
- package/src/process-blueprint/__tests__/compliance-lane.test.ts +320 -320
- package/src/process-blueprint/__tests__/layout.test.ts +240 -195
- package/src/process-blueprint/__tests__/validate.test.ts +310 -310
- package/src/process-blueprint/index.ts +31 -31
- package/src/process-blueprint/layout.ts +535 -531
- package/src/process-blueprint/types.ts +263 -242
- package/src/process-blueprint/validate.ts +181 -181
- package/src/process-map/__tests__/validate.test.ts +223 -223
- package/src/process-map/index.ts +10 -10
- package/src/process-map/types.ts +36 -36
- package/src/process-map/validate.ts +135 -135
- package/src/products/__tests__/validate.test.ts +239 -239
- package/src/products/index.ts +9 -9
- package/src/products/types.ts +31 -31
- package/src/products/validate.ts +112 -112
- package/src/repo-validate/__tests__/validate-repo.test.ts +149 -149
- package/src/repo-validate/index.ts +2 -2
- package/src/repo-validate/types.ts +50 -50
- package/src/repo-validate/validate-repo.ts +196 -196
- package/src/requirement/__tests__/example.test.ts +24 -24
- package/src/requirement/__tests__/validate.test.ts +96 -96
- package/src/requirement/index.ts +4 -4
- package/src/requirement/types.ts +35 -35
- package/src/requirement/validate.ts +92 -92
- package/src/scenarios/__tests__/validate.test.ts +209 -209
- package/src/scenarios/index.ts +15 -15
- package/src/scenarios/types.ts +37 -37
- package/src/scenarios/validate.ts +122 -122
- package/src/schema-version.ts +16 -16
- package/src/scope.ts +46 -46
- package/src/stakeholder/__tests__/validate.test.ts +51 -47
- package/src/stakeholder/index.ts +1 -0
- package/src/stakeholder/types.ts +7 -7
- package/src/stakeholder/validate.ts +79 -52
- package/src/target-state/__tests__/validate.test.ts +48 -27
- package/src/target-state/index.ts +2 -0
- package/src/target-state/types.ts +11 -7
- package/src/target-state/validate.ts +76 -46
- package/src/theme/index.ts +4 -4
- package/src/theme/themes.ts +237 -237
- package/src/theme/tokens.ts +133 -133
- package/src/typed-id.ts +56 -56
- package/src/validation-types.ts +36 -36
- package/src/webview/__tests__/entry-notations.test.ts +77 -77
- package/src/webview/__tests__/entry.test.ts +64 -64
- package/src/webview/__tests__/render-goals.test.ts +72 -72
- package/src/webview/entry.ts +292 -292
- package/src/webview/render-activities.ts +5 -3
- package/src/webview/render-activity-card.ts +266 -234
- package/src/webview/render-fgca.ts +6 -5
- package/src/webview/render-goals.ts +111 -110
- package/src/webview/render-process-blueprint.ts +207 -207
- package/src/webview/styles.css +322 -323
- package/src/yaml-normalize.ts +56 -56
- package/dist/element-envelope.d.ts +0 -15
- package/dist/element-envelope.js +0 -55
- package/dist/issues/__tests__/layout.test.js +0 -62
- package/dist/issues/__tests__/validate.test.d.ts +0 -1
- package/dist/issues/__tests__/validate.test.js +0 -82
- package/dist/issues/index.d.ts +0 -4
- package/dist/issues/index.js +0 -2
- package/dist/issues/layout.d.ts +0 -10
- package/dist/issues/layout.js +0 -82
- package/dist/issues/types.d.ts +0 -61
- package/dist/issues/types.js +0 -6
- package/dist/issues/validate.d.ts +0 -3
- package/dist/issues/validate.js +0 -143
- package/dist/webview/transitrix-render.css +0 -323
- package/dist/webview/transitrix-render.js +0 -6452
- package/dist/webview/transitrix-render.js.map +0 -7
- package/src/element-envelope.ts +0 -76
- /package/dist/{issues/__tests__/layout.test.d.ts → fgca/__tests__/resolver.test.d.ts} +0 -0
|
@@ -1,248 +1,248 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import * as fs from 'node:fs';
|
|
3
|
-
import * as path from 'node:path';
|
|
4
|
-
import yaml from 'js-yaml';
|
|
5
|
-
import { parseCanonicalFGCA, parseCanonicalFGA } from '../parse-canonical.js';
|
|
6
|
-
|
|
7
|
-
const VALID = {
|
|
8
|
-
notation: 'fgca',
|
|
9
|
-
spec_version: '0.1',
|
|
10
|
-
id: 'FGCA-SAMPLE-1',
|
|
11
|
-
name: 'Sample FGCA chain',
|
|
12
|
-
factors: [
|
|
13
|
-
{ id: 'FACTOR-1', name: 'Driver one', type: 'external' },
|
|
14
|
-
],
|
|
15
|
-
goals: [
|
|
16
|
-
{ id: 'GOAL-1', name: 'Outcome one', factors: ['FACTOR-1'] },
|
|
17
|
-
],
|
|
18
|
-
changes: [
|
|
19
|
-
{ id: 'CHANGE-1', name: 'Transformation', goals: ['GOAL-1'] },
|
|
20
|
-
],
|
|
21
|
-
activities: [
|
|
22
|
-
{ id: 'ACTIVITY-1', name: 'Workstream', changes: ['CHANGE-1'] },
|
|
23
|
-
],
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
describe('parseCanonicalFGCA', () => {
|
|
27
|
-
it('accepts a valid canonical document', () => {
|
|
28
|
-
const r = parseCanonicalFGCA(VALID);
|
|
29
|
-
expect(r.valid).toBe(true);
|
|
30
|
-
expect(r.errors).toEqual([]);
|
|
31
|
-
expect(r.parsed).toBeDefined();
|
|
32
|
-
expect(r.parsed?.factors).toHaveLength(1);
|
|
33
|
-
expect(r.parsed?.goals).toHaveLength(1);
|
|
34
|
-
expect(r.parsed?.changes).toHaveLength(1);
|
|
35
|
-
expect(r.parsed?.activities).toHaveLength(1);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('FGCA-001: rejects non-object input', () => {
|
|
39
|
-
expect(parseCanonicalFGCA(null).errors[0].code).toBe('FGCA-001');
|
|
40
|
-
expect(parseCanonicalFGCA('string').errors[0].code).toBe('FGCA-001');
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('FGCA-001: rejects wrong notation', () => {
|
|
44
|
-
const r = parseCanonicalFGCA({ ...VALID, notation: 'fga' });
|
|
45
|
-
expect(r.valid).toBe(false);
|
|
46
|
-
expect(r.errors.some((e) => e.code === 'FGCA-001')).toBe(true);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('FGCA-002: rejects malformed doc id', () => {
|
|
50
|
-
const r = parseCanonicalFGCA({ ...VALID, id: 'fgca-lowercase-1' });
|
|
51
|
-
expect(r.valid).toBe(false);
|
|
52
|
-
expect(r.errors.some((e) => e.code === 'FGCA-002')).toBe(true);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('FGCA-003: rejects missing name', () => {
|
|
56
|
-
const { name: _, ...rest } = VALID;
|
|
57
|
-
const r = parseCanonicalFGCA(rest);
|
|
58
|
-
expect(r.valid).toBe(false);
|
|
59
|
-
expect(r.errors.some((e) => e.code === 'FGCA-003')).toBe(true);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('FGCA-004: rejects missing factors array', () => {
|
|
63
|
-
const { factors: _, ...rest } = VALID;
|
|
64
|
-
const r = parseCanonicalFGCA(rest);
|
|
65
|
-
expect(r.valid).toBe(false);
|
|
66
|
-
expect(r.errors.some((e) => e.code === 'FGCA-004')).toBe(true);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('FGCA-006: rejects duplicate IDs within a layer', () => {
|
|
70
|
-
const r = parseCanonicalFGCA({
|
|
71
|
-
...VALID,
|
|
72
|
-
factors: [
|
|
73
|
-
{ id: 'FACTOR-1', name: 'A' },
|
|
74
|
-
{ id: 'FACTOR-1', name: 'B' },
|
|
75
|
-
],
|
|
76
|
-
});
|
|
77
|
-
expect(r.valid).toBe(false);
|
|
78
|
-
expect(r.errors.some((e) => e.code === 'FGCA-006')).toBe(true);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('FGCA-007: rejects malformed factor id', () => {
|
|
82
|
-
const r = parseCanonicalFGCA({
|
|
83
|
-
...VALID,
|
|
84
|
-
factors: [{ id: 'F-1', name: 'A' }],
|
|
85
|
-
});
|
|
86
|
-
expect(r.valid).toBe(false);
|
|
87
|
-
expect(r.errors.some((e) => e.code === 'FGCA-007')).toBe(true);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('FGCA-008: rejects goal.factors[] referencing undefined factor', () => {
|
|
91
|
-
const r = parseCanonicalFGCA({
|
|
92
|
-
...VALID,
|
|
93
|
-
goals: [{ id: 'GOAL-1', name: 'A', factors: ['FACTOR-99'] }],
|
|
94
|
-
});
|
|
95
|
-
expect(r.valid).toBe(false);
|
|
96
|
-
expect(r.errors.some((e) => e.code === 'FGCA-008')).toBe(true);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('FGCA-009: rejects change.goals[] referencing undefined goal', () => {
|
|
100
|
-
const r = parseCanonicalFGCA({
|
|
101
|
-
...VALID,
|
|
102
|
-
changes: [{ id: 'CHANGE-1', name: 'A', goals: ['GOAL-99'] }],
|
|
103
|
-
});
|
|
104
|
-
expect(r.valid).toBe(false);
|
|
105
|
-
expect(r.errors.some((e) => e.code === 'FGCA-009')).toBe(true);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('FGCA-010: rejects activity.changes[] referencing undefined change', () => {
|
|
109
|
-
const r = parseCanonicalFGCA({
|
|
110
|
-
...VALID,
|
|
111
|
-
activities: [{ id: 'ACTIVITY-1', name: 'A', changes: ['CHANGE-99'] }],
|
|
112
|
-
});
|
|
113
|
-
expect(r.valid).toBe(false);
|
|
114
|
-
expect(r.errors.some((e) => e.code === 'FGCA-010')).toBe(true);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('FGCA-015: rejects factor.references_constraint with malformed ID', () => {
|
|
118
|
-
const r = parseCanonicalFGCA({
|
|
119
|
-
...VALID,
|
|
120
|
-
factors: [{ id: 'FACTOR-1', name: 'A', references_constraint: ['BAD-1'] }],
|
|
121
|
-
});
|
|
122
|
-
expect(r.valid).toBe(false);
|
|
123
|
-
expect(r.errors.some((e) => e.code === 'FGCA-015')).toBe(true);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('accepts factor.references_constraint with valid CONSTRAINT ID', () => {
|
|
127
|
-
const r = parseCanonicalFGCA({
|
|
128
|
-
...VALID,
|
|
129
|
-
factors: [{ id: 'FACTOR-1', name: 'A', references_constraint: ['CONSTRAINT-GDPR-1'] }],
|
|
130
|
-
});
|
|
131
|
-
expect(r.valid).toBe(true);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it('populates internal change.activity_ids from canonical activity.changes (reversed direction)', () => {
|
|
135
|
-
const r = parseCanonicalFGCA({
|
|
136
|
-
...VALID,
|
|
137
|
-
changes: [{ id: 'CHANGE-1', name: 'C', goals: ['GOAL-1'] }],
|
|
138
|
-
activities: [
|
|
139
|
-
{ id: 'ACTIVITY-1', name: 'A1', changes: ['CHANGE-1'] },
|
|
140
|
-
{ id: 'ACTIVITY-2', name: 'A2', changes: ['CHANGE-1'] },
|
|
141
|
-
],
|
|
142
|
-
});
|
|
143
|
-
expect(r.valid).toBe(true);
|
|
144
|
-
expect(r.parsed?.changes[0].activity_ids).toHaveLength(2);
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
// The "FGCA preview blank / FGA no edges" regression (vkgeorgia/strategy#65,
|
|
149
|
-
// transitrix/methodology#65) was a data defect: the canonical flat shape was
|
|
150
|
-
// not being mapped to the internal cross-ref fields the renderer turns into
|
|
151
|
-
// edges. These assert the edge-driving fields are populated, so the renderer
|
|
152
|
-
// has something to draw — locking the fix.
|
|
153
|
-
describe('parseCanonicalFGCA — edge-driving fields populated', () => {
|
|
154
|
-
it('maps goal.factors → goal.factor[], change.goals → change.goal_id, activity.changes → change.activity_ids', () => {
|
|
155
|
-
const r = parseCanonicalFGCA(VALID);
|
|
156
|
-
expect(r.valid).toBe(true);
|
|
157
|
-
const parsed = r.parsed!;
|
|
158
|
-
// factor → goal edge source
|
|
159
|
-
expect(parsed.goals[0].factor).toHaveLength(1);
|
|
160
|
-
// goal → change edge
|
|
161
|
-
expect(parsed.changes[0].goal_id).not.toBe(0);
|
|
162
|
-
// change → activity edge
|
|
163
|
-
expect(parsed.changes[0].activity_ids).toHaveLength(1);
|
|
164
|
-
});
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
describe('parseCanonicalFGA', () => {
|
|
168
|
-
const VALID_FGA = {
|
|
169
|
-
notation: 'fga',
|
|
170
|
-
spec_version: '0.1',
|
|
171
|
-
id: 'FGA-SAMPLE-1',
|
|
172
|
-
name: 'Sample FGA chain',
|
|
173
|
-
factors: [{ id: 'FACTOR-1', name: 'Driver' }],
|
|
174
|
-
goals: [{ id: 'GOAL-1', name: 'Outcome', factors: ['FACTOR-1'] }],
|
|
175
|
-
activities: [{ id: 'ACTIVITY-1', name: 'Workstream', goals: ['GOAL-1'] }],
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
it('accepts a valid canonical FGA document (no changes layer)', () => {
|
|
179
|
-
const r = parseCanonicalFGA(VALID_FGA);
|
|
180
|
-
expect(r.valid).toBe(true);
|
|
181
|
-
expect(r.errors).toEqual([]);
|
|
182
|
-
expect(r.parsed?.factors).toHaveLength(1);
|
|
183
|
-
expect(r.parsed?.goals).toHaveLength(1);
|
|
184
|
-
expect(r.parsed?.activities).toHaveLength(1);
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
it('populates activity.goal_id from activity.goals[] (the FGA edge-driving field)', () => {
|
|
188
|
-
// The "FGA nodes render, no edges" bug: activities had no resolvable
|
|
189
|
-
// goal_id, so the renderer drew no goal → activity edges.
|
|
190
|
-
const r = parseCanonicalFGA(VALID_FGA);
|
|
191
|
-
expect(r.valid).toBe(true);
|
|
192
|
-
expect(r.parsed?.activities[0].goal_id).not.toBeNull();
|
|
193
|
-
expect(r.parsed?.activities[0].goal_id).not.toBe(0);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it('FGA-001: rejects wrong notation', () => {
|
|
197
|
-
const r = parseCanonicalFGA({ ...VALID_FGA, notation: 'fgca' });
|
|
198
|
-
expect(r.valid).toBe(false);
|
|
199
|
-
expect(r.errors.some((e) => e.code === 'FGA-001')).toBe(true);
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it('FGA-002: rejects malformed doc id', () => {
|
|
203
|
-
const r = parseCanonicalFGA({ ...VALID_FGA, id: 'fga-lower-1' });
|
|
204
|
-
expect(r.valid).toBe(false);
|
|
205
|
-
expect(r.errors.some((e) => e.code === 'FGA-002')).toBe(true);
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
it('remaps FGCA layer codes into the FGA registry (e.g. FGA-007 for a bad id)', () => {
|
|
209
|
-
const r = parseCanonicalFGA({ ...VALID_FGA, factors: [{ id: 'F-1', name: 'bad' }] });
|
|
210
|
-
expect(r.valid).toBe(false);
|
|
211
|
-
expect(r.errors.some((e) => e.code === 'FGA-007')).toBe(true);
|
|
212
|
-
// No raw FGCA-prefixed code should leak through the remap.
|
|
213
|
-
expect(r.errors.every((e) => !e.code.startsWith('FGCA-'))).toBe(true);
|
|
214
|
-
});
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
describe('parseCanonicalFGCA — example file regression', () => {
|
|
218
|
-
const EXAMPLES_DIR = path.resolve(process.cwd(), '..', '..', 'tests', 'fixtures', 'notation-corpus', 'fgca');
|
|
219
|
-
const files = fs.existsSync(EXAMPLES_DIR)
|
|
220
|
-
? fs.readdirSync(EXAMPLES_DIR).filter((f) => f.endsWith('.transitrix.yaml'))
|
|
221
|
-
: [];
|
|
222
|
-
for (const file of files) {
|
|
223
|
-
it(`accepts tests/fixtures/notation-corpus/fgca/${file}`, () => {
|
|
224
|
-
const text = fs.readFileSync(path.join(EXAMPLES_DIR, file), 'utf8');
|
|
225
|
-
const parsedYaml = yaml.load(text);
|
|
226
|
-
const r = parseCanonicalFGCA(parsedYaml);
|
|
227
|
-
expect(r.errors).toEqual([]);
|
|
228
|
-
expect(r.valid).toBe(true);
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
describe('parseCanonicalFGA — example file regression', () => {
|
|
234
|
-
const EXAMPLES_DIR = path.resolve(process.cwd(), '..', '..', 'tests', 'fixtures', 'notation-corpus', 'fga');
|
|
235
|
-
const files = fs.existsSync(EXAMPLES_DIR)
|
|
236
|
-
? fs.readdirSync(EXAMPLES_DIR).filter((f) => f.endsWith('.transitrix.yaml'))
|
|
237
|
-
: [];
|
|
238
|
-
for (const file of files) {
|
|
239
|
-
it(`accepts tests/fixtures/notation-corpus/fga/${file} and resolves every activity to a goal`, () => {
|
|
240
|
-
const text = fs.readFileSync(path.join(EXAMPLES_DIR, file), 'utf8');
|
|
241
|
-
const r = parseCanonicalFGA(yaml.load(text));
|
|
242
|
-
expect(r.errors).toEqual([]);
|
|
243
|
-
expect(r.valid).toBe(true);
|
|
244
|
-
// Every activity must carry a goal_id, or the FGA preview draws no edges.
|
|
245
|
-
expect(r.parsed!.activities.every((a) => a.goal_id != null && a.goal_id !== 0)).toBe(true);
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
});
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import yaml from 'js-yaml';
|
|
5
|
+
import { parseCanonicalFGCA, parseCanonicalFGA } from '../parse-canonical.js';
|
|
6
|
+
|
|
7
|
+
const VALID = {
|
|
8
|
+
notation: 'fgca',
|
|
9
|
+
spec_version: '0.1',
|
|
10
|
+
id: 'FGCA-SAMPLE-1',
|
|
11
|
+
name: 'Sample FGCA chain',
|
|
12
|
+
factors: [
|
|
13
|
+
{ id: 'FACTOR-1', name: 'Driver one', type: 'external' },
|
|
14
|
+
],
|
|
15
|
+
goals: [
|
|
16
|
+
{ id: 'GOAL-1', name: 'Outcome one', factors: ['FACTOR-1'] },
|
|
17
|
+
],
|
|
18
|
+
changes: [
|
|
19
|
+
{ id: 'CHANGE-1', name: 'Transformation', goals: ['GOAL-1'] },
|
|
20
|
+
],
|
|
21
|
+
activities: [
|
|
22
|
+
{ id: 'ACTIVITY-1', name: 'Workstream', changes: ['CHANGE-1'] },
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
describe('parseCanonicalFGCA', () => {
|
|
27
|
+
it('accepts a valid canonical document', () => {
|
|
28
|
+
const r = parseCanonicalFGCA(VALID);
|
|
29
|
+
expect(r.valid).toBe(true);
|
|
30
|
+
expect(r.errors).toEqual([]);
|
|
31
|
+
expect(r.parsed).toBeDefined();
|
|
32
|
+
expect(r.parsed?.factors).toHaveLength(1);
|
|
33
|
+
expect(r.parsed?.goals).toHaveLength(1);
|
|
34
|
+
expect(r.parsed?.changes).toHaveLength(1);
|
|
35
|
+
expect(r.parsed?.activities).toHaveLength(1);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('FGCA-001: rejects non-object input', () => {
|
|
39
|
+
expect(parseCanonicalFGCA(null).errors[0].code).toBe('FGCA-001');
|
|
40
|
+
expect(parseCanonicalFGCA('string').errors[0].code).toBe('FGCA-001');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('FGCA-001: rejects wrong notation', () => {
|
|
44
|
+
const r = parseCanonicalFGCA({ ...VALID, notation: 'fga' });
|
|
45
|
+
expect(r.valid).toBe(false);
|
|
46
|
+
expect(r.errors.some((e) => e.code === 'FGCA-001')).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('FGCA-002: rejects malformed doc id', () => {
|
|
50
|
+
const r = parseCanonicalFGCA({ ...VALID, id: 'fgca-lowercase-1' });
|
|
51
|
+
expect(r.valid).toBe(false);
|
|
52
|
+
expect(r.errors.some((e) => e.code === 'FGCA-002')).toBe(true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('FGCA-003: rejects missing name', () => {
|
|
56
|
+
const { name: _, ...rest } = VALID;
|
|
57
|
+
const r = parseCanonicalFGCA(rest);
|
|
58
|
+
expect(r.valid).toBe(false);
|
|
59
|
+
expect(r.errors.some((e) => e.code === 'FGCA-003')).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('FGCA-004: rejects missing factors array', () => {
|
|
63
|
+
const { factors: _, ...rest } = VALID;
|
|
64
|
+
const r = parseCanonicalFGCA(rest);
|
|
65
|
+
expect(r.valid).toBe(false);
|
|
66
|
+
expect(r.errors.some((e) => e.code === 'FGCA-004')).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('FGCA-006: rejects duplicate IDs within a layer', () => {
|
|
70
|
+
const r = parseCanonicalFGCA({
|
|
71
|
+
...VALID,
|
|
72
|
+
factors: [
|
|
73
|
+
{ id: 'FACTOR-1', name: 'A' },
|
|
74
|
+
{ id: 'FACTOR-1', name: 'B' },
|
|
75
|
+
],
|
|
76
|
+
});
|
|
77
|
+
expect(r.valid).toBe(false);
|
|
78
|
+
expect(r.errors.some((e) => e.code === 'FGCA-006')).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('FGCA-007: rejects malformed factor id', () => {
|
|
82
|
+
const r = parseCanonicalFGCA({
|
|
83
|
+
...VALID,
|
|
84
|
+
factors: [{ id: 'F-1', name: 'A' }],
|
|
85
|
+
});
|
|
86
|
+
expect(r.valid).toBe(false);
|
|
87
|
+
expect(r.errors.some((e) => e.code === 'FGCA-007')).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('FGCA-008: rejects goal.factors[] referencing undefined factor', () => {
|
|
91
|
+
const r = parseCanonicalFGCA({
|
|
92
|
+
...VALID,
|
|
93
|
+
goals: [{ id: 'GOAL-1', name: 'A', factors: ['FACTOR-99'] }],
|
|
94
|
+
});
|
|
95
|
+
expect(r.valid).toBe(false);
|
|
96
|
+
expect(r.errors.some((e) => e.code === 'FGCA-008')).toBe(true);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('FGCA-009: rejects change.goals[] referencing undefined goal', () => {
|
|
100
|
+
const r = parseCanonicalFGCA({
|
|
101
|
+
...VALID,
|
|
102
|
+
changes: [{ id: 'CHANGE-1', name: 'A', goals: ['GOAL-99'] }],
|
|
103
|
+
});
|
|
104
|
+
expect(r.valid).toBe(false);
|
|
105
|
+
expect(r.errors.some((e) => e.code === 'FGCA-009')).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('FGCA-010: rejects activity.changes[] referencing undefined change', () => {
|
|
109
|
+
const r = parseCanonicalFGCA({
|
|
110
|
+
...VALID,
|
|
111
|
+
activities: [{ id: 'ACTIVITY-1', name: 'A', changes: ['CHANGE-99'] }],
|
|
112
|
+
});
|
|
113
|
+
expect(r.valid).toBe(false);
|
|
114
|
+
expect(r.errors.some((e) => e.code === 'FGCA-010')).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('FGCA-015: rejects factor.references_constraint with malformed ID', () => {
|
|
118
|
+
const r = parseCanonicalFGCA({
|
|
119
|
+
...VALID,
|
|
120
|
+
factors: [{ id: 'FACTOR-1', name: 'A', references_constraint: ['BAD-1'] }],
|
|
121
|
+
});
|
|
122
|
+
expect(r.valid).toBe(false);
|
|
123
|
+
expect(r.errors.some((e) => e.code === 'FGCA-015')).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('accepts factor.references_constraint with valid CONSTRAINT ID', () => {
|
|
127
|
+
const r = parseCanonicalFGCA({
|
|
128
|
+
...VALID,
|
|
129
|
+
factors: [{ id: 'FACTOR-1', name: 'A', references_constraint: ['CONSTRAINT-GDPR-1'] }],
|
|
130
|
+
});
|
|
131
|
+
expect(r.valid).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('populates internal change.activity_ids from canonical activity.changes (reversed direction)', () => {
|
|
135
|
+
const r = parseCanonicalFGCA({
|
|
136
|
+
...VALID,
|
|
137
|
+
changes: [{ id: 'CHANGE-1', name: 'C', goals: ['GOAL-1'] }],
|
|
138
|
+
activities: [
|
|
139
|
+
{ id: 'ACTIVITY-1', name: 'A1', changes: ['CHANGE-1'] },
|
|
140
|
+
{ id: 'ACTIVITY-2', name: 'A2', changes: ['CHANGE-1'] },
|
|
141
|
+
],
|
|
142
|
+
});
|
|
143
|
+
expect(r.valid).toBe(true);
|
|
144
|
+
expect(r.parsed?.changes[0].activity_ids).toHaveLength(2);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// The "FGCA preview blank / FGA no edges" regression (vkgeorgia/strategy#65,
|
|
149
|
+
// transitrix/methodology#65) was a data defect: the canonical flat shape was
|
|
150
|
+
// not being mapped to the internal cross-ref fields the renderer turns into
|
|
151
|
+
// edges. These assert the edge-driving fields are populated, so the renderer
|
|
152
|
+
// has something to draw — locking the fix.
|
|
153
|
+
describe('parseCanonicalFGCA — edge-driving fields populated', () => {
|
|
154
|
+
it('maps goal.factors → goal.factor[], change.goals → change.goal_id, activity.changes → change.activity_ids', () => {
|
|
155
|
+
const r = parseCanonicalFGCA(VALID);
|
|
156
|
+
expect(r.valid).toBe(true);
|
|
157
|
+
const parsed = r.parsed!;
|
|
158
|
+
// factor → goal edge source
|
|
159
|
+
expect(parsed.goals[0].factor).toHaveLength(1);
|
|
160
|
+
// goal → change edge
|
|
161
|
+
expect(parsed.changes[0].goal_id).not.toBe(0);
|
|
162
|
+
// change → activity edge
|
|
163
|
+
expect(parsed.changes[0].activity_ids).toHaveLength(1);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('parseCanonicalFGA', () => {
|
|
168
|
+
const VALID_FGA = {
|
|
169
|
+
notation: 'fga',
|
|
170
|
+
spec_version: '0.1',
|
|
171
|
+
id: 'FGA-SAMPLE-1',
|
|
172
|
+
name: 'Sample FGA chain',
|
|
173
|
+
factors: [{ id: 'FACTOR-1', name: 'Driver' }],
|
|
174
|
+
goals: [{ id: 'GOAL-1', name: 'Outcome', factors: ['FACTOR-1'] }],
|
|
175
|
+
activities: [{ id: 'ACTIVITY-1', name: 'Workstream', goals: ['GOAL-1'] }],
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
it('accepts a valid canonical FGA document (no changes layer)', () => {
|
|
179
|
+
const r = parseCanonicalFGA(VALID_FGA);
|
|
180
|
+
expect(r.valid).toBe(true);
|
|
181
|
+
expect(r.errors).toEqual([]);
|
|
182
|
+
expect(r.parsed?.factors).toHaveLength(1);
|
|
183
|
+
expect(r.parsed?.goals).toHaveLength(1);
|
|
184
|
+
expect(r.parsed?.activities).toHaveLength(1);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('populates activity.goal_id from activity.goals[] (the FGA edge-driving field)', () => {
|
|
188
|
+
// The "FGA nodes render, no edges" bug: activities had no resolvable
|
|
189
|
+
// goal_id, so the renderer drew no goal → activity edges.
|
|
190
|
+
const r = parseCanonicalFGA(VALID_FGA);
|
|
191
|
+
expect(r.valid).toBe(true);
|
|
192
|
+
expect(r.parsed?.activities[0].goal_id).not.toBeNull();
|
|
193
|
+
expect(r.parsed?.activities[0].goal_id).not.toBe(0);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('FGA-001: rejects wrong notation', () => {
|
|
197
|
+
const r = parseCanonicalFGA({ ...VALID_FGA, notation: 'fgca' });
|
|
198
|
+
expect(r.valid).toBe(false);
|
|
199
|
+
expect(r.errors.some((e) => e.code === 'FGA-001')).toBe(true);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('FGA-002: rejects malformed doc id', () => {
|
|
203
|
+
const r = parseCanonicalFGA({ ...VALID_FGA, id: 'fga-lower-1' });
|
|
204
|
+
expect(r.valid).toBe(false);
|
|
205
|
+
expect(r.errors.some((e) => e.code === 'FGA-002')).toBe(true);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('remaps FGCA layer codes into the FGA registry (e.g. FGA-007 for a bad id)', () => {
|
|
209
|
+
const r = parseCanonicalFGA({ ...VALID_FGA, factors: [{ id: 'F-1', name: 'bad' }] });
|
|
210
|
+
expect(r.valid).toBe(false);
|
|
211
|
+
expect(r.errors.some((e) => e.code === 'FGA-007')).toBe(true);
|
|
212
|
+
// No raw FGCA-prefixed code should leak through the remap.
|
|
213
|
+
expect(r.errors.every((e) => !e.code.startsWith('FGCA-'))).toBe(true);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe('parseCanonicalFGCA — example file regression', () => {
|
|
218
|
+
const EXAMPLES_DIR = path.resolve(process.cwd(), '..', '..', 'tests', 'fixtures', 'notation-corpus', 'fgca');
|
|
219
|
+
const files = fs.existsSync(EXAMPLES_DIR)
|
|
220
|
+
? fs.readdirSync(EXAMPLES_DIR).filter((f) => f.endsWith('.transitrix.yaml'))
|
|
221
|
+
: [];
|
|
222
|
+
for (const file of files) {
|
|
223
|
+
it(`accepts tests/fixtures/notation-corpus/fgca/${file}`, () => {
|
|
224
|
+
const text = fs.readFileSync(path.join(EXAMPLES_DIR, file), 'utf8');
|
|
225
|
+
const parsedYaml = yaml.load(text);
|
|
226
|
+
const r = parseCanonicalFGCA(parsedYaml);
|
|
227
|
+
expect(r.errors).toEqual([]);
|
|
228
|
+
expect(r.valid).toBe(true);
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe('parseCanonicalFGA — example file regression', () => {
|
|
234
|
+
const EXAMPLES_DIR = path.resolve(process.cwd(), '..', '..', 'tests', 'fixtures', 'notation-corpus', 'fga');
|
|
235
|
+
const files = fs.existsSync(EXAMPLES_DIR)
|
|
236
|
+
? fs.readdirSync(EXAMPLES_DIR).filter((f) => f.endsWith('.transitrix.yaml'))
|
|
237
|
+
: [];
|
|
238
|
+
for (const file of files) {
|
|
239
|
+
it(`accepts tests/fixtures/notation-corpus/fga/${file} and resolves every activity to a goal`, () => {
|
|
240
|
+
const text = fs.readFileSync(path.join(EXAMPLES_DIR, file), 'utf8');
|
|
241
|
+
const r = parseCanonicalFGA(yaml.load(text));
|
|
242
|
+
expect(r.errors).toEqual([]);
|
|
243
|
+
expect(r.valid).toBe(true);
|
|
244
|
+
// Every activity must carry a goal_id, or the FGA preview draws no edges.
|
|
245
|
+
expect(r.parsed!.activities.every((a) => a.goal_id != null && a.goal_id !== 0)).toBe(true);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
});
|