@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.
- package/dist/capability-map/CapabilityCardNode.d.ts +19 -0
- package/dist/capability-map/CapabilityCardNode.js +66 -0
- package/dist/capability-map/CapabilityMapView.d.ts +59 -0
- package/dist/capability-map/CapabilityMapView.js +204 -0
- package/dist/capability-map/CapabilityRootNode.d.ts +12 -0
- package/dist/capability-map/CapabilityRootNode.js +23 -0
- package/dist/capability-map/__tests__/CapabilityMapView.test.d.ts +1 -0
- package/dist/capability-map/__tests__/CapabilityMapView.test.js +217 -0
- package/dist/capability-map/__tests__/address.test.d.ts +1 -0
- package/dist/capability-map/__tests__/address.test.js +49 -0
- package/dist/capability-map/__tests__/dsm-layout.test.d.ts +1 -0
- package/dist/capability-map/__tests__/dsm-layout.test.js +46 -0
- package/dist/capability-map/__tests__/dsm-mutations.test.d.ts +1 -0
- package/dist/capability-map/__tests__/dsm-mutations.test.js +125 -0
- package/dist/capability-map/__tests__/dsm-validate.test.d.ts +1 -0
- package/dist/capability-map/__tests__/dsm-validate.test.js +56 -0
- package/dist/capability-map/address.d.ts +12 -0
- package/dist/capability-map/address.js +48 -0
- package/dist/capability-map/dsm-layout.d.ts +9 -0
- package/dist/capability-map/dsm-layout.js +125 -0
- package/dist/capability-map/dsm-mutations.d.ts +28 -0
- package/dist/capability-map/dsm-mutations.js +196 -0
- package/dist/capability-map/dsm-schema.d.ts +71 -0
- package/dist/capability-map/dsm-schema.js +14 -0
- package/dist/capability-map/dsm-theme.d.ts +20 -0
- package/dist/capability-map/dsm-theme.js +36 -0
- package/dist/capability-map/dsm-validate.d.ts +2 -0
- package/dist/capability-map/dsm-validate.js +93 -0
- package/dist/capability-map/index.d.ts +9 -0
- package/dist/capability-map/index.js +6 -0
- 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__/trace.test.d.ts +1 -0
- package/dist/compliance/__tests__/trace.test.js +144 -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/classify.js +7 -1
- 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 +8 -5
- package/dist/compliance/index.js +4 -2
- package/dist/compliance/reverse-index.js +4 -1
- package/dist/compliance/trace.d.ts +26 -0
- package/dist/compliance/trace.js +89 -0
- package/dist/compliance/types.d.ts +94 -1
- 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/goals/GoalNode.d.ts +19 -0
- package/dist/goals/GoalNode.js +96 -0
- package/dist/goals/GoalTreeView.d.ts +59 -0
- package/dist/goals/GoalTreeView.js +245 -0
- package/dist/goals/__tests__/GoalTreeView.test.d.ts +1 -0
- package/dist/goals/__tests__/GoalTreeView.test.js +337 -0
- package/dist/goals/__tests__/mutations.test.js +39 -0
- package/dist/goals/__tests__/parse-canonical.test.js +2 -0
- package/dist/goals/index.d.ts +3 -0
- package/dist/goals/index.js +1 -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/theme.d.ts +22 -0
- package/dist/goals/theme.js +32 -0
- package/dist/goals/types.d.ts +2 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/dist/repo-validate/types.js +2 -3
- package/dist/webview/__tests__/render-goals.test.js +17 -3
- package/dist/webview/render-goals.js +2 -1
- package/package.json +4 -1
- package/src/capability-map/CapabilityCardNode.tsx +146 -0
- package/src/capability-map/CapabilityMapView.tsx +331 -0
- package/src/capability-map/CapabilityRootNode.tsx +38 -0
- package/src/capability-map/__tests__/CapabilityMapView.test.tsx +232 -0
- package/src/capability-map/__tests__/address.test.ts +59 -0
- package/src/capability-map/__tests__/dsm-layout.test.ts +52 -0
- package/src/capability-map/__tests__/dsm-mutations.test.ts +141 -0
- package/src/capability-map/__tests__/dsm-validate.test.ts +65 -0
- package/src/capability-map/address.ts +48 -0
- package/src/capability-map/dsm-layout.ts +142 -0
- package/src/capability-map/dsm-mutations.ts +208 -0
- package/src/capability-map/dsm-schema.ts +80 -0
- package/src/capability-map/dsm-theme.ts +53 -0
- package/src/capability-map/dsm-validate.ts +103 -0
- package/src/capability-map/index.ts +35 -0
- 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__/trace.test.ts +157 -0
- package/src/compliance/__tests__/view-resolution.test.ts +80 -0
- package/src/compliance/canon-catalog.ts +76 -0
- package/src/compliance/classify.ts +7 -1
- package/src/compliance/coverage-metric.ts +50 -0
- package/src/compliance/impact.ts +67 -1
- package/src/compliance/index.ts +16 -1
- package/src/compliance/reverse-index.ts +3 -1
- package/src/compliance/trace.ts +118 -0
- package/src/compliance/types.ts +101 -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/goals/GoalNode.tsx +219 -0
- package/src/goals/GoalTreeView.tsx +384 -0
- package/src/goals/__tests__/GoalTreeView.test.tsx +361 -0
- package/src/goals/__tests__/mutations.test.ts +42 -0
- package/src/goals/__tests__/parse-canonical.test.ts +2 -0
- package/src/goals/index.ts +4 -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/theme.ts +54 -0
- package/src/goals/types.ts +2 -0
- package/src/index.ts +29 -1
- package/src/repo-validate/types.ts +2 -3
- package/src/webview/__tests__/render-goals.test.ts +17 -3
- package/src/webview/render-goals.ts +2 -1
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for the DSM-migration capability map (docs/extraction/capability-map.md).
|
|
3
|
+
*
|
|
4
|
+
* Distinct from `./types.ts` (`CapabilityNode`/`CapabilityMapFile`), which is
|
|
5
|
+
* Transitrix's own native `*.capability-map.transitrix.yaml` notation — a
|
|
6
|
+
* nested-children tree, statically rendered to SVG. This module is a
|
|
7
|
+
* separate, flat, address-triple data model matching DSM's internal
|
|
8
|
+
* capability editor, for the interactive `CapabilityMapView` component. The
|
|
9
|
+
* two coexist in this folder by design (see the address-helper/component
|
|
10
|
+
* deliverables in the extraction spec); only the validator function name
|
|
11
|
+
* collided, so it's `validateCapabilityMapData` here, not
|
|
12
|
+
* `validateCapabilityMap` (already taken by `./validate.ts`).
|
|
13
|
+
*/
|
|
14
|
+
export interface MaturitySnapshot {
|
|
15
|
+
date: string;
|
|
16
|
+
level: number;
|
|
17
|
+
}
|
|
18
|
+
export interface Capability {
|
|
19
|
+
id: number;
|
|
20
|
+
name: string;
|
|
21
|
+
/** 'X.Y.Z' triple. X=0 or backlog=true means "not on the diagram". */
|
|
22
|
+
address: string;
|
|
23
|
+
backlog?: boolean;
|
|
24
|
+
description?: string;
|
|
25
|
+
maturity?: MaturitySnapshot[];
|
|
26
|
+
}
|
|
27
|
+
export interface CapabilityMap {
|
|
28
|
+
organisation: string;
|
|
29
|
+
set_id: string;
|
|
30
|
+
capabilities: Capability[];
|
|
31
|
+
}
|
|
32
|
+
export type { ValidationError, ValidationWarning, ValidationResult, } from '../validation-types.js';
|
|
33
|
+
import type { ValidationError } from '../validation-types.js';
|
|
34
|
+
export interface MutationResult<T> {
|
|
35
|
+
ok: boolean;
|
|
36
|
+
result?: T;
|
|
37
|
+
error?: ValidationError;
|
|
38
|
+
}
|
|
39
|
+
export interface LayoutOptions {
|
|
40
|
+
rankdir?: 'LR' | 'TB';
|
|
41
|
+
nodeWidth?: number;
|
|
42
|
+
nodeHeight?: number;
|
|
43
|
+
rankSep?: number;
|
|
44
|
+
nodeSep?: number;
|
|
45
|
+
hideCollapsed?: number[];
|
|
46
|
+
organisationLabel?: string;
|
|
47
|
+
}
|
|
48
|
+
export interface LaidOutNode {
|
|
49
|
+
id: number;
|
|
50
|
+
x: number;
|
|
51
|
+
y: number;
|
|
52
|
+
width: number;
|
|
53
|
+
height: number;
|
|
54
|
+
data: Capability | null;
|
|
55
|
+
hasHiddenChildren: boolean;
|
|
56
|
+
}
|
|
57
|
+
export interface LaidOutEdge {
|
|
58
|
+
source: number | 'root';
|
|
59
|
+
target: number;
|
|
60
|
+
}
|
|
61
|
+
export interface CapabilityMapLayout {
|
|
62
|
+
rootNode: LaidOutNode;
|
|
63
|
+
nodes: LaidOutNode[];
|
|
64
|
+
edges: LaidOutEdge[];
|
|
65
|
+
bounds: {
|
|
66
|
+
x: number;
|
|
67
|
+
y: number;
|
|
68
|
+
width: number;
|
|
69
|
+
height: number;
|
|
70
|
+
};
|
|
71
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Types for the DSM-migration capability map (docs/extraction/capability-map.md).
|
|
3
|
+
*
|
|
4
|
+
* Distinct from `./types.ts` (`CapabilityNode`/`CapabilityMapFile`), which is
|
|
5
|
+
* Transitrix's own native `*.capability-map.transitrix.yaml` notation — a
|
|
6
|
+
* nested-children tree, statically rendered to SVG. This module is a
|
|
7
|
+
* separate, flat, address-triple data model matching DSM's internal
|
|
8
|
+
* capability editor, for the interactive `CapabilityMapView` component. The
|
|
9
|
+
* two coexist in this folder by design (see the address-helper/component
|
|
10
|
+
* deliverables in the extraction spec); only the validator function name
|
|
11
|
+
* collided, so it's `validateCapabilityMapData` here, not
|
|
12
|
+
* `validateCapabilityMap` (already taken by `./validate.ts`).
|
|
13
|
+
*/
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** Theme tokens for CapabilityMapView — same host-agnostic-prop pattern as
|
|
2
|
+
* the goals module's ThemeTokens (see ../goals/theme.ts). */
|
|
3
|
+
export interface ThemeTokens {
|
|
4
|
+
cardFill?: string;
|
|
5
|
+
cardBorderColor?: string;
|
|
6
|
+
cardBorderWidth?: number;
|
|
7
|
+
cardTextColor?: string;
|
|
8
|
+
cardMetaColor?: string;
|
|
9
|
+
edgeColor?: string;
|
|
10
|
+
edgeWidth?: number;
|
|
11
|
+
dropTargetBorderColor?: string;
|
|
12
|
+
rootFill?: string;
|
|
13
|
+
rootTextColor?: string;
|
|
14
|
+
}
|
|
15
|
+
/** Default 1..5 maturity-dot colour scale — overridable via the
|
|
16
|
+
* `maturityColours` prop (spec §6.2), which is separate from ThemeTokens
|
|
17
|
+
* since it's keyed by maturity level, not a fixed named token. */
|
|
18
|
+
export declare const DEFAULT_MATURITY_COLOURS: Record<number, string>;
|
|
19
|
+
export declare const DEFAULT_THEME: Required<ThemeTokens>;
|
|
20
|
+
export declare function resolveTheme(theme?: ThemeTokens): Required<ThemeTokens>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Default 1..5 maturity-dot colour scale — overridable via the
|
|
2
|
+
* `maturityColours` prop (spec §6.2), which is separate from ThemeTokens
|
|
3
|
+
* since it's keyed by maturity level, not a fixed named token. */
|
|
4
|
+
export const DEFAULT_MATURITY_COLOURS = {
|
|
5
|
+
1: '#ef4444',
|
|
6
|
+
2: '#f97316',
|
|
7
|
+
3: '#eab308',
|
|
8
|
+
4: '#84cc16',
|
|
9
|
+
5: '#22c55e',
|
|
10
|
+
};
|
|
11
|
+
export const DEFAULT_THEME = {
|
|
12
|
+
cardFill: '#eff6ff',
|
|
13
|
+
cardBorderColor: '#94a3b8',
|
|
14
|
+
cardBorderWidth: 1,
|
|
15
|
+
cardTextColor: '#0f172a',
|
|
16
|
+
cardMetaColor: '#64748b',
|
|
17
|
+
edgeColor: '#94a3b8',
|
|
18
|
+
edgeWidth: 1.5,
|
|
19
|
+
dropTargetBorderColor: '#2563eb',
|
|
20
|
+
rootFill: '#e2e8f0',
|
|
21
|
+
rootTextColor: '#0f172a',
|
|
22
|
+
};
|
|
23
|
+
export function resolveTheme(theme) {
|
|
24
|
+
return {
|
|
25
|
+
cardFill: theme?.cardFill ?? DEFAULT_THEME.cardFill,
|
|
26
|
+
cardBorderColor: theme?.cardBorderColor ?? DEFAULT_THEME.cardBorderColor,
|
|
27
|
+
cardBorderWidth: theme?.cardBorderWidth ?? DEFAULT_THEME.cardBorderWidth,
|
|
28
|
+
cardTextColor: theme?.cardTextColor ?? DEFAULT_THEME.cardTextColor,
|
|
29
|
+
cardMetaColor: theme?.cardMetaColor ?? DEFAULT_THEME.cardMetaColor,
|
|
30
|
+
edgeColor: theme?.edgeColor ?? DEFAULT_THEME.edgeColor,
|
|
31
|
+
edgeWidth: theme?.edgeWidth ?? DEFAULT_THEME.edgeWidth,
|
|
32
|
+
dropTargetBorderColor: theme?.dropTargetBorderColor ?? DEFAULT_THEME.dropTargetBorderColor,
|
|
33
|
+
rootFill: theme?.rootFill ?? DEFAULT_THEME.rootFill,
|
|
34
|
+
rootTextColor: theme?.rootTextColor ?? DEFAULT_THEME.rootTextColor,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/** Parses an address string loosely enough to tell "wrong shape"
|
|
2
|
+
* (INVALID_ADDRESS_FORMAT) apart from "right shape, too many levels"
|
|
3
|
+
* (MAX_DEPTH_EXCEEDED, e.g. '1.2.3.4') — a strict 3-segment regex would
|
|
4
|
+
* reject both identically and lose that distinction. */
|
|
5
|
+
function parseLoose(address) {
|
|
6
|
+
const parts = address.trim().split('.');
|
|
7
|
+
if (parts.length < 3 || !parts.every((p) => /^\d+$/.test(p)))
|
|
8
|
+
return null;
|
|
9
|
+
if (parts.length > 3)
|
|
10
|
+
return { tooDeep: true };
|
|
11
|
+
return { triple: [Number(parts[0]), Number(parts[1]), Number(parts[2])] };
|
|
12
|
+
}
|
|
13
|
+
export function validateCapabilityMapData(input) {
|
|
14
|
+
const errors = [];
|
|
15
|
+
const warnings = [];
|
|
16
|
+
if (!input || typeof input !== 'object') {
|
|
17
|
+
return { valid: false, errors: [{ code: 'SCHEMA_INVALID', message: 'Input must be an object' }], warnings: [] };
|
|
18
|
+
}
|
|
19
|
+
const raw = input;
|
|
20
|
+
if (!Array.isArray(raw.capabilities)) {
|
|
21
|
+
errors.push({ code: 'SCHEMA_INVALID', message: 'capabilities must be an array', path: 'capabilities' });
|
|
22
|
+
return { valid: false, errors, warnings };
|
|
23
|
+
}
|
|
24
|
+
const map = raw;
|
|
25
|
+
const idSet = new Set();
|
|
26
|
+
const addressToId = new Map();
|
|
27
|
+
const onDiagram = new Map();
|
|
28
|
+
for (let i = 0; i < map.capabilities.length; i++) {
|
|
29
|
+
const c = map.capabilities[i];
|
|
30
|
+
const path = `capabilities[${i}]`;
|
|
31
|
+
if (!c || typeof c !== 'object') {
|
|
32
|
+
errors.push({ code: 'SCHEMA_INVALID', message: 'capability entry must be an object', path });
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const cap = c;
|
|
36
|
+
if (typeof cap.id !== 'number') {
|
|
37
|
+
errors.push({ code: 'SCHEMA_INVALID', message: 'capability id must be a number', path });
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (idSet.has(cap.id)) {
|
|
41
|
+
errors.push({ code: 'DUPLICATE_ID', message: `Duplicate capability id: ${cap.id}`, path });
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
idSet.add(cap.id);
|
|
45
|
+
}
|
|
46
|
+
if (!cap.name || typeof cap.name !== 'string' || cap.name.trim() === '') {
|
|
47
|
+
errors.push({ code: 'EMPTY_NAME', message: `Capability ${cap.id} has empty name`, path });
|
|
48
|
+
}
|
|
49
|
+
if (typeof cap.address !== 'string') {
|
|
50
|
+
errors.push({ code: 'INVALID_ADDRESS_FORMAT', message: `Capability ${cap.id} address must be a string`, path });
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const parsed = parseLoose(cap.address);
|
|
54
|
+
if (parsed === null) {
|
|
55
|
+
errors.push({ code: 'INVALID_ADDRESS_FORMAT', message: `Capability ${cap.id} address "${cap.address}" is not in 'X.Y.Z' form`, path });
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if ('tooDeep' in parsed) {
|
|
59
|
+
errors.push({ code: 'MAX_DEPTH_EXCEEDED', message: `Capability ${cap.id} address "${cap.address}" has more than 3 levels`, path });
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const triple = parsed.triple;
|
|
63
|
+
const isBacklog = cap.backlog === true || triple[0] === 0;
|
|
64
|
+
if (!isBacklog) {
|
|
65
|
+
if (addressToId.has(cap.address)) {
|
|
66
|
+
errors.push({ code: 'DUPLICATE_ADDRESS', message: `Duplicate address "${cap.address}" (capability ${cap.id})`, path });
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
addressToId.set(cap.address, cap.id);
|
|
70
|
+
}
|
|
71
|
+
onDiagram.set(cap.id, triple);
|
|
72
|
+
}
|
|
73
|
+
if (Array.isArray(cap.maturity)) {
|
|
74
|
+
for (const snap of cap.maturity) {
|
|
75
|
+
const level = snap?.level;
|
|
76
|
+
if (typeof level === 'number' && (level < 1 || level > 5)) {
|
|
77
|
+
warnings.push({ code: 'INVALID_MATURITY_LEVEL', message: `Capability ${cap.id} has maturity level ${level} outside 1..5`, path });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (errors.length > 0)
|
|
83
|
+
return { valid: false, errors, warnings };
|
|
84
|
+
for (const [id, [x, y, z]] of onDiagram) {
|
|
85
|
+
if (z !== 0 && !addressToId.has(`${x}.${y}.0`)) {
|
|
86
|
+
warnings.push({ code: 'MISSING_PARENT_BY_ADDRESS', message: `Capability ${id} (${x}.${y}.${z}) has no L2 parent at ${x}.${y}.0` });
|
|
87
|
+
}
|
|
88
|
+
else if (y !== 0 && z === 0 && !addressToId.has(`${x}.0.0`)) {
|
|
89
|
+
warnings.push({ code: 'MISSING_PARENT_BY_ADDRESS', message: `Capability ${id} (${x}.${y}.0) has no L1 parent at ${x}.0.0` });
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
93
|
+
}
|
|
@@ -4,3 +4,12 @@ export type { CapabilityTreeNode, CapabilityTreeEdge, CapabilityTreeLevelCounts,
|
|
|
4
4
|
export { layoutCapabilityTree, TREE_NODE_WIDTH, TREE_NODE_HEIGHT, TREE_RANK_SEP, TREE_NODE_SEP } from './layout-tree.js';
|
|
5
5
|
export type { RenderCapabilityTreeOptions } from './render-capability-tree.js';
|
|
6
6
|
export { renderCapabilityTreeSvg } from './render-capability-tree.js';
|
|
7
|
+
export type { Capability, CapabilityMap, MaturitySnapshot, MutationResult, LayoutOptions, LaidOutNode, LaidOutEdge, CapabilityMapLayout, } from './dsm-schema.js';
|
|
8
|
+
export { parseAddress, formatAddress, getLevel, getParentAddress, getFirstFreeAddress, isAddressTaken, } from './address.js';
|
|
9
|
+
export { validateCapabilityMapData } from './dsm-validate.js';
|
|
10
|
+
export { layoutCapabilityMap } from './dsm-layout.js';
|
|
11
|
+
export { reparent, addChild, deleteWithDescendants, moveBranchToBacklog, restoreFromBacklog, normaliseAddresses } from './dsm-mutations.js';
|
|
12
|
+
export { CapabilityMapView } from './CapabilityMapView.js';
|
|
13
|
+
export type { CapabilityMapViewProps, CapabilityMapChange } from './CapabilityMapView.js';
|
|
14
|
+
export type { ThemeTokens } from './dsm-theme.js';
|
|
15
|
+
export { DEFAULT_MATURITY_COLOURS } from './dsm-theme.js';
|
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
export { validateCapabilityMap } from './validate.js';
|
|
2
2
|
export { layoutCapabilityTree, TREE_NODE_WIDTH, TREE_NODE_HEIGHT, TREE_RANK_SEP, TREE_NODE_SEP } from './layout-tree.js';
|
|
3
3
|
export { renderCapabilityTreeSvg } from './render-capability-tree.js';
|
|
4
|
+
export { parseAddress, formatAddress, getLevel, getParentAddress, getFirstFreeAddress, isAddressTaken, } from './address.js';
|
|
5
|
+
export { validateCapabilityMapData } from './dsm-validate.js';
|
|
6
|
+
export { layoutCapabilityMap } from './dsm-layout.js';
|
|
7
|
+
export { reparent, addChild, deleteWithDescendants, moveBranchToBacklog, restoreFromBacklog, normaliseAddresses } from './dsm-mutations.js';
|
|
8
|
+
export { CapabilityMapView } from './CapabilityMapView.js';
|
|
9
|
+
export { DEFAULT_MATURITY_COLOURS } from './dsm-theme.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Conformance test for CODEX worked examples under
|
|
2
|
+
// `tests/fixtures/notation-corpus/codex/` — pins #518 Phase C2 corpus signal.
|
|
3
|
+
import { describe, it, expect } from 'vitest';
|
|
4
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import yaml from 'js-yaml';
|
|
8
|
+
import { validateCodex, folderJurisdictionFromPath } from '../validate.js';
|
|
9
|
+
const corpusRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../../../tests/fixtures/notation-corpus/codex');
|
|
10
|
+
function codexFixturePaths() {
|
|
11
|
+
const out = [];
|
|
12
|
+
for (const zone of ['external/EU', 'internal']) {
|
|
13
|
+
const dir = path.join(corpusRoot, zone);
|
|
14
|
+
for (const f of readdirSync(dir).filter((x) => x.endsWith('.yaml'))) {
|
|
15
|
+
const rel = path.join('codex', zone, f).replace(/\\/g, '/');
|
|
16
|
+
out.push({
|
|
17
|
+
rel,
|
|
18
|
+
folderJurisdiction: folderJurisdictionFromPath(rel),
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
describe('codex worked examples (notation-corpus)', () => {
|
|
25
|
+
for (const { rel, folderJurisdiction } of codexFixturePaths()) {
|
|
26
|
+
it(`validates ${rel} without error`, () => {
|
|
27
|
+
const data = yaml.load(readFileSync(path.join(corpusRoot, rel.replace(/^codex\//, '')), 'utf-8'));
|
|
28
|
+
const v = validateCodex(data, { folderJurisdiction });
|
|
29
|
+
expect(v.valid, JSON.stringify(v.errors)).toBe(true);
|
|
30
|
+
expect(v.errors).toHaveLength(0);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { validateCodex, isCodexDoc, folderJurisdictionFromPath, } from '../validate.js';
|
|
3
|
+
const VALID_EXTERNAL = {
|
|
4
|
+
zone: 'codex',
|
|
5
|
+
id: 'LAW-GDPR-1',
|
|
6
|
+
name: 'General Data Protection Regulation',
|
|
7
|
+
type: 'LAW',
|
|
8
|
+
jurisdiction: 'eu',
|
|
9
|
+
effective_date: '2018-05-25',
|
|
10
|
+
admitted_at: '2026-06-01',
|
|
11
|
+
admitted_by: 'legal.team',
|
|
12
|
+
gate_checks: { uniqueness: 'pass' },
|
|
13
|
+
};
|
|
14
|
+
const VALID_INTERNAL = {
|
|
15
|
+
zone: 'codex',
|
|
16
|
+
id: 'INTERNAL_STANDARD-coding-conventions-1',
|
|
17
|
+
name: 'Engineering Coding Conventions',
|
|
18
|
+
type: 'INTERNAL_STANDARD',
|
|
19
|
+
issuing_authority: 'VP Engineering',
|
|
20
|
+
effective_date: '2026-01-01',
|
|
21
|
+
admitted_at: '2026-05-27',
|
|
22
|
+
admitted_by: 'vp.engineering',
|
|
23
|
+
gate_checks: { source_authority: 'VP Engineering' },
|
|
24
|
+
};
|
|
25
|
+
describe('validateCodex', () => {
|
|
26
|
+
it('accepts a valid external artefact', () => {
|
|
27
|
+
const r = validateCodex(VALID_EXTERNAL, { folderJurisdiction: 'eu' });
|
|
28
|
+
expect(r.valid, JSON.stringify(r.errors)).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
it('accepts a valid internal artefact', () => {
|
|
31
|
+
expect(validateCodex(VALID_INTERNAL).valid).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
it('CODEX-001 rejects non-mapping input', () => {
|
|
34
|
+
expect(validateCodex(null).errors.some((e) => e.code === 'CODEX-001')).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
it('CODEX-001 rejects wrong zone', () => {
|
|
37
|
+
const r = validateCodex({ ...VALID_EXTERNAL, zone: 'canon' });
|
|
38
|
+
expect(r.errors.some((e) => e.code === 'CODEX-001' && e.path === 'zone')).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
it('CODEX-002 rejects type/id prefix mismatch', () => {
|
|
41
|
+
const r = validateCodex({ ...VALID_EXTERNAL, type: 'REGULATION' });
|
|
42
|
+
expect(r.errors.some((e) => e.code === 'CODEX-002')).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
it('CODEX-003 requires jurisdiction on external artefacts', () => {
|
|
45
|
+
const r = validateCodex({ ...VALID_EXTERNAL, jurisdiction: '' });
|
|
46
|
+
expect(r.errors.some((e) => e.code === 'CODEX-003' && e.path === 'jurisdiction')).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
it('CODEX-005 enforces folder jurisdiction match', () => {
|
|
49
|
+
const r = validateCodex(VALID_EXTERNAL, { folderJurisdiction: 'ge' });
|
|
50
|
+
expect(r.errors.some((e) => e.code === 'CODEX-005')).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
it('CODEX-004 requires issuing_authority on internal artefacts', () => {
|
|
53
|
+
const r = validateCodex({ ...VALID_INTERNAL, issuing_authority: '' });
|
|
54
|
+
expect(r.errors.some((e) => e.code === 'CODEX-004')).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
describe('isCodexDoc', () => {
|
|
58
|
+
it('detects zone: codex', () => {
|
|
59
|
+
expect(isCodexDoc({ zone: 'codex', id: 'LAW-1' })).toBe(true);
|
|
60
|
+
expect(isCodexDoc({ zone: 'canon' })).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe('folderJurisdictionFromPath', () => {
|
|
64
|
+
it('parses codex/external/<jurisdiction>/ paths', () => {
|
|
65
|
+
expect(folderJurisdictionFromPath('codex/external/eu/LAW-GDPR-1.yaml')).toBe('eu');
|
|
66
|
+
expect(folderJurisdictionFromPath('codex/internal/POLICY-1.yaml')).toBeUndefined();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { CODEX_ARTEFACT_TYPES, EXTERNAL_CODEX_TYPES, INTERNAL_CODEX_TYPES } from './types.js';
|
|
2
|
+
export type { CodexArtefactType } from './types.js';
|
|
3
|
+
export { validateCodex, isCodexDoc, folderJurisdictionFromPath } from './validate.js';
|
|
4
|
+
export type { CodexValidateOptions } from './validate.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** TYPE prefixes admitted in the codex zone (REQ-003 / 14-codex.md). */
|
|
2
|
+
export declare const CODEX_ARTEFACT_TYPES: readonly ["LAW", "REGULATION", "POLICY", "INTERNAL_STANDARD"];
|
|
3
|
+
export type CodexArtefactType = (typeof CODEX_ARTEFACT_TYPES)[number];
|
|
4
|
+
/** External codex artefacts (codex/external/<jurisdiction>/). */
|
|
5
|
+
export declare const EXTERNAL_CODEX_TYPES: readonly CodexArtefactType[];
|
|
6
|
+
/** Internal codex artefacts (codex/internal/). */
|
|
7
|
+
export declare const INTERNAL_CODEX_TYPES: readonly CodexArtefactType[];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Codex artefact — external laws/regulations and internal policies/standards.
|
|
2
|
+
// Schema: methodology notations/elements/14-codex.md §3–§4.
|
|
3
|
+
/** TYPE prefixes admitted in the codex zone (REQ-003 / 14-codex.md). */
|
|
4
|
+
export const CODEX_ARTEFACT_TYPES = ['LAW', 'REGULATION', 'POLICY', 'INTERNAL_STANDARD'];
|
|
5
|
+
/** External codex artefacts (codex/external/<jurisdiction>/). */
|
|
6
|
+
export const EXTERNAL_CODEX_TYPES = ['LAW', 'REGULATION'];
|
|
7
|
+
/** Internal codex artefacts (codex/internal/). */
|
|
8
|
+
export const INTERNAL_CODEX_TYPES = ['POLICY', 'INTERNAL_STANDARD'];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ValidationResult } from '../validation-types.js';
|
|
2
|
+
export interface CodexValidateOptions {
|
|
3
|
+
/** Lowercased jurisdiction folder segment from `codex/external/<jurisdiction>/`. */
|
|
4
|
+
folderJurisdiction?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function validateCodex(input: unknown, options?: CodexValidateOptions): ValidationResult;
|
|
7
|
+
/** True when a parsed document is a codex-zone artefact candidate. */
|
|
8
|
+
export declare function isCodexDoc(data: unknown): boolean;
|
|
9
|
+
/** Infer codex folder jurisdiction from a repo-relative path such as
|
|
10
|
+
* `codex/external/eu/LAW-GDPR-1.yaml` → `eu`. */
|
|
11
|
+
export declare function folderJurisdictionFromPath(filePath: string): string | undefined;
|
|
@@ -0,0 +1,103 @@
|
|
|
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
|
+
import { isCanonicalId, typeOfId } from '../typed-id.js';
|
|
10
|
+
import { CODEX_ARTEFACT_TYPES, EXTERNAL_CODEX_TYPES, INTERNAL_CODEX_TYPES, } from './types.js';
|
|
11
|
+
function isCodexType(type) {
|
|
12
|
+
return type !== null && CODEX_ARTEFACT_TYPES.includes(type);
|
|
13
|
+
}
|
|
14
|
+
function normalizeType(value) {
|
|
15
|
+
if (typeof value !== 'string' || value.trim() === '')
|
|
16
|
+
return undefined;
|
|
17
|
+
return value.trim().toUpperCase();
|
|
18
|
+
}
|
|
19
|
+
export function validateCodex(input, options = {}) {
|
|
20
|
+
const errors = [];
|
|
21
|
+
const warnings = [];
|
|
22
|
+
if (input === null || typeof input !== 'object' || Array.isArray(input)) {
|
|
23
|
+
errors.push({ code: 'CODEX-001', message: 'Codex artefact must be a YAML mapping.' });
|
|
24
|
+
return { valid: false, errors, warnings };
|
|
25
|
+
}
|
|
26
|
+
const c = input;
|
|
27
|
+
if (c.zone !== 'codex') {
|
|
28
|
+
errors.push({ code: 'CODEX-001', message: 'zone must be the fixed value "codex".', path: 'zone' });
|
|
29
|
+
}
|
|
30
|
+
const idType = typeOfId(c.id);
|
|
31
|
+
if (!isCanonicalId(c.id) || !isCodexType(idType)) {
|
|
32
|
+
errors.push({
|
|
33
|
+
code: 'CODEX-001',
|
|
34
|
+
message: `id "${String(c.id)}" must be a typed codex id (LAW, REGULATION, POLICY, or INTERNAL_STANDARD).`,
|
|
35
|
+
path: 'id',
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
if (typeof c.name !== 'string' || c.name.trim() === '') {
|
|
39
|
+
errors.push({ code: 'CODEX-001', message: 'name is required.', path: 'name' });
|
|
40
|
+
}
|
|
41
|
+
for (const field of ['admitted_at', 'admitted_by']) {
|
|
42
|
+
const v = c[field];
|
|
43
|
+
if (typeof v !== 'string' || v.trim() === '') {
|
|
44
|
+
errors.push({ code: 'CODEX-001', message: `${field} is required.`, path: field });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (c.gate_checks === null || typeof c.gate_checks !== 'object' || Array.isArray(c.gate_checks)) {
|
|
48
|
+
errors.push({ code: 'CODEX-001', message: 'gate_checks is required and must be a mapping.', path: 'gate_checks' });
|
|
49
|
+
}
|
|
50
|
+
const declaredType = normalizeType(c.type);
|
|
51
|
+
if (declaredType !== undefined && idType && declaredType !== idType) {
|
|
52
|
+
errors.push({
|
|
53
|
+
code: 'CODEX-002',
|
|
54
|
+
message: `type "${String(c.type)}" does not match the id TYPE prefix "${idType}".`,
|
|
55
|
+
path: 'type',
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const artefactType = (declaredType ?? idType);
|
|
59
|
+
if (artefactType && EXTERNAL_CODEX_TYPES.includes(artefactType)) {
|
|
60
|
+
const jurisdiction = typeof c.jurisdiction === 'string' ? c.jurisdiction.trim() : '';
|
|
61
|
+
if (!jurisdiction) {
|
|
62
|
+
errors.push({ code: 'CODEX-003', message: 'jurisdiction is required for external codex artefacts.', path: 'jurisdiction' });
|
|
63
|
+
}
|
|
64
|
+
else if (options.folderJurisdiction && jurisdiction.toLowerCase() !== options.folderJurisdiction.toLowerCase()) {
|
|
65
|
+
errors.push({
|
|
66
|
+
code: 'CODEX-005',
|
|
67
|
+
message: `jurisdiction "${jurisdiction}" must match the parent folder "${options.folderJurisdiction}" (CODEX-001).`,
|
|
68
|
+
path: 'jurisdiction',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (typeof c.effective_date !== 'string' || c.effective_date.trim() === '') {
|
|
72
|
+
errors.push({ code: 'CODEX-003', message: 'effective_date is required for external codex artefacts.', path: 'effective_date' });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (artefactType && INTERNAL_CODEX_TYPES.includes(artefactType)) {
|
|
76
|
+
if (typeof c.issuing_authority !== 'string' || c.issuing_authority.trim() === '') {
|
|
77
|
+
errors.push({
|
|
78
|
+
code: 'CODEX-004',
|
|
79
|
+
message: 'issuing_authority is required for internal codex artefacts.',
|
|
80
|
+
path: 'issuing_authority',
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
if (typeof c.effective_date !== 'string' || c.effective_date.trim() === '') {
|
|
84
|
+
errors.push({ code: 'CODEX-004', message: 'effective_date is required for internal codex artefacts.', path: 'effective_date' });
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
88
|
+
}
|
|
89
|
+
/** True when a parsed document is a codex-zone artefact candidate. */
|
|
90
|
+
export function isCodexDoc(data) {
|
|
91
|
+
if (data === null || typeof data !== 'object' || Array.isArray(data))
|
|
92
|
+
return false;
|
|
93
|
+
return data.zone === 'codex';
|
|
94
|
+
}
|
|
95
|
+
/** Infer codex folder jurisdiction from a repo-relative path such as
|
|
96
|
+
* `codex/external/eu/LAW-GDPR-1.yaml` → `eu`. */
|
|
97
|
+
export function folderJurisdictionFromPath(filePath) {
|
|
98
|
+
const parts = filePath.replace(/\\/g, '/').toLowerCase().split('/');
|
|
99
|
+
const extIdx = parts.indexOf('external');
|
|
100
|
+
if (extIdx < 0 || extIdx + 1 >= parts.length)
|
|
101
|
+
return undefined;
|
|
102
|
+
return parts[extIdx + 1] || undefined;
|
|
103
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { admitDocumentToCatalog, buildComplianceScan, catalogFromMap, } from '../canon-catalog.js';
|
|
3
|
+
describe('buildComplianceScan', () => {
|
|
4
|
+
it('admits codex, requirement, and assertion ids into the catalogue', () => {
|
|
5
|
+
const docs = [
|
|
6
|
+
{
|
|
7
|
+
path: 'codex/external/ge/LAW-PERSONAL-DATA-2017-1.yaml',
|
|
8
|
+
data: {
|
|
9
|
+
zone: 'codex',
|
|
10
|
+
id: 'LAW-PERSONAL-DATA-2017-1',
|
|
11
|
+
name: 'Law',
|
|
12
|
+
type: 'LAW',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
path: 'canon/elements/01_motivation/requirements/REQUIREMENT-DATA-ERASURE-1.yaml',
|
|
17
|
+
data: {
|
|
18
|
+
notation: 'requirement',
|
|
19
|
+
id: 'REQUIREMENT-DATA-ERASURE-1',
|
|
20
|
+
name: 'Erasure',
|
|
21
|
+
derived_from: ['LAW-PERSONAL-DATA-2017-1'],
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
path: 'canon/elements/02_business/products/PRODUCT-MOBILE-1.yaml',
|
|
26
|
+
data: { notation: 'product', id: 'PRODUCT-MOBILE-1', name: 'Mobile' },
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
path: 'canon/assertions/ASSERTION-MOBILE-DATA-ERASURE-1.yaml',
|
|
30
|
+
data: {
|
|
31
|
+
notation: 'assertion',
|
|
32
|
+
id: 'ASSERTION-MOBILE-DATA-ERASURE-1',
|
|
33
|
+
about: 'REQUIREMENT-DATA-ERASURE-1',
|
|
34
|
+
subject: 'PRODUCT-MOBILE-1',
|
|
35
|
+
status: 'compliant',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
const { catalog, complianceCanon, pathById } = buildComplianceScan(docs);
|
|
40
|
+
expect(catalog.typeOf('LAW-PERSONAL-DATA-2017-1')).toBe('LAW');
|
|
41
|
+
expect(catalog.typeOf('REQUIREMENT-DATA-ERASURE-1')).toBe('REQUIREMENT');
|
|
42
|
+
expect(catalog.typeOf('PRODUCT-MOBILE-1')).toBe('PRODUCT');
|
|
43
|
+
expect(complianceCanon.requirements).toHaveLength(1);
|
|
44
|
+
expect(complianceCanon.assertions).toHaveLength(1);
|
|
45
|
+
expect(complianceCanon.codex).toHaveLength(1);
|
|
46
|
+
expect(pathById.get('REQUIREMENT-DATA-ERASURE-1')).toContain('requirements/');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
describe('admitDocumentToCatalog', () => {
|
|
50
|
+
it('uses explicit codex type when the id prefix is ambiguous', () => {
|
|
51
|
+
const map = new Map();
|
|
52
|
+
admitDocumentToCatalog(map, {
|
|
53
|
+
zone: 'codex',
|
|
54
|
+
id: 'INTERNAL_STANDARD-coding-conventions-1',
|
|
55
|
+
type: 'INTERNAL_STANDARD',
|
|
56
|
+
});
|
|
57
|
+
const catalog = catalogFromMap(map);
|
|
58
|
+
expect(catalog.typeOf('INTERNAL_STANDARD-coding-conventions-1')).toBe('INTERNAL_STANDARD');
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|