@transitrix/diagrams 1.0.0 → 1.1.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/LICENSE +21 -21
- package/README.md +47 -47
- package/dist/__tests__/edge-path.test.js +8 -0
- package/dist/activities/cpm.js +11 -2
- package/dist/activities/gantt.js +6 -3
- package/dist/activities/types.d.ts +1 -0
- package/dist/activities/validate.js +3 -3
- 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 +161 -7
- 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 +115 -28
- 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 +509 -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 +78 -81
- package/dist/compliance/impact.d.ts +100 -16
- package/dist/compliance/impact.js +272 -28
- 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 +75 -46
- package/dist/factor/index.d.ts +3 -2
- package/dist/factor/index.js +1 -1
- package/dist/factor/types.d.ts +5 -8
- 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__/chain-table.test.js +2 -2
- package/dist/fgca/__tests__/layout.test.js +11 -11
- package/dist/fgca/__tests__/preview-layout.test.js +7 -7
- package/dist/fgca/__tests__/resolver.test.js +185 -0
- package/dist/fgca/chain-table.d.ts +1 -1
- package/dist/fgca/chain-table.js +4 -4
- package/dist/fgca/index.d.ts +4 -2
- package/dist/fgca/index.js +2 -1
- package/dist/fgca/layout.d.ts +2 -2
- package/dist/fgca/layout.js +9 -9
- package/dist/fgca/nodeTypes.d.ts +1 -1
- package/dist/fgca/nodeTypes.js +2 -2
- package/dist/fgca/nodes/FGCAFactorNode.d.ts +3 -3
- package/dist/fgca/nodes/FGCAFactorNode.js +5 -5
- package/dist/fgca/parse-canonical.js +15 -10
- package/dist/fgca/preview-layout.d.ts +1 -1
- package/dist/fgca/preview-layout.js +4 -4
- package/dist/fgca/resolver.d.ts +49 -0
- package/dist/fgca/resolver.js +126 -0
- package/dist/fgca/types.d.ts +11 -11
- package/dist/fgca/types.js +2 -2
- package/dist/fgca/validate.d.ts +2 -2
- package/dist/fgca/validate.js +17 -14
- 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/index.d.ts +1 -1
- package/dist/process-blueprint/layout.js +66 -35
- package/dist/process-blueprint/types.d.ts +36 -1
- package/dist/scenarios/index.d.ts +1 -1
- package/dist/scenarios/types.d.ts +4 -4
- 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 +71 -69
- package/dist/theme/tokens.d.ts +7 -1
- package/dist/theme/tokens.js +3 -1
- package/dist/webview/__tests__/entry-notations.test.js +2 -0
- package/dist/webview/__tests__/entry.test.js +11 -11
- package/dist/webview/__tests__/render-process.test.js +463 -0
- package/dist/webview/__tests__/render-util.test.d.ts +1 -0
- package/dist/webview/__tests__/render-util.test.js +54 -0
- package/dist/webview/entry.d.ts +1 -1
- package/dist/webview/entry.js +9 -5
- package/dist/webview/render-activities.d.ts +24 -2
- package/dist/webview/render-activities.js +92 -57
- package/dist/webview/render-activity-card.d.ts +11 -1
- package/dist/webview/render-activity-card.js +115 -79
- package/dist/webview/render-blocks.d.ts +24 -1
- package/dist/webview/render-blocks.js +58 -24
- package/dist/webview/render-fgca.d.ts +31 -0
- package/dist/webview/render-fgca.js +30 -19
- package/dist/webview/render-goals.d.ts +27 -1
- package/dist/webview/render-goals.js +42 -32
- package/dist/webview/render-process-blueprint.d.ts +11 -1
- package/dist/webview/render-process-blueprint.js +118 -53
- package/dist/webview/render-process.d.ts +95 -0
- package/dist/webview/render-process.js +392 -0
- package/dist/webview/render-scenarios.js +2 -2
- package/dist/webview/render-util.d.ts +19 -6
- package/dist/webview/render-util.js +23 -15
- package/package.json +54 -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 +107 -95
- package/src/activities/gantt.ts +277 -276
- package/src/activities/index.ts +28 -28
- package/src/activities/layout.ts +112 -112
- package/src/activities/types.ts +149 -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 +330 -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 +422 -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 +1059 -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 +291 -295
- package/src/compliance/impact.ts +979 -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 +81 -46
- package/src/factor/index.ts +3 -7
- package/src/factor/types.ts +15 -19
- 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 +405 -400
- package/src/fgca/preview-layout.ts +217 -217
- package/src/fgca/resolver.ts +159 -0
- package/src/fgca/types.ts +50 -50
- package/src/fgca/validate.ts +129 -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 +32 -31
- package/src/process-blueprint/layout.ts +564 -531
- package/src/process-blueprint/types.ts +278 -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 +239 -237
- package/src/theme/tokens.ts +135 -133
- package/src/typed-id.ts +56 -56
- package/src/validation-types.ts +36 -36
- package/src/webview/__tests__/entry-notations.test.ts +79 -77
- package/src/webview/__tests__/entry.test.ts +64 -64
- package/src/webview/__tests__/render-goals.test.ts +72 -72
- package/src/webview/__tests__/render-process.test.ts +538 -0
- package/src/webview/__tests__/render-util.test.ts +66 -0
- package/src/webview/entry.ts +298 -292
- package/src/webview/render-activities.ts +111 -69
- package/src/webview/render-activity-card.ts +275 -234
- package/src/webview/render-blocks.ts +78 -28
- package/src/webview/render-fgca.ts +53 -28
- package/src/webview/render-goals.ts +133 -110
- package/src/webview/render-process-blueprint.ts +281 -207
- package/src/webview/render-process.ts +507 -0
- package/src/webview/render-scenarios.ts +3 -3
- package/src/webview/render-util.ts +27 -14
- 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.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
- /package/dist/{issues/__tests__/validate.test.d.ts → webview/__tests__/render-process.test.d.ts} +0 -0
|
@@ -1,320 +1,320 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { layoutProcessBlueprint } from '../layout.js';
|
|
3
|
-
import type {
|
|
4
|
-
ComplianceLaneAssertion,
|
|
5
|
-
ComplianceLaneInput,
|
|
6
|
-
ComplianceLaneRequirement,
|
|
7
|
-
ProcessBlueprintFile,
|
|
8
|
-
} from '../types.js';
|
|
9
|
-
|
|
10
|
-
// ── fixtures ────────────────────────────────────────────────────────────────
|
|
11
|
-
|
|
12
|
-
const STAGES: ProcessBlueprintFile['process_blueprint']['stages'] = [
|
|
13
|
-
{ id: 'STAGE-1', name: 'Receive', goal: 'g1', result: 'r1' },
|
|
14
|
-
{ id: 'STAGE-2', name: 'Process', goal: 'g2', result: 'r2' },
|
|
15
|
-
{ id: 'STAGE-3', name: 'Ship', goal: 'g3', result: 'r3' },
|
|
16
|
-
];
|
|
17
|
-
|
|
18
|
-
function buildFile(overrides: Partial<ProcessBlueprintFile['process_blueprint']> = {}): ProcessBlueprintFile {
|
|
19
|
-
return {
|
|
20
|
-
notation: 'process-blueprint',
|
|
21
|
-
process_blueprint: { id: 'PROCESS_BLUEPRINT-T-1', name: 'Test', stages: STAGES, ...overrides },
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const REQ_A: ComplianceLaneRequirement = { id: 'REQ-1', derived_from: ['LAW-GDPR-1'] };
|
|
26
|
-
const REQ_B: ComplianceLaneRequirement = { id: 'REQ-2', derived_from: ['LAW-GDPR-1', 'LAW-SOX-2'] };
|
|
27
|
-
const REQ_DEADLINE: ComplianceLaneRequirement = {
|
|
28
|
-
id: 'REQ-3',
|
|
29
|
-
derived_from: ['LAW-PCI-3'],
|
|
30
|
-
deadline: '2020-01-01', // past_due
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
// ── helpers ─────────────────────────────────────────────────────────────────
|
|
34
|
-
|
|
35
|
-
function makeAssertion(
|
|
36
|
-
about: string,
|
|
37
|
-
status: ComplianceLaneAssertion['status'],
|
|
38
|
-
realisedVia: string[],
|
|
39
|
-
): ComplianceLaneAssertion {
|
|
40
|
-
return { about, status, realised_via: realisedVia };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function layout(
|
|
44
|
-
file: ProcessBlueprintFile,
|
|
45
|
-
input: ComplianceLaneInput,
|
|
46
|
-
overrides: { jurisdictions?: string[]; previousSnapshot?: Record<string, string[]> } = {},
|
|
47
|
-
) {
|
|
48
|
-
return layoutProcessBlueprint(file, {
|
|
49
|
-
complianceLane: { enabled: true, ...overrides },
|
|
50
|
-
complianceInput: input,
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// ── tests ───────────────────────────────────────────────────────────────────
|
|
55
|
-
|
|
56
|
-
describe('compliance lane derivation', () => {
|
|
57
|
-
it('returns no complianceRow when lane is disabled (default)', () => {
|
|
58
|
-
const l = layoutProcessBlueprint(buildFile(), {});
|
|
59
|
-
expect(l.complianceRow).toBeUndefined();
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('returns no complianceRow when enabled but no input provided', () => {
|
|
63
|
-
const l = layoutProcessBlueprint(buildFile(), { complianceLane: { enabled: true } });
|
|
64
|
-
expect(l.complianceRow).toBeUndefined();
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('returns no complianceRow when assertions have no realised_via matching stage IDs', () => {
|
|
68
|
-
const input: ComplianceLaneInput = {
|
|
69
|
-
assertions: [makeAssertion('REQ-1', 'compliant', ['STEP-99'])],
|
|
70
|
-
requirements: [REQ_A],
|
|
71
|
-
};
|
|
72
|
-
const l = layout(buildFile(), input);
|
|
73
|
-
expect(l.complianceRow).toBeUndefined();
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('derives a chip for each law-stage pair', () => {
|
|
77
|
-
const input: ComplianceLaneInput = {
|
|
78
|
-
assertions: [makeAssertion('REQ-1', 'compliant', ['STAGE-1'])],
|
|
79
|
-
requirements: [REQ_A],
|
|
80
|
-
};
|
|
81
|
-
const l = layout(buildFile(), input);
|
|
82
|
-
expect(l.complianceRow).toBeDefined();
|
|
83
|
-
const chips = l.complianceRow!.chips;
|
|
84
|
-
expect(chips).toHaveLength(1);
|
|
85
|
-
expect(chips[0].lawId).toBe('LAW-GDPR-1');
|
|
86
|
-
expect(chips[0].stageIndex).toBe(0);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('produces one chip per law per stage (same law, two stages → two chips)', () => {
|
|
90
|
-
const input: ComplianceLaneInput = {
|
|
91
|
-
assertions: [makeAssertion('REQ-1', 'compliant', ['STAGE-1', 'STAGE-2'])],
|
|
92
|
-
requirements: [REQ_A],
|
|
93
|
-
};
|
|
94
|
-
const l = layout(buildFile(), input);
|
|
95
|
-
const chips = l.complianceRow!.chips;
|
|
96
|
-
expect(chips).toHaveLength(2);
|
|
97
|
-
expect(chips.map(c => c.stageIndex).sort()).toEqual([0, 1]);
|
|
98
|
-
expect(chips.every(c => c.lawId === 'LAW-GDPR-1')).toBe(true);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('merges multiple assertions for the same law-stage into one chip', () => {
|
|
102
|
-
const input: ComplianceLaneInput = {
|
|
103
|
-
assertions: [
|
|
104
|
-
makeAssertion('REQ-1', 'compliant', ['STAGE-1']),
|
|
105
|
-
makeAssertion('REQ-1', 'partial', ['STAGE-1']),
|
|
106
|
-
],
|
|
107
|
-
requirements: [REQ_A],
|
|
108
|
-
};
|
|
109
|
-
const l = layout(buildFile(), input);
|
|
110
|
-
expect(l.complianceRow!.chips).toHaveLength(1);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
describe('decoration: new', () => {
|
|
114
|
-
it('marks chip as new when law is not in previous snapshot', () => {
|
|
115
|
-
const input: ComplianceLaneInput = {
|
|
116
|
-
assertions: [makeAssertion('REQ-1', 'compliant', ['STAGE-1'])],
|
|
117
|
-
requirements: [REQ_A],
|
|
118
|
-
};
|
|
119
|
-
const l = layout(buildFile(), input, { previousSnapshot: { 'STAGE-1': [] } });
|
|
120
|
-
expect(l.complianceRow!.chips[0].decorations).toContain('new');
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('does not mark chip as new when law is in previous snapshot', () => {
|
|
124
|
-
const input: ComplianceLaneInput = {
|
|
125
|
-
assertions: [makeAssertion('REQ-1', 'compliant', ['STAGE-1'])],
|
|
126
|
-
requirements: [REQ_A],
|
|
127
|
-
};
|
|
128
|
-
const l = layout(buildFile(), input, { previousSnapshot: { 'STAGE-1': ['LAW-GDPR-1'] } });
|
|
129
|
-
expect(l.complianceRow!.chips[0].decorations).not.toContain('new');
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
describe('decoration: gap', () => {
|
|
134
|
-
it('marks chip as gap when assertion status is non_compliant', () => {
|
|
135
|
-
const input: ComplianceLaneInput = {
|
|
136
|
-
assertions: [makeAssertion('REQ-1', 'non_compliant', ['STAGE-1'])],
|
|
137
|
-
requirements: [REQ_A],
|
|
138
|
-
};
|
|
139
|
-
const l = layout(buildFile(), input);
|
|
140
|
-
expect(l.complianceRow!.chips[0].decorations).toContain('gap');
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it('marks chip as gap when assertion status is partial', () => {
|
|
144
|
-
const input: ComplianceLaneInput = {
|
|
145
|
-
assertions: [makeAssertion('REQ-1', 'partial', ['STAGE-2'])],
|
|
146
|
-
requirements: [REQ_A],
|
|
147
|
-
};
|
|
148
|
-
const l = layout(buildFile(), input);
|
|
149
|
-
expect(l.complianceRow!.chips[0].decorations).toContain('gap');
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it('does not mark chip as gap for compliant assertion', () => {
|
|
153
|
-
const input: ComplianceLaneInput = {
|
|
154
|
-
assertions: [makeAssertion('REQ-1', 'compliant', ['STAGE-1'])],
|
|
155
|
-
requirements: [REQ_A],
|
|
156
|
-
};
|
|
157
|
-
const l = layout(buildFile(), input);
|
|
158
|
-
expect(l.complianceRow!.chips[0].decorations).not.toContain('gap');
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it('propagates gap if any assertion for the same law-stage is non-compliant', () => {
|
|
162
|
-
const input: ComplianceLaneInput = {
|
|
163
|
-
assertions: [
|
|
164
|
-
makeAssertion('REQ-1', 'compliant', ['STAGE-1']),
|
|
165
|
-
makeAssertion('REQ-1', 'non_compliant', ['STAGE-1']),
|
|
166
|
-
],
|
|
167
|
-
requirements: [REQ_A],
|
|
168
|
-
};
|
|
169
|
-
const l = layout(buildFile(), input);
|
|
170
|
-
expect(l.complianceRow!.chips[0].decorations).toContain('gap');
|
|
171
|
-
});
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
describe('decoration: deadline', () => {
|
|
175
|
-
it('marks chip as deadline when gap exists and requirement deadline is past_due', () => {
|
|
176
|
-
const input: ComplianceLaneInput = {
|
|
177
|
-
assertions: [makeAssertion('REQ-3', 'non_compliant', ['STAGE-1'])],
|
|
178
|
-
requirements: [REQ_DEADLINE],
|
|
179
|
-
};
|
|
180
|
-
const l = layout(buildFile(), input);
|
|
181
|
-
const d = l.complianceRow!.chips[0].decorations;
|
|
182
|
-
expect(d).toContain('gap');
|
|
183
|
-
expect(d).toContain('deadline');
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it('does not mark deadline when gap is absent even if deadline is past_due', () => {
|
|
187
|
-
const input: ComplianceLaneInput = {
|
|
188
|
-
assertions: [makeAssertion('REQ-3', 'compliant', ['STAGE-1'])],
|
|
189
|
-
requirements: [REQ_DEADLINE],
|
|
190
|
-
};
|
|
191
|
-
const l = layout(buildFile(), input);
|
|
192
|
-
expect(l.complianceRow!.chips[0].decorations).not.toContain('deadline');
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it('does not mark deadline when no deadline on requirement', () => {
|
|
196
|
-
const input: ComplianceLaneInput = {
|
|
197
|
-
assertions: [makeAssertion('REQ-1', 'non_compliant', ['STAGE-1'])],
|
|
198
|
-
requirements: [REQ_A],
|
|
199
|
-
};
|
|
200
|
-
const l = layout(buildFile(), input);
|
|
201
|
-
expect(l.complianceRow!.chips[0].decorations).not.toContain('deadline');
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
it('all three decorations can stack on the same chip', () => {
|
|
205
|
-
const input: ComplianceLaneInput = {
|
|
206
|
-
assertions: [makeAssertion('REQ-3', 'non_compliant', ['STAGE-1'])],
|
|
207
|
-
requirements: [REQ_DEADLINE],
|
|
208
|
-
};
|
|
209
|
-
const l = layout(buildFile(), input, { previousSnapshot: { 'STAGE-1': [] } });
|
|
210
|
-
const d = l.complianceRow!.chips[0].decorations;
|
|
211
|
-
expect(d).toContain('new');
|
|
212
|
-
expect(d).toContain('gap');
|
|
213
|
-
expect(d).toContain('deadline');
|
|
214
|
-
});
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
describe('jurisdiction filter', () => {
|
|
218
|
-
it('passes through all laws when no jurisdictions filter', () => {
|
|
219
|
-
const input: ComplianceLaneInput = {
|
|
220
|
-
assertions: [makeAssertion('REQ-2', 'compliant', ['STAGE-1'])],
|
|
221
|
-
requirements: [REQ_B], // derived_from: [LAW-GDPR-1, LAW-SOX-2]
|
|
222
|
-
codexJurisdictions: { 'LAW-GDPR-1': 'EU', 'LAW-SOX-2': 'US' },
|
|
223
|
-
};
|
|
224
|
-
const l = layout(buildFile(), input, { jurisdictions: [] });
|
|
225
|
-
expect(l.complianceRow!.chips).toHaveLength(2);
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it('filters to only the selected jurisdictions', () => {
|
|
229
|
-
const input: ComplianceLaneInput = {
|
|
230
|
-
assertions: [makeAssertion('REQ-2', 'compliant', ['STAGE-1'])],
|
|
231
|
-
requirements: [REQ_B],
|
|
232
|
-
codexJurisdictions: { 'LAW-GDPR-1': 'EU', 'LAW-SOX-2': 'US' },
|
|
233
|
-
};
|
|
234
|
-
const l = layout(buildFile(), input, { jurisdictions: ['EU'] });
|
|
235
|
-
expect(l.complianceRow!.chips).toHaveLength(1);
|
|
236
|
-
expect(l.complianceRow!.chips[0].lawId).toBe('LAW-GDPR-1');
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it('returns no row when jurisdiction filter excludes all laws', () => {
|
|
240
|
-
const input: ComplianceLaneInput = {
|
|
241
|
-
assertions: [makeAssertion('REQ-2', 'compliant', ['STAGE-1'])],
|
|
242
|
-
requirements: [REQ_B],
|
|
243
|
-
codexJurisdictions: { 'LAW-GDPR-1': 'EU', 'LAW-SOX-2': 'US' },
|
|
244
|
-
};
|
|
245
|
-
const l = layout(buildFile(), input, { jurisdictions: ['AU'] });
|
|
246
|
-
expect(l.complianceRow).toBeUndefined();
|
|
247
|
-
});
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
describe('null-guards', () => {
|
|
251
|
-
it('tolerates null assertion entries without throwing', () => {
|
|
252
|
-
const input: ComplianceLaneInput = {
|
|
253
|
-
assertions: [null as unknown as ComplianceLaneAssertion, makeAssertion('REQ-1', 'compliant', ['STAGE-1'])],
|
|
254
|
-
requirements: [REQ_A],
|
|
255
|
-
};
|
|
256
|
-
expect(() => layout(buildFile(), input)).not.toThrow();
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
it('skips assertions with no realised_via', () => {
|
|
260
|
-
const input: ComplianceLaneInput = {
|
|
261
|
-
assertions: [{ about: 'REQ-1', status: 'compliant' }],
|
|
262
|
-
requirements: [REQ_A],
|
|
263
|
-
};
|
|
264
|
-
const l = layout(buildFile(), input);
|
|
265
|
-
expect(l.complianceRow).toBeUndefined();
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
it('skips assertions whose requirement is not in the input', () => {
|
|
269
|
-
const input: ComplianceLaneInput = {
|
|
270
|
-
assertions: [makeAssertion('REQ-MISSING', 'non_compliant', ['STAGE-1'])],
|
|
271
|
-
requirements: [REQ_A],
|
|
272
|
-
};
|
|
273
|
-
const l = layout(buildFile(), input);
|
|
274
|
-
expect(l.complianceRow).toBeUndefined();
|
|
275
|
-
});
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
describe('legend', () => {
|
|
279
|
-
it('adds a compliance legend entry when the row is present', () => {
|
|
280
|
-
const input: ComplianceLaneInput = {
|
|
281
|
-
assertions: [makeAssertion('REQ-1', 'compliant', ['STAGE-1'])],
|
|
282
|
-
requirements: [REQ_A],
|
|
283
|
-
};
|
|
284
|
-
const l = layout(buildFile(), input);
|
|
285
|
-
const complianceLegend = l.legend.find(entry => entry.kind === 'compliance');
|
|
286
|
-
expect(complianceLegend).toBeDefined();
|
|
287
|
-
expect(complianceLegend!.label).toBe('Compliance');
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
it('does not add compliance legend when row is absent', () => {
|
|
291
|
-
const l = layoutProcessBlueprint(buildFile(), {});
|
|
292
|
-
expect(l.legend.some(e => e.kind === 'compliance')).toBe(false);
|
|
293
|
-
});
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
describe('layout geometry', () => {
|
|
297
|
-
it('total height grows by the compliance row height', () => {
|
|
298
|
-
const withoutLane = layoutProcessBlueprint(buildFile(), {});
|
|
299
|
-
const input: ComplianceLaneInput = {
|
|
300
|
-
assertions: [makeAssertion('REQ-1', 'compliant', ['STAGE-1'])],
|
|
301
|
-
requirements: [REQ_A],
|
|
302
|
-
};
|
|
303
|
-
const withLane = layout(buildFile(), input);
|
|
304
|
-
expect(withLane.bounds.height).toBeGreaterThan(withoutLane.bounds.height);
|
|
305
|
-
expect(withLane.bounds.height).toBe(withoutLane.bounds.height + withLane.complianceRow!.height);
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
it('chip x aligns to the stage column', () => {
|
|
309
|
-
const input: ComplianceLaneInput = {
|
|
310
|
-
assertions: [makeAssertion('REQ-1', 'compliant', ['STAGE-2'])],
|
|
311
|
-
requirements: [REQ_A],
|
|
312
|
-
};
|
|
313
|
-
const l = layout(buildFile(), input);
|
|
314
|
-
const chip = l.complianceRow!.chips[0];
|
|
315
|
-
// Chip x must be at the start of stage-2's column (index 1) + cellPadding.
|
|
316
|
-
const expectedX = l.legendColumnWidth + 1 * l.stageColumnWidth + 8; // default cellPadding = 8
|
|
317
|
-
expect(chip.x).toBe(expectedX);
|
|
318
|
-
});
|
|
319
|
-
});
|
|
320
|
-
});
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { layoutProcessBlueprint } from '../layout.js';
|
|
3
|
+
import type {
|
|
4
|
+
ComplianceLaneAssertion,
|
|
5
|
+
ComplianceLaneInput,
|
|
6
|
+
ComplianceLaneRequirement,
|
|
7
|
+
ProcessBlueprintFile,
|
|
8
|
+
} from '../types.js';
|
|
9
|
+
|
|
10
|
+
// ── fixtures ────────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
const STAGES: ProcessBlueprintFile['process_blueprint']['stages'] = [
|
|
13
|
+
{ id: 'STAGE-1', name: 'Receive', goal: 'g1', result: 'r1' },
|
|
14
|
+
{ id: 'STAGE-2', name: 'Process', goal: 'g2', result: 'r2' },
|
|
15
|
+
{ id: 'STAGE-3', name: 'Ship', goal: 'g3', result: 'r3' },
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
function buildFile(overrides: Partial<ProcessBlueprintFile['process_blueprint']> = {}): ProcessBlueprintFile {
|
|
19
|
+
return {
|
|
20
|
+
notation: 'process-blueprint',
|
|
21
|
+
process_blueprint: { id: 'PROCESS_BLUEPRINT-T-1', name: 'Test', stages: STAGES, ...overrides },
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const REQ_A: ComplianceLaneRequirement = { id: 'REQ-1', derived_from: ['LAW-GDPR-1'] };
|
|
26
|
+
const REQ_B: ComplianceLaneRequirement = { id: 'REQ-2', derived_from: ['LAW-GDPR-1', 'LAW-SOX-2'] };
|
|
27
|
+
const REQ_DEADLINE: ComplianceLaneRequirement = {
|
|
28
|
+
id: 'REQ-3',
|
|
29
|
+
derived_from: ['LAW-PCI-3'],
|
|
30
|
+
deadline: '2020-01-01', // past_due
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// ── helpers ─────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
function makeAssertion(
|
|
36
|
+
about: string,
|
|
37
|
+
status: ComplianceLaneAssertion['status'],
|
|
38
|
+
realisedVia: string[],
|
|
39
|
+
): ComplianceLaneAssertion {
|
|
40
|
+
return { about, status, realised_via: realisedVia };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function layout(
|
|
44
|
+
file: ProcessBlueprintFile,
|
|
45
|
+
input: ComplianceLaneInput,
|
|
46
|
+
overrides: { jurisdictions?: string[]; previousSnapshot?: Record<string, string[]> } = {},
|
|
47
|
+
) {
|
|
48
|
+
return layoutProcessBlueprint(file, {
|
|
49
|
+
complianceLane: { enabled: true, ...overrides },
|
|
50
|
+
complianceInput: input,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ── tests ───────────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
describe('compliance lane derivation', () => {
|
|
57
|
+
it('returns no complianceRow when lane is disabled (default)', () => {
|
|
58
|
+
const l = layoutProcessBlueprint(buildFile(), {});
|
|
59
|
+
expect(l.complianceRow).toBeUndefined();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('returns no complianceRow when enabled but no input provided', () => {
|
|
63
|
+
const l = layoutProcessBlueprint(buildFile(), { complianceLane: { enabled: true } });
|
|
64
|
+
expect(l.complianceRow).toBeUndefined();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('returns no complianceRow when assertions have no realised_via matching stage IDs', () => {
|
|
68
|
+
const input: ComplianceLaneInput = {
|
|
69
|
+
assertions: [makeAssertion('REQ-1', 'compliant', ['STEP-99'])],
|
|
70
|
+
requirements: [REQ_A],
|
|
71
|
+
};
|
|
72
|
+
const l = layout(buildFile(), input);
|
|
73
|
+
expect(l.complianceRow).toBeUndefined();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('derives a chip for each law-stage pair', () => {
|
|
77
|
+
const input: ComplianceLaneInput = {
|
|
78
|
+
assertions: [makeAssertion('REQ-1', 'compliant', ['STAGE-1'])],
|
|
79
|
+
requirements: [REQ_A],
|
|
80
|
+
};
|
|
81
|
+
const l = layout(buildFile(), input);
|
|
82
|
+
expect(l.complianceRow).toBeDefined();
|
|
83
|
+
const chips = l.complianceRow!.chips;
|
|
84
|
+
expect(chips).toHaveLength(1);
|
|
85
|
+
expect(chips[0].lawId).toBe('LAW-GDPR-1');
|
|
86
|
+
expect(chips[0].stageIndex).toBe(0);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('produces one chip per law per stage (same law, two stages → two chips)', () => {
|
|
90
|
+
const input: ComplianceLaneInput = {
|
|
91
|
+
assertions: [makeAssertion('REQ-1', 'compliant', ['STAGE-1', 'STAGE-2'])],
|
|
92
|
+
requirements: [REQ_A],
|
|
93
|
+
};
|
|
94
|
+
const l = layout(buildFile(), input);
|
|
95
|
+
const chips = l.complianceRow!.chips;
|
|
96
|
+
expect(chips).toHaveLength(2);
|
|
97
|
+
expect(chips.map(c => c.stageIndex).sort()).toEqual([0, 1]);
|
|
98
|
+
expect(chips.every(c => c.lawId === 'LAW-GDPR-1')).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('merges multiple assertions for the same law-stage into one chip', () => {
|
|
102
|
+
const input: ComplianceLaneInput = {
|
|
103
|
+
assertions: [
|
|
104
|
+
makeAssertion('REQ-1', 'compliant', ['STAGE-1']),
|
|
105
|
+
makeAssertion('REQ-1', 'partial', ['STAGE-1']),
|
|
106
|
+
],
|
|
107
|
+
requirements: [REQ_A],
|
|
108
|
+
};
|
|
109
|
+
const l = layout(buildFile(), input);
|
|
110
|
+
expect(l.complianceRow!.chips).toHaveLength(1);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('decoration: new', () => {
|
|
114
|
+
it('marks chip as new when law is not in previous snapshot', () => {
|
|
115
|
+
const input: ComplianceLaneInput = {
|
|
116
|
+
assertions: [makeAssertion('REQ-1', 'compliant', ['STAGE-1'])],
|
|
117
|
+
requirements: [REQ_A],
|
|
118
|
+
};
|
|
119
|
+
const l = layout(buildFile(), input, { previousSnapshot: { 'STAGE-1': [] } });
|
|
120
|
+
expect(l.complianceRow!.chips[0].decorations).toContain('new');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('does not mark chip as new when law is in previous snapshot', () => {
|
|
124
|
+
const input: ComplianceLaneInput = {
|
|
125
|
+
assertions: [makeAssertion('REQ-1', 'compliant', ['STAGE-1'])],
|
|
126
|
+
requirements: [REQ_A],
|
|
127
|
+
};
|
|
128
|
+
const l = layout(buildFile(), input, { previousSnapshot: { 'STAGE-1': ['LAW-GDPR-1'] } });
|
|
129
|
+
expect(l.complianceRow!.chips[0].decorations).not.toContain('new');
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe('decoration: gap', () => {
|
|
134
|
+
it('marks chip as gap when assertion status is non_compliant', () => {
|
|
135
|
+
const input: ComplianceLaneInput = {
|
|
136
|
+
assertions: [makeAssertion('REQ-1', 'non_compliant', ['STAGE-1'])],
|
|
137
|
+
requirements: [REQ_A],
|
|
138
|
+
};
|
|
139
|
+
const l = layout(buildFile(), input);
|
|
140
|
+
expect(l.complianceRow!.chips[0].decorations).toContain('gap');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('marks chip as gap when assertion status is partial', () => {
|
|
144
|
+
const input: ComplianceLaneInput = {
|
|
145
|
+
assertions: [makeAssertion('REQ-1', 'partial', ['STAGE-2'])],
|
|
146
|
+
requirements: [REQ_A],
|
|
147
|
+
};
|
|
148
|
+
const l = layout(buildFile(), input);
|
|
149
|
+
expect(l.complianceRow!.chips[0].decorations).toContain('gap');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('does not mark chip as gap for compliant assertion', () => {
|
|
153
|
+
const input: ComplianceLaneInput = {
|
|
154
|
+
assertions: [makeAssertion('REQ-1', 'compliant', ['STAGE-1'])],
|
|
155
|
+
requirements: [REQ_A],
|
|
156
|
+
};
|
|
157
|
+
const l = layout(buildFile(), input);
|
|
158
|
+
expect(l.complianceRow!.chips[0].decorations).not.toContain('gap');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('propagates gap if any assertion for the same law-stage is non-compliant', () => {
|
|
162
|
+
const input: ComplianceLaneInput = {
|
|
163
|
+
assertions: [
|
|
164
|
+
makeAssertion('REQ-1', 'compliant', ['STAGE-1']),
|
|
165
|
+
makeAssertion('REQ-1', 'non_compliant', ['STAGE-1']),
|
|
166
|
+
],
|
|
167
|
+
requirements: [REQ_A],
|
|
168
|
+
};
|
|
169
|
+
const l = layout(buildFile(), input);
|
|
170
|
+
expect(l.complianceRow!.chips[0].decorations).toContain('gap');
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe('decoration: deadline', () => {
|
|
175
|
+
it('marks chip as deadline when gap exists and requirement deadline is past_due', () => {
|
|
176
|
+
const input: ComplianceLaneInput = {
|
|
177
|
+
assertions: [makeAssertion('REQ-3', 'non_compliant', ['STAGE-1'])],
|
|
178
|
+
requirements: [REQ_DEADLINE],
|
|
179
|
+
};
|
|
180
|
+
const l = layout(buildFile(), input);
|
|
181
|
+
const d = l.complianceRow!.chips[0].decorations;
|
|
182
|
+
expect(d).toContain('gap');
|
|
183
|
+
expect(d).toContain('deadline');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('does not mark deadline when gap is absent even if deadline is past_due', () => {
|
|
187
|
+
const input: ComplianceLaneInput = {
|
|
188
|
+
assertions: [makeAssertion('REQ-3', 'compliant', ['STAGE-1'])],
|
|
189
|
+
requirements: [REQ_DEADLINE],
|
|
190
|
+
};
|
|
191
|
+
const l = layout(buildFile(), input);
|
|
192
|
+
expect(l.complianceRow!.chips[0].decorations).not.toContain('deadline');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('does not mark deadline when no deadline on requirement', () => {
|
|
196
|
+
const input: ComplianceLaneInput = {
|
|
197
|
+
assertions: [makeAssertion('REQ-1', 'non_compliant', ['STAGE-1'])],
|
|
198
|
+
requirements: [REQ_A],
|
|
199
|
+
};
|
|
200
|
+
const l = layout(buildFile(), input);
|
|
201
|
+
expect(l.complianceRow!.chips[0].decorations).not.toContain('deadline');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('all three decorations can stack on the same chip', () => {
|
|
205
|
+
const input: ComplianceLaneInput = {
|
|
206
|
+
assertions: [makeAssertion('REQ-3', 'non_compliant', ['STAGE-1'])],
|
|
207
|
+
requirements: [REQ_DEADLINE],
|
|
208
|
+
};
|
|
209
|
+
const l = layout(buildFile(), input, { previousSnapshot: { 'STAGE-1': [] } });
|
|
210
|
+
const d = l.complianceRow!.chips[0].decorations;
|
|
211
|
+
expect(d).toContain('new');
|
|
212
|
+
expect(d).toContain('gap');
|
|
213
|
+
expect(d).toContain('deadline');
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
describe('jurisdiction filter', () => {
|
|
218
|
+
it('passes through all laws when no jurisdictions filter', () => {
|
|
219
|
+
const input: ComplianceLaneInput = {
|
|
220
|
+
assertions: [makeAssertion('REQ-2', 'compliant', ['STAGE-1'])],
|
|
221
|
+
requirements: [REQ_B], // derived_from: [LAW-GDPR-1, LAW-SOX-2]
|
|
222
|
+
codexJurisdictions: { 'LAW-GDPR-1': 'EU', 'LAW-SOX-2': 'US' },
|
|
223
|
+
};
|
|
224
|
+
const l = layout(buildFile(), input, { jurisdictions: [] });
|
|
225
|
+
expect(l.complianceRow!.chips).toHaveLength(2);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('filters to only the selected jurisdictions', () => {
|
|
229
|
+
const input: ComplianceLaneInput = {
|
|
230
|
+
assertions: [makeAssertion('REQ-2', 'compliant', ['STAGE-1'])],
|
|
231
|
+
requirements: [REQ_B],
|
|
232
|
+
codexJurisdictions: { 'LAW-GDPR-1': 'EU', 'LAW-SOX-2': 'US' },
|
|
233
|
+
};
|
|
234
|
+
const l = layout(buildFile(), input, { jurisdictions: ['EU'] });
|
|
235
|
+
expect(l.complianceRow!.chips).toHaveLength(1);
|
|
236
|
+
expect(l.complianceRow!.chips[0].lawId).toBe('LAW-GDPR-1');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('returns no row when jurisdiction filter excludes all laws', () => {
|
|
240
|
+
const input: ComplianceLaneInput = {
|
|
241
|
+
assertions: [makeAssertion('REQ-2', 'compliant', ['STAGE-1'])],
|
|
242
|
+
requirements: [REQ_B],
|
|
243
|
+
codexJurisdictions: { 'LAW-GDPR-1': 'EU', 'LAW-SOX-2': 'US' },
|
|
244
|
+
};
|
|
245
|
+
const l = layout(buildFile(), input, { jurisdictions: ['AU'] });
|
|
246
|
+
expect(l.complianceRow).toBeUndefined();
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe('null-guards', () => {
|
|
251
|
+
it('tolerates null assertion entries without throwing', () => {
|
|
252
|
+
const input: ComplianceLaneInput = {
|
|
253
|
+
assertions: [null as unknown as ComplianceLaneAssertion, makeAssertion('REQ-1', 'compliant', ['STAGE-1'])],
|
|
254
|
+
requirements: [REQ_A],
|
|
255
|
+
};
|
|
256
|
+
expect(() => layout(buildFile(), input)).not.toThrow();
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('skips assertions with no realised_via', () => {
|
|
260
|
+
const input: ComplianceLaneInput = {
|
|
261
|
+
assertions: [{ about: 'REQ-1', status: 'compliant' }],
|
|
262
|
+
requirements: [REQ_A],
|
|
263
|
+
};
|
|
264
|
+
const l = layout(buildFile(), input);
|
|
265
|
+
expect(l.complianceRow).toBeUndefined();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('skips assertions whose requirement is not in the input', () => {
|
|
269
|
+
const input: ComplianceLaneInput = {
|
|
270
|
+
assertions: [makeAssertion('REQ-MISSING', 'non_compliant', ['STAGE-1'])],
|
|
271
|
+
requirements: [REQ_A],
|
|
272
|
+
};
|
|
273
|
+
const l = layout(buildFile(), input);
|
|
274
|
+
expect(l.complianceRow).toBeUndefined();
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
describe('legend', () => {
|
|
279
|
+
it('adds a compliance legend entry when the row is present', () => {
|
|
280
|
+
const input: ComplianceLaneInput = {
|
|
281
|
+
assertions: [makeAssertion('REQ-1', 'compliant', ['STAGE-1'])],
|
|
282
|
+
requirements: [REQ_A],
|
|
283
|
+
};
|
|
284
|
+
const l = layout(buildFile(), input);
|
|
285
|
+
const complianceLegend = l.legend.find(entry => entry.kind === 'compliance');
|
|
286
|
+
expect(complianceLegend).toBeDefined();
|
|
287
|
+
expect(complianceLegend!.label).toBe('Compliance');
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it('does not add compliance legend when row is absent', () => {
|
|
291
|
+
const l = layoutProcessBlueprint(buildFile(), {});
|
|
292
|
+
expect(l.legend.some(e => e.kind === 'compliance')).toBe(false);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
describe('layout geometry', () => {
|
|
297
|
+
it('total height grows by the compliance row height', () => {
|
|
298
|
+
const withoutLane = layoutProcessBlueprint(buildFile(), {});
|
|
299
|
+
const input: ComplianceLaneInput = {
|
|
300
|
+
assertions: [makeAssertion('REQ-1', 'compliant', ['STAGE-1'])],
|
|
301
|
+
requirements: [REQ_A],
|
|
302
|
+
};
|
|
303
|
+
const withLane = layout(buildFile(), input);
|
|
304
|
+
expect(withLane.bounds.height).toBeGreaterThan(withoutLane.bounds.height);
|
|
305
|
+
expect(withLane.bounds.height).toBe(withoutLane.bounds.height + withLane.complianceRow!.height);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('chip x aligns to the stage column', () => {
|
|
309
|
+
const input: ComplianceLaneInput = {
|
|
310
|
+
assertions: [makeAssertion('REQ-1', 'compliant', ['STAGE-2'])],
|
|
311
|
+
requirements: [REQ_A],
|
|
312
|
+
};
|
|
313
|
+
const l = layout(buildFile(), input);
|
|
314
|
+
const chip = l.complianceRow!.chips[0];
|
|
315
|
+
// Chip x must be at the start of stage-2's column (index 1) + cellPadding.
|
|
316
|
+
const expectedX = l.legendColumnWidth + 1 * l.stageColumnWidth + 8; // default cellPadding = 8
|
|
317
|
+
expect(chip.x).toBe(expectedX);
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
});
|