@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
|
@@ -15,59 +15,65 @@ function valid() {
|
|
|
15
15
|
valid_to: null,
|
|
16
16
|
};
|
|
17
17
|
}
|
|
18
|
-
const codes = (input) => validateChange(input).errors.map(e => e.code);
|
|
18
|
+
const codes = (input, opts) => validateChange(input, opts).errors.map(e => e.code);
|
|
19
19
|
describe('validateChange — positive', () => {
|
|
20
|
-
it('accepts
|
|
20
|
+
it('accepts a well-formed change', () => {
|
|
21
21
|
const r = validateChange(valid());
|
|
22
22
|
expect(r.valid, JSON.stringify(r.errors)).toBe(true);
|
|
23
|
-
expect(r.errors).toHaveLength(0);
|
|
24
23
|
});
|
|
25
|
-
it('accepts a minimal change
|
|
26
|
-
const
|
|
27
|
-
delete
|
|
28
|
-
|
|
24
|
+
it('accepts a minimal change with no optional fields', () => {
|
|
25
|
+
const c = valid();
|
|
26
|
+
delete c.goals;
|
|
27
|
+
delete c.description;
|
|
28
|
+
expect(validateChange(c).valid).toBe(true);
|
|
29
29
|
});
|
|
30
30
|
it('accepts a child change with a CHANGE parent', () => {
|
|
31
|
-
|
|
31
|
+
const c = valid();
|
|
32
|
+
c.id = 'CHANGE-EU-CRM-2';
|
|
33
|
+
c.parent = 'CHANGE-EU-CRM-1';
|
|
34
|
+
expect(validateChange(c).valid).toBe(true);
|
|
32
35
|
});
|
|
33
36
|
});
|
|
34
|
-
describe('validateChange — CHANGE-001 (
|
|
35
|
-
it('flags a
|
|
36
|
-
|
|
37
|
+
describe('validateChange — CHANGE-001 (shape / id grammar)', () => {
|
|
38
|
+
it('flags a missing required envelope field', () => {
|
|
39
|
+
const c = valid();
|
|
40
|
+
delete c.name;
|
|
41
|
+
expect(codes(c)).toContain('CHANGE-001');
|
|
37
42
|
});
|
|
38
|
-
it('flags
|
|
39
|
-
expect(codes({ ...valid(),
|
|
43
|
+
it('flags an id that violates the grammar', () => {
|
|
44
|
+
expect(codes({ ...valid(), id: 'CHANGE-EU-CRM-001' })).toContain('CHANGE-001');
|
|
45
|
+
expect(codes({ ...valid(), id: 'CHG-1' })).toContain('CHANGE-001');
|
|
40
46
|
});
|
|
41
|
-
it('flags
|
|
42
|
-
expect(codes({ ...valid(),
|
|
47
|
+
it('flags a wrong notation tag', () => {
|
|
48
|
+
expect(codes({ ...valid(), notation: 'gap' })).toContain('CHANGE-001');
|
|
43
49
|
});
|
|
44
|
-
it('flags a
|
|
45
|
-
|
|
46
|
-
delete r.admitted_at;
|
|
47
|
-
expect(codes(r)).toContain('CHANGE-001');
|
|
50
|
+
it('flags a non-array goals', () => {
|
|
51
|
+
expect(codes({ ...valid(), goals: 'GOAL-EU-1' })).toContain('CHANGE-001');
|
|
48
52
|
});
|
|
49
|
-
it('
|
|
50
|
-
|
|
51
|
-
delete r.valid_to;
|
|
52
|
-
expect(codes(r)).toContain('CHANGE-001');
|
|
53
|
+
it('rejects a non-object', () => {
|
|
54
|
+
expect(codes(null)).toEqual(['CHANGE-001']);
|
|
53
55
|
});
|
|
54
56
|
});
|
|
55
|
-
describe('validateChange — CHANGE-002 (goals
|
|
56
|
-
it('flags a
|
|
57
|
-
expect(codes({ ...valid(), goals: ['CHANGE-EU-CRM-1'] })).toContain('CHANGE-002');
|
|
58
|
-
});
|
|
59
|
-
it('flags a malformed entry in goals', () => {
|
|
57
|
+
describe('validateChange — CHANGE-002 (goals references)', () => {
|
|
58
|
+
it('flags a malformed goals entry', () => {
|
|
60
59
|
expect(codes({ ...valid(), goals: ['not an id'] })).toContain('CHANGE-002');
|
|
61
60
|
});
|
|
62
|
-
it('flags goals
|
|
63
|
-
expect(codes({ ...valid(), goals: '
|
|
61
|
+
it('flags a goals entry of the wrong TYPE', () => {
|
|
62
|
+
expect(codes({ ...valid(), goals: ['FACTOR-X-1'] })).toContain('CHANGE-002');
|
|
63
|
+
});
|
|
64
|
+
it('flags an unresolved id when a catalog is supplied', () => {
|
|
65
|
+
const catalog = { typeOf: () => undefined };
|
|
66
|
+
expect(codes(valid(), { catalog })).toContain('CHANGE-002');
|
|
64
67
|
});
|
|
65
68
|
});
|
|
66
|
-
describe('validateChange — CHANGE-003 (parent
|
|
67
|
-
it('flags a
|
|
68
|
-
expect(codes({ ...valid(), parent: 'GOAL-
|
|
69
|
+
describe('validateChange — CHANGE-003 (parent)', () => {
|
|
70
|
+
it('flags a parent of the wrong TYPE', () => {
|
|
71
|
+
expect(codes({ ...valid(), parent: 'GOAL-X-1' })).toContain('CHANGE-003');
|
|
69
72
|
});
|
|
70
|
-
it('flags self
|
|
73
|
+
it('flags a parent equal to self', () => {
|
|
71
74
|
expect(codes({ ...valid(), parent: 'CHANGE-EU-CRM-1' })).toContain('CHANGE-003');
|
|
72
75
|
});
|
|
76
|
+
it('flags a malformed parent', () => {
|
|
77
|
+
expect(codes({ ...valid(), parent: 'not an id' })).toContain('CHANGE-003');
|
|
78
|
+
});
|
|
73
79
|
});
|
package/dist/change/index.d.ts
CHANGED
package/dist/change/types.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export interface Change {
|
|
|
5
5
|
name: string;
|
|
6
6
|
/** GOAL-… IDs this change delivers. */
|
|
7
7
|
goals?: string[];
|
|
8
|
-
/**
|
|
8
|
+
/** Higher-scale CHANGE this one decomposes from (recursive, §6.1). */
|
|
9
9
|
parent?: string;
|
|
10
10
|
description?: string;
|
|
11
11
|
zone: 'canon';
|
package/dist/change/types.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
// CHANGE — implementation-layer
|
|
2
|
-
// Schema: methodology notations/ELEMENT_PRIMITIVES.md §7.3
|
|
1
|
+
// CHANGE — implementation-layer Gap (ArchiMate). The BDN change layer.
|
|
2
|
+
// Schema: methodology notations/ELEMENT_PRIMITIVES.md §7.3.
|
|
3
3
|
export {};
|
|
@@ -1,2 +1,7 @@
|
|
|
1
1
|
import type { ValidationResult } from '../validation-types.js';
|
|
2
|
-
|
|
2
|
+
import { type CanonCatalog } from '../typed-id.js';
|
|
3
|
+
export interface ChangeValidateOptions {
|
|
4
|
+
/** When provided, enforces that each `goals` / `parent` id is admitted. */
|
|
5
|
+
catalog?: CanonCatalog;
|
|
6
|
+
}
|
|
7
|
+
export declare function validateChange(input: unknown, options?: ChangeValidateOptions): ValidationResult;
|
package/dist/change/validate.js
CHANGED
|
@@ -1,56 +1,83 @@
|
|
|
1
1
|
// CHANGE validator — methodology notations/ELEMENT_PRIMITIVES.md §7.3.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
// Codes:
|
|
4
|
+
// CHANGE-001 — shape / id grammar / required envelope fields.
|
|
5
|
+
// CHANGE-002 — `goals` entry malformed or wrong TYPE (must be GOAL-…).
|
|
6
|
+
// CHANGE-003 — `parent` malformed, wrong TYPE (must be CHANGE-…), or equals self.
|
|
7
|
+
import { typeOfId, isCanonicalIdOfType } from '../typed-id.js';
|
|
8
|
+
const REQUIRED_STRING_FIELDS = [
|
|
9
|
+
'name', 'admitted_at', 'admitted_by', 'valid_from',
|
|
10
|
+
];
|
|
11
|
+
export function validateChange(input, options = {}) {
|
|
11
12
|
const errors = [];
|
|
12
13
|
const warnings = [];
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
idType: 'CHANGE',
|
|
16
|
-
code: 'CHANGE-001',
|
|
17
|
-
});
|
|
18
|
-
errors.push(...envErrors);
|
|
19
|
-
if (!record)
|
|
14
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
|
15
|
+
errors.push({ code: 'CHANGE-001', message: 'Change must be a YAML mapping.' });
|
|
20
16
|
return { valid: false, errors, warnings };
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
17
|
+
}
|
|
18
|
+
const c = input;
|
|
19
|
+
// CHANGE-001 — id grammar + notation tag + envelope.
|
|
20
|
+
if (!isCanonicalIdOfType(c.id, 'CHANGE')) {
|
|
21
|
+
errors.push({ code: 'CHANGE-001', message: `id "${String(c.id)}" must match CHANGE-[<middle>-]<INTEGER>.`, path: 'id' });
|
|
22
|
+
}
|
|
23
|
+
if (c.notation !== 'change') {
|
|
24
|
+
errors.push({ code: 'CHANGE-001', message: 'notation must be the fixed value "change".', path: 'notation' });
|
|
25
|
+
}
|
|
26
|
+
if (c.zone !== undefined && c.zone !== 'canon') {
|
|
27
|
+
errors.push({ code: 'CHANGE-001', message: 'zone must be "canon" for a CHANGE.', path: 'zone' });
|
|
28
|
+
}
|
|
29
|
+
else if (c.zone === undefined) {
|
|
30
|
+
errors.push({ code: 'CHANGE-001', message: 'zone is required.', path: 'zone' });
|
|
31
|
+
}
|
|
32
|
+
for (const field of REQUIRED_STRING_FIELDS) {
|
|
33
|
+
const v = c[field];
|
|
34
|
+
if (typeof v !== 'string' || v.trim() === '') {
|
|
35
|
+
errors.push({ code: 'CHANGE-001', message: `${field} is required.`, path: field });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (c.gate_checks === null || typeof c.gate_checks !== 'object') {
|
|
39
|
+
errors.push({ code: 'CHANGE-001', message: 'gate_checks is required and must be a mapping.', path: 'gate_checks' });
|
|
40
|
+
}
|
|
41
|
+
if (!('valid_to' in c) || !(typeof c.valid_to === 'string' || c.valid_to === null)) {
|
|
42
|
+
errors.push({ code: 'CHANGE-001', message: 'valid_to is required (an ISO date string or null).', path: 'valid_to' });
|
|
43
|
+
}
|
|
44
|
+
// CHANGE-002 — `goals` references.
|
|
45
|
+
if (c.goals !== undefined) {
|
|
46
|
+
if (!Array.isArray(c.goals)) {
|
|
47
|
+
errors.push({ code: 'CHANGE-001', message: 'goals must be a list of typed IDs.', path: 'goals' });
|
|
25
48
|
}
|
|
26
49
|
else {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
50
|
+
c.goals.forEach((ref, i) => {
|
|
51
|
+
const type = typeOfId(ref);
|
|
52
|
+
if (!type) {
|
|
53
|
+
errors.push({ code: 'CHANGE-002', message: `goals[${i}] "${String(ref)}" is not a resolvable typed ID.`, path: `goals[${i}]` });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (type !== 'GOAL') {
|
|
57
|
+
errors.push({ code: 'CHANGE-002', message: `goals[${i}] TYPE "${type}" must be GOAL.`, path: `goals[${i}]` });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (options.catalog && options.catalog.typeOf(ref) === undefined) {
|
|
61
|
+
errors.push({ code: 'CHANGE-002', message: `goals[${i}] "${String(ref)}" does not resolve to an admitted artefact.`, path: `goals[${i}]` });
|
|
34
62
|
}
|
|
35
63
|
});
|
|
36
64
|
}
|
|
37
65
|
}
|
|
38
|
-
// CHANGE-003 — parent
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
path: 'parent',
|
|
45
|
-
});
|
|
66
|
+
// CHANGE-003 — `parent` (CHANGE-…; not self).
|
|
67
|
+
if (c.parent !== undefined) {
|
|
68
|
+
const p = c.parent;
|
|
69
|
+
const type = typeOfId(p);
|
|
70
|
+
if (typeof p !== 'string' || !type) {
|
|
71
|
+
errors.push({ code: 'CHANGE-003', message: `parent "${String(p)}" is not a resolvable typed ID.`, path: 'parent' });
|
|
46
72
|
}
|
|
47
|
-
else if (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
73
|
+
else if (type !== 'CHANGE') {
|
|
74
|
+
errors.push({ code: 'CHANGE-003', message: `parent TYPE "${type}" must be CHANGE.`, path: 'parent' });
|
|
75
|
+
}
|
|
76
|
+
else if (typeof c.id === 'string' && p === c.id) {
|
|
77
|
+
errors.push({ code: 'CHANGE-003', message: `parent "${p}" must not equal the element's own id.`, path: 'parent' });
|
|
78
|
+
}
|
|
79
|
+
else if (options.catalog && options.catalog.typeOf(p) === undefined) {
|
|
80
|
+
errors.push({ code: 'CHANGE-003', message: `parent "${p}" does not resolve to an admitted CHANGE.`, path: 'parent' });
|
|
54
81
|
}
|
|
55
82
|
}
|
|
56
83
|
return { valid: errors.length === 0, errors, warnings };
|
|
@@ -11,6 +11,39 @@ describe('ingestComplianceDoc', () => {
|
|
|
11
11
|
expect(canon.requirements[0]).toMatchObject({ id: 'REQUIREMENT-1', severity: 'high', derived_from: ['LAW-1'] });
|
|
12
12
|
expect(canon.assertions[0]).toMatchObject({ id: 'ASSERTION-1', status: 'compliant', evidenceCount: 1 });
|
|
13
13
|
expect(canon.codex[0]).toMatchObject({ id: 'LAW-1', type: 'LAW', jurisdiction: 'ge' });
|
|
14
|
+
expect(canon.subjects).toEqual([]);
|
|
15
|
+
});
|
|
16
|
+
it('ingests capability, process, application, system into canon.subjects', () => {
|
|
17
|
+
const canon = emptyCanon();
|
|
18
|
+
expect(ingestComplianceDoc(canon, { notation: 'capability', id: 'CAPABILITY-V2', name: 'CRM' })).toBe('CAPABILITY-V2');
|
|
19
|
+
expect(ingestComplianceDoc(canon, { notation: 'process', id: 'PROCESS-CS-1', name: 'Support' })).toBe('PROCESS-CS-1');
|
|
20
|
+
expect(ingestComplianceDoc(canon, { notation: 'application', id: 'APP-ERP-1', name: 'ERP System' })).toBe('APP-ERP-1');
|
|
21
|
+
expect(ingestComplianceDoc(canon, { notation: 'system', id: 'SYSTEM-IAM-1', name: 'IAM' })).toBe('SYSTEM-IAM-1');
|
|
22
|
+
expect(canon.subjects).toEqual([
|
|
23
|
+
{ id: 'CAPABILITY-V2', name: 'CRM' },
|
|
24
|
+
{ id: 'PROCESS-CS-1', name: 'Support' },
|
|
25
|
+
{ id: 'APP-ERP-1', name: 'ERP System' },
|
|
26
|
+
{ id: 'SYSTEM-IAM-1', name: 'IAM' },
|
|
27
|
+
]);
|
|
28
|
+
expect(canon.products).toEqual([]);
|
|
29
|
+
});
|
|
30
|
+
it('falls back to id when name is absent for subjects', () => {
|
|
31
|
+
const canon = emptyCanon();
|
|
32
|
+
ingestComplianceDoc(canon, { notation: 'capability', id: 'CAPABILITY-X-1' });
|
|
33
|
+
expect(canon.subjects[0]).toEqual({ id: 'CAPABILITY-X-1', name: 'CAPABILITY-X-1' });
|
|
34
|
+
});
|
|
35
|
+
it('captures owner_to_confirm on pending_owner assertions', () => {
|
|
36
|
+
const canon = emptyCanon();
|
|
37
|
+
ingestComplianceDoc(canon, {
|
|
38
|
+
notation: 'assertion', id: 'ASSERTION-PO-1',
|
|
39
|
+
about: 'REQUIREMENT-1', subject: 'PRODUCT-1',
|
|
40
|
+
status: 'pending_owner', owner_to_confirm: 'alice@example.com',
|
|
41
|
+
});
|
|
42
|
+
expect(canon.assertions[0]).toMatchObject({
|
|
43
|
+
id: 'ASSERTION-PO-1',
|
|
44
|
+
status: 'pending_owner',
|
|
45
|
+
owner_to_confirm: 'alice@example.com',
|
|
46
|
+
});
|
|
14
47
|
});
|
|
15
48
|
it('returns null for non-artefacts, missing id, and malformed assertions', () => {
|
|
16
49
|
const canon = emptyCanon();
|
|
@@ -20,5 +53,6 @@ describe('ingestComplianceDoc', () => {
|
|
|
20
53
|
expect(ingestComplianceDoc(canon, null)).toBeNull();
|
|
21
54
|
expect(ingestComplianceDoc(canon, 'str')).toBeNull();
|
|
22
55
|
expect(canon.products).toEqual([]);
|
|
56
|
+
expect(canon.subjects).toEqual([]);
|
|
23
57
|
});
|
|
24
58
|
});
|
|
@@ -8,17 +8,14 @@ describe('parseCoverageMetricConfig', () => {
|
|
|
8
8
|
expect(r.ok).toBe(false);
|
|
9
9
|
});
|
|
10
10
|
it('rejects missing id', () => {
|
|
11
|
-
const r = parseCoverageMetricConfig({
|
|
11
|
+
const r = parseCoverageMetricConfig({ view: { name: 'x' } });
|
|
12
12
|
expect(r.ok).toBe(false);
|
|
13
13
|
if (!r.ok)
|
|
14
14
|
expect(r.errors.some(e => e.includes('id'))).toBe(true);
|
|
15
15
|
});
|
|
16
|
-
it('parses minimal config', () => {
|
|
16
|
+
it('parses minimal config via view: wrapper', () => {
|
|
17
17
|
const r = parseCoverageMetricConfig({
|
|
18
|
-
|
|
19
|
-
id: 'CM-1',
|
|
20
|
-
name: 'Test',
|
|
21
|
-
},
|
|
18
|
+
view: { id: 'CM-1', name: 'Test' },
|
|
22
19
|
});
|
|
23
20
|
expect(r.ok).toBe(true);
|
|
24
21
|
if (r.ok) {
|
|
@@ -26,47 +23,105 @@ describe('parseCoverageMetricConfig', () => {
|
|
|
26
23
|
expect(r.config.name).toBe('Test');
|
|
27
24
|
expect(r.config.thresholds.green).toBe(0.8);
|
|
28
25
|
expect(r.config.thresholds.amber).toBe(0.5);
|
|
26
|
+
expect(r.config.regimes).toBeUndefined();
|
|
29
27
|
}
|
|
30
28
|
});
|
|
31
|
-
it('parses
|
|
29
|
+
it('parses view: wrapper with regimes.include and subjects', () => {
|
|
32
30
|
const r = parseCoverageMetricConfig({
|
|
33
|
-
|
|
31
|
+
view: {
|
|
34
32
|
id: 'CM-2',
|
|
35
33
|
name: 'EU Coverage',
|
|
36
34
|
description: 'desc',
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
codex: ['LAW-GDPR-1'],
|
|
40
|
-
subjects: { products: ['PRODUCT-1'] },
|
|
41
|
-
},
|
|
35
|
+
regimes: { include: ['LAW-GDPR-1'] },
|
|
36
|
+
subjects: { products: ['PRODUCT-1'] },
|
|
42
37
|
thresholds: { green: 0.9, amber: 0.6 },
|
|
43
38
|
},
|
|
44
39
|
});
|
|
45
40
|
expect(r.ok).toBe(true);
|
|
46
41
|
if (r.ok) {
|
|
47
|
-
expect(r.config.
|
|
48
|
-
expect(r.config.
|
|
42
|
+
expect(r.config.regimes?.include).toEqual(['LAW-GDPR-1']);
|
|
43
|
+
expect(r.config.subjects?.products).toEqual(['PRODUCT-1']);
|
|
49
44
|
expect(r.config.thresholds.green).toBe(0.9);
|
|
50
45
|
expect(r.config.thresholds.amber).toBe(0.6);
|
|
46
|
+
expect(r.config.warnings).toBeUndefined();
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
it('parses view: wrapper with regimes.filter', () => {
|
|
50
|
+
const r = parseCoverageMetricConfig({
|
|
51
|
+
view: {
|
|
52
|
+
id: 'CM-3',
|
|
53
|
+
name: 'EU Filter',
|
|
54
|
+
regimes: { filter: { jurisdiction: ['EU'], codex_type: ['regulation'] } },
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
expect(r.ok).toBe(true);
|
|
58
|
+
if (r.ok) {
|
|
59
|
+
expect(r.config.regimes?.filter?.jurisdiction).toEqual(['EU']);
|
|
60
|
+
expect(r.config.regimes?.filter?.codex_type).toEqual(['regulation']);
|
|
61
|
+
expect(r.config.regimes?.include).toBeUndefined();
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
it('warns COVMET-007 when both regimes.include and regimes.filter are set', () => {
|
|
65
|
+
const r = parseCoverageMetricConfig({
|
|
66
|
+
view: {
|
|
67
|
+
id: 'CM-4',
|
|
68
|
+
name: 'Both',
|
|
69
|
+
regimes: {
|
|
70
|
+
include: ['LAW-GDPR-1'],
|
|
71
|
+
filter: { jurisdiction: ['EU'] },
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
expect(r.ok).toBe(true);
|
|
76
|
+
if (r.ok) {
|
|
77
|
+
expect(r.config.warnings?.some(w => w.includes('COVMET-007'))).toBe(true);
|
|
51
78
|
}
|
|
52
79
|
});
|
|
53
|
-
it('accepts bare (unwrapped) object', () => {
|
|
54
|
-
const r = parseCoverageMetricConfig({ id: 'CM-
|
|
80
|
+
it('accepts bare (unwrapped) object without wrapper warning', () => {
|
|
81
|
+
const r = parseCoverageMetricConfig({ id: 'CM-5', name: 'Bare' });
|
|
55
82
|
expect(r.ok).toBe(true);
|
|
83
|
+
if (r.ok) {
|
|
84
|
+
expect(r.config.warnings?.some(w => w.includes('COVMET-DEPRECATED'))).toBeFalsy();
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
// ── Backward compatibility: deprecated coverage_metric: wrapper ──────────────
|
|
88
|
+
it('accepts deprecated coverage_metric: wrapper with COVMET-DEPRECATED warning', () => {
|
|
89
|
+
const r = parseCoverageMetricConfig({
|
|
90
|
+
coverage_metric: { id: 'CM-6', name: 'Legacy' },
|
|
91
|
+
});
|
|
92
|
+
expect(r.ok).toBe(true);
|
|
93
|
+
if (r.ok) {
|
|
94
|
+
expect(r.config.warnings?.some(w => w.includes('COVMET-DEPRECATED'))).toBe(true);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
it('maps deprecated scope.codex to regimes.include', () => {
|
|
98
|
+
const r = parseCoverageMetricConfig({
|
|
99
|
+
coverage_metric: {
|
|
100
|
+
id: 'CM-7',
|
|
101
|
+
name: 'Legacy Scope',
|
|
102
|
+
scope: {
|
|
103
|
+
codex: ['LAW-GDPR-1', 'LAW-NIS2-1'],
|
|
104
|
+
subjects: { products: ['PRODUCT-1'] },
|
|
105
|
+
},
|
|
106
|
+
thresholds: { green: 0.9, amber: 0.6 },
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
expect(r.ok).toBe(true);
|
|
110
|
+
if (r.ok) {
|
|
111
|
+
expect(r.config.regimes?.include).toEqual(['LAW-GDPR-1', 'LAW-NIS2-1']);
|
|
112
|
+
expect(r.config.subjects?.products).toEqual(['PRODUCT-1']);
|
|
113
|
+
expect(r.config.thresholds.green).toBe(0.9);
|
|
114
|
+
}
|
|
56
115
|
});
|
|
57
116
|
});
|
|
58
117
|
// ── buildCoverageMatrix ───────────────────────────────────────────────────────
|
|
59
118
|
function makeCanon() {
|
|
60
119
|
const canon = emptyCanon();
|
|
61
|
-
// Codex: GDPR law
|
|
62
120
|
ingestComplianceDoc(canon, { id: 'LAW-GDPR-1', name: 'GDPR', zone: 'codex', jurisdiction: 'EU' });
|
|
63
|
-
// Requirements
|
|
64
121
|
ingestComplianceDoc(canon, { notation: 'requirement', id: 'REQ-1', name: 'Lawful basis', derived_from: ['LAW-GDPR-1'] });
|
|
65
122
|
ingestComplianceDoc(canon, { notation: 'requirement', id: 'REQ-2', name: 'Data minimisation', derived_from: ['LAW-GDPR-1'] });
|
|
66
123
|
ingestComplianceDoc(canon, { notation: 'requirement', id: 'REQ-3', name: 'Breach notification', derived_from: ['LAW-GDPR-1'] });
|
|
67
|
-
// Products
|
|
68
124
|
ingestComplianceDoc(canon, { notation: 'product', id: 'PRODUCT-1', name: 'E-commerce' });
|
|
69
|
-
// Assertions
|
|
70
125
|
ingestComplianceDoc(canon, { notation: 'assertion', id: 'ASSERTION-1', about: 'REQ-1', subject: 'PRODUCT-1', status: 'compliant' });
|
|
71
126
|
ingestComplianceDoc(canon, { notation: 'assertion', id: 'ASSERTION-2', about: 'REQ-2', subject: 'PRODUCT-1', status: 'partial' });
|
|
72
127
|
// REQ-3 has no assertion → gap
|
|
@@ -76,19 +131,15 @@ function makeConfig(overrides) {
|
|
|
76
131
|
return {
|
|
77
132
|
id: 'CM-TEST-1',
|
|
78
133
|
name: 'Test Coverage',
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
subjects: { products: ['PRODUCT-1'] },
|
|
82
|
-
},
|
|
134
|
+
regimes: { include: ['LAW-GDPR-1'] },
|
|
135
|
+
subjects: { products: ['PRODUCT-1'] },
|
|
83
136
|
thresholds: { green: 0.8, amber: 0.5 },
|
|
84
137
|
...overrides,
|
|
85
138
|
};
|
|
86
139
|
}
|
|
87
140
|
describe('buildCoverageMatrix', () => {
|
|
88
141
|
it('returns correct row counts', () => {
|
|
89
|
-
const
|
|
90
|
-
const config = makeConfig();
|
|
91
|
-
const matrix = buildCoverageMatrix(canon, config);
|
|
142
|
+
const matrix = buildCoverageMatrix(makeCanon(), makeConfig());
|
|
92
143
|
expect(matrix.rows).toHaveLength(1);
|
|
93
144
|
const row = matrix.rows[0];
|
|
94
145
|
expect(row.codexId).toBe('LAW-GDPR-1');
|
|
@@ -99,18 +150,14 @@ describe('buildCoverageMatrix', () => {
|
|
|
99
150
|
expect(row.coveredCount).toBe(2);
|
|
100
151
|
});
|
|
101
152
|
it('computes coverage percentage correctly', () => {
|
|
102
|
-
const
|
|
103
|
-
const row = matrix.rows[0];
|
|
104
|
-
// 2 covered out of 3 → ~66.7%
|
|
153
|
+
const row = buildCoverageMatrix(makeCanon(), makeConfig()).rows[0];
|
|
105
154
|
expect(row.coveragePct).toBeCloseTo(2 / 3, 5);
|
|
106
155
|
});
|
|
107
156
|
it('assigns amber RAG status for ~67% coverage with green=0.80, amber=0.50', () => {
|
|
108
|
-
|
|
109
|
-
expect(matrix.rows[0].ragStatus).toBe('amber');
|
|
157
|
+
expect(buildCoverageMatrix(makeCanon(), makeConfig()).rows[0].ragStatus).toBe('amber');
|
|
110
158
|
});
|
|
111
159
|
it('assigns green RAG when all requirements covered', () => {
|
|
112
160
|
const canon = makeCanon();
|
|
113
|
-
// Add assertion for REQ-3 so all 3 are covered
|
|
114
161
|
ingestComplianceDoc(canon, { notation: 'assertion', id: 'ASSERTION-3', about: 'REQ-3', subject: 'PRODUCT-1', status: 'compliant' });
|
|
115
162
|
const matrix = buildCoverageMatrix(canon, makeConfig());
|
|
116
163
|
expect(matrix.rows[0].ragStatus).toBe('green');
|
|
@@ -118,38 +165,35 @@ describe('buildCoverageMatrix', () => {
|
|
|
118
165
|
});
|
|
119
166
|
it('assigns red RAG when coverage < amber threshold', () => {
|
|
120
167
|
const matrix = buildCoverageMatrix(makeCanon(), makeConfig({ thresholds: { green: 0.9, amber: 0.8 } }));
|
|
121
|
-
// ~67% < 80% → red
|
|
122
168
|
expect(matrix.rows[0].ragStatus).toBe('red');
|
|
123
169
|
});
|
|
124
170
|
it('returns no_data when codex has no requirements', () => {
|
|
125
171
|
const canon = emptyCanon();
|
|
126
172
|
ingestComplianceDoc(canon, { id: 'LAW-EMPTY-1', name: 'Empty Law', zone: 'codex', jurisdiction: 'XX' });
|
|
127
|
-
const
|
|
128
|
-
const matrix = buildCoverageMatrix(canon, config);
|
|
173
|
+
const matrix = buildCoverageMatrix(canon, makeConfig({ regimes: { include: ['LAW-EMPTY-1'] } }));
|
|
129
174
|
expect(matrix.rows[0].ragStatus).toBe('no_data');
|
|
130
175
|
expect(matrix.rows[0].totalRequirements).toBe(0);
|
|
131
176
|
});
|
|
132
177
|
it('includes jurisdiction from scanned canon', () => {
|
|
133
|
-
|
|
134
|
-
expect(matrix.rows[0].jurisdiction).toBe('EU');
|
|
178
|
+
expect(buildCoverageMatrix(makeCanon(), makeConfig()).rows[0].jurisdiction).toBe('EU');
|
|
135
179
|
});
|
|
136
|
-
it('handles multiple codex entries', () => {
|
|
180
|
+
it('handles multiple codex entries via regimes.include', () => {
|
|
137
181
|
const canon = makeCanon();
|
|
138
182
|
ingestComplianceDoc(canon, { id: 'LAW-NIS2-1', name: 'NIS2', zone: 'codex', jurisdiction: 'EU' });
|
|
139
183
|
ingestComplianceDoc(canon, { notation: 'requirement', id: 'REQ-NIS-1', name: 'Incident reporting', derived_from: ['LAW-NIS2-1'] });
|
|
140
|
-
const
|
|
141
|
-
const matrix = buildCoverageMatrix(canon, config);
|
|
184
|
+
const matrix = buildCoverageMatrix(canon, makeConfig({ regimes: { include: ['LAW-GDPR-1', 'LAW-NIS2-1'] } }));
|
|
142
185
|
expect(matrix.rows).toHaveLength(2);
|
|
143
186
|
expect(matrix.rows[1].codexId).toBe('LAW-NIS2-1');
|
|
144
187
|
expect(matrix.rows[1].gap).toBe(1);
|
|
145
188
|
});
|
|
146
189
|
it('respects product scope — ignores assertions for other products', () => {
|
|
147
190
|
const canon = makeCanon();
|
|
148
|
-
// Add another product with compliant assertion for REQ-3
|
|
149
191
|
ingestComplianceDoc(canon, { notation: 'product', id: 'PRODUCT-2', name: 'Support' });
|
|
150
192
|
ingestComplianceDoc(canon, { notation: 'assertion', id: 'ASSERTION-OUT', about: 'REQ-3', subject: 'PRODUCT-2', status: 'compliant' });
|
|
151
|
-
|
|
152
|
-
|
|
193
|
+
const matrix = buildCoverageMatrix(canon, makeConfig({
|
|
194
|
+
regimes: { include: ['LAW-GDPR-1'] },
|
|
195
|
+
subjects: { products: ['PRODUCT-1'] },
|
|
196
|
+
}));
|
|
153
197
|
expect(matrix.rows[0].gap).toBe(1);
|
|
154
198
|
});
|
|
155
199
|
it('counts non_compliant separately', () => {
|
|
@@ -158,7 +202,63 @@ describe('buildCoverageMatrix', () => {
|
|
|
158
202
|
const matrix = buildCoverageMatrix(canon, makeConfig());
|
|
159
203
|
expect(matrix.rows[0].non_compliant).toBe(1);
|
|
160
204
|
expect(matrix.rows[0].gap).toBe(0);
|
|
161
|
-
// non_compliant does not count as covered
|
|
162
205
|
expect(matrix.rows[0].coveredCount).toBe(2);
|
|
163
206
|
});
|
|
207
|
+
it('counts pending_owner separately and does not include it in coveredCount', () => {
|
|
208
|
+
const canon = makeCanon();
|
|
209
|
+
ingestComplianceDoc(canon, { notation: 'assertion', id: 'ASSERTION-PO', about: 'REQ-3', subject: 'PRODUCT-1', status: 'pending_owner', owner_to_confirm: 'alice' });
|
|
210
|
+
const matrix = buildCoverageMatrix(canon, makeConfig());
|
|
211
|
+
expect(matrix.rows[0].pending_owner).toBe(1);
|
|
212
|
+
expect(matrix.rows[0].gap).toBe(0);
|
|
213
|
+
expect(matrix.rows[0].coveredCount).toBe(2);
|
|
214
|
+
});
|
|
215
|
+
// ── regimes.filter ───────────────────────────────────────────────────────────
|
|
216
|
+
it('resolves codex via regimes.filter.jurisdiction', () => {
|
|
217
|
+
const canon = makeCanon();
|
|
218
|
+
ingestComplianceDoc(canon, { id: 'LAW-CCPA-1', name: 'CCPA', zone: 'codex', jurisdiction: 'US' });
|
|
219
|
+
// regimes.filter: only EU laws → should return LAW-GDPR-1 only
|
|
220
|
+
const matrix = buildCoverageMatrix(canon, makeConfig({
|
|
221
|
+
regimes: { filter: { jurisdiction: ['EU'] } },
|
|
222
|
+
}));
|
|
223
|
+
expect(matrix.rows.every(r => r.codexId !== 'LAW-CCPA-1')).toBe(true);
|
|
224
|
+
expect(matrix.rows.some(r => r.codexId === 'LAW-GDPR-1')).toBe(true);
|
|
225
|
+
});
|
|
226
|
+
it('resolves codex via regimes.filter.codex_type', () => {
|
|
227
|
+
const canon = emptyCanon();
|
|
228
|
+
ingestComplianceDoc(canon, { id: 'LAW-A', name: 'Regulation A', zone: 'codex', type: 'regulation' });
|
|
229
|
+
ingestComplianceDoc(canon, { id: 'LAW-B', name: 'Standard B', zone: 'codex', type: 'standard' });
|
|
230
|
+
const matrix = buildCoverageMatrix(canon, makeConfig({
|
|
231
|
+
regimes: { filter: { codex_type: ['regulation'] } },
|
|
232
|
+
}));
|
|
233
|
+
expect(matrix.rows.map(r => r.codexId)).toEqual(['LAW-A']);
|
|
234
|
+
});
|
|
235
|
+
// ── Zero-config: all canon codex entries ─────────────────────────────────────
|
|
236
|
+
it('zero-config (no regimes) enumerates all canon codex', () => {
|
|
237
|
+
const canon = makeCanon();
|
|
238
|
+
ingestComplianceDoc(canon, { id: 'LAW-NIS2-1', name: 'NIS2', zone: 'codex', jurisdiction: 'EU' });
|
|
239
|
+
const matrix = buildCoverageMatrix(canon, makeConfig({ regimes: undefined }));
|
|
240
|
+
expect(matrix.rows.map(r => r.codexId)).toContain('LAW-GDPR-1');
|
|
241
|
+
expect(matrix.rows.map(r => r.codexId)).toContain('LAW-NIS2-1');
|
|
242
|
+
});
|
|
243
|
+
// ── Backward compat: deprecated config still produces correct matrix ─────────
|
|
244
|
+
it('deprecated scope.codex via parseCoverageMetricConfig maps to correct matrix', () => {
|
|
245
|
+
const r = parseCoverageMetricConfig({
|
|
246
|
+
coverage_metric: {
|
|
247
|
+
id: 'CM-LEGACY',
|
|
248
|
+
name: 'Legacy',
|
|
249
|
+
scope: {
|
|
250
|
+
codex: ['LAW-GDPR-1'],
|
|
251
|
+
subjects: { products: ['PRODUCT-1'] },
|
|
252
|
+
},
|
|
253
|
+
thresholds: { green: 0.8, amber: 0.5 },
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
expect(r.ok).toBe(true);
|
|
257
|
+
if (r.ok) {
|
|
258
|
+
const matrix = buildCoverageMatrix(makeCanon(), r.config);
|
|
259
|
+
expect(matrix.rows).toHaveLength(1);
|
|
260
|
+
expect(matrix.rows[0].codexId).toBe('LAW-GDPR-1');
|
|
261
|
+
expect(matrix.rows[0].coveredCount).toBe(2);
|
|
262
|
+
}
|
|
263
|
+
});
|
|
164
264
|
});
|
|
@@ -104,7 +104,7 @@ describe('renderImpactMatrixHtml (CV-6)', () => {
|
|
|
104
104
|
ingestComplianceDoc(canon, { notation: 'product', id: 'PROD-1', name: 'Product One' });
|
|
105
105
|
ingestComplianceDoc(canon, { notation: 'requirement', id: 'REQ-1', name: 'Req One', severity: 'high' });
|
|
106
106
|
ingestComplianceDoc(canon, { notation: 'assertion', id: 'ASS-1', about: 'REQ-1', subject: 'PROD-1', status: 'compliant' });
|
|
107
|
-
const matrix = buildImpactMatrix({ products: canon.products, requirements: canon.requirements, assertions: canon.assertions, codex: canon.codex }, { id: 'VIEW-1', name: 'My View', subjects: { products: ['PROD-1'] }, obligations: {} });
|
|
107
|
+
const matrix = buildImpactMatrix({ products: canon.products, requirements: canon.requirements, assertions: canon.assertions, codex: canon.codex, subjects: canon.subjects }, { id: 'VIEW-1', name: 'My View', subjects: { products: ['PROD-1'] }, obligations: {} });
|
|
108
108
|
const html = renderImpactMatrixHtml(matrix, { today: '2026-06-09' });
|
|
109
109
|
expect(html).toContain('<table>');
|
|
110
110
|
expect(html).toContain('My View');
|
|
@@ -115,7 +115,7 @@ describe('renderImpactMatrixHtml (CV-6)', () => {
|
|
|
115
115
|
const canon = emptyCanon();
|
|
116
116
|
ingestComplianceDoc(canon, { notation: 'product', id: 'PROD-1', name: 'P1' });
|
|
117
117
|
ingestComplianceDoc(canon, { notation: 'requirement', id: 'REQ-1', name: 'R1' });
|
|
118
|
-
const matrix = buildImpactMatrix({ products: canon.products, requirements: canon.requirements, assertions: canon.assertions, codex: canon.codex }, { id: 'V2', name: 'V2', subjects: { products: ['PROD-1'] }, obligations: {} });
|
|
118
|
+
const matrix = buildImpactMatrix({ products: canon.products, requirements: canon.requirements, assertions: canon.assertions, codex: canon.codex, subjects: canon.subjects }, { id: 'V2', name: 'V2', subjects: { products: ['PROD-1'] }, obligations: {} });
|
|
119
119
|
const html = renderImpactMatrixHtml(matrix);
|
|
120
120
|
expect(html).toContain('No mapped obligation');
|
|
121
121
|
});
|