@transitrix/diagrams 1.6.0 → 1.8.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/dist/activities/layout.js +7 -7
- package/dist/activities/types.d.ts +4 -0
- package/dist/capability-map/dsm-schema.d.ts +1 -1
- package/dist/capability-map/dsm-schema.js +1 -1
- package/dist/capability-map/layout-tree.d.ts +5 -1
- package/dist/capability-map/layout-tree.js +11 -9
- package/dist/capability-map/render-capability-tree.d.ts +2 -9
- package/dist/capability-map/render-capability-tree.js +16 -28
- package/dist/codex/__tests__/example.test.d.ts +1 -0
- package/dist/codex/__tests__/example.test.js +33 -0
- package/dist/codex/__tests__/validate.test.d.ts +1 -0
- package/dist/codex/__tests__/validate.test.js +68 -0
- package/dist/codex/index.d.ts +4 -0
- package/dist/codex/index.js +2 -0
- package/dist/codex/types.d.ts +7 -0
- package/dist/codex/types.js +8 -0
- package/dist/codex/validate.d.ts +11 -0
- package/dist/codex/validate.js +103 -0
- package/dist/compliance/__tests__/canon-catalog.test.d.ts +1 -0
- package/dist/compliance/__tests__/canon-catalog.test.js +60 -0
- package/dist/compliance/__tests__/view-resolution.test.d.ts +1 -0
- package/dist/compliance/__tests__/view-resolution.test.js +59 -0
- package/dist/compliance/canon-catalog.d.ts +19 -0
- package/dist/compliance/canon-catalog.js +59 -0
- package/dist/compliance/coverage-metric.d.ts +12 -0
- package/dist/compliance/coverage-metric.js +35 -0
- package/dist/compliance/impact.d.ts +7 -1
- package/dist/compliance/impact.js +53 -0
- package/dist/compliance/index.d.ts +6 -4
- package/dist/compliance/index.js +3 -2
- package/dist/constraint/__tests__/example.test.d.ts +1 -0
- package/dist/constraint/__tests__/example.test.js +14 -0
- package/dist/constraint/__tests__/validate.test.d.ts +1 -0
- package/dist/constraint/__tests__/validate.test.js +58 -0
- package/dist/constraint/index.d.ts +4 -0
- package/dist/constraint/index.js +2 -0
- package/dist/constraint/types.d.ts +4 -0
- package/dist/constraint/types.js +2 -0
- package/dist/constraint/validate.d.ts +7 -0
- package/dist/constraint/validate.js +115 -0
- package/dist/fgca/__tests__/parse-canonical.test.js +31 -0
- package/dist/fgca/parse-canonical.d.ts +1 -1
- package/dist/fgca/parse-canonical.js +18 -4
- package/dist/fgca/preview-layout.d.ts +8 -4
- package/dist/fgca/preview-layout.js +13 -13
- package/dist/goals/GoalNode.d.ts +2 -1
- package/dist/goals/GoalNode.js +7 -2
- package/dist/goals/GoalTreeView.d.ts +3 -1
- package/dist/goals/GoalTreeView.js +31 -14
- package/dist/goals/__tests__/GoalTreeView.test.js +41 -0
- package/dist/goals/__tests__/mutations.test.js +39 -0
- package/dist/goals/__tests__/parse-canonical.test.js +2 -0
- package/dist/goals/layout.js +2 -1
- package/dist/goals/mutations.js +6 -6
- package/dist/goals/parse-canonical.d.ts +5 -0
- package/dist/goals/parse-canonical.js +5 -0
- package/dist/goals/types.d.ts +2 -0
- package/dist/node-size-presets.d.ts +50 -0
- package/dist/node-size-presets.js +103 -0
- package/dist/process-blueprint/layout.js +4 -47
- package/dist/repo-validate/types.js +2 -3
- package/dist/webview/__tests__/entity-text-layout.test.d.ts +1 -0
- package/dist/webview/__tests__/entity-text-layout.test.js +43 -0
- package/dist/webview/__tests__/entry.test.js +3 -3
- package/dist/webview/__tests__/render-blocks.test.js +1 -7
- package/dist/webview/__tests__/render-goals.test.js +17 -3
- package/dist/webview/entity-text-layout.d.ts +95 -0
- package/dist/webview/entity-text-layout.js +279 -0
- package/dist/webview/entry.js +7 -3
- package/dist/webview/notation-style.d.ts +1 -1
- package/dist/webview/notation-style.js +1 -1
- package/dist/webview/render-activities.d.ts +3 -1
- package/dist/webview/render-activities.js +29 -32
- package/dist/webview/render-blocks.d.ts +4 -1
- package/dist/webview/render-blocks.js +27 -108
- package/dist/webview/render-fgca.d.ts +15 -8
- package/dist/webview/render-fgca.js +41 -44
- package/dist/webview/render-goals.d.ts +2 -0
- package/dist/webview/render-goals.js +23 -37
- package/dist/webview/render-process-blueprint.d.ts +4 -10
- package/dist/webview/render-process-blueprint.js +18 -64
- package/package.json +1 -1
- package/src/activities/layout.ts +12 -7
- package/src/activities/types.ts +4 -0
- package/src/capability-map/dsm-schema.ts +1 -1
- package/src/capability-map/index.ts +1 -1
- package/src/capability-map/layout-tree.ts +16 -8
- package/src/capability-map/render-capability-tree.ts +18 -30
- package/src/codex/__tests__/example.test.ts +40 -0
- package/src/codex/__tests__/validate.test.ts +84 -0
- package/src/codex/index.ts +4 -0
- package/src/codex/types.ts +13 -0
- package/src/codex/validate.ts +125 -0
- package/src/compliance/__tests__/canon-catalog.test.ts +68 -0
- package/src/compliance/__tests__/view-resolution.test.ts +80 -0
- package/src/compliance/canon-catalog.ts +76 -0
- package/src/compliance/coverage-metric.ts +50 -0
- package/src/compliance/impact.ts +67 -1
- package/src/compliance/index.ts +10 -1
- package/src/constraint/__tests__/example.test.ts +19 -0
- package/src/constraint/__tests__/validate.test.ts +68 -0
- package/src/constraint/index.ts +4 -0
- package/src/constraint/types.ts +5 -0
- package/src/constraint/validate.ts +131 -0
- package/src/fgca/__tests__/parse-canonical.test.ts +34 -0
- package/src/fgca/parse-canonical.ts +17 -4
- package/src/fgca/preview-layout.ts +21 -15
- package/src/goals/GoalNode.tsx +8 -1
- package/src/goals/GoalTreeView.tsx +61 -27
- package/src/goals/__tests__/GoalTreeView.test.tsx +48 -0
- package/src/goals/__tests__/mutations.test.ts +42 -0
- package/src/goals/__tests__/parse-canonical.test.ts +2 -0
- package/src/goals/layout.ts +2 -1
- package/src/goals/mutations.ts +6 -6
- package/src/goals/parse-canonical.ts +6 -0
- package/src/goals/types.ts +2 -0
- package/src/node-size-presets.ts +157 -0
- package/src/process-blueprint/layout.ts +4 -48
- package/src/repo-validate/types.ts +2 -3
- package/src/webview/__tests__/entity-text-layout.test.ts +52 -0
- package/src/webview/__tests__/entry.test.ts +3 -3
- package/src/webview/__tests__/render-blocks.test.ts +1 -9
- package/src/webview/__tests__/render-goals.test.ts +17 -3
- package/src/webview/entity-text-layout.ts +355 -0
- package/src/webview/entry.ts +8 -4
- package/src/webview/notation-style.ts +1 -1
- package/src/webview/render-activities.ts +38 -27
- package/src/webview/render-blocks.ts +35 -118
- package/src/webview/render-fgca.ts +59 -44
- package/src/webview/render-goals.ts +24 -35
- package/src/webview/render-process-blueprint.ts +30 -65
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// CODEX validator — methodology notations/elements/14-codex.md §4.
|
|
2
|
+
//
|
|
3
|
+
// CODEX-001 — shape, zone, id grammar, admission envelope.
|
|
4
|
+
// CODEX-002 — `type` field matches the id TYPE prefix when present.
|
|
5
|
+
// CODEX-003 — external artefact frontmatter (jurisdiction, effective_date).
|
|
6
|
+
// CODEX-004 — internal artefact frontmatter (issuing_authority, effective_date).
|
|
7
|
+
// CODEX-005 — jurisdiction must match the parent folder under codex/external/
|
|
8
|
+
// (when `folderJurisdiction` is supplied by the repo sweep).
|
|
9
|
+
|
|
10
|
+
import type { ValidationError, ValidationWarning, ValidationResult } from '../validation-types.js';
|
|
11
|
+
import { isCanonicalId, typeOfId } from '../typed-id.js';
|
|
12
|
+
import {
|
|
13
|
+
CODEX_ARTEFACT_TYPES,
|
|
14
|
+
EXTERNAL_CODEX_TYPES,
|
|
15
|
+
INTERNAL_CODEX_TYPES,
|
|
16
|
+
type CodexArtefactType,
|
|
17
|
+
} from './types.js';
|
|
18
|
+
|
|
19
|
+
export interface CodexValidateOptions {
|
|
20
|
+
/** Lowercased jurisdiction folder segment from `codex/external/<jurisdiction>/`. */
|
|
21
|
+
folderJurisdiction?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isCodexType(type: string | null): type is CodexArtefactType {
|
|
25
|
+
return type !== null && (CODEX_ARTEFACT_TYPES as readonly string[]).includes(type);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeType(value: unknown): string | undefined {
|
|
29
|
+
if (typeof value !== 'string' || value.trim() === '') return undefined;
|
|
30
|
+
return value.trim().toUpperCase();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function validateCodex(input: unknown, options: CodexValidateOptions = {}): ValidationResult {
|
|
34
|
+
const errors: ValidationError[] = [];
|
|
35
|
+
const warnings: ValidationWarning[] = [];
|
|
36
|
+
|
|
37
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
|
38
|
+
errors.push({ code: 'CODEX-001', message: 'Codex artefact must be a YAML mapping.' });
|
|
39
|
+
return { valid: false, errors, warnings };
|
|
40
|
+
}
|
|
41
|
+
const c = input as Record<string, unknown>;
|
|
42
|
+
|
|
43
|
+
if (c.zone !== 'codex') {
|
|
44
|
+
errors.push({ code: 'CODEX-001', message: 'zone must be the fixed value "codex".', path: 'zone' });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const idType = typeOfId(c.id);
|
|
48
|
+
if (!isCanonicalId(c.id) || !isCodexType(idType)) {
|
|
49
|
+
errors.push({
|
|
50
|
+
code: 'CODEX-001',
|
|
51
|
+
message: `id "${String(c.id)}" must be a typed codex id (LAW, REGULATION, POLICY, or INTERNAL_STANDARD).`,
|
|
52
|
+
path: 'id',
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (typeof c.name !== 'string' || c.name.trim() === '') {
|
|
57
|
+
errors.push({ code: 'CODEX-001', message: 'name is required.', path: 'name' });
|
|
58
|
+
}
|
|
59
|
+
for (const field of ['admitted_at', 'admitted_by'] as const) {
|
|
60
|
+
const v = c[field];
|
|
61
|
+
if (typeof v !== 'string' || v.trim() === '') {
|
|
62
|
+
errors.push({ code: 'CODEX-001', message: `${field} is required.`, path: field });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (c.gate_checks === null || typeof c.gate_checks !== 'object' || Array.isArray(c.gate_checks)) {
|
|
66
|
+
errors.push({ code: 'CODEX-001', message: 'gate_checks is required and must be a mapping.', path: 'gate_checks' });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const declaredType = normalizeType(c.type);
|
|
70
|
+
if (declaredType !== undefined && idType && declaredType !== idType) {
|
|
71
|
+
errors.push({
|
|
72
|
+
code: 'CODEX-002',
|
|
73
|
+
message: `type "${String(c.type)}" does not match the id TYPE prefix "${idType}".`,
|
|
74
|
+
path: 'type',
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const artefactType = (declaredType ?? idType) as CodexArtefactType | null;
|
|
79
|
+
|
|
80
|
+
if (artefactType && (EXTERNAL_CODEX_TYPES as readonly string[]).includes(artefactType)) {
|
|
81
|
+
const jurisdiction = typeof c.jurisdiction === 'string' ? c.jurisdiction.trim() : '';
|
|
82
|
+
if (!jurisdiction) {
|
|
83
|
+
errors.push({ code: 'CODEX-003', message: 'jurisdiction is required for external codex artefacts.', path: 'jurisdiction' });
|
|
84
|
+
} else if (options.folderJurisdiction && jurisdiction.toLowerCase() !== options.folderJurisdiction.toLowerCase()) {
|
|
85
|
+
errors.push({
|
|
86
|
+
code: 'CODEX-005',
|
|
87
|
+
message: `jurisdiction "${jurisdiction}" must match the parent folder "${options.folderJurisdiction}" (CODEX-001).`,
|
|
88
|
+
path: 'jurisdiction',
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
if (typeof c.effective_date !== 'string' || c.effective_date.trim() === '') {
|
|
92
|
+
errors.push({ code: 'CODEX-003', message: 'effective_date is required for external codex artefacts.', path: 'effective_date' });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (artefactType && (INTERNAL_CODEX_TYPES as readonly string[]).includes(artefactType)) {
|
|
97
|
+
if (typeof c.issuing_authority !== 'string' || c.issuing_authority.trim() === '') {
|
|
98
|
+
errors.push({
|
|
99
|
+
code: 'CODEX-004',
|
|
100
|
+
message: 'issuing_authority is required for internal codex artefacts.',
|
|
101
|
+
path: 'issuing_authority',
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
if (typeof c.effective_date !== 'string' || c.effective_date.trim() === '') {
|
|
105
|
+
errors.push({ code: 'CODEX-004', message: 'effective_date is required for internal codex artefacts.', path: 'effective_date' });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** True when a parsed document is a codex-zone artefact candidate. */
|
|
113
|
+
export function isCodexDoc(data: unknown): boolean {
|
|
114
|
+
if (data === null || typeof data !== 'object' || Array.isArray(data)) return false;
|
|
115
|
+
return (data as Record<string, unknown>).zone === 'codex';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Infer codex folder jurisdiction from a repo-relative path such as
|
|
119
|
+
* `codex/external/eu/LAW-GDPR-1.yaml` → `eu`. */
|
|
120
|
+
export function folderJurisdictionFromPath(filePath: string): string | undefined {
|
|
121
|
+
const parts = filePath.replace(/\\/g, '/').toLowerCase().split('/');
|
|
122
|
+
const extIdx = parts.indexOf('external');
|
|
123
|
+
if (extIdx < 0 || extIdx + 1 >= parts.length) return undefined;
|
|
124
|
+
return parts[extIdx + 1] || undefined;
|
|
125
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
admitDocumentToCatalog,
|
|
5
|
+
buildComplianceScan,
|
|
6
|
+
catalogFromMap,
|
|
7
|
+
} from '../canon-catalog.js';
|
|
8
|
+
|
|
9
|
+
describe('buildComplianceScan', () => {
|
|
10
|
+
it('admits codex, requirement, and assertion ids into the catalogue', () => {
|
|
11
|
+
const docs = [
|
|
12
|
+
{
|
|
13
|
+
path: 'codex/external/ge/LAW-PERSONAL-DATA-2017-1.yaml',
|
|
14
|
+
data: {
|
|
15
|
+
zone: 'codex',
|
|
16
|
+
id: 'LAW-PERSONAL-DATA-2017-1',
|
|
17
|
+
name: 'Law',
|
|
18
|
+
type: 'LAW',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
path: 'canon/elements/01_motivation/requirements/REQUIREMENT-DATA-ERASURE-1.yaml',
|
|
23
|
+
data: {
|
|
24
|
+
notation: 'requirement',
|
|
25
|
+
id: 'REQUIREMENT-DATA-ERASURE-1',
|
|
26
|
+
name: 'Erasure',
|
|
27
|
+
derived_from: ['LAW-PERSONAL-DATA-2017-1'],
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
path: 'canon/elements/02_business/products/PRODUCT-MOBILE-1.yaml',
|
|
32
|
+
data: { notation: 'product', id: 'PRODUCT-MOBILE-1', name: 'Mobile' },
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
path: 'canon/assertions/ASSERTION-MOBILE-DATA-ERASURE-1.yaml',
|
|
36
|
+
data: {
|
|
37
|
+
notation: 'assertion',
|
|
38
|
+
id: 'ASSERTION-MOBILE-DATA-ERASURE-1',
|
|
39
|
+
about: 'REQUIREMENT-DATA-ERASURE-1',
|
|
40
|
+
subject: 'PRODUCT-MOBILE-1',
|
|
41
|
+
status: 'compliant',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const { catalog, complianceCanon, pathById } = buildComplianceScan(docs);
|
|
47
|
+
expect(catalog.typeOf('LAW-PERSONAL-DATA-2017-1')).toBe('LAW');
|
|
48
|
+
expect(catalog.typeOf('REQUIREMENT-DATA-ERASURE-1')).toBe('REQUIREMENT');
|
|
49
|
+
expect(catalog.typeOf('PRODUCT-MOBILE-1')).toBe('PRODUCT');
|
|
50
|
+
expect(complianceCanon.requirements).toHaveLength(1);
|
|
51
|
+
expect(complianceCanon.assertions).toHaveLength(1);
|
|
52
|
+
expect(complianceCanon.codex).toHaveLength(1);
|
|
53
|
+
expect(pathById.get('REQUIREMENT-DATA-ERASURE-1')).toContain('requirements/');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('admitDocumentToCatalog', () => {
|
|
58
|
+
it('uses explicit codex type when the id prefix is ambiguous', () => {
|
|
59
|
+
const map = new Map<string, string>();
|
|
60
|
+
admitDocumentToCatalog(map, {
|
|
61
|
+
zone: 'codex',
|
|
62
|
+
id: 'INTERNAL_STANDARD-coding-conventions-1',
|
|
63
|
+
type: 'INTERNAL_STANDARD',
|
|
64
|
+
});
|
|
65
|
+
const catalog = catalogFromMap(map);
|
|
66
|
+
expect(catalog.typeOf('INTERNAL_STANDARD-coding-conventions-1')).toBe('INTERNAL_STANDARD');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import { emptyCanon } from '../classify.js';
|
|
4
|
+
import { catalogFromMap } from '../canon-catalog.js';
|
|
5
|
+
import { collectImpactViewResolutionFindings } from '../impact.js';
|
|
6
|
+
import { collectCoverageViewResolutionFindings } from '../coverage-metric.js';
|
|
7
|
+
import type { ImpactViewConfig } from '../impact.js';
|
|
8
|
+
import type { CoverageMetricConfig } from '../coverage-metric.js';
|
|
9
|
+
|
|
10
|
+
const catalog = catalogFromMap(
|
|
11
|
+
new Map([
|
|
12
|
+
['PRODUCT-ECOMM-1', 'PRODUCT'],
|
|
13
|
+
['REQUIREMENT-GDPR-1', 'REQUIREMENT'],
|
|
14
|
+
['LAW-GDPR-1', 'LAW'],
|
|
15
|
+
]),
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const complianceCanon = emptyCanon();
|
|
19
|
+
complianceCanon.products.push({ id: 'PRODUCT-ECOMM-1', name: 'E-Commerce' });
|
|
20
|
+
complianceCanon.requirements.push({ id: 'REQUIREMENT-GDPR-1', name: 'GDPR req' });
|
|
21
|
+
complianceCanon.codex.push({ id: 'LAW-GDPR-1', name: 'GDPR' });
|
|
22
|
+
|
|
23
|
+
describe('collectImpactViewResolutionFindings', () => {
|
|
24
|
+
const base: ImpactViewConfig = {
|
|
25
|
+
id: 'test-impact',
|
|
26
|
+
name: 'Test',
|
|
27
|
+
subjects: { products: ['PRODUCT-ECOMM-1'], processes: [], capabilities: [] },
|
|
28
|
+
obligations: { filter: { derived_from_codex: ['LAW-GDPR-1'] } },
|
|
29
|
+
status_display: { show: ['compliant'] },
|
|
30
|
+
empty_cells: {},
|
|
31
|
+
order_rows_by: 'id',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
it('passes when subjects and regimes resolve', () => {
|
|
35
|
+
expect(collectImpactViewResolutionFindings(base, catalog, complianceCanon)).toEqual([]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('flags unresolved subjects and regimes', () => {
|
|
39
|
+
const findings = collectImpactViewResolutionFindings(
|
|
40
|
+
{
|
|
41
|
+
...base,
|
|
42
|
+
subjects: { products: ['PRODUCT-MISSING-1'], processes: [], capabilities: [] },
|
|
43
|
+
obligations: { include: ['REQUIREMENT-MISSING-1'], filter: { derived_from_codex: ['LAW-MISSING-1'] } },
|
|
44
|
+
},
|
|
45
|
+
catalog,
|
|
46
|
+
complianceCanon,
|
|
47
|
+
);
|
|
48
|
+
expect(findings.some((f) => f.code === 'COMPIMP-REF' && f.message.includes('PRODUCT-MISSING-1'))).toBe(true);
|
|
49
|
+
expect(findings.some((f) => f.message.includes('REQUIREMENT-MISSING-1'))).toBe(true);
|
|
50
|
+
expect(findings.some((f) => f.message.includes('LAW-MISSING-1'))).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('collectCoverageViewResolutionFindings', () => {
|
|
55
|
+
const base: CoverageMetricConfig = {
|
|
56
|
+
id: 'test-coverage',
|
|
57
|
+
name: 'Test',
|
|
58
|
+
thresholds: { green: 0.9, amber: 0.7 },
|
|
59
|
+
subjects: { products: ['PRODUCT-ECOMM-1'] },
|
|
60
|
+
regimes: { include: ['LAW-GDPR-1'] },
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
it('passes when products and regimes resolve', () => {
|
|
64
|
+
expect(collectCoverageViewResolutionFindings(base, catalog, complianceCanon)).toEqual([]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('flags unresolved product and regime refs', () => {
|
|
68
|
+
const findings = collectCoverageViewResolutionFindings(
|
|
69
|
+
{
|
|
70
|
+
...base,
|
|
71
|
+
subjects: { products: ['PRODUCT-MISSING-1'] },
|
|
72
|
+
regimes: { include: ['LAW-MISSING-1'] },
|
|
73
|
+
},
|
|
74
|
+
catalog,
|
|
75
|
+
complianceCanon,
|
|
76
|
+
);
|
|
77
|
+
expect(findings.some((f) => f.code === 'COVMET-REF')).toBe(true);
|
|
78
|
+
expect(findings).toHaveLength(2);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
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
|
+
|
|
7
|
+
import { typeOfId, type CanonCatalog } from '../typed-id.js';
|
|
8
|
+
import { emptyCanon, ingestComplianceDoc, type ComplianceCanon } from './classify.js';
|
|
9
|
+
|
|
10
|
+
export interface ScannedYamlDoc {
|
|
11
|
+
path: string;
|
|
12
|
+
data: unknown;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ComplianceScanResult {
|
|
16
|
+
complianceCanon: ComplianceCanon;
|
|
17
|
+
catalog: CanonCatalog;
|
|
18
|
+
/** First path where each admitted id was seen (for gap-dashboard warnings). */
|
|
19
|
+
pathById: Map<string, string>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function strId(doc: unknown): string | undefined {
|
|
23
|
+
if (doc === null || typeof doc !== 'object' || Array.isArray(doc)) return undefined;
|
|
24
|
+
const id = (doc as Record<string, unknown>).id;
|
|
25
|
+
return typeof id === 'string' && id.trim() !== '' ? id.trim() : undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Admit one parsed document into the id → TYPE map. */
|
|
29
|
+
export function admitDocumentToCatalog(map: Map<string, string>, doc: unknown): string | undefined {
|
|
30
|
+
const id = strId(doc);
|
|
31
|
+
if (!id) return undefined;
|
|
32
|
+
|
|
33
|
+
const fromPrefix = typeOfId(id);
|
|
34
|
+
if (fromPrefix) {
|
|
35
|
+
map.set(id, fromPrefix);
|
|
36
|
+
return id;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (doc !== null && typeof doc === 'object' && !Array.isArray(doc)) {
|
|
40
|
+
const d = doc as Record<string, unknown>;
|
|
41
|
+
if (d.zone === 'codex') {
|
|
42
|
+
const explicit = typeof d.type === 'string' ? d.type.trim().toUpperCase() : undefined;
|
|
43
|
+
if (explicit) {
|
|
44
|
+
map.set(id, explicit);
|
|
45
|
+
return id;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Build a read-only `CanonCatalog` from a populated id → TYPE map. */
|
|
53
|
+
export function catalogFromMap(map: Map<string, string>): CanonCatalog {
|
|
54
|
+
return { typeOf: (id: string) => map.get(id) };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Ingest compliance projections and admit cross-reference targets from a batch
|
|
58
|
+
* of already-parsed YAML documents (no IO). */
|
|
59
|
+
export function buildComplianceScan(docs: ScannedYamlDoc[]): ComplianceScanResult {
|
|
60
|
+
const complianceCanon = emptyCanon();
|
|
61
|
+
const catalogMap = new Map<string, string>();
|
|
62
|
+
const pathById = new Map<string, string>();
|
|
63
|
+
|
|
64
|
+
for (const { path, data } of docs) {
|
|
65
|
+
const ingested = ingestComplianceDoc(complianceCanon, data);
|
|
66
|
+
if (ingested) pathById.set(ingested, path);
|
|
67
|
+
const admitted = admitDocumentToCatalog(catalogMap, data);
|
|
68
|
+
if (admitted && !pathById.has(admitted)) pathById.set(admitted, path);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
complianceCanon,
|
|
73
|
+
catalog: catalogFromMap(catalogMap),
|
|
74
|
+
pathById,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import type { AssertionStatus } from '../assertion/types.js';
|
|
9
9
|
import type { ComplianceCanon } from './classify.js';
|
|
10
10
|
import { buildComplianceIndex } from './reverse-index.js';
|
|
11
|
+
import type { CanonCatalog } from '../typed-id.js';
|
|
11
12
|
import type { IndexAssertion } from './types.js';
|
|
12
13
|
|
|
13
14
|
// ── Config types ─────────────────────────────────────────────────────────────
|
|
@@ -93,6 +94,55 @@ export interface CoverageMatrix {
|
|
|
93
94
|
thresholds: CoverageMetricThresholds;
|
|
94
95
|
}
|
|
95
96
|
|
|
97
|
+
/** Repo-scope cross-reference finding for a coverage-metric view (#518 C3). */
|
|
98
|
+
export interface CoverageViewFinding {
|
|
99
|
+
code: string;
|
|
100
|
+
severity: 'error' | 'warning';
|
|
101
|
+
message: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Repo-scope cross-reference checks for a parsed coverage-metric view config.
|
|
106
|
+
* Surfaces unresolved product and regime ids before `buildCoverageMatrix` runs.
|
|
107
|
+
*/
|
|
108
|
+
export function collectCoverageViewResolutionFindings(
|
|
109
|
+
config: CoverageMetricConfig,
|
|
110
|
+
catalog: CanonCatalog,
|
|
111
|
+
complianceCanon: ComplianceCanon,
|
|
112
|
+
): CoverageViewFinding[] {
|
|
113
|
+
const findings: CoverageViewFinding[] = [];
|
|
114
|
+
const codexIds = new Set(complianceCanon.codex.map((c) => c.id));
|
|
115
|
+
|
|
116
|
+
for (const id of config.subjects?.products ?? []) {
|
|
117
|
+
const t = catalog.typeOf(id);
|
|
118
|
+
if (t === undefined) {
|
|
119
|
+
findings.push({
|
|
120
|
+
code: 'COVMET-REF',
|
|
121
|
+
severity: 'error',
|
|
122
|
+
message: `subjects.products: "${id}" does not resolve to an admitted artefact.`,
|
|
123
|
+
});
|
|
124
|
+
} else if (t !== 'PRODUCT') {
|
|
125
|
+
findings.push({
|
|
126
|
+
code: 'COVMET-REF',
|
|
127
|
+
severity: 'error',
|
|
128
|
+
message: `subjects.products: "${id}" resolves to ${t}, not PRODUCT.`,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
for (const id of config.regimes?.include ?? []) {
|
|
134
|
+
if (!codexIds.has(id)) {
|
|
135
|
+
findings.push({
|
|
136
|
+
code: 'COVMET-REF',
|
|
137
|
+
severity: 'error',
|
|
138
|
+
message: `regimes.include: "${id}" does not resolve to an admitted codex artefact.`,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return findings;
|
|
144
|
+
}
|
|
145
|
+
|
|
96
146
|
// ── Parser ───────────────────────────────────────────────────────────────────
|
|
97
147
|
|
|
98
148
|
function parseRegimes(raw: Record<string, unknown>): { regimes?: CoverageMetricRegimes; warnings: string[] } {
|
package/src/compliance/impact.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import type { AssertionStatus } from '../assertion/types.js';
|
|
11
11
|
import type { ComplianceCanon } from './classify.js';
|
|
12
12
|
import { buildComplianceIndex } from './reverse-index.js';
|
|
13
|
+
import type { CanonCatalog } from '../typed-id.js';
|
|
13
14
|
import type { ComplianceIndex, IndexAssertion, IndexRequirement, ObjectDetailInput, ObjectDetailDef, DeadlineStatus } from './types.js';
|
|
14
15
|
|
|
15
16
|
/** Filter selecting REQUIREMENTs by codex source (jurisdiction / regime keys
|
|
@@ -75,13 +76,78 @@ export interface ImpactColumn {
|
|
|
75
76
|
subjectType: 'product' | 'process' | 'capability';
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
/** A structural validation finding emitted by the matrix builder. */
|
|
79
79
|
export interface ImpactFinding {
|
|
80
80
|
code: string;
|
|
81
81
|
severity: 'error' | 'warning';
|
|
82
82
|
message: string;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Repo-scope cross-reference checks for a parsed compliance-impact view config
|
|
87
|
+
* (#518 Phase C3). Surfaces unresolved subject, obligation, and regime ids before
|
|
88
|
+
* `buildImpactMatrix` runs.
|
|
89
|
+
*/
|
|
90
|
+
export function collectImpactViewResolutionFindings(
|
|
91
|
+
config: ImpactViewConfig,
|
|
92
|
+
catalog: CanonCatalog,
|
|
93
|
+
complianceCanon: ComplianceCanon,
|
|
94
|
+
): ImpactFinding[] {
|
|
95
|
+
const findings: ImpactFinding[] = [];
|
|
96
|
+
const codexIds = new Set(complianceCanon.codex.map((c) => c.id));
|
|
97
|
+
|
|
98
|
+
const subjectChecks: Array<{
|
|
99
|
+
ids: string[] | undefined;
|
|
100
|
+
expectedType: string;
|
|
101
|
+
path: string;
|
|
102
|
+
}> = [
|
|
103
|
+
{ ids: config.subjects.products, expectedType: 'PRODUCT', path: 'subjects.products' },
|
|
104
|
+
{ ids: config.subjects.processes, expectedType: 'PROCESS', path: 'subjects.processes' },
|
|
105
|
+
{ ids: config.subjects.capabilities, expectedType: 'CAPABILITY', path: 'subjects.capabilities' },
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
for (const { ids, expectedType, path } of subjectChecks) {
|
|
109
|
+
for (const id of ids ?? []) {
|
|
110
|
+
const t = catalog.typeOf(id);
|
|
111
|
+
if (t === undefined) {
|
|
112
|
+
findings.push({
|
|
113
|
+
code: 'COMPIMP-REF',
|
|
114
|
+
severity: 'error',
|
|
115
|
+
message: `${path}: "${id}" does not resolve to an admitted artefact.`,
|
|
116
|
+
});
|
|
117
|
+
} else if (t !== expectedType) {
|
|
118
|
+
findings.push({
|
|
119
|
+
code: 'COMPIMP-REF',
|
|
120
|
+
severity: 'error',
|
|
121
|
+
message: `${path}: "${id}" resolves to ${t}, not ${expectedType}.`,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for (const id of config.obligations.include ?? []) {
|
|
128
|
+
const t = catalog.typeOf(id);
|
|
129
|
+
if (t === undefined || t !== 'REQUIREMENT') {
|
|
130
|
+
findings.push({
|
|
131
|
+
code: 'COMPIMP-REF',
|
|
132
|
+
severity: 'error',
|
|
133
|
+
message: `obligations.include: "${id}" does not resolve to an admitted REQUIREMENT.`,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for (const id of config.obligations.filter?.derived_from_codex ?? []) {
|
|
139
|
+
if (!codexIds.has(id)) {
|
|
140
|
+
findings.push({
|
|
141
|
+
code: 'COMPIMP-REF',
|
|
142
|
+
severity: 'error',
|
|
143
|
+
message: `obligations.filter.derived_from_codex: "${id}" does not resolve to an admitted codex artefact.`,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return findings;
|
|
149
|
+
}
|
|
150
|
+
|
|
85
151
|
/**
|
|
86
152
|
* Grouping options for matrix columns.
|
|
87
153
|
*
|
package/src/compliance/index.ts
CHANGED
|
@@ -5,15 +5,22 @@ export { buildGapReport } from './gap-report.js';
|
|
|
5
5
|
export type { GapReport, GapReportOptions } from './gap-report.js';
|
|
6
6
|
export { emptyCanon, ingestComplianceDoc } from './classify.js';
|
|
7
7
|
export type { ComplianceCanon, ComplianceProduct, ComplianceCodexDoc } from './classify.js';
|
|
8
|
+
export {
|
|
9
|
+
admitDocumentToCatalog,
|
|
10
|
+
buildComplianceScan,
|
|
11
|
+
catalogFromMap,
|
|
12
|
+
} from './canon-catalog.js';
|
|
13
|
+
export type { ComplianceScanResult, ScannedYamlDoc } from './canon-catalog.js';
|
|
8
14
|
export { renderComplianceMarkdown } from './markdown.js';
|
|
9
15
|
export type { ReportScope, MarkdownOptions } from './markdown.js';
|
|
10
16
|
export { renderComplianceHtml, renderImpactMatrixHtml } from './html.js';
|
|
11
17
|
export type { HtmlOptions, ImpactMatrixHtmlOptions } from './html.js';
|
|
12
|
-
export { buildImpactMatrix, renderImpactMarkdown, parseImpactViewConfig, COMPLIANCE_IMPACT_DEFAULTS, extractObjectDetails, extractProcessFlowTasks, mergeStageTaskDetails, computeDeadlineStatus } from './impact.js';
|
|
18
|
+
export { buildImpactMatrix, renderImpactMarkdown, parseImpactViewConfig, collectImpactViewResolutionFindings, COMPLIANCE_IMPACT_DEFAULTS, extractObjectDetails, extractProcessFlowTasks, mergeStageTaskDetails, computeDeadlineStatus } from './impact.js';
|
|
13
19
|
export type {
|
|
14
20
|
ImpactCell,
|
|
15
21
|
ImpactColumn,
|
|
16
22
|
ImpactEmptyCellLabels,
|
|
23
|
+
ImpactFinding,
|
|
17
24
|
ImpactGrouping,
|
|
18
25
|
ImpactMatrix,
|
|
19
26
|
ImpactObligationFilter,
|
|
@@ -31,6 +38,7 @@ export {
|
|
|
31
38
|
export {
|
|
32
39
|
parseCoverageMetricConfig,
|
|
33
40
|
buildCoverageMatrix,
|
|
41
|
+
collectCoverageViewResolutionFindings,
|
|
34
42
|
} from './coverage-metric.js';
|
|
35
43
|
export type {
|
|
36
44
|
CoverageMetricConfig,
|
|
@@ -40,6 +48,7 @@ export type {
|
|
|
40
48
|
CoverageMetricThresholds,
|
|
41
49
|
CoverageMatrix,
|
|
42
50
|
CoverageRow,
|
|
51
|
+
CoverageViewFinding,
|
|
43
52
|
ParseCoverageMetricResult,
|
|
44
53
|
RagStatus,
|
|
45
54
|
} from './coverage-metric.js';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import yaml from 'js-yaml';
|
|
6
|
+
import { validateConstraint } from '../validate.js';
|
|
7
|
+
|
|
8
|
+
const dir = path.resolve(
|
|
9
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
10
|
+
'../../../../../tests/fixtures/notation-corpus/constraint',
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
describe('constraint worked example (notation-corpus)', () => {
|
|
14
|
+
it('validates CONSTRAINT-GDPR-RESIDENCY-1.yaml without error', () => {
|
|
15
|
+
const data = yaml.load(readFileSync(path.join(dir, 'CONSTRAINT-GDPR-RESIDENCY-1.yaml'), 'utf-8'));
|
|
16
|
+
const v = validateConstraint(data);
|
|
17
|
+
expect(v.valid, JSON.stringify(v.errors)).toBe(true);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { validateConstraint } from '../validate.js';
|
|
3
|
+
import type { CanonCatalog } from '../../typed-id.js';
|
|
4
|
+
|
|
5
|
+
function valid(): Record<string, unknown> {
|
|
6
|
+
return {
|
|
7
|
+
notation: 'constraint',
|
|
8
|
+
id: 'CONSTRAINT-GDPR-RESIDENCY-1',
|
|
9
|
+
name: 'EU customer personal data must be stored within EU/EEA jurisdictions',
|
|
10
|
+
statement: 'Personal data of EU customers MUST NOT be persisted outside EU / EEA jurisdictions.',
|
|
11
|
+
status: 'active',
|
|
12
|
+
zone: 'canon',
|
|
13
|
+
admitted_at: '2026-05-29',
|
|
14
|
+
admitted_by: 'v.korobeinikov',
|
|
15
|
+
gate_checks: { uniqueness: 'pass', consistency: 'pass', completeness: 'pass' },
|
|
16
|
+
valid_from: '2018-05-25',
|
|
17
|
+
valid_to: null,
|
|
18
|
+
severity: 'mandatory',
|
|
19
|
+
owner_role: 'ROLE-DPO-1',
|
|
20
|
+
applies_to: ['APPLICATION-CRM-1'],
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const codes = (input: unknown, opts?: Parameters<typeof validateConstraint>[1]): string[] =>
|
|
25
|
+
validateConstraint(input, opts).errors.map((e) => e.code);
|
|
26
|
+
|
|
27
|
+
describe('validateConstraint — positive', () => {
|
|
28
|
+
it('accepts a well-formed constraint', () => {
|
|
29
|
+
const r = validateConstraint(valid());
|
|
30
|
+
expect(r.valid, JSON.stringify(r.errors)).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('validateConstraint — CONST-001', () => {
|
|
35
|
+
it('flags missing required fields and wrong notation', () => {
|
|
36
|
+
const r = valid();
|
|
37
|
+
delete r.statement;
|
|
38
|
+
expect(codes(r)).toContain('CONST-001');
|
|
39
|
+
expect(codes({ ...valid(), notation: 'requirement' })).toContain('CONST-001');
|
|
40
|
+
expect(codes({ ...valid(), id: 'CONSTRAINT-001' })).toContain('CONST-001');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe('validateConstraint — CONST-002/003', () => {
|
|
45
|
+
it('flags invalid status and severity', () => {
|
|
46
|
+
expect(codes({ ...valid(), status: 'enabled' })).toContain('CONST-002');
|
|
47
|
+
expect(codes({ ...valid(), severity: 'critical' })).toContain('CONST-003');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('validateConstraint — CONST-004/005 (catalog)', () => {
|
|
52
|
+
it('flags unresolved applies_to and owner_role with catalog', () => {
|
|
53
|
+
const catalog: CanonCatalog = { typeOf: () => undefined };
|
|
54
|
+
expect(codes(valid(), { catalog })).toContain('CONST-004');
|
|
55
|
+
expect(codes(valid(), { catalog })).toContain('CONST-005');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('does not flag CONST-004 without catalog for well-formed applies_to', () => {
|
|
59
|
+
expect(codes(valid())).not.toContain('CONST-004');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('resolves applies_to when catalogue admits the target', () => {
|
|
63
|
+
const catalog: CanonCatalog = {
|
|
64
|
+
typeOf: (id) => (id === 'APPLICATION-CRM-1' ? 'APPLICATION' : id === 'ROLE-DPO-1' ? 'ROLE' : undefined),
|
|
65
|
+
};
|
|
66
|
+
expect(validateConstraint(valid(), { catalog }).valid).toBe(true);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export const CONSTRAINT_STATUSES = ['active', 'proposed', 'deprecated', 'retired'] as const;
|
|
2
|
+
export type ConstraintStatus = (typeof CONSTRAINT_STATUSES)[number];
|
|
3
|
+
|
|
4
|
+
export const CONSTRAINT_SEVERITIES = ['mandatory', 'recommended', 'advisory'] as const;
|
|
5
|
+
export type ConstraintSeverity = (typeof CONSTRAINT_SEVERITIES)[number];
|