@transitrix/diagrams 1.5.0 → 1.7.6

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.
Files changed (148) hide show
  1. package/dist/capability-map/CapabilityCardNode.d.ts +19 -0
  2. package/dist/capability-map/CapabilityCardNode.js +66 -0
  3. package/dist/capability-map/CapabilityMapView.d.ts +59 -0
  4. package/dist/capability-map/CapabilityMapView.js +204 -0
  5. package/dist/capability-map/CapabilityRootNode.d.ts +12 -0
  6. package/dist/capability-map/CapabilityRootNode.js +23 -0
  7. package/dist/capability-map/__tests__/CapabilityMapView.test.d.ts +1 -0
  8. package/dist/capability-map/__tests__/CapabilityMapView.test.js +217 -0
  9. package/dist/capability-map/__tests__/address.test.d.ts +1 -0
  10. package/dist/capability-map/__tests__/address.test.js +49 -0
  11. package/dist/capability-map/__tests__/dsm-layout.test.d.ts +1 -0
  12. package/dist/capability-map/__tests__/dsm-layout.test.js +46 -0
  13. package/dist/capability-map/__tests__/dsm-mutations.test.d.ts +1 -0
  14. package/dist/capability-map/__tests__/dsm-mutations.test.js +125 -0
  15. package/dist/capability-map/__tests__/dsm-validate.test.d.ts +1 -0
  16. package/dist/capability-map/__tests__/dsm-validate.test.js +56 -0
  17. package/dist/capability-map/address.d.ts +12 -0
  18. package/dist/capability-map/address.js +48 -0
  19. package/dist/capability-map/dsm-layout.d.ts +9 -0
  20. package/dist/capability-map/dsm-layout.js +125 -0
  21. package/dist/capability-map/dsm-mutations.d.ts +28 -0
  22. package/dist/capability-map/dsm-mutations.js +196 -0
  23. package/dist/capability-map/dsm-schema.d.ts +71 -0
  24. package/dist/capability-map/dsm-schema.js +14 -0
  25. package/dist/capability-map/dsm-theme.d.ts +20 -0
  26. package/dist/capability-map/dsm-theme.js +36 -0
  27. package/dist/capability-map/dsm-validate.d.ts +2 -0
  28. package/dist/capability-map/dsm-validate.js +93 -0
  29. package/dist/capability-map/index.d.ts +9 -0
  30. package/dist/capability-map/index.js +6 -0
  31. package/dist/codex/__tests__/example.test.d.ts +1 -0
  32. package/dist/codex/__tests__/example.test.js +33 -0
  33. package/dist/codex/__tests__/validate.test.d.ts +1 -0
  34. package/dist/codex/__tests__/validate.test.js +68 -0
  35. package/dist/codex/index.d.ts +4 -0
  36. package/dist/codex/index.js +2 -0
  37. package/dist/codex/types.d.ts +7 -0
  38. package/dist/codex/types.js +8 -0
  39. package/dist/codex/validate.d.ts +11 -0
  40. package/dist/codex/validate.js +103 -0
  41. package/dist/compliance/__tests__/canon-catalog.test.d.ts +1 -0
  42. package/dist/compliance/__tests__/canon-catalog.test.js +60 -0
  43. package/dist/compliance/__tests__/trace.test.d.ts +1 -0
  44. package/dist/compliance/__tests__/trace.test.js +144 -0
  45. package/dist/compliance/__tests__/view-resolution.test.d.ts +1 -0
  46. package/dist/compliance/__tests__/view-resolution.test.js +59 -0
  47. package/dist/compliance/canon-catalog.d.ts +19 -0
  48. package/dist/compliance/canon-catalog.js +59 -0
  49. package/dist/compliance/classify.js +7 -1
  50. package/dist/compliance/coverage-metric.d.ts +12 -0
  51. package/dist/compliance/coverage-metric.js +35 -0
  52. package/dist/compliance/impact.d.ts +7 -1
  53. package/dist/compliance/impact.js +53 -0
  54. package/dist/compliance/index.d.ts +8 -5
  55. package/dist/compliance/index.js +4 -2
  56. package/dist/compliance/reverse-index.js +4 -1
  57. package/dist/compliance/trace.d.ts +26 -0
  58. package/dist/compliance/trace.js +89 -0
  59. package/dist/compliance/types.d.ts +94 -1
  60. package/dist/constraint/__tests__/example.test.d.ts +1 -0
  61. package/dist/constraint/__tests__/example.test.js +14 -0
  62. package/dist/constraint/__tests__/validate.test.d.ts +1 -0
  63. package/dist/constraint/__tests__/validate.test.js +58 -0
  64. package/dist/constraint/index.d.ts +4 -0
  65. package/dist/constraint/index.js +2 -0
  66. package/dist/constraint/types.d.ts +4 -0
  67. package/dist/constraint/types.js +2 -0
  68. package/dist/constraint/validate.d.ts +7 -0
  69. package/dist/constraint/validate.js +115 -0
  70. package/dist/fgca/__tests__/parse-canonical.test.js +31 -0
  71. package/dist/fgca/parse-canonical.d.ts +1 -1
  72. package/dist/fgca/parse-canonical.js +18 -4
  73. package/dist/goals/GoalNode.d.ts +19 -0
  74. package/dist/goals/GoalNode.js +96 -0
  75. package/dist/goals/GoalTreeView.d.ts +59 -0
  76. package/dist/goals/GoalTreeView.js +245 -0
  77. package/dist/goals/__tests__/GoalTreeView.test.d.ts +1 -0
  78. package/dist/goals/__tests__/GoalTreeView.test.js +337 -0
  79. package/dist/goals/__tests__/mutations.test.js +39 -0
  80. package/dist/goals/__tests__/parse-canonical.test.js +2 -0
  81. package/dist/goals/index.d.ts +3 -0
  82. package/dist/goals/index.js +1 -0
  83. package/dist/goals/layout.js +2 -1
  84. package/dist/goals/mutations.js +6 -6
  85. package/dist/goals/parse-canonical.d.ts +5 -0
  86. package/dist/goals/parse-canonical.js +5 -0
  87. package/dist/goals/theme.d.ts +22 -0
  88. package/dist/goals/theme.js +32 -0
  89. package/dist/goals/types.d.ts +2 -0
  90. package/dist/index.d.ts +2 -1
  91. package/dist/index.js +1 -1
  92. package/dist/repo-validate/types.js +2 -3
  93. package/dist/webview/__tests__/render-goals.test.js +17 -3
  94. package/dist/webview/render-goals.js +2 -1
  95. package/package.json +4 -1
  96. package/src/capability-map/CapabilityCardNode.tsx +146 -0
  97. package/src/capability-map/CapabilityMapView.tsx +331 -0
  98. package/src/capability-map/CapabilityRootNode.tsx +38 -0
  99. package/src/capability-map/__tests__/CapabilityMapView.test.tsx +232 -0
  100. package/src/capability-map/__tests__/address.test.ts +59 -0
  101. package/src/capability-map/__tests__/dsm-layout.test.ts +52 -0
  102. package/src/capability-map/__tests__/dsm-mutations.test.ts +141 -0
  103. package/src/capability-map/__tests__/dsm-validate.test.ts +65 -0
  104. package/src/capability-map/address.ts +48 -0
  105. package/src/capability-map/dsm-layout.ts +142 -0
  106. package/src/capability-map/dsm-mutations.ts +208 -0
  107. package/src/capability-map/dsm-schema.ts +80 -0
  108. package/src/capability-map/dsm-theme.ts +53 -0
  109. package/src/capability-map/dsm-validate.ts +103 -0
  110. package/src/capability-map/index.ts +35 -0
  111. package/src/codex/__tests__/example.test.ts +40 -0
  112. package/src/codex/__tests__/validate.test.ts +84 -0
  113. package/src/codex/index.ts +4 -0
  114. package/src/codex/types.ts +13 -0
  115. package/src/codex/validate.ts +125 -0
  116. package/src/compliance/__tests__/canon-catalog.test.ts +68 -0
  117. package/src/compliance/__tests__/trace.test.ts +157 -0
  118. package/src/compliance/__tests__/view-resolution.test.ts +80 -0
  119. package/src/compliance/canon-catalog.ts +76 -0
  120. package/src/compliance/classify.ts +7 -1
  121. package/src/compliance/coverage-metric.ts +50 -0
  122. package/src/compliance/impact.ts +67 -1
  123. package/src/compliance/index.ts +16 -1
  124. package/src/compliance/reverse-index.ts +3 -1
  125. package/src/compliance/trace.ts +118 -0
  126. package/src/compliance/types.ts +101 -1
  127. package/src/constraint/__tests__/example.test.ts +19 -0
  128. package/src/constraint/__tests__/validate.test.ts +68 -0
  129. package/src/constraint/index.ts +4 -0
  130. package/src/constraint/types.ts +5 -0
  131. package/src/constraint/validate.ts +131 -0
  132. package/src/fgca/__tests__/parse-canonical.test.ts +34 -0
  133. package/src/fgca/parse-canonical.ts +17 -4
  134. package/src/goals/GoalNode.tsx +219 -0
  135. package/src/goals/GoalTreeView.tsx +384 -0
  136. package/src/goals/__tests__/GoalTreeView.test.tsx +361 -0
  137. package/src/goals/__tests__/mutations.test.ts +42 -0
  138. package/src/goals/__tests__/parse-canonical.test.ts +2 -0
  139. package/src/goals/index.ts +4 -0
  140. package/src/goals/layout.ts +2 -1
  141. package/src/goals/mutations.ts +6 -6
  142. package/src/goals/parse-canonical.ts +6 -0
  143. package/src/goals/theme.ts +54 -0
  144. package/src/goals/types.ts +2 -0
  145. package/src/index.ts +29 -1
  146. package/src/repo-validate/types.ts +2 -3
  147. package/src/webview/__tests__/render-goals.test.ts +17 -3
  148. package/src/webview/render-goals.ts +2 -1
