@transitrix/diagrams 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +47 -47
- package/dist/__tests__/edge-path.test.js +8 -0
- package/dist/activity-card/__tests__/example.test.js +20 -1
- package/dist/activity-card/__tests__/layout.test.js +102 -11
- package/dist/activity-card/__tests__/resolver.test.js +139 -6
- package/dist/activity-card/index.d.ts +1 -1
- package/dist/activity-card/layout.d.ts +1 -1
- package/dist/activity-card/layout.js +220 -103
- package/dist/activity-card/resolver.js +107 -26
- package/dist/activity-card/types.d.ts +150 -27
- package/dist/activity-card/validate.js +3 -0
- package/dist/actor/__tests__/validate.test.js +63 -31
- package/dist/actor/index.d.ts +1 -1
- package/dist/actor/index.js +1 -1
- package/dist/actor/types.d.ts +7 -3
- package/dist/actor/types.js +17 -3
- package/dist/actor/validate.js +55 -26
- package/dist/assertion/types.d.ts +3 -1
- package/dist/assertion/types.js +1 -1
- package/dist/change/__tests__/validate.test.js +40 -34
- package/dist/change/index.d.ts +1 -0
- package/dist/change/types.d.ts +1 -1
- package/dist/change/types.js +2 -2
- package/dist/change/validate.d.ts +6 -1
- package/dist/change/validate.js +68 -41
- package/dist/compliance/__tests__/classify.test.js +34 -0
- package/dist/compliance/__tests__/coverage-metric.test.js +146 -46
- package/dist/compliance/__tests__/html.test.js +2 -2
- package/dist/compliance/__tests__/impact.test.js +311 -6
- package/dist/compliance/classify.d.ts +6 -0
- package/dist/compliance/classify.js +9 -1
- package/dist/compliance/coverage-metric.d.ts +31 -11
- package/dist/compliance/coverage-metric.js +149 -39
- package/dist/compliance/html.js +77 -74
- package/dist/compliance/impact.d.ts +77 -16
- package/dist/compliance/impact.js +193 -25
- package/dist/compliance/index.d.ts +2 -2
- package/dist/compliance/index.js +1 -1
- package/dist/compliance/markdown.js +1 -0
- package/dist/compliance/types.d.ts +25 -6
- package/dist/edge-path.d.ts +8 -4
- package/dist/edge-path.js +11 -6
- package/dist/factor/__tests__/validate.test.js +74 -45
- package/dist/factor/index.d.ts +3 -2
- package/dist/factor/index.js +1 -1
- package/dist/factor/types.d.ts +4 -7
- package/dist/factor/types.js +10 -5
- package/dist/factor/validate.d.ts +6 -1
- package/dist/factor/validate.js +68 -50
- package/dist/fgca/__tests__/resolver.test.js +185 -0
- package/dist/fgca/index.d.ts +2 -0
- package/dist/fgca/index.js +1 -0
- package/dist/fgca/resolver.d.ts +49 -0
- package/dist/fgca/resolver.js +124 -0
- package/dist/goals/index.d.ts +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/process-blueprint/__tests__/layout.test.js +37 -0
- package/dist/process-blueprint/layout.js +5 -1
- package/dist/process-blueprint/types.d.ts +22 -1
- package/dist/stakeholder/__tests__/validate.test.js +49 -47
- package/dist/stakeholder/index.d.ts +1 -0
- package/dist/stakeholder/types.d.ts +3 -4
- package/dist/stakeholder/types.js +2 -3
- package/dist/stakeholder/validate.d.ts +6 -1
- package/dist/stakeholder/validate.js +71 -50
- package/dist/target-state/__tests__/validate.test.js +43 -27
- package/dist/target-state/index.d.ts +2 -0
- package/dist/target-state/index.js +1 -0
- package/dist/target-state/types.d.ts +6 -1
- package/dist/target-state/types.js +8 -7
- package/dist/target-state/validate.d.ts +6 -1
- package/dist/target-state/validate.js +69 -45
- package/dist/theme/themes.js +69 -69
- package/dist/webview/__tests__/entry.test.js +11 -11
- package/dist/webview/render-activities.d.ts +3 -1
- package/dist/webview/render-activities.js +2 -2
- package/dist/webview/render-activity-card.js +104 -77
- package/dist/webview/render-fgca.d.ts +4 -0
- package/dist/webview/render-fgca.js +2 -5
- package/dist/webview/render-goals.d.ts +1 -0
- package/dist/webview/render-goals.js +15 -15
- package/dist/webview/render-process-blueprint.js +10 -10
- package/package.json +40 -40
- package/src/__tests__/edge-path.test.ts +58 -46
- package/src/__tests__/scope.test.ts +27 -27
- package/src/__tests__/yaml-normalize.test.ts +158 -158
- package/src/activities/__tests__/cpm.test.ts +164 -164
- package/src/activities/__tests__/gantt.test.ts +245 -245
- package/src/activities/__tests__/layout.test.ts +112 -112
- package/src/activities/__tests__/validate.test.ts +409 -409
- package/src/activities/cpm.ts +95 -95
- package/src/activities/gantt.ts +276 -276
- package/src/activities/index.ts +28 -28
- package/src/activities/layout.ts +112 -112
- package/src/activities/types.ts +148 -148
- package/src/activities/validate.ts +357 -357
- package/src/activity-card/__tests__/example.test.ts +85 -66
- package/src/activity-card/__tests__/layout.test.ts +169 -63
- package/src/activity-card/__tests__/resolver.test.ts +307 -161
- package/src/activity-card/__tests__/validate.test.ts +131 -131
- package/src/activity-card/index.ts +38 -32
- package/src/activity-card/layout.ts +324 -194
- package/src/activity-card/resolver.ts +415 -337
- package/src/activity-card/types.ts +376 -237
- package/src/activity-card/validate.ts +162 -158
- package/src/actor/__tests__/validate.test.ts +70 -31
- package/src/actor/index.ts +1 -1
- package/src/actor/types.ts +21 -7
- package/src/actor/validate.ts +59 -27
- package/src/applications/__tests__/validate.test.ts +256 -256
- package/src/applications/index.ts +11 -11
- package/src/applications/types.ts +43 -43
- package/src/applications/validate.ts +131 -131
- package/src/assertion/__tests__/example.test.ts +37 -37
- package/src/assertion/__tests__/validate.test.ts +142 -142
- package/src/assertion/index.ts +12 -12
- package/src/assertion/types.ts +60 -58
- package/src/assertion/validate.ts +145 -145
- package/src/blocks/__tests__/layout.test.ts +169 -169
- package/src/blocks/__tests__/validate.test.ts +260 -260
- package/src/blocks/index.ts +17 -17
- package/src/blocks/layout.ts +186 -186
- package/src/blocks/types.ts +62 -62
- package/src/blocks/validate.ts +206 -206
- package/src/capability-map/__tests__/validate.test.ts +238 -238
- package/src/capability-map/index.ts +8 -8
- package/src/capability-map/types.ts +29 -29
- package/src/capability-map/validate.ts +127 -127
- package/src/change/__tests__/validate.test.ts +43 -34
- package/src/change/index.ts +1 -0
- package/src/change/types.ts +5 -3
- package/src/change/validate.ts +75 -43
- package/src/compliance/__tests__/classify.test.ts +64 -27
- package/src/compliance/__tests__/confidence.test.ts +98 -98
- package/src/compliance/__tests__/coverage-metric.test.ts +300 -188
- package/src/compliance/__tests__/demo-scenario.test.ts +273 -273
- package/src/compliance/__tests__/example.test.ts +57 -57
- package/src/compliance/__tests__/gap-report.test.ts +128 -128
- package/src/compliance/__tests__/html.test.ts +141 -141
- package/src/compliance/__tests__/impact.test.ts +840 -481
- package/src/compliance/__tests__/markdown.test.ts +94 -94
- package/src/compliance/__tests__/views.test.ts +64 -64
- package/src/compliance/classify.ts +100 -84
- package/src/compliance/confidence.ts +69 -69
- package/src/compliance/coverage-metric.ts +404 -263
- package/src/compliance/gap-report.ts +94 -94
- package/src/compliance/html.ts +298 -295
- package/src/compliance/impact.ts +861 -677
- package/src/compliance/index.ts +57 -56
- package/src/compliance/markdown.ts +136 -135
- package/src/compliance/reverse-index.ts +33 -33
- package/src/compliance/types.ts +136 -120
- package/src/compliance/views.ts +46 -46
- package/src/compliance-matrix/__tests__/build.test.ts +200 -200
- package/src/compliance-matrix/__tests__/example.test.ts +57 -57
- package/src/compliance-matrix/index.ts +11 -11
- package/src/compliance-matrix/types.ts +102 -102
- package/src/edge-path.ts +50 -44
- package/src/factor/__tests__/validate.test.ts +80 -45
- package/src/factor/index.ts +3 -7
- package/src/factor/types.ts +14 -18
- package/src/factor/validate.ts +77 -51
- package/src/fgca/__tests__/chain-table.test.ts +172 -172
- package/src/fgca/__tests__/layout.test.ts +179 -179
- package/src/fgca/__tests__/parse-canonical.test.ts +248 -248
- package/src/fgca/__tests__/preview-layout.test.ts +124 -124
- package/src/fgca/__tests__/resolver.test.ts +209 -0
- package/src/fgca/__tests__/validate.test.ts +142 -142
- package/src/fgca/chain-table.ts +225 -225
- package/src/fgca/index.ts +47 -45
- package/src/fgca/layout.ts +204 -204
- package/src/fgca/nodeTypes.ts +11 -11
- package/src/fgca/nodes/FGCAActivityNode.tsx +65 -65
- package/src/fgca/nodes/FGCAChangeNode.tsx +64 -64
- package/src/fgca/nodes/FGCAFactorNode.tsx +64 -64
- package/src/fgca/nodes/FGCAGoalNode.tsx +72 -72
- package/src/fgca/parse-canonical.ts +400 -400
- package/src/fgca/preview-layout.ts +217 -217
- package/src/fgca/resolver.ts +157 -0
- package/src/fgca/types.ts +50 -50
- package/src/fgca/validate.ts +126 -126
- package/src/geometry.ts +9 -9
- package/src/goals/__tests__/layout.test.ts +175 -175
- package/src/goals/__tests__/mutations.test.ts +111 -111
- package/src/goals/__tests__/parse-canonical.test.ts +145 -145
- package/src/goals/__tests__/validate.test.ts +202 -202
- package/src/goals/index.ts +20 -21
- package/src/goals/layout.ts +172 -172
- package/src/goals/mutations.ts +123 -123
- package/src/goals/parse-canonical.ts +286 -286
- package/src/goals/types.ts +85 -85
- package/src/goals/validate.ts +136 -136
- package/src/index.ts +28 -23
- package/src/process-blueprint/__tests__/compliance-lane.test.ts +320 -320
- package/src/process-blueprint/__tests__/layout.test.ts +240 -195
- package/src/process-blueprint/__tests__/validate.test.ts +310 -310
- package/src/process-blueprint/index.ts +31 -31
- package/src/process-blueprint/layout.ts +535 -531
- package/src/process-blueprint/types.ts +263 -242
- package/src/process-blueprint/validate.ts +181 -181
- package/src/process-map/__tests__/validate.test.ts +223 -223
- package/src/process-map/index.ts +10 -10
- package/src/process-map/types.ts +36 -36
- package/src/process-map/validate.ts +135 -135
- package/src/products/__tests__/validate.test.ts +239 -239
- package/src/products/index.ts +9 -9
- package/src/products/types.ts +31 -31
- package/src/products/validate.ts +112 -112
- package/src/repo-validate/__tests__/validate-repo.test.ts +149 -149
- package/src/repo-validate/index.ts +2 -2
- package/src/repo-validate/types.ts +50 -50
- package/src/repo-validate/validate-repo.ts +196 -196
- package/src/requirement/__tests__/example.test.ts +24 -24
- package/src/requirement/__tests__/validate.test.ts +96 -96
- package/src/requirement/index.ts +4 -4
- package/src/requirement/types.ts +35 -35
- package/src/requirement/validate.ts +92 -92
- package/src/scenarios/__tests__/validate.test.ts +209 -209
- package/src/scenarios/index.ts +15 -15
- package/src/scenarios/types.ts +37 -37
- package/src/scenarios/validate.ts +122 -122
- package/src/schema-version.ts +16 -16
- package/src/scope.ts +46 -46
- package/src/stakeholder/__tests__/validate.test.ts +51 -47
- package/src/stakeholder/index.ts +1 -0
- package/src/stakeholder/types.ts +7 -7
- package/src/stakeholder/validate.ts +79 -52
- package/src/target-state/__tests__/validate.test.ts +48 -27
- package/src/target-state/index.ts +2 -0
- package/src/target-state/types.ts +11 -7
- package/src/target-state/validate.ts +76 -46
- package/src/theme/index.ts +4 -4
- package/src/theme/themes.ts +237 -237
- package/src/theme/tokens.ts +133 -133
- package/src/typed-id.ts +56 -56
- package/src/validation-types.ts +36 -36
- package/src/webview/__tests__/entry-notations.test.ts +77 -77
- package/src/webview/__tests__/entry.test.ts +64 -64
- package/src/webview/__tests__/render-goals.test.ts +72 -72
- package/src/webview/entry.ts +292 -292
- package/src/webview/render-activities.ts +5 -3
- package/src/webview/render-activity-card.ts +266 -234
- package/src/webview/render-fgca.ts +6 -5
- package/src/webview/render-goals.ts +111 -110
- package/src/webview/render-process-blueprint.ts +207 -207
- package/src/webview/styles.css +322 -323
- package/src/yaml-normalize.ts +56 -56
- package/dist/element-envelope.d.ts +0 -15
- package/dist/element-envelope.js +0 -55
- package/dist/issues/__tests__/layout.test.js +0 -62
- package/dist/issues/__tests__/validate.test.d.ts +0 -1
- package/dist/issues/__tests__/validate.test.js +0 -82
- package/dist/issues/index.d.ts +0 -4
- package/dist/issues/index.js +0 -2
- package/dist/issues/layout.d.ts +0 -10
- package/dist/issues/layout.js +0 -82
- package/dist/issues/types.d.ts +0 -61
- package/dist/issues/types.js +0 -6
- package/dist/issues/validate.d.ts +0 -3
- package/dist/issues/validate.js +0 -143
- package/dist/webview/transitrix-render.css +0 -323
- package/dist/webview/transitrix-render.js +0 -6452
- package/dist/webview/transitrix-render.js.map +0 -7
- package/src/element-envelope.ts +0 -76
- /package/dist/{issues/__tests__/layout.test.d.ts → fgca/__tests__/resolver.test.d.ts} +0 -0
|
@@ -1,95 +1,95 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { readdirSync, readFileSync } from 'node:fs';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import yaml from 'js-yaml';
|
|
6
|
-
import { renderComplianceMarkdown } from '../markdown.js';
|
|
7
|
-
import { emptyCanon, ingestComplianceDoc, type ComplianceCanon } from '../classify.js';
|
|
8
|
-
|
|
9
|
-
function synthetic(): ComplianceCanon {
|
|
10
|
-
const canon = emptyCanon();
|
|
11
|
-
ingestComplianceDoc(canon, { notation: 'product', id: 'PRODUCT-M-1', name: 'Mobile App' });
|
|
12
|
-
ingestComplianceDoc(canon, { notation: 'requirement', id: 'REQUIREMENT-ERASURE-1', name: 'Erasure', severity: 'high', derived_from: ['LAW-X-1'] });
|
|
13
|
-
ingestComplianceDoc(canon, { notation: 'requirement', id: 'REQUIREMENT-LOG-1', name: 'Logs', severity: 'low' });
|
|
14
|
-
ingestComplianceDoc(canon, { notation: 'assertion', id: 'ASSERTION-1', about: 'REQUIREMENT-ERASURE-1', subject: 'PRODUCT-M-1', status: 'compliant', evidence: [{ kind: 'note', text: 'x' }], next_review_at: '2026-09-01' });
|
|
15
|
-
ingestComplianceDoc(canon, { id: 'LAW-X-1', name: 'Privacy Law', zone: 'codex', type: 'LAW' });
|
|
16
|
-
return canon;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
describe('renderComplianceMarkdown — matrix', () => {
|
|
20
|
-
it('renders a table with status cells and gap dashes', () => {
|
|
21
|
-
const md = renderComplianceMarkdown(synthetic(), { mode: 'matrix' });
|
|
22
|
-
expect(md).toContain('# Compliance Matrix');
|
|
23
|
-
expect(md).toContain('| Product \\ Requirement | Erasure | Logs |');
|
|
24
|
-
expect(md).toMatch(/\| Mobile App \| Compliant \| — \|/);
|
|
25
|
-
});
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
describe('renderComplianceMarkdown — law', () => {
|
|
29
|
-
it('renders the law name and its requirement with the assertion bullet', () => {
|
|
30
|
-
const md = renderComplianceMarkdown(synthetic(), { mode: 'law', id: 'LAW-X-1' });
|
|
31
|
-
expect(md).toContain('# Compliance — Privacy Law');
|
|
32
|
-
expect(md).toContain('## Erasure (`REQUIREMENT-ERASURE-1`) — severity: high');
|
|
33
|
-
expect(md).toContain('- **Compliant** — `ASSERTION-1`');
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
describe('renderComplianceMarkdown — product', () => {
|
|
38
|
-
it('renders the product table of asserted requirements', () => {
|
|
39
|
-
const md = renderComplianceMarkdown(synthetic(), { mode: 'product', id: 'PRODUCT-M-1' });
|
|
40
|
-
expect(md).toContain('# Compliance — Mobile App');
|
|
41
|
-
expect(md).toContain('| Requirement | Status | Assertion | Next review |');
|
|
42
|
-
expect(md).toMatch(/\| Erasure \(`REQUIREMENT-ERASURE-1`\) \| Compliant \| `ASSERTION-1` \| 2026-09-01 \|/);
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
describe('renderComplianceMarkdown — gap', () => {
|
|
47
|
-
it('lists the un-asserted requirement as a checklist item', () => {
|
|
48
|
-
const md = renderComplianceMarkdown(synthetic(), { mode: 'gap' }, { today: '2026-06-02' });
|
|
49
|
-
expect(md).toContain('## Requirements without assertions (1)');
|
|
50
|
-
expect(md).toContain('- [ ] `REQUIREMENT-LOG-1` Logs — severity: low');
|
|
51
|
-
expect(md).toContain('## Assertions without evidence — ASSERT-007 (0)');
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
// ── Conformance: build canon by ingesting the acme example files ────────────
|
|
56
|
-
|
|
57
|
-
const examples = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../../../tests/fixtures/notation-corpus');
|
|
58
|
-
function ingestDir(canon: ComplianceCanon, dir: string): void {
|
|
59
|
-
const full = path.join(examples, dir);
|
|
60
|
-
for (const f of readdirSync(full)) {
|
|
61
|
-
if (!f.endsWith('.yaml')) continue;
|
|
62
|
-
ingestComplianceDoc(canon, yaml.load(readFileSync(path.join(full, f), 'utf-8')));
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
describe('renderComplianceMarkdown — acme conformance', () => {
|
|
67
|
-
const canon = emptyCanon();
|
|
68
|
-
ingestDir(canon, 'product');
|
|
69
|
-
ingestDir(canon, 'requirement');
|
|
70
|
-
ingestDir(canon, 'assertion');
|
|
71
|
-
|
|
72
|
-
it('matrix marks the dangling PRODUCT-MOBILE-1 row and shows compliant + gaps', () => {
|
|
73
|
-
const md = renderComplianceMarkdown(canon, { mode: 'matrix' });
|
|
74
|
-
expect(md).toContain('PRODUCT-MOBILE-1 ⚠');
|
|
75
|
-
expect(md).toContain('Compliant');
|
|
76
|
-
expect(md).toContain('—'); // at least one gap dash
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('gap report surfaces the un-asserted audit-log requirement', () => {
|
|
80
|
-
const md = renderComplianceMarkdown(canon, { mode: 'gap' }, { today: '2026-06-02' });
|
|
81
|
-
expect(md).toContain('`REQUIREMENT-AUDIT-LOG-RETENTION-1`');
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
describe('gapMarkdown CV-5 pastDeadlineRequirements section', () => {
|
|
86
|
-
it('includes the 4th section in output', () => {
|
|
87
|
-
const canon = emptyCanon();
|
|
88
|
-
ingestComplianceDoc(canon, { notation: 'requirement', id: 'REQ-PD-1', name: 'Past', deadline: '2020-01-01' });
|
|
89
|
-
const md = renderComplianceMarkdown(canon, { mode: 'gap' }, { today: '2026-06-09' });
|
|
90
|
-
expect(md).toContain('Past-deadline requirements');
|
|
91
|
-
expect(md).toContain('REQ-PD-1');
|
|
92
|
-
expect(md).toContain('2020-01-01');
|
|
93
|
-
expect(md).toContain('4 checks');
|
|
94
|
-
});
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import yaml from 'js-yaml';
|
|
6
|
+
import { renderComplianceMarkdown } from '../markdown.js';
|
|
7
|
+
import { emptyCanon, ingestComplianceDoc, type ComplianceCanon } from '../classify.js';
|
|
8
|
+
|
|
9
|
+
function synthetic(): ComplianceCanon {
|
|
10
|
+
const canon = emptyCanon();
|
|
11
|
+
ingestComplianceDoc(canon, { notation: 'product', id: 'PRODUCT-M-1', name: 'Mobile App' });
|
|
12
|
+
ingestComplianceDoc(canon, { notation: 'requirement', id: 'REQUIREMENT-ERASURE-1', name: 'Erasure', severity: 'high', derived_from: ['LAW-X-1'] });
|
|
13
|
+
ingestComplianceDoc(canon, { notation: 'requirement', id: 'REQUIREMENT-LOG-1', name: 'Logs', severity: 'low' });
|
|
14
|
+
ingestComplianceDoc(canon, { notation: 'assertion', id: 'ASSERTION-1', about: 'REQUIREMENT-ERASURE-1', subject: 'PRODUCT-M-1', status: 'compliant', evidence: [{ kind: 'note', text: 'x' }], next_review_at: '2026-09-01' });
|
|
15
|
+
ingestComplianceDoc(canon, { id: 'LAW-X-1', name: 'Privacy Law', zone: 'codex', type: 'LAW' });
|
|
16
|
+
return canon;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('renderComplianceMarkdown — matrix', () => {
|
|
20
|
+
it('renders a table with status cells and gap dashes', () => {
|
|
21
|
+
const md = renderComplianceMarkdown(synthetic(), { mode: 'matrix' });
|
|
22
|
+
expect(md).toContain('# Compliance Matrix');
|
|
23
|
+
expect(md).toContain('| Product \\ Requirement | Erasure | Logs |');
|
|
24
|
+
expect(md).toMatch(/\| Mobile App \| Compliant \| — \|/);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('renderComplianceMarkdown — law', () => {
|
|
29
|
+
it('renders the law name and its requirement with the assertion bullet', () => {
|
|
30
|
+
const md = renderComplianceMarkdown(synthetic(), { mode: 'law', id: 'LAW-X-1' });
|
|
31
|
+
expect(md).toContain('# Compliance — Privacy Law');
|
|
32
|
+
expect(md).toContain('## Erasure (`REQUIREMENT-ERASURE-1`) — severity: high');
|
|
33
|
+
expect(md).toContain('- **Compliant** — `ASSERTION-1`');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('renderComplianceMarkdown — product', () => {
|
|
38
|
+
it('renders the product table of asserted requirements', () => {
|
|
39
|
+
const md = renderComplianceMarkdown(synthetic(), { mode: 'product', id: 'PRODUCT-M-1' });
|
|
40
|
+
expect(md).toContain('# Compliance — Mobile App');
|
|
41
|
+
expect(md).toContain('| Requirement | Status | Assertion | Next review |');
|
|
42
|
+
expect(md).toMatch(/\| Erasure \(`REQUIREMENT-ERASURE-1`\) \| Compliant \| `ASSERTION-1` \| 2026-09-01 \|/);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('renderComplianceMarkdown — gap', () => {
|
|
47
|
+
it('lists the un-asserted requirement as a checklist item', () => {
|
|
48
|
+
const md = renderComplianceMarkdown(synthetic(), { mode: 'gap' }, { today: '2026-06-02' });
|
|
49
|
+
expect(md).toContain('## Requirements without assertions (1)');
|
|
50
|
+
expect(md).toContain('- [ ] `REQUIREMENT-LOG-1` Logs — severity: low');
|
|
51
|
+
expect(md).toContain('## Assertions without evidence — ASSERT-007 (0)');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// ── Conformance: build canon by ingesting the acme example files ────────────
|
|
56
|
+
|
|
57
|
+
const examples = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../../../tests/fixtures/notation-corpus');
|
|
58
|
+
function ingestDir(canon: ComplianceCanon, dir: string): void {
|
|
59
|
+
const full = path.join(examples, dir);
|
|
60
|
+
for (const f of readdirSync(full)) {
|
|
61
|
+
if (!f.endsWith('.yaml')) continue;
|
|
62
|
+
ingestComplianceDoc(canon, yaml.load(readFileSync(path.join(full, f), 'utf-8')));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe('renderComplianceMarkdown — acme conformance', () => {
|
|
67
|
+
const canon = emptyCanon();
|
|
68
|
+
ingestDir(canon, 'product');
|
|
69
|
+
ingestDir(canon, 'requirement');
|
|
70
|
+
ingestDir(canon, 'assertion');
|
|
71
|
+
|
|
72
|
+
it('matrix marks the dangling PRODUCT-MOBILE-1 row and shows compliant + gaps', () => {
|
|
73
|
+
const md = renderComplianceMarkdown(canon, { mode: 'matrix' });
|
|
74
|
+
expect(md).toContain('PRODUCT-MOBILE-1 ⚠');
|
|
75
|
+
expect(md).toContain('Compliant');
|
|
76
|
+
expect(md).toContain('—'); // at least one gap dash
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('gap report surfaces the un-asserted audit-log requirement', () => {
|
|
80
|
+
const md = renderComplianceMarkdown(canon, { mode: 'gap' }, { today: '2026-06-02' });
|
|
81
|
+
expect(md).toContain('`REQUIREMENT-AUDIT-LOG-RETENTION-1`');
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('gapMarkdown CV-5 pastDeadlineRequirements section', () => {
|
|
86
|
+
it('includes the 4th section in output', () => {
|
|
87
|
+
const canon = emptyCanon();
|
|
88
|
+
ingestComplianceDoc(canon, { notation: 'requirement', id: 'REQ-PD-1', name: 'Past', deadline: '2020-01-01' });
|
|
89
|
+
const md = renderComplianceMarkdown(canon, { mode: 'gap' }, { today: '2026-06-09' });
|
|
90
|
+
expect(md).toContain('Past-deadline requirements');
|
|
91
|
+
expect(md).toContain('REQ-PD-1');
|
|
92
|
+
expect(md).toContain('2020-01-01');
|
|
93
|
+
expect(md).toContain('4 checks');
|
|
94
|
+
});
|
|
95
95
|
});
|
|
@@ -1,64 +1,64 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { buildComplianceIndex } from '../reverse-index.js';
|
|
3
|
-
import { buildLawTree, buildProductView } from '../views.js';
|
|
4
|
-
import type { ComplianceIndexInput } from '../types.js';
|
|
5
|
-
|
|
6
|
-
const input: ComplianceIndexInput = {
|
|
7
|
-
requirements: [
|
|
8
|
-
{ id: 'REQUIREMENT-A-1', name: 'A', severity: 'high', derived_from: ['LAW-X-1'] },
|
|
9
|
-
{ id: 'REQUIREMENT-B-1', name: 'B', severity: 'low', derived_from: ['LAW-X-1', 'REGULATION-Y-1'] },
|
|
10
|
-
{ id: 'REQUIREMENT-C-1', name: 'C' }, // internal-only, no law
|
|
11
|
-
],
|
|
12
|
-
assertions: [
|
|
13
|
-
{ id: 'ASSERTION-2', about: 'REQUIREMENT-A-1', subject: 'PRODUCT-M-1', status: 'compliant' },
|
|
14
|
-
{ id: 'ASSERTION-1', about: 'REQUIREMENT-A-1', subject: 'CAPABILITY-V2', status: 'under_review' },
|
|
15
|
-
{ id: 'ASSERTION-3', about: 'REQUIREMENT-B-1', subject: 'PRODUCT-M-1', status: 'partial' },
|
|
16
|
-
],
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
describe('buildComplianceIndex', () => {
|
|
20
|
-
const idx = buildComplianceIndex(input);
|
|
21
|
-
it('indexes requirements by the laws they derive from', () => {
|
|
22
|
-
expect(idx.requirementsByLaw.get('LAW-X-1')?.map(r => r.id)).toEqual(['REQUIREMENT-A-1', 'REQUIREMENT-B-1']);
|
|
23
|
-
expect(idx.requirementsByLaw.get('REGULATION-Y-1')?.map(r => r.id)).toEqual(['REQUIREMENT-B-1']);
|
|
24
|
-
expect(idx.requirementsByLaw.has('LAW-NONE-1')).toBe(false);
|
|
25
|
-
});
|
|
26
|
-
it('indexes assertions by requirement and by subject', () => {
|
|
27
|
-
expect(idx.assertionsByRequirement.get('REQUIREMENT-A-1')?.map(a => a.id).sort()).toEqual(['ASSERTION-1', 'ASSERTION-2']);
|
|
28
|
-
expect(idx.assertionsBySubject.get('PRODUCT-M-1')?.map(a => a.id).sort()).toEqual(['ASSERTION-2', 'ASSERTION-3']);
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
describe('buildLawTree', () => {
|
|
33
|
-
const idx = buildComplianceIndex(input);
|
|
34
|
-
it('lists requirements derived from the law, each with its id-sorted assertions', () => {
|
|
35
|
-
const tree = buildLawTree('LAW-X-1', idx);
|
|
36
|
-
expect(tree.requirements.map(r => r.requirement.id)).toEqual(['REQUIREMENT-A-1', 'REQUIREMENT-B-1']);
|
|
37
|
-
expect(tree.requirements[0].assertions.map(a => a.id)).toEqual(['ASSERTION-1', 'ASSERTION-2']);
|
|
38
|
-
expect(tree.requirements[0].assertions.map(a => a.status)).toEqual(['under_review', 'compliant']);
|
|
39
|
-
});
|
|
40
|
-
it('returns an empty tree for an unknown law', () => {
|
|
41
|
-
expect(buildLawTree('LAW-NONE-1', idx).requirements).toEqual([]);
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
describe('buildProductView', () => {
|
|
46
|
-
const idx = buildComplianceIndex(input);
|
|
47
|
-
it('lists the requirements the product is asserted against, with status, requirement-id-sorted', () => {
|
|
48
|
-
const view = buildProductView('PRODUCT-M-1', idx);
|
|
49
|
-
expect(view.requirements.map(r => r.requirement.id)).toEqual(['REQUIREMENT-A-1', 'REQUIREMENT-B-1']);
|
|
50
|
-
expect(view.requirements.map(r => r.assertion.status)).toEqual(['compliant', 'partial']);
|
|
51
|
-
expect(view.requirements[0].requirement.name).toBe('A');
|
|
52
|
-
});
|
|
53
|
-
it('falls back to the id as name when the requirement is not in the scan (dangling about)', () => {
|
|
54
|
-
const idx2 = buildComplianceIndex({
|
|
55
|
-
requirements: [],
|
|
56
|
-
assertions: [{ id: 'ASSERTION-9', about: 'REQUIREMENT-GONE-1', subject: 'PRODUCT-M-1', status: 'n_a' }],
|
|
57
|
-
});
|
|
58
|
-
const view = buildProductView('PRODUCT-M-1', idx2);
|
|
59
|
-
expect(view.requirements[0].requirement).toEqual({ id: 'REQUIREMENT-GONE-1', name: 'REQUIREMENT-GONE-1' });
|
|
60
|
-
});
|
|
61
|
-
it('returns an empty view for a product with no assertions', () => {
|
|
62
|
-
expect(buildProductView('PRODUCT-NONE-1', idx).requirements).toEqual([]);
|
|
63
|
-
});
|
|
64
|
-
});
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { buildComplianceIndex } from '../reverse-index.js';
|
|
3
|
+
import { buildLawTree, buildProductView } from '../views.js';
|
|
4
|
+
import type { ComplianceIndexInput } from '../types.js';
|
|
5
|
+
|
|
6
|
+
const input: ComplianceIndexInput = {
|
|
7
|
+
requirements: [
|
|
8
|
+
{ id: 'REQUIREMENT-A-1', name: 'A', severity: 'high', derived_from: ['LAW-X-1'] },
|
|
9
|
+
{ id: 'REQUIREMENT-B-1', name: 'B', severity: 'low', derived_from: ['LAW-X-1', 'REGULATION-Y-1'] },
|
|
10
|
+
{ id: 'REQUIREMENT-C-1', name: 'C' }, // internal-only, no law
|
|
11
|
+
],
|
|
12
|
+
assertions: [
|
|
13
|
+
{ id: 'ASSERTION-2', about: 'REQUIREMENT-A-1', subject: 'PRODUCT-M-1', status: 'compliant' },
|
|
14
|
+
{ id: 'ASSERTION-1', about: 'REQUIREMENT-A-1', subject: 'CAPABILITY-V2', status: 'under_review' },
|
|
15
|
+
{ id: 'ASSERTION-3', about: 'REQUIREMENT-B-1', subject: 'PRODUCT-M-1', status: 'partial' },
|
|
16
|
+
],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
describe('buildComplianceIndex', () => {
|
|
20
|
+
const idx = buildComplianceIndex(input);
|
|
21
|
+
it('indexes requirements by the laws they derive from', () => {
|
|
22
|
+
expect(idx.requirementsByLaw.get('LAW-X-1')?.map(r => r.id)).toEqual(['REQUIREMENT-A-1', 'REQUIREMENT-B-1']);
|
|
23
|
+
expect(idx.requirementsByLaw.get('REGULATION-Y-1')?.map(r => r.id)).toEqual(['REQUIREMENT-B-1']);
|
|
24
|
+
expect(idx.requirementsByLaw.has('LAW-NONE-1')).toBe(false);
|
|
25
|
+
});
|
|
26
|
+
it('indexes assertions by requirement and by subject', () => {
|
|
27
|
+
expect(idx.assertionsByRequirement.get('REQUIREMENT-A-1')?.map(a => a.id).sort()).toEqual(['ASSERTION-1', 'ASSERTION-2']);
|
|
28
|
+
expect(idx.assertionsBySubject.get('PRODUCT-M-1')?.map(a => a.id).sort()).toEqual(['ASSERTION-2', 'ASSERTION-3']);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('buildLawTree', () => {
|
|
33
|
+
const idx = buildComplianceIndex(input);
|
|
34
|
+
it('lists requirements derived from the law, each with its id-sorted assertions', () => {
|
|
35
|
+
const tree = buildLawTree('LAW-X-1', idx);
|
|
36
|
+
expect(tree.requirements.map(r => r.requirement.id)).toEqual(['REQUIREMENT-A-1', 'REQUIREMENT-B-1']);
|
|
37
|
+
expect(tree.requirements[0].assertions.map(a => a.id)).toEqual(['ASSERTION-1', 'ASSERTION-2']);
|
|
38
|
+
expect(tree.requirements[0].assertions.map(a => a.status)).toEqual(['under_review', 'compliant']);
|
|
39
|
+
});
|
|
40
|
+
it('returns an empty tree for an unknown law', () => {
|
|
41
|
+
expect(buildLawTree('LAW-NONE-1', idx).requirements).toEqual([]);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('buildProductView', () => {
|
|
46
|
+
const idx = buildComplianceIndex(input);
|
|
47
|
+
it('lists the requirements the product is asserted against, with status, requirement-id-sorted', () => {
|
|
48
|
+
const view = buildProductView('PRODUCT-M-1', idx);
|
|
49
|
+
expect(view.requirements.map(r => r.requirement.id)).toEqual(['REQUIREMENT-A-1', 'REQUIREMENT-B-1']);
|
|
50
|
+
expect(view.requirements.map(r => r.assertion.status)).toEqual(['compliant', 'partial']);
|
|
51
|
+
expect(view.requirements[0].requirement.name).toBe('A');
|
|
52
|
+
});
|
|
53
|
+
it('falls back to the id as name when the requirement is not in the scan (dangling about)', () => {
|
|
54
|
+
const idx2 = buildComplianceIndex({
|
|
55
|
+
requirements: [],
|
|
56
|
+
assertions: [{ id: 'ASSERTION-9', about: 'REQUIREMENT-GONE-1', subject: 'PRODUCT-M-1', status: 'n_a' }],
|
|
57
|
+
});
|
|
58
|
+
const view = buildProductView('PRODUCT-M-1', idx2);
|
|
59
|
+
expect(view.requirements[0].requirement).toEqual({ id: 'REQUIREMENT-GONE-1', name: 'REQUIREMENT-GONE-1' });
|
|
60
|
+
});
|
|
61
|
+
it('returns an empty view for a product with no assertions', () => {
|
|
62
|
+
expect(buildProductView('PRODUCT-NONE-1', idx).requirements).toEqual([]);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -1,84 +1,100 @@
|
|
|
1
|
-
// Canon-artefact classification (vkgeorgia/strategy#84). The single authority
|
|
2
|
-
// for "what is a compliance artefact" — used by both the Studio extension scan
|
|
3
|
-
// (webview previews) and the CLI scan (`export-compliance`), so the recognition
|
|
4
|
-
// rules live once. Pure: takes a parsed YAML document, no IO.
|
|
5
|
-
|
|
6
|
-
import type { AssertionStatus } from '../assertion/types.js';
|
|
7
|
-
import type { IndexAssertion, IndexRequirement } from './types.js';
|
|
8
|
-
|
|
9
|
-
export interface ComplianceProduct {
|
|
10
|
-
id: string;
|
|
11
|
-
name: string;
|
|
12
|
-
}
|
|
13
|
-
export interface ComplianceCodexDoc {
|
|
14
|
-
id: string;
|
|
15
|
-
name: string;
|
|
16
|
-
type?: string;
|
|
17
|
-
jurisdiction?: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** The bucketed result of scanning a repo for compliance canon. */
|
|
21
|
-
export interface ComplianceCanon {
|
|
22
|
-
products: ComplianceProduct[];
|
|
23
|
-
requirements: IndexRequirement[];
|
|
24
|
-
assertions: IndexAssertion[];
|
|
25
|
-
codex: ComplianceCodexDoc[];
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
canon.
|
|
70
|
-
id,
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
admitted_at: str(d.admitted_at),
|
|
75
|
-
|
|
76
|
-
});
|
|
77
|
-
return id;
|
|
78
|
-
}
|
|
79
|
-
if (d.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
1
|
+
// Canon-artefact classification (vkgeorgia/strategy#84). The single authority
|
|
2
|
+
// for "what is a compliance artefact" — used by both the Studio extension scan
|
|
3
|
+
// (webview previews) and the CLI scan (`export-compliance`), so the recognition
|
|
4
|
+
// rules live once. Pure: takes a parsed YAML document, no IO.
|
|
5
|
+
|
|
6
|
+
import type { AssertionStatus } from '../assertion/types.js';
|
|
7
|
+
import type { IndexAssertion, IndexRequirement } from './types.js';
|
|
8
|
+
|
|
9
|
+
export interface ComplianceProduct {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
}
|
|
13
|
+
export interface ComplianceCodexDoc {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
type?: string;
|
|
17
|
+
jurisdiction?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The bucketed result of scanning a repo for compliance canon. */
|
|
21
|
+
export interface ComplianceCanon {
|
|
22
|
+
products: ComplianceProduct[];
|
|
23
|
+
requirements: IndexRequirement[];
|
|
24
|
+
assertions: IndexAssertion[];
|
|
25
|
+
codex: ComplianceCodexDoc[];
|
|
26
|
+
/**
|
|
27
|
+
* Named elements that can serve as assertion subjects beyond `products`:
|
|
28
|
+
* capabilities, processes, applications, and systems. Populated by
|
|
29
|
+
* `ingestComplianceDoc` for the corresponding notation values.
|
|
30
|
+
*/
|
|
31
|
+
subjects: ComplianceProduct[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function emptyCanon(): ComplianceCanon {
|
|
35
|
+
return { products: [], requirements: [], assertions: [], codex: [], subjects: [] };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const str = (v: unknown): string | undefined => (typeof v === 'string' ? v : undefined);
|
|
39
|
+
const strArray = (v: unknown): string[] | undefined =>
|
|
40
|
+
Array.isArray(v) ? v.filter((x): x is string => typeof x === 'string') : undefined;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Classifies one parsed YAML document and, if it is a compliance artefact,
|
|
44
|
+
* pushes its projection into `canon`. Products / requirements / assertions are
|
|
45
|
+
* identified by their `notation` tag; codex source documents by `zone: codex`.
|
|
46
|
+
* Returns the artefact id when ingested (so the caller can record its path), or
|
|
47
|
+
* null when the document is not a (well-formed) compliance artefact.
|
|
48
|
+
*/
|
|
49
|
+
export function ingestComplianceDoc(canon: ComplianceCanon, doc: unknown): string | null {
|
|
50
|
+
if (doc === null || typeof doc !== 'object' || Array.isArray(doc)) return null;
|
|
51
|
+
const d = doc as Record<string, unknown>;
|
|
52
|
+
const id = str(d.id);
|
|
53
|
+
if (!id) return null;
|
|
54
|
+
|
|
55
|
+
if (d.notation === 'product') {
|
|
56
|
+
canon.products.push({ id, name: str(d.name) ?? id });
|
|
57
|
+
return id;
|
|
58
|
+
}
|
|
59
|
+
if (
|
|
60
|
+
d.notation === 'capability' ||
|
|
61
|
+
d.notation === 'process' ||
|
|
62
|
+
d.notation === 'application' ||
|
|
63
|
+
d.notation === 'system'
|
|
64
|
+
) {
|
|
65
|
+
canon.subjects.push({ id, name: str(d.name) ?? id });
|
|
66
|
+
return id;
|
|
67
|
+
}
|
|
68
|
+
if (d.notation === 'requirement') {
|
|
69
|
+
canon.requirements.push({
|
|
70
|
+
id,
|
|
71
|
+
name: str(d.name) ?? id,
|
|
72
|
+
severity: str(d.severity),
|
|
73
|
+
derived_from: strArray(d.derived_from),
|
|
74
|
+
admitted_at: str(d.admitted_at),
|
|
75
|
+
deadline: str(d.deadline),
|
|
76
|
+
});
|
|
77
|
+
return id;
|
|
78
|
+
}
|
|
79
|
+
if (d.notation === 'assertion') {
|
|
80
|
+
const about = str(d.about);
|
|
81
|
+
const subject = str(d.subject);
|
|
82
|
+
const status = str(d.status) as AssertionStatus | undefined;
|
|
83
|
+
if (!about || !subject || !status) return null;
|
|
84
|
+
canon.assertions.push({
|
|
85
|
+
id, about, subject, status,
|
|
86
|
+
assessed_at: str(d.assessed_at),
|
|
87
|
+
next_review_at: str(d.next_review_at),
|
|
88
|
+
evidenceCount: Array.isArray(d.evidence) ? d.evidence.length : 0,
|
|
89
|
+
admitted_at: str(d.admitted_at),
|
|
90
|
+
realised_via: strArray(d.realised_via),
|
|
91
|
+
owner_to_confirm: str(d.owner_to_confirm),
|
|
92
|
+
});
|
|
93
|
+
return id;
|
|
94
|
+
}
|
|
95
|
+
if (d.zone === 'codex') {
|
|
96
|
+
canon.codex.push({ id, name: str(d.name) ?? id, type: str(d.type), jurisdiction: str(d.jurisdiction) });
|
|
97
|
+
return id;
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|