@transitrix/diagrams 1.3.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 +2 -2
- package/dist/activity-card/__tests__/validate.test.js +29 -18
- 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 +10 -10
- package/dist/fgca/__tests__/resolver.test.js +7 -7
- package/dist/fgca/__tests__/validate.test.js +11 -11
- package/dist/fgca/parse-canonical.js +7 -11
- 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-blocks.js +97 -18
- 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 +2 -2
- package/src/activity-card/__tests__/validate.test.ts +30 -18
- 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 +10 -10
- package/src/fgca/__tests__/resolver.test.ts +7 -7
- package/src/fgca/__tests__/validate.test.ts +11 -11
- package/src/fgca/parse-canonical.ts +7 -11
- 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-blocks.ts +101 -20
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
export type ApplicationType = 'application' | 'integration' | 'platform' | 'data_store';
|
|
2
2
|
export type ApplicationStatus = 'Draft' | 'Active' | 'Deprecated' | 'Decommissioning';
|
|
3
3
|
export type IntegrationDirection = 'inbound' | 'outbound' | 'bidirectional';
|
|
4
|
+
export type IntegrationSensitivity = 'public' | 'internal' | 'confidential' | 'restricted';
|
|
5
|
+
export type IntegrationDirectionality = 'producer' | 'consumer' | 'request_reply' | 'bidirectional_stream';
|
|
4
6
|
export interface ApplicationIntegration {
|
|
5
7
|
target?: string;
|
|
8
|
+
source?: string;
|
|
6
9
|
direction?: IntegrationDirection;
|
|
7
10
|
protocol?: string;
|
|
8
11
|
description?: string;
|
|
12
|
+
interface_semantics?: boolean;
|
|
13
|
+
payload_class?: string;
|
|
14
|
+
sensitivity?: IntegrationSensitivity;
|
|
15
|
+
directionality?: IntegrationDirectionality;
|
|
9
16
|
}
|
|
10
17
|
export interface Application {
|
|
11
18
|
app_id: string;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
const VALID_TYPES = new Set(['application', 'integration', 'platform', 'data_store']);
|
|
2
2
|
const VALID_STATUSES = new Set(['Draft', 'Active', 'Deprecated', 'Decommissioning']);
|
|
3
3
|
const VALID_DIRECTIONS = new Set(['inbound', 'outbound', 'bidirectional']);
|
|
4
|
+
const VALID_SENSITIVITY = new Set(['public', 'internal', 'confidential', 'restricted']);
|
|
5
|
+
const VALID_DIRECTIONALITY = new Set(['producer', 'consumer', 'request_reply', 'bidirectional_stream']);
|
|
4
6
|
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
7
|
+
const IFACE_CONDITIONAL = ['protocol', 'payload_class', 'sensitivity', 'directionality'];
|
|
5
8
|
export function validateApplicationsCatalogue(input) {
|
|
6
9
|
const errors = [];
|
|
7
10
|
const warnings = [];
|
|
@@ -102,6 +105,21 @@ export function validateApplicationsCatalogue(input) {
|
|
|
102
105
|
if (intg['direction'] !== undefined && !VALID_DIRECTIONS.has(intg['direction'])) {
|
|
103
106
|
errors.push({ code: 'APP-009', message: `${idx}.integrations[${j}]: direction "${intg['direction']}" must be one of: inbound, outbound, bidirectional` });
|
|
104
107
|
}
|
|
108
|
+
// INT-001 — when interface_semantics: true, four fields become required.
|
|
109
|
+
if (intg['interface_semantics'] === true) {
|
|
110
|
+
for (const field of IFACE_CONDITIONAL) {
|
|
111
|
+
const v = intg[field];
|
|
112
|
+
if (typeof v !== 'string' || v.trim() === '') {
|
|
113
|
+
errors.push({ code: 'INT-001', message: `${idx}.integrations[${j}]: ${field} is required when interface_semantics is true.`, path: `${idx}.integrations[${j}].${field}` });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (typeof intg['sensitivity'] === 'string' && !VALID_SENSITIVITY.has(intg['sensitivity'])) {
|
|
117
|
+
errors.push({ code: 'INT-001', message: `${idx}.integrations[${j}]: sensitivity "${intg['sensitivity']}" must be one of public, internal, confidential, restricted.`, path: `${idx}.integrations[${j}].sensitivity` });
|
|
118
|
+
}
|
|
119
|
+
if (typeof intg['directionality'] === 'string' && !VALID_DIRECTIONALITY.has(intg['directionality'])) {
|
|
120
|
+
errors.push({ code: 'INT-001', message: `${idx}.integrations[${j}]: directionality "${intg['directionality']}" must be one of producer, consumer, request_reply, bidirectional_stream.`, path: `${idx}.integrations[${j}].directionality` });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
105
123
|
}
|
|
106
124
|
}
|
|
107
125
|
}
|
|
@@ -109,7 +109,7 @@ describe('layoutNestedBlocks', () => {
|
|
|
109
109
|
});
|
|
110
110
|
const a = layout.blocks[0];
|
|
111
111
|
expect(a.width).toBe(160);
|
|
112
|
-
expect(a.height).toBe(
|
|
112
|
+
expect(a.height).toBe(72);
|
|
113
113
|
});
|
|
114
114
|
it('honours custom layout options', () => {
|
|
115
115
|
const layout = layoutNestedBlocks({
|
package/dist/blocks/layout.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { validateBusinessService } from '../validate.js';
|
|
3
|
+
function valid() {
|
|
4
|
+
return {
|
|
5
|
+
notation: 'business-service',
|
|
6
|
+
id: 'BUSINESS_SERVICE-CRM-1',
|
|
7
|
+
name: 'CRM Service',
|
|
8
|
+
type: 'internal',
|
|
9
|
+
description: 'Customer relationship management service offering contact management and deal tracking.',
|
|
10
|
+
offering_unit: 'ACTOR-SALES-OPS-1',
|
|
11
|
+
capability: 'CAPABILITY-V2.1',
|
|
12
|
+
zone: 'canon',
|
|
13
|
+
admitted_at: '2026-06-28',
|
|
14
|
+
admitted_by: 'v.korobeinikov',
|
|
15
|
+
gate_checks: { uniqueness: 'pass', consistency: 'pass', completeness: 'pass' },
|
|
16
|
+
valid_from: '2024-01-01',
|
|
17
|
+
valid_to: null,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const codes = (input) => validateBusinessService(input).errors.map(e => e.code);
|
|
21
|
+
describe('validateBusinessService — positive', () => {
|
|
22
|
+
it('accepts a well-formed internal service with all optional fields', () => {
|
|
23
|
+
const r = validateBusinessService(valid());
|
|
24
|
+
expect(r.valid, JSON.stringify(r.errors)).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
it('accepts an external service with no optional fields', () => {
|
|
27
|
+
const a = valid();
|
|
28
|
+
a.id = 'BUSINESS_SERVICE-ONBOARDING-1';
|
|
29
|
+
a.name = 'Onboarding Service';
|
|
30
|
+
a.type = 'external';
|
|
31
|
+
delete a.description;
|
|
32
|
+
delete a.offering_unit;
|
|
33
|
+
delete a.capability;
|
|
34
|
+
expect(validateBusinessService(a).valid).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
it('accepts a shared service offered by a ROLE', () => {
|
|
37
|
+
expect(validateBusinessService({ ...valid(), type: 'shared', offering_unit: 'ROLE-PLATFORM-OWNER-1' }).valid).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
it('accepts valid_to as a date string', () => {
|
|
40
|
+
expect(validateBusinessService({ ...valid(), valid_to: '2026-12-31' }).valid).toBe(true);
|
|
41
|
+
});
|
|
42
|
+
it('accepts a service with no offering_unit or capability', () => {
|
|
43
|
+
const a = valid();
|
|
44
|
+
delete a.offering_unit;
|
|
45
|
+
delete a.capability;
|
|
46
|
+
expect(validateBusinessService(a).valid).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
describe('validateBusinessService — BSV-001 (shape / id grammar)', () => {
|
|
50
|
+
it('flags a missing required envelope field (name)', () => {
|
|
51
|
+
const a = valid();
|
|
52
|
+
delete a.name;
|
|
53
|
+
expect(codes(a)).toContain('BSV-001');
|
|
54
|
+
});
|
|
55
|
+
it('flags an id that violates the grammar', () => {
|
|
56
|
+
expect(codes({ ...valid(), id: 'BUSINESS_SERVICE-001' })).toContain('BSV-001');
|
|
57
|
+
expect(codes({ ...valid(), id: 'BSV-1' })).toContain('BSV-001');
|
|
58
|
+
expect(codes({ ...valid(), id: 'business-service-1' })).toContain('BSV-001');
|
|
59
|
+
});
|
|
60
|
+
it('flags a wrong notation tag', () => {
|
|
61
|
+
expect(codes({ ...valid(), notation: 'service' })).toContain('BSV-001');
|
|
62
|
+
expect(codes({ ...valid(), notation: 'business_service' })).toContain('BSV-001');
|
|
63
|
+
});
|
|
64
|
+
it('flags a non-canon zone', () => {
|
|
65
|
+
expect(codes({ ...valid(), zone: 'sandbox' })).toContain('BSV-001');
|
|
66
|
+
});
|
|
67
|
+
it('flags a missing zone', () => {
|
|
68
|
+
const a = valid();
|
|
69
|
+
delete a.zone;
|
|
70
|
+
expect(codes(a)).toContain('BSV-001');
|
|
71
|
+
});
|
|
72
|
+
it('flags a missing type', () => {
|
|
73
|
+
const a = valid();
|
|
74
|
+
delete a.type;
|
|
75
|
+
expect(codes(a)).toContain('BSV-001');
|
|
76
|
+
});
|
|
77
|
+
it('flags a missing valid_to key', () => {
|
|
78
|
+
const a = valid();
|
|
79
|
+
delete a.valid_to;
|
|
80
|
+
expect(codes(a)).toContain('BSV-001');
|
|
81
|
+
});
|
|
82
|
+
it('flags missing gate_checks', () => {
|
|
83
|
+
const a = valid();
|
|
84
|
+
delete a.gate_checks;
|
|
85
|
+
expect(codes(a)).toContain('BSV-001');
|
|
86
|
+
});
|
|
87
|
+
it('rejects a non-object', () => {
|
|
88
|
+
expect(codes(null)).toEqual(['BSV-001']);
|
|
89
|
+
expect(codes('string')).toEqual(['BSV-001']);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
describe('validateBusinessService — BSV-002 (type enum)', () => {
|
|
93
|
+
it('flags a type outside the allowed enum', () => {
|
|
94
|
+
expect(codes({ ...valid(), type: 'public' })).toContain('BSV-002');
|
|
95
|
+
expect(codes({ ...valid(), type: 'platform' })).toContain('BSV-002');
|
|
96
|
+
});
|
|
97
|
+
it('accepts all valid type values', () => {
|
|
98
|
+
for (const t of ['internal', 'external', 'shared']) {
|
|
99
|
+
expect(codes({ ...valid(), type: t })).not.toContain('BSV-002');
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
describe('validateBusinessService — BSV-003 (offering_unit grammar)', () => {
|
|
104
|
+
it('flags an offering_unit that is not an ACTOR-… or ROLE-… id', () => {
|
|
105
|
+
expect(codes({ ...valid(), offering_unit: 'PRODUCT-PLATFORM-1' })).toContain('BSV-003');
|
|
106
|
+
expect(codes({ ...valid(), offering_unit: 'not-an-id' })).toContain('BSV-003');
|
|
107
|
+
expect(codes({ ...valid(), offering_unit: 'BUSINESS_SERVICE-CRM-1' })).toContain('BSV-003');
|
|
108
|
+
});
|
|
109
|
+
it('accepts a null or absent offering_unit without error', () => {
|
|
110
|
+
expect(codes({ ...valid(), offering_unit: undefined })).not.toContain('BSV-003');
|
|
111
|
+
const a = valid();
|
|
112
|
+
delete a.offering_unit;
|
|
113
|
+
expect(codes(a)).not.toContain('BSV-003');
|
|
114
|
+
});
|
|
115
|
+
it('accepts ACTOR-… and ROLE-… ids', () => {
|
|
116
|
+
expect(codes({ ...valid(), offering_unit: 'ACTOR-OPS-1' })).not.toContain('BSV-003');
|
|
117
|
+
expect(codes({ ...valid(), offering_unit: 'ROLE-SERVICE-OWNER-1' })).not.toContain('BSV-003');
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
describe('validateBusinessService — BSV-004 (capability grammar)', () => {
|
|
121
|
+
it('flags a capability that is not a CAPABILITY-… id', () => {
|
|
122
|
+
expect(codes({ ...valid(), capability: 'PRODUCT-MOBILE-1' })).toContain('BSV-004');
|
|
123
|
+
expect(codes({ ...valid(), capability: 'not-a-capability' })).toContain('BSV-004');
|
|
124
|
+
});
|
|
125
|
+
it('accepts a null or absent capability without error', () => {
|
|
126
|
+
expect(codes({ ...valid(), capability: undefined })).not.toContain('BSV-004');
|
|
127
|
+
const a = valid();
|
|
128
|
+
delete a.capability;
|
|
129
|
+
expect(codes(a)).not.toContain('BSV-004');
|
|
130
|
+
});
|
|
131
|
+
it('accepts a CAPABILITY-… id', () => {
|
|
132
|
+
expect(codes({ ...valid(), capability: 'CAPABILITY-V2.1' })).not.toContain('BSV-004');
|
|
133
|
+
expect(codes({ ...valid(), capability: 'CAPABILITY-H3.1' })).not.toContain('BSV-004');
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { GateChecks } from '../requirement/types.js';
|
|
2
|
+
export declare const BUSINESS_SERVICE_TYPES: readonly ["internal", "external", "shared"];
|
|
3
|
+
export type BusinessServiceType = typeof BUSINESS_SERVICE_TYPES[number];
|
|
4
|
+
export interface BusinessService {
|
|
5
|
+
notation: 'business-service';
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
type: BusinessServiceType;
|
|
9
|
+
description?: string;
|
|
10
|
+
offering_unit?: string;
|
|
11
|
+
capability?: string;
|
|
12
|
+
owner_role?: string;
|
|
13
|
+
status?: string;
|
|
14
|
+
zone: 'canon';
|
|
15
|
+
admitted_at: string;
|
|
16
|
+
admitted_by: string;
|
|
17
|
+
gate_checks: GateChecks;
|
|
18
|
+
valid_from: string;
|
|
19
|
+
valid_to: string | null;
|
|
20
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// BUSINESS_SERVICE validator — methodology notations/elements/25-business-services.md §5.
|
|
2
|
+
//
|
|
3
|
+
// Implements BSV-001..004. The shared HDR-/LIFECYCLE- rules are
|
|
4
|
+
// cross-cutting concerns (CONTRACT.md §8) and out of scope for the
|
|
5
|
+
// single-artefact validator.
|
|
6
|
+
import { isCanonicalIdOfType, typeOfId } from '../typed-id.js';
|
|
7
|
+
import { BUSINESS_SERVICE_TYPES } from './types.js';
|
|
8
|
+
const REQUIRED_STRING_FIELDS = [
|
|
9
|
+
'name', 'admitted_at', 'admitted_by', 'valid_from',
|
|
10
|
+
];
|
|
11
|
+
const TYPES = BUSINESS_SERVICE_TYPES;
|
|
12
|
+
export function validateBusinessService(input) {
|
|
13
|
+
const errors = [];
|
|
14
|
+
const warnings = [];
|
|
15
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
|
16
|
+
errors.push({ code: 'BSV-001', message: 'Business service must be a YAML mapping.' });
|
|
17
|
+
return { valid: false, errors, warnings };
|
|
18
|
+
}
|
|
19
|
+
const a = input;
|
|
20
|
+
// BSV-001 — id grammar + notation tag + envelope.
|
|
21
|
+
if (!isCanonicalIdOfType(a.id, 'BUSINESS_SERVICE')) {
|
|
22
|
+
errors.push({ code: 'BSV-001', message: `id "${String(a.id)}" must match BUSINESS_SERVICE-[<middle>-]<INTEGER>.`, path: 'id' });
|
|
23
|
+
}
|
|
24
|
+
if (a.notation !== 'business-service') {
|
|
25
|
+
errors.push({ code: 'BSV-001', message: 'notation must be the fixed value "business-service".', path: 'notation' });
|
|
26
|
+
}
|
|
27
|
+
if (a.zone !== undefined && a.zone !== 'canon') {
|
|
28
|
+
errors.push({ code: 'BSV-001', message: 'zone must be "canon" for a BUSINESS_SERVICE.', path: 'zone' });
|
|
29
|
+
}
|
|
30
|
+
else if (a.zone === undefined) {
|
|
31
|
+
errors.push({ code: 'BSV-001', message: 'zone is required.', path: 'zone' });
|
|
32
|
+
}
|
|
33
|
+
for (const field of REQUIRED_STRING_FIELDS) {
|
|
34
|
+
const v = a[field];
|
|
35
|
+
if (typeof v !== 'string' || v.trim() === '') {
|
|
36
|
+
errors.push({ code: 'BSV-001', message: `${field} is required.`, path: field });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (a.gate_checks === null || typeof a.gate_checks !== 'object') {
|
|
40
|
+
errors.push({ code: 'BSV-001', message: 'gate_checks is required and must be a mapping.', path: 'gate_checks' });
|
|
41
|
+
}
|
|
42
|
+
if (!('valid_to' in a) || !(typeof a.valid_to === 'string' || a.valid_to === null)) {
|
|
43
|
+
errors.push({ code: 'BSV-001', message: 'valid_to is required (an ISO date string or null).', path: 'valid_to' });
|
|
44
|
+
}
|
|
45
|
+
if (typeof a.type !== 'string' || a.type.trim() === '') {
|
|
46
|
+
errors.push({ code: 'BSV-001', message: 'type is required.', path: 'type' });
|
|
47
|
+
}
|
|
48
|
+
else if (!TYPES.includes(a.type)) {
|
|
49
|
+
// BSV-002 — type enum.
|
|
50
|
+
errors.push({ code: 'BSV-002', message: `type "${a.type}" must be one of ${TYPES.join(', ')}.`, path: 'type' });
|
|
51
|
+
}
|
|
52
|
+
// BSV-003 — offering_unit must resolve to ACTOR(business_unit) or ROLE when present.
|
|
53
|
+
if ('offering_unit' in a && a.offering_unit !== undefined && a.offering_unit !== null) {
|
|
54
|
+
const t = typeOfId(a.offering_unit);
|
|
55
|
+
if (t !== 'ACTOR' && t !== 'ROLE') {
|
|
56
|
+
errors.push({
|
|
57
|
+
code: 'BSV-003',
|
|
58
|
+
message: `offering_unit "${String(a.offering_unit)}" must be an ACTOR-… or ROLE-… canonical id.`,
|
|
59
|
+
path: 'offering_unit',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// BSV-004 — capability must resolve to CAPABILITY when present.
|
|
64
|
+
if ('capability' in a && a.capability !== undefined && a.capability !== null) {
|
|
65
|
+
if (typeOfId(a.capability) !== 'CAPABILITY') {
|
|
66
|
+
errors.push({
|
|
67
|
+
code: 'BSV-004',
|
|
68
|
+
message: `capability "${String(a.capability)}" must be a CAPABILITY-… canonical id.`,
|
|
69
|
+
path: 'capability',
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
74
|
+
}
|
|
@@ -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
|
],
|
|
@@ -162,13 +162,13 @@ describe('parseCanonicalFGCA — edge-driving fields populated', () => {
|
|
|
162
162
|
});
|
|
163
163
|
describe('parseCanonicalFGA', () => {
|
|
164
164
|
const VALID_FGA = {
|
|
165
|
-
notation: '
|
|
165
|
+
notation: 'dga',
|
|
166
166
|
spec_version: '0.1',
|
|
167
167
|
id: 'FGA-SAMPLE-1',
|
|
168
168
|
name: 'Sample FGA chain',
|
|
169
169
|
factors: [{ id: 'FACTOR-1', name: 'Driver' }],
|
|
170
170
|
goals: [{ id: 'GOAL-1', name: 'Outcome', factors: ['FACTOR-1'] }],
|
|
171
|
-
|
|
171
|
+
actions: [{ id: 'ACTIVITY-1', name: 'Workstream', goals: ['GOAL-1'] }],
|
|
172
172
|
};
|
|
173
173
|
it('accepts a valid canonical FGA document (no changes layer)', () => {
|
|
174
174
|
const r = parseCanonicalFGA(VALID_FGA);
|
|
@@ -205,12 +205,12 @@ describe('parseCanonicalFGA', () => {
|
|
|
205
205
|
});
|
|
206
206
|
});
|
|
207
207
|
describe('parseCanonicalFGCA — example file regression', () => {
|
|
208
|
-
const EXAMPLES_DIR = path.resolve(process.cwd(), '..', '..', 'tests', 'fixtures', 'notation-corpus', '
|
|
208
|
+
const EXAMPLES_DIR = path.resolve(process.cwd(), '..', '..', 'tests', 'fixtures', 'notation-corpus', 'dgca');
|
|
209
209
|
const files = fs.existsSync(EXAMPLES_DIR)
|
|
210
210
|
? fs.readdirSync(EXAMPLES_DIR).filter((f) => f.endsWith('.transitrix.yaml'))
|
|
211
211
|
: [];
|
|
212
212
|
for (const file of files) {
|
|
213
|
-
it(`accepts tests/fixtures/notation-corpus/
|
|
213
|
+
it(`accepts tests/fixtures/notation-corpus/dgca/${file}`, () => {
|
|
214
214
|
const text = fs.readFileSync(path.join(EXAMPLES_DIR, file), 'utf8');
|
|
215
215
|
const parsedYaml = yaml.load(text);
|
|
216
216
|
const r = parseCanonicalFGCA(parsedYaml);
|
|
@@ -220,12 +220,12 @@ describe('parseCanonicalFGCA — example file regression', () => {
|
|
|
220
220
|
}
|
|
221
221
|
});
|
|
222
222
|
describe('parseCanonicalFGA — example file regression', () => {
|
|
223
|
-
const EXAMPLES_DIR = path.resolve(process.cwd(), '..', '..', 'tests', 'fixtures', 'notation-corpus', '
|
|
223
|
+
const EXAMPLES_DIR = path.resolve(process.cwd(), '..', '..', 'tests', 'fixtures', 'notation-corpus', 'dga');
|
|
224
224
|
const files = fs.existsSync(EXAMPLES_DIR)
|
|
225
225
|
? fs.readdirSync(EXAMPLES_DIR).filter((f) => f.endsWith('.transitrix.yaml'))
|
|
226
226
|
: [];
|
|
227
227
|
for (const file of files) {
|
|
228
|
-
it(`accepts tests/fixtures/notation-corpus/
|
|
228
|
+
it(`accepts tests/fixtures/notation-corpus/dga/${file} and resolves every activity to a goal`, () => {
|
|
229
229
|
const text = fs.readFileSync(path.join(EXAMPLES_DIR, file), 'utf8');
|
|
230
230
|
const r = parseCanonicalFGA(yaml.load(text));
|
|
231
231
|
expect(r.errors).toEqual([]);
|
|
@@ -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' });
|
|
@@ -294,10 +290,10 @@ export function parseCanonicalFGA(input) {
|
|
|
294
290
|
return { valid: false, errors: [{ code: 'FGA-001', message: 'document root is not an object' }], warnings: [] };
|
|
295
291
|
}
|
|
296
292
|
const raw = input;
|
|
297
|
-
if ('notation' in raw && raw['notation'] !== '
|
|
293
|
+
if ('notation' in raw && raw['notation'] !== 'dga') {
|
|
298
294
|
return {
|
|
299
295
|
valid: false,
|
|
300
|
-
errors: [{ code: 'FGA-001', message: `notation must be "dga"
|
|
296
|
+
errors: [{ code: 'FGA-001', message: `notation must be "dga", got "${String(raw['notation'])}"` }],
|
|
301
297
|
warnings: [],
|
|
302
298
|
};
|
|
303
299
|
}
|
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
|
}
|