@@ -0,0 +1,144 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { buildComplianceIndex } from '../reverse-index.js';
3
+ import { buildRequirementTrace, buildTraceElementCatalog } from '../trace.js';
4
+ // Requirement traceability + hierarchy view.
5
+ //
6
+ // The two halves the view exposes:
7
+ // 1. Trace chain — derived_from → REQUIREMENT → ASSERTION → subject +
8
+ // realised_via.
9
+ // 2. Hierarchy — parent chain + children via the `parent` field
10
+ // (15-requirement.md §2.4 / ELEMENT_PRIMITIVES.md §7.13).
11
+ const input = {
12
+ requirements: [
13
+ // A broad legislative parent decomposed into two child requirements.
14
+ { id: 'REQUIREMENT-DATA-PROTECTION-1', name: 'Personal-data protection', origin: 'legislative', derived_from: ['REGULATION-GDPR-2016-1'], element_kind: 'requirement' },
15
+ { id: 'REQUIREMENT-DATA-ERASURE-1', name: 'Erase on request within 30 days', origin: 'legislative', parent: 'REQUIREMENT-DATA-PROTECTION-1', derived_from: ['REGULATION-GDPR-2016-1'], element_kind: 'requirement' },
16
+ { id: 'REQUIREMENT-DATA-CONSENT-1', name: 'Obtain consent before processing', origin: 'legislative', parent: 'REQUIREMENT-DATA-PROTECTION-1', element_kind: 'requirement' },
17
+ // A process-product sub-requirement below the erasure duty (cross-origin).
18
+ { id: 'REQUIREMENT-ERASURE-SLA-1', name: 'Erasure SOP — 30d target', origin: 'process-product', parent: 'REQUIREMENT-DATA-ERASURE-1', element_kind: 'requirement' },
19
+ // Constraint hierarchy (mirror pattern).
20
+ { id: 'CONSTRAINT-EEA-TRANSFER-1', name: 'No transfer outside EEA without safeguards', element_kind: 'constraint' },
21
+ { id: 'CONSTRAINT-EEA-ANALYTICS-1', name: 'No PII in analytics logs', parent: 'CONSTRAINT-EEA-TRANSFER-1', element_kind: 'constraint' },
22
+ // Internal-only requirement — no source, no parent — for the empty-case sanity check.
23
+ { id: 'REQUIREMENT-INTERNAL-1', name: 'Internal-only', element_kind: 'requirement' },
24
+ ],
25
+ assertions: [
26
+ { id: 'ASSERTION-1', about: 'REQUIREMENT-DATA-ERASURE-1', subject: 'PRODUCT-MOBILE-1', realised_via: ['CAPABILITY-V1', 'PROCESS-PURGE-1'], status: 'compliant' },
27
+ { id: 'ASSERTION-2', about: 'REQUIREMENT-DATA-ERASURE-1', subject: 'PRODUCT-WEB-1', status: 'partial' },
28
+ { id: 'ASSERTION-3', about: 'REQUIREMENT-DATA-CONSENT-1', subject: 'PRODUCT-WEB-1', status: 'compliant' },
29
+ ],
30
+ };
31
+ const codex = [
32
+ { id: 'REGULATION-GDPR-2016-1', name: 'GDPR', type: 'REGULATION', jurisdiction: 'EU' },
33
+ ];
34
+ const catalog = buildTraceElementCatalog([{ id: 'PRODUCT-MOBILE-1', name: 'Mobile app' }, { id: 'PRODUCT-WEB-1', name: 'Web app' }], [{ id: 'CAPABILITY-V1', name: 'Erasure capability v1' }, { id: 'PROCESS-PURGE-1', name: 'Purge process' }]);
35
+ describe('buildComplianceIndex — requirementsByParent', () => {
36
+ const idx = buildComplianceIndex(input);
37
+ it('groups children of the same parent', () => {
38
+ const children = idx.requirementsByParent.get('REQUIREMENT-DATA-PROTECTION-1');
39
+ expect(children?.map(r => r.id).sort()).toEqual(['REQUIREMENT-DATA-CONSENT-1', 'REQUIREMENT-DATA-ERASURE-1']);
40
+ });
41
+ it('groups CONSTRAINT children the same way (element_kind-agnostic)', () => {
42
+ const children = idx.requirementsByParent.get('CONSTRAINT-EEA-TRANSFER-1');
43
+ expect(children?.map(r => r.id)).toEqual(['CONSTRAINT-EEA-ANALYTICS-1']);
44
+ });
45
+ it('returns undefined for a leaf with no children', () => {
46
+ expect(idx.requirementsByParent.has('REQUIREMENT-INTERNAL-1')).toBe(false);
47
+ });
48
+ });
49
+ describe('buildRequirementTrace — trace chain', () => {
50
+ const idx = buildComplianceIndex(input);
51
+ it('resolves derived_from codex artefacts on the sources block', () => {
52
+ const trace = buildRequirementTrace('REQUIREMENT-DATA-ERASURE-1', idx, catalog, codex);
53
+ expect(trace.sources).toHaveLength(1);
54
+ expect(trace.sources[0].id).toBe('REGULATION-GDPR-2016-1');
55
+ expect(trace.sources[0].codex?.jurisdiction).toBe('EU');
56
+ });
57
+ it('keeps a dangling derived_from ref with no codex when the artefact is missing', () => {
58
+ const local = {
59
+ requirements: [{ id: 'REQUIREMENT-X-1', name: 'X', derived_from: ['LAW-MISSING-1'] }],
60
+ assertions: [],
61
+ };
62
+ const trace = buildRequirementTrace('REQUIREMENT-X-1', buildComplianceIndex(local), catalog);
63
+ expect(trace.sources).toEqual([{ id: 'LAW-MISSING-1' }]);
64
+ });
65
+ it('lists ASSERTIONs id-sorted, each with resolved subject + realised_via names', () => {
66
+ const trace = buildRequirementTrace('REQUIREMENT-DATA-ERASURE-1', idx, catalog, codex);
67
+ expect(trace.assertions.map(a => a.assertion.id)).toEqual(['ASSERTION-1', 'ASSERTION-2']);
68
+ expect(trace.assertions[0].subject).toEqual({ id: 'PRODUCT-MOBILE-1', name: 'Mobile app' });
69
+ expect(trace.assertions[0].realisedVia.map(r => r.name)).toEqual(['Erasure capability v1', 'Purge process']);
70
+ });
71
+ it('returns an empty assertions list for a REQUIREMENT with no filed claim', () => {
72
+ const trace = buildRequirementTrace('REQUIREMENT-DATA-PROTECTION-1', idx, catalog);
73
+ expect(trace.assertions).toEqual([]);
74
+ });
75
+ it('returns an empty assertions list for a CONSTRAINT (16-assertion.md §1 — CONSTRAINT-side out of v1)', () => {
76
+ const trace = buildRequirementTrace('CONSTRAINT-EEA-TRANSFER-1', idx, catalog);
77
+ expect(trace.assertions).toEqual([]);
78
+ });
79
+ it('falls back to id when the subject / realised_via element is not in the catalog (dangling ref)', () => {
80
+ const local = {
81
+ requirements: [{ id: 'REQUIREMENT-Y-1', name: 'Y' }],
82
+ assertions: [{ id: 'ASSERTION-9', about: 'REQUIREMENT-Y-1', subject: 'PRODUCT-GONE-1', realised_via: ['CAPABILITY-GONE-1'], status: 'n_a' }],
83
+ };
84
+ const trace = buildRequirementTrace('REQUIREMENT-Y-1', buildComplianceIndex(local), catalog);
85
+ expect(trace.assertions[0].subject).toEqual({ id: 'PRODUCT-GONE-1' });
86
+ expect(trace.assertions[0].realisedVia).toEqual([{ id: 'CAPABILITY-GONE-1' }]);
87
+ });
88
+ });
89
+ describe('buildRequirementTrace — hierarchy', () => {
90
+ const idx = buildComplianceIndex(input);
91
+ it('walks ancestors: immediate parent first, root last', () => {
92
+ const trace = buildRequirementTrace('REQUIREMENT-ERASURE-SLA-1', idx, catalog);
93
+ expect(trace.ancestors.map(r => r.id)).toEqual([
94
+ 'REQUIREMENT-DATA-ERASURE-1',
95
+ 'REQUIREMENT-DATA-PROTECTION-1',
96
+ ]);
97
+ });
98
+ it('lists direct children id-sorted', () => {
99
+ const trace = buildRequirementTrace('REQUIREMENT-DATA-PROTECTION-1', idx, catalog);
100
+ expect(trace.children.map(r => r.id)).toEqual([
101
+ 'REQUIREMENT-DATA-CONSENT-1',
102
+ 'REQUIREMENT-DATA-ERASURE-1',
103
+ ]);
104
+ });
105
+ it('handles a top-level requirement (no ancestors) with no children', () => {
106
+ const trace = buildRequirementTrace('REQUIREMENT-INTERNAL-1', idx, catalog);
107
+ expect(trace.ancestors).toEqual([]);
108
+ expect(trace.children).toEqual([]);
109
+ });
110
+ it('supports CONSTRAINT hierarchy the same way', () => {
111
+ const trace = buildRequirementTrace('CONSTRAINT-EEA-ANALYTICS-1', idx, catalog);
112
+ expect(trace.ancestors.map(r => r.id)).toEqual(['CONSTRAINT-EEA-TRANSFER-1']);
113
+ });
114
+ it('terminates at a missing parent without throwing (broken model)', () => {
115
+ const local = {
116
+ requirements: [{ id: 'REQUIREMENT-ORPHAN-1', name: 'Orphan', parent: 'REQUIREMENT-GONE-1' }],
117
+ assertions: [],
118
+ };
119
+ const trace = buildRequirementTrace('REQUIREMENT-ORPHAN-1', buildComplianceIndex(local), catalog);
120
+ expect(trace.ancestors).toEqual([]);
121
+ });
122
+ it('breaks a parent cycle rather than looping', () => {
123
+ // A → B → A (misauthored).
124
+ const local = {
125
+ requirements: [
126
+ { id: 'REQUIREMENT-A-1', name: 'A', parent: 'REQUIREMENT-B-1' },
127
+ { id: 'REQUIREMENT-B-1', name: 'B', parent: 'REQUIREMENT-A-1' },
128
+ ],
129
+ assertions: [],
130
+ };
131
+ const trace = buildRequirementTrace('REQUIREMENT-A-1', buildComplianceIndex(local), catalog);
132
+ expect(trace.ancestors.map(r => r.id)).toEqual(['REQUIREMENT-B-1']);
133
+ });
134
+ });
135
+ describe('buildRequirementTrace — dangling target', () => {
136
+ it('returns a stub carrying the id as name when the requirement is not scanned', () => {
137
+ const trace = buildRequirementTrace('REQUIREMENT-UNKNOWN-1', buildComplianceIndex({ requirements: [], assertions: [] }), catalog);
138
+ expect(trace.requirement).toEqual({ id: 'REQUIREMENT-UNKNOWN-1', name: 'REQUIREMENT-UNKNOWN-1' });
139
+ expect(trace.sources).toEqual([]);
140
+ expect(trace.assertions).toEqual([]);
141
+ expect(trace.ancestors).toEqual([]);
142
+ expect(trace.children).toEqual([]);
143
+ });
144
+ });
@@ -0,0 +1,59 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { emptyCanon } from '../classify.js';
3
+ import { catalogFromMap } from '../canon-catalog.js';
4
+ import { collectImpactViewResolutionFindings } from '../impact.js';
5
+ import { collectCoverageViewResolutionFindings } from '../coverage-metric.js';
6
+ const catalog = catalogFromMap(new Map([
7
+ ['PRODUCT-ECOMM-1', 'PRODUCT'],
8
+ ['REQUIREMENT-GDPR-1', 'REQUIREMENT'],
9
+ ['LAW-GDPR-1', 'LAW'],
10
+ ]));
11
+ const complianceCanon = emptyCanon();
12
+ complianceCanon.products.push({ id: 'PRODUCT-ECOMM-1', name: 'E-Commerce' });
13
+ complianceCanon.requirements.push({ id: 'REQUIREMENT-GDPR-1', name: 'GDPR req' });
14
+ complianceCanon.codex.push({ id: 'LAW-GDPR-1', name: 'GDPR' });
15
+ describe('collectImpactViewResolutionFindings', () => {
16
+ const base = {
17
+ id: 'test-impact',
18
+ name: 'Test',
19
+ subjects: { products: ['PRODUCT-ECOMM-1'], processes: [], capabilities: [] },
20
+ obligations: { filter: { derived_from_codex: ['LAW-GDPR-1'] } },
21
+ status_display: { show: ['compliant'] },
22
+ empty_cells: {},
23
+ order_rows_by: 'id',
24
+ };
25
+ it('passes when subjects and regimes resolve', () => {
26
+ expect(collectImpactViewResolutionFindings(base, catalog, complianceCanon)).toEqual([]);
27
+ });
28
+ it('flags unresolved subjects and regimes', () => {
29
+ const findings = collectImpactViewResolutionFindings({
30
+ ...base,
31
+ subjects: { products: ['PRODUCT-MISSING-1'], processes: [], capabilities: [] },
32
+ obligations: { include: ['REQUIREMENT-MISSING-1'], filter: { derived_from_codex: ['LAW-MISSING-1'] } },
33
+ }, catalog, complianceCanon);
34
+ expect(findings.some((f) => f.code === 'COMPIMP-REF' && f.message.includes('PRODUCT-MISSING-1'))).toBe(true);
35
+ expect(findings.some((f) => f.message.includes('REQUIREMENT-MISSING-1'))).toBe(true);
36
+ expect(findings.some((f) => f.message.includes('LAW-MISSING-1'))).toBe(true);
37
+ });
38
+ });
39
+ describe('collectCoverageViewResolutionFindings', () => {
40
+ const base = {
41
+ id: 'test-coverage',
42
+ name: 'Test',
43
+ thresholds: { green: 0.9, amber: 0.7 },
44
+ subjects: { products: ['PRODUCT-ECOMM-1'] },
45
+ regimes: { include: ['LAW-GDPR-1'] },
46
+ };
47
+ it('passes when products and regimes resolve', () => {
48
+ expect(collectCoverageViewResolutionFindings(base, catalog, complianceCanon)).toEqual([]);
49
+ });
50
+ it('flags unresolved product and regime refs', () => {
51
+ const findings = collectCoverageViewResolutionFindings({
52
+ ...base,
53
+ subjects: { products: ['PRODUCT-MISSING-1'] },
54
+ regimes: { include: ['LAW-MISSING-1'] },
55
+ }, catalog, complianceCanon);
56
+ expect(findings.some((f) => f.code === 'COVMET-REF')).toBe(true);
57
+ expect(findings).toHaveLength(2);
58
+ });
59
+ });
@@ -0,0 +1,19 @@
1
+ import { type CanonCatalog } from '../typed-id.js';
2
+ import { type ComplianceCanon } from './classify.js';
3
+ export interface ScannedYamlDoc {
4
+ path: string;
5
+ data: unknown;
6
+ }
7
+ export interface ComplianceScanResult {
8
+ complianceCanon: ComplianceCanon;
9
+ catalog: CanonCatalog;
10
+ /** First path where each admitted id was seen (for gap-dashboard warnings). */
11
+ pathById: Map<string, string>;
12
+ }
13
+ /** Admit one parsed document into the id → TYPE map. */
14
+ export declare function admitDocumentToCatalog(map: Map<string, string>, doc: unknown): string | undefined;
15
+ /** Build a read-only `CanonCatalog` from a populated id → TYPE map. */
16
+ export declare function catalogFromMap(map: Map<string, string>): CanonCatalog;
17
+ /** Ingest compliance projections and admit cross-reference targets from a batch
18
+ * of already-parsed YAML documents (no IO). */
19
+ export declare function buildComplianceScan(docs: ScannedYamlDoc[]): ComplianceScanResult;
@@ -0,0 +1,59 @@
1
+ // CanonCatalog builder — repo-scope cross-reference resolution (#518 Phase C3).
2
+ //
3
+ // Scans admitted YAML documents and builds the `{ typeOf }` catalogue that
4
+ // `validateRequirement` / `validateAssertion` accept for REQ-002 and
5
+ // ASSERT-002..005 resolution.
6
+ import { typeOfId } from '../typed-id.js';
7
+ import { emptyCanon, ingestComplianceDoc } from './classify.js';
8
+ function strId(doc) {
9
+ if (doc === null || typeof doc !== 'object' || Array.isArray(doc))
10
+ return undefined;
11
+ const id = doc.id;
12
+ return typeof id === 'string' && id.trim() !== '' ? id.trim() : undefined;
13
+ }
14
+ /** Admit one parsed document into the id → TYPE map. */
15
+ export function admitDocumentToCatalog(map, doc) {
16
+ const id = strId(doc);
17
+ if (!id)
18
+ return undefined;
19
+ const fromPrefix = typeOfId(id);
20
+ if (fromPrefix) {
21
+ map.set(id, fromPrefix);
22
+ return id;
23
+ }
24
+ if (doc !== null && typeof doc === 'object' && !Array.isArray(doc)) {
25
+ const d = doc;
26
+ if (d.zone === 'codex') {
27
+ const explicit = typeof d.type === 'string' ? d.type.trim().toUpperCase() : undefined;
28
+ if (explicit) {
29
+ map.set(id, explicit);
30
+ return id;
31
+ }
32
+ }
33
+ }
34
+ return undefined;
35
+ }
36
+ /** Build a read-only `CanonCatalog` from a populated id → TYPE map. */
37
+ export function catalogFromMap(map) {
38
+ return { typeOf: (id) => map.get(id) };
39
+ }
40
+ /** Ingest compliance projections and admit cross-reference targets from a batch
41
+ * of already-parsed YAML documents (no IO). */
42
+ export function buildComplianceScan(docs) {
43
+ const complianceCanon = emptyCanon();
44
+ const catalogMap = new Map();
45
+ const pathById = new Map();
46
+ for (const { path, data } of docs) {
47
+ const ingested = ingestComplianceDoc(complianceCanon, data);
48
+ if (ingested)
49
+ pathById.set(ingested, path);
50
+ const admitted = admitDocumentToCatalog(catalogMap, data);
51
+ if (admitted && !pathById.has(admitted))
52
+ pathById.set(admitted, path);
53
+ }
54
+ return {
55
+ complianceCanon,
56
+ catalog: catalogFromMap(catalogMap),
57
+ pathById,
58
+ };
59
+ }
@@ -32,7 +32,8 @@ export function ingestComplianceDoc(canon, doc) {
32
32
  canon.subjects.push({ id, name: str(d.name) ?? id });
33
33
  return id;
34
34
  }
35
- if (d.notation === 'requirement') {
35
+ if (d.notation === 'requirement' || d.notation === 'constraint') {
36
+ const origin = str(d.origin);
36
37
  canon.requirements.push({
37
38
  id,
38
39
  name: str(d.name) ?? id,
@@ -40,6 +41,11 @@ export function ingestComplianceDoc(canon, doc) {
40
41
  derived_from: strArray(d.derived_from),
41
42
  admitted_at: str(d.admitted_at),
42
43
  deadline: str(d.deadline),
44
+ origin: origin === 'legislative' || origin === 'process-product' || origin === 'project-product' ? origin : undefined,
45
+ parent: str(d.parent),
46
+ element_kind: d.notation,
47
+ description: str(d.description),
48
+ next_review_at: str(d.next_review_at),
43
49
  });
44
50
  return id;
45
51
  }
@@ -1,4 +1,5 @@
1
1
  import type { ComplianceCanon } from './classify.js';
2
+ import type { CanonCatalog } from '../typed-id.js';
2
3
  /** Declarative filter resolved at build time from the canon's codex catalogue. */
3
4
  export interface CoverageMetricRegimesFilter {
4
5
  jurisdiction?: string[];
@@ -73,6 +74,17 @@ export interface CoverageMatrix {
73
74
  rows: CoverageRow[];
74
75
  thresholds: CoverageMetricThresholds;
75
76
  }
77
+ /** Repo-scope cross-reference finding for a coverage-metric view (#518 C3). */
78
+ export interface CoverageViewFinding {
79
+ code: string;
80
+ severity: 'error' | 'warning';
81
+ message: string;
82
+ }
83
+ /**
84
+ * Repo-scope cross-reference checks for a parsed coverage-metric view config.
85
+ * Surfaces unresolved product and regime ids before `buildCoverageMatrix` runs.
86
+ */
87
+ export declare function collectCoverageViewResolutionFindings(config: CoverageMetricConfig, catalog: CanonCatalog, complianceCanon: ComplianceCanon): CoverageViewFinding[];
76
88
  /**
77
89
  * Parse and validate a raw (YAML-parsed) document into a `CoverageMetricConfig`.
78
90
  *
@@ -5,6 +5,41 @@
5
5
  // across the scoped products, and what is the RAG status against the configured
6
6
  // thresholds.
7
7
  import { buildComplianceIndex } from './reverse-index.js';
8
+ /**
9
+ * Repo-scope cross-reference checks for a parsed coverage-metric view config.
10
+ * Surfaces unresolved product and regime ids before `buildCoverageMatrix` runs.
11
+ */
12
+ export function collectCoverageViewResolutionFindings(config, catalog, complianceCanon) {
13
+ const findings = [];
14
+ const codexIds = new Set(complianceCanon.codex.map((c) => c.id));
15
+ for (const id of config.subjects?.products ?? []) {
16
+ const t = catalog.typeOf(id);
17
+ if (t === undefined) {
18
+ findings.push({
19
+ code: 'COVMET-REF',
20
+ severity: 'error',
21
+ message: `subjects.products: "${id}" does not resolve to an admitted artefact.`,
22
+ });
23
+ }
24
+ else if (t !== 'PRODUCT') {
25
+ findings.push({
26
+ code: 'COVMET-REF',
27
+ severity: 'error',
28
+ message: `subjects.products: "${id}" resolves to ${t}, not PRODUCT.`,
29
+ });
30
+ }
31
+ }
32
+ for (const id of config.regimes?.include ?? []) {
33
+ if (!codexIds.has(id)) {
34
+ findings.push({
35
+ code: 'COVMET-REF',
36
+ severity: 'error',
37
+ message: `regimes.include: "${id}" does not resolve to an admitted codex artefact.`,
38
+ });
39
+ }
40
+ }
41
+ return findings;
42
+ }
8
43
  // ── Parser ───────────────────────────────────────────────────────────────────
9
44
  function parseRegimes(raw) {
10
45
  const warnings = [];
@@ -1,5 +1,6 @@
1
1
  import type { AssertionStatus } from '../assertion/types.js';
2
2
  import type { ComplianceCanon } from './classify.js';
3
+ import type { CanonCatalog } from '../typed-id.js';
3
4
  import type { IndexAssertion, IndexRequirement, ObjectDetailInput, DeadlineStatus } from './types.js';
4
5
  /** Filter selecting REQUIREMENTs by codex source (jurisdiction / regime keys
5
6
  * are accepted for forward compatibility but not yet honoured — the canon
@@ -57,12 +58,17 @@ export interface ImpactColumn {
57
58
  * Used to enforce the subject type label invariant (§5.2, COMPIMP-010). */
58
59
  subjectType: 'product' | 'process' | 'capability';
59
60
  }
60
- /** A structural validation finding emitted by the matrix builder. */
61
61
  export interface ImpactFinding {
62
62
  code: string;
63
63
  severity: 'error' | 'warning';
64
64
  message: string;
65
65
  }
66
+ /**
67
+ * Repo-scope cross-reference checks for a parsed compliance-impact view config
68
+ * (#518 Phase C3). Surfaces unresolved subject, obligation, and regime ids before
69
+ * `buildImpactMatrix` runs.
70
+ */
71
+ export declare function collectImpactViewResolutionFindings(config: ImpactViewConfig, catalog: CanonCatalog, complianceCanon: ComplianceCanon): ImpactFinding[];
66
72
  /**
67
73
  * Grouping options for matrix columns.
68
74
  *
@@ -7,6 +7,59 @@
7
7
  // (`product` columns × `obligation` rows). Stage / task grouping is planned
8
8
  // for CV-3a and intentionally out of scope here.
9
9
  import { buildComplianceIndex } from './reverse-index.js';
10
+ /**
11
+ * Repo-scope cross-reference checks for a parsed compliance-impact view config
12
+ * (#518 Phase C3). Surfaces unresolved subject, obligation, and regime ids before
13
+ * `buildImpactMatrix` runs.
14
+ */
15
+ export function collectImpactViewResolutionFindings(config, catalog, complianceCanon) {
16
+ const findings = [];
17
+ const codexIds = new Set(complianceCanon.codex.map((c) => c.id));
18
+ const subjectChecks = [
19
+ { ids: config.subjects.products, expectedType: 'PRODUCT', path: 'subjects.products' },
20
+ { ids: config.subjects.processes, expectedType: 'PROCESS', path: 'subjects.processes' },
21
+ { ids: config.subjects.capabilities, expectedType: 'CAPABILITY', path: 'subjects.capabilities' },
22
+ ];
23
+ for (const { ids, expectedType, path } of subjectChecks) {
24
+ for (const id of ids ?? []) {
25
+ const t = catalog.typeOf(id);
26
+ if (t === undefined) {
27
+ findings.push({
28
+ code: 'COMPIMP-REF',
29
+ severity: 'error',
30
+ message: `${path}: "${id}" does not resolve to an admitted artefact.`,
31
+ });
32
+ }
33
+ else if (t !== expectedType) {
34
+ findings.push({
35
+ code: 'COMPIMP-REF',
36
+ severity: 'error',
37
+ message: `${path}: "${id}" resolves to ${t}, not ${expectedType}.`,
38
+ });
39
+ }
40
+ }
41
+ }
42
+ for (const id of config.obligations.include ?? []) {
43
+ const t = catalog.typeOf(id);
44
+ if (t === undefined || t !== 'REQUIREMENT') {
45
+ findings.push({
46
+ code: 'COMPIMP-REF',
47
+ severity: 'error',
48
+ message: `obligations.include: "${id}" does not resolve to an admitted REQUIREMENT.`,
49
+ });
50
+ }
51
+ }
52
+ for (const id of config.obligations.filter?.derived_from_codex ?? []) {
53
+ if (!codexIds.has(id)) {
54
+ findings.push({
55
+ code: 'COMPIMP-REF',
56
+ severity: 'error',
57
+ message: `obligations.filter.derived_from_codex: "${id}" does not resolve to an admitted codex artefact.`,
58
+ });
59
+ }
60
+ }
61
+ return findings;
62
+ }
10
63
  // ── Pinned defaults ─────────────────────────────────────────────────────────
11
64
  /**
12
65
  * Explicit defaults for every optional field in ImpactViewConfig.
@@ -1,16 +1,19 @@
1
1
  export { buildComplianceIndex } from './reverse-index.js';
2
2
  export { buildLawTree, buildProductView } from './views.js';
3
+ export { buildRequirementTrace, buildTraceElementCatalog } from './trace.js';
3
4
  export { buildGapReport } from './gap-report.js';
4
5
  export type { GapReport, GapReportOptions } from './gap-report.js';
5
6
  export { emptyCanon, ingestComplianceDoc } from './classify.js';
6
7
  export type { ComplianceCanon, ComplianceProduct, ComplianceCodexDoc } from './classify.js';
8
+ export { admitDocumentToCatalog, buildComplianceScan, catalogFromMap, } from './canon-catalog.js';
9
+ export type { ComplianceScanResult, ScannedYamlDoc } from './canon-catalog.js';
7
10
  export { renderComplianceMarkdown } from './markdown.js';
8
11
  export type { ReportScope, MarkdownOptions } from './markdown.js';
9
12
  export { renderComplianceHtml, renderImpactMatrixHtml } from './html.js';
10
13
  export type { HtmlOptions, ImpactMatrixHtmlOptions } from './html.js';
11
- export { buildImpactMatrix, renderImpactMarkdown, parseImpactViewConfig, COMPLIANCE_IMPACT_DEFAULTS, extractObjectDetails, extractProcessFlowTasks, mergeStageTaskDetails, computeDeadlineStatus } from './impact.js';
12
- export type { ImpactCell, ImpactColumn, ImpactEmptyCellLabels, ImpactGrouping, ImpactMatrix, ImpactObligationFilter, ImpactStatusDisplay, ImpactSubjects, ImpactViewConfig, ParseImpactViewConfigResult, } from './impact.js';
14
+ export { buildImpactMatrix, renderImpactMarkdown, parseImpactViewConfig, collectImpactViewResolutionFindings, COMPLIANCE_IMPACT_DEFAULTS, extractObjectDetails, extractProcessFlowTasks, mergeStageTaskDetails, computeDeadlineStatus } from './impact.js';
15
+ export type { ImpactCell, ImpactColumn, ImpactEmptyCellLabels, ImpactFinding, ImpactGrouping, ImpactMatrix, ImpactObligationFilter, ImpactStatusDisplay, ImpactSubjects, ImpactViewConfig, ParseImpactViewConfigResult, } from './impact.js';
13
16
  export { assertionToScoringElement, complianceConfidenceHeader, requirementToScoringElement, scoreComplianceView, } from './confidence.js';
14
- export { parseCoverageMetricConfig, buildCoverageMatrix, } from './coverage-metric.js';
15
- export type { CoverageMetricConfig, CoverageMetricRegimes, CoverageMetricRegimesFilter, CoverageMetricSubjects, CoverageMetricThresholds, CoverageMatrix, CoverageRow, ParseCoverageMetricResult, RagStatus, } from './coverage-metric.js';
16
- export type { ComplianceIndex, ComplianceIndexInput, DeadlineStatus, IndexRequirement, IndexAssertion, LawTree, LawTreeRequirement, ProductView, ProductRequirementStatus, ObjectDetailDef, ObjectDetailInput, } from './types.js';
17
+ export { parseCoverageMetricConfig, buildCoverageMatrix, collectCoverageViewResolutionFindings, } from './coverage-metric.js';
18
+ export type { CoverageMetricConfig, CoverageMetricRegimes, CoverageMetricRegimesFilter, CoverageMetricSubjects, CoverageMetricThresholds, CoverageMatrix, CoverageRow, CoverageViewFinding, ParseCoverageMetricResult, RagStatus, } from './coverage-metric.js';
19
+ export type { ComplianceIndex, ComplianceIndexInput, DeadlineStatus, IndexRequirement, IndexAssertion, LawTree, LawTreeRequirement, ProductView, ProductRequirementStatus, ObjectDetailDef, ObjectDetailInput, RequirementTrace, TraceAssertionRow, TraceElementCatalog, TraceElementRef, TraceSourceRef, } from './types.js';
@@ -1,9 +1,11 @@
1
1
  export { buildComplianceIndex } from './reverse-index.js';
2
2
  export { buildLawTree, buildProductView } from './views.js';
3
+ export { buildRequirementTrace, buildTraceElementCatalog } from './trace.js';
3
4
  export { buildGapReport } from './gap-report.js';
4
5
  export { emptyCanon, ingestComplianceDoc } from './classify.js';
6
+ export { admitDocumentToCatalog, buildComplianceScan, catalogFromMap, } from './canon-catalog.js';
5
7
  export { renderComplianceMarkdown } from './markdown.js';
6
8
  export { renderComplianceHtml, renderImpactMatrixHtml } from './html.js';
7
- export { buildImpactMatrix, renderImpactMarkdown, parseImpactViewConfig, COMPLIANCE_IMPACT_DEFAULTS, extractObjectDetails, extractProcessFlowTasks, mergeStageTaskDetails, computeDeadlineStatus } from './impact.js';
9
+ export { buildImpactMatrix, renderImpactMarkdown, parseImpactViewConfig, collectImpactViewResolutionFindings, COMPLIANCE_IMPACT_DEFAULTS, extractObjectDetails, extractProcessFlowTasks, mergeStageTaskDetails, computeDeadlineStatus } from './impact.js';
8
10
  export { assertionToScoringElement, complianceConfidenceHeader, requirementToScoringElement, scoreComplianceView, } from './confidence.js';
9
- export { parseCoverageMetricConfig, buildCoverageMatrix, } from './coverage-metric.js';
11
+ export { parseCoverageMetricConfig, buildCoverageMatrix, collectCoverageViewResolutionFindings, } from './coverage-metric.js';
@@ -17,14 +17,17 @@ export function buildComplianceIndex(input) {
17
17
  const requirementsByLaw = new Map();
18
18
  const assertionsByRequirement = new Map();
19
19
  const assertionsBySubject = new Map();
20
+ const requirementsByParent = new Map();
20
21
  for (const r of input.requirements) {
21
22
  requirementById.set(r.id, r);
22
23
  for (const law of r.derived_from ?? [])
23
24
  push(requirementsByLaw, law, r);
25
+ if (r.parent)
26
+ push(requirementsByParent, r.parent, r);
24
27
  }
25
28
  for (const a of input.assertions) {
26
29
  push(assertionsByRequirement, a.about, a);
27
30
  push(assertionsBySubject, a.subject, a);
28
31
  }
29
- return { requirementById, requirementsByLaw, assertionsByRequirement, assertionsBySubject };
32
+ return { requirementById, requirementsByLaw, assertionsByRequirement, assertionsBySubject, requirementsByParent };
30
33
  }
@@ -0,0 +1,26 @@
1
+ import type { ComplianceIndex, RequirementTrace, TraceElementCatalog } from './types.js';
2
+ import type { ComplianceCodexDoc } from './classify.js';
3
+ /**
4
+ * Builds the REQUIREMENT / CONSTRAINT traceability view.
5
+ *
6
+ * @param requirementId typed id of the element to trace
7
+ * @param index reverse index built by `buildComplianceIndex`
8
+ * @param catalog name-resolution map for subject / realised_via refs
9
+ * @param codex optional codex artefact list (from the scanner); when
10
+ * supplied, `sources[].codex` is populated with the
11
+ * resolved codex artefact for jurisdiction display
12
+ */
13
+ export declare function buildRequirementTrace(requirementId: string, index: ComplianceIndex, catalog: TraceElementCatalog, codex?: ComplianceCodexDoc[]): RequirementTrace;
14
+ /**
15
+ * Convenience: build a `TraceElementCatalog` from the scanned canon's product
16
+ * and subject buckets. Requirements/assertions are excluded — they are indexed
17
+ * separately by `buildComplianceIndex`; the catalog is only for resolving the
18
+ * elements a trace references (subjects + realised_via).
19
+ */
20
+ export declare function buildTraceElementCatalog(products: Array<{
21
+ id: string;
22
+ name: string;
23
+ }>, subjects: Array<{
24
+ id: string;
25
+ name: string;
26
+ }>): TraceElementCatalog;
@@ -0,0 +1,89 @@
1
+ // Requirement traceability + hierarchy builder.
2
+ //
3
+ // Two halves, both origin-agnostic per 15-requirement.md §2.1:
4
+ //
5
+ // 1. Trace chain — `derived_from` sources → the REQUIREMENT / CONSTRAINT
6
+ // itself → any ASSERTION targeting it (`about`) → the asserted `subject`
7
+ // + `realised_via` elements. Assertion coverage applies to REQUIREMENT
8
+ // only (16-assertion.md §1); CONSTRAINT trace shows sources + hierarchy
9
+ // only.
10
+ //
11
+ // 2. Hierarchy — the `parent` chain (ancestors, root last) plus the direct
12
+ // children (elements whose `parent` names this one). Same-TYPE only per
13
+ // 15-requirement.md §2.4; the chain terminates at the first missing or
14
+ // cyclic parent so a broken model still renders something useful.
15
+ //
16
+ // Pure: takes the reverse index + a name-resolution map + optional codex list;
17
+ // no IO. `ComplianceCodexDoc` comes from the scanner so we get jurisdiction
18
+ // alongside the codex id in the sources block.
19
+ function byId(a, b) {
20
+ return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
21
+ }
22
+ function resolveElement(id, catalog) {
23
+ const name = catalog.nameById.get(id);
24
+ return name ? { id, name } : { id };
25
+ }
26
+ /**
27
+ * Walk the `parent` chain upward from `requirement`, stopping at the first
28
+ * missing or already-seen parent (cycle guard). Returns immediate parent first,
29
+ * root last. Empty array when the element has no `parent`.
30
+ */
31
+ function collectAncestors(requirement, index) {
32
+ const out = [];
33
+ const seen = new Set([requirement.id]);
34
+ let current = requirement;
35
+ while (current?.parent && !seen.has(current.parent)) {
36
+ seen.add(current.parent);
37
+ const next = index.requirementById.get(current.parent);
38
+ if (!next)
39
+ break;
40
+ out.push(next);
41
+ current = next;
42
+ }
43
+ return out;
44
+ }
45
+ /**
46
+ * Builds the REQUIREMENT / CONSTRAINT traceability view.
47
+ *
48
+ * @param requirementId typed id of the element to trace
49
+ * @param index reverse index built by `buildComplianceIndex`
50
+ * @param catalog name-resolution map for subject / realised_via refs
51
+ * @param codex optional codex artefact list (from the scanner); when
52
+ * supplied, `sources[].codex` is populated with the
53
+ * resolved codex artefact for jurisdiction display
54
+ */
55
+ export function buildRequirementTrace(requirementId, index, catalog, codex = []) {
56
+ const requirement = index.requirementById.get(requirementId) ?? { id: requirementId, name: requirementId };
57
+ const codexById = new Map();
58
+ for (const c of codex)
59
+ codexById.set(c.id, c);
60
+ const sources = (requirement.derived_from ?? []).map(sid => {
61
+ const c = codexById.get(sid);
62
+ return c ? { id: sid, codex: c } : { id: sid };
63
+ });
64
+ const rawAssertions = index.assertionsByRequirement.get(requirementId) ?? [];
65
+ const assertions = [...rawAssertions]
66
+ .sort(byId)
67
+ .map(assertion => ({
68
+ assertion,
69
+ subject: resolveElement(assertion.subject, catalog),
70
+ realisedVia: (assertion.realised_via ?? []).map(rid => resolveElement(rid, catalog)),
71
+ }));
72
+ const ancestors = collectAncestors(requirement, index);
73
+ const children = [...(index.requirementsByParent.get(requirementId) ?? [])].sort(byId);
74
+ return { requirement, sources, assertions, ancestors, children };
75
+ }
76
+ /**
77
+ * Convenience: build a `TraceElementCatalog` from the scanned canon's product
78
+ * and subject buckets. Requirements/assertions are excluded — they are indexed
79
+ * separately by `buildComplianceIndex`; the catalog is only for resolving the
80
+ * elements a trace references (subjects + realised_via).
81
+ */
82
+ export function buildTraceElementCatalog(products, subjects) {
83
+ const nameById = new Map();
84
+ for (const p of products)
85
+ nameById.set(p.id, p.name);
86
+ for (const s of subjects)
87
+ nameById.set(s.id, s.name);
88
+ return { nameById };
89
+ }