@transitrix/diagrams 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (267) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +47 -47
  3. package/dist/__tests__/edge-path.test.js +8 -0
  4. package/dist/activity-card/__tests__/example.test.js +20 -1
  5. package/dist/activity-card/__tests__/layout.test.js +102 -11
  6. package/dist/activity-card/__tests__/resolver.test.js +139 -6
  7. package/dist/activity-card/index.d.ts +1 -1
  8. package/dist/activity-card/layout.d.ts +1 -1
  9. package/dist/activity-card/layout.js +220 -103
  10. package/dist/activity-card/resolver.js +107 -26
  11. package/dist/activity-card/types.d.ts +150 -27
  12. package/dist/activity-card/validate.js +3 -0
  13. package/dist/actor/__tests__/validate.test.js +63 -31
  14. package/dist/actor/index.d.ts +1 -1
  15. package/dist/actor/index.js +1 -1
  16. package/dist/actor/types.d.ts +7 -3
  17. package/dist/actor/types.js +17 -3
  18. package/dist/actor/validate.js +55 -26
  19. package/dist/assertion/types.d.ts +3 -1
  20. package/dist/assertion/types.js +1 -1
  21. package/dist/change/__tests__/validate.test.js +40 -34
  22. package/dist/change/index.d.ts +1 -0
  23. package/dist/change/types.d.ts +1 -1
  24. package/dist/change/types.js +2 -2
  25. package/dist/change/validate.d.ts +6 -1
  26. package/dist/change/validate.js +68 -41
  27. package/dist/compliance/__tests__/classify.test.js +34 -0
  28. package/dist/compliance/__tests__/coverage-metric.test.js +146 -46
  29. package/dist/compliance/__tests__/html.test.js +2 -2
  30. package/dist/compliance/__tests__/impact.test.js +311 -6
  31. package/dist/compliance/classify.d.ts +6 -0
  32. package/dist/compliance/classify.js +9 -1
  33. package/dist/compliance/coverage-metric.d.ts +31 -11
  34. package/dist/compliance/coverage-metric.js +149 -39
  35. package/dist/compliance/html.js +77 -74
  36. package/dist/compliance/impact.d.ts +77 -16
  37. package/dist/compliance/impact.js +193 -25
  38. package/dist/compliance/index.d.ts +2 -2
  39. package/dist/compliance/index.js +1 -1
  40. package/dist/compliance/markdown.js +1 -0
  41. package/dist/compliance/types.d.ts +25 -6
  42. package/dist/edge-path.d.ts +8 -4
  43. package/dist/edge-path.js +11 -6
  44. package/dist/factor/__tests__/validate.test.js +74 -45
  45. package/dist/factor/index.d.ts +3 -2
  46. package/dist/factor/index.js +1 -1
  47. package/dist/factor/types.d.ts +4 -7
  48. package/dist/factor/types.js +10 -5
  49. package/dist/factor/validate.d.ts +6 -1
  50. package/dist/factor/validate.js +68 -50
  51. package/dist/fgca/__tests__/resolver.test.js +185 -0
  52. package/dist/fgca/index.d.ts +2 -0
  53. package/dist/fgca/index.js +1 -0
  54. package/dist/fgca/resolver.d.ts +49 -0
  55. package/dist/fgca/resolver.js +124 -0
  56. package/dist/goals/index.d.ts +1 -1
  57. package/dist/index.d.ts +5 -0
  58. package/dist/index.js +5 -0
  59. package/dist/process-blueprint/__tests__/layout.test.js +37 -0
  60. package/dist/process-blueprint/layout.js +5 -1
  61. package/dist/process-blueprint/types.d.ts +22 -1
  62. package/dist/stakeholder/__tests__/validate.test.js +49 -47
  63. package/dist/stakeholder/index.d.ts +1 -0
  64. package/dist/stakeholder/types.d.ts +3 -4
  65. package/dist/stakeholder/types.js +2 -3
  66. package/dist/stakeholder/validate.d.ts +6 -1
  67. package/dist/stakeholder/validate.js +71 -50
  68. package/dist/target-state/__tests__/validate.test.js +43 -27
  69. package/dist/target-state/index.d.ts +2 -0
  70. package/dist/target-state/index.js +1 -0
  71. package/dist/target-state/types.d.ts +6 -1
  72. package/dist/target-state/types.js +8 -7
  73. package/dist/target-state/validate.d.ts +6 -1
  74. package/dist/target-state/validate.js +69 -45
  75. package/dist/theme/themes.js +69 -69
  76. package/dist/webview/__tests__/entry.test.js +11 -11
  77. package/dist/webview/render-activities.d.ts +3 -1
  78. package/dist/webview/render-activities.js +2 -2
  79. package/dist/webview/render-activity-card.js +104 -77
  80. package/dist/webview/render-fgca.d.ts +4 -0
  81. package/dist/webview/render-fgca.js +2 -5
  82. package/dist/webview/render-goals.d.ts +1 -0
  83. package/dist/webview/render-goals.js +15 -15
  84. package/dist/webview/render-process-blueprint.js +10 -10
  85. package/package.json +40 -40
  86. package/src/__tests__/edge-path.test.ts +58 -46
  87. package/src/__tests__/scope.test.ts +27 -27
  88. package/src/__tests__/yaml-normalize.test.ts +158 -158
  89. package/src/activities/__tests__/cpm.test.ts +164 -164
  90. package/src/activities/__tests__/gantt.test.ts +245 -245
  91. package/src/activities/__tests__/layout.test.ts +112 -112
  92. package/src/activities/__tests__/validate.test.ts +409 -409
  93. package/src/activities/cpm.ts +95 -95
  94. package/src/activities/gantt.ts +276 -276
  95. package/src/activities/index.ts +28 -28
  96. package/src/activities/layout.ts +112 -112
  97. package/src/activities/types.ts +148 -148
  98. package/src/activities/validate.ts +357 -357
  99. package/src/activity-card/__tests__/example.test.ts +85 -66
  100. package/src/activity-card/__tests__/layout.test.ts +169 -63
  101. package/src/activity-card/__tests__/resolver.test.ts +307 -161
  102. package/src/activity-card/__tests__/validate.test.ts +131 -131
  103. package/src/activity-card/index.ts +38 -32
  104. package/src/activity-card/layout.ts +324 -194
  105. package/src/activity-card/resolver.ts +415 -337
  106. package/src/activity-card/types.ts +376 -237
  107. package/src/activity-card/validate.ts +162 -158
  108. package/src/actor/__tests__/validate.test.ts +70 -31
  109. package/src/actor/index.ts +1 -1
  110. package/src/actor/types.ts +21 -7
  111. package/src/actor/validate.ts +59 -27
  112. package/src/applications/__tests__/validate.test.ts +256 -256
  113. package/src/applications/index.ts +11 -11
  114. package/src/applications/types.ts +43 -43
  115. package/src/applications/validate.ts +131 -131
  116. package/src/assertion/__tests__/example.test.ts +37 -37
  117. package/src/assertion/__tests__/validate.test.ts +142 -142
  118. package/src/assertion/index.ts +12 -12
  119. package/src/assertion/types.ts +60 -58
  120. package/src/assertion/validate.ts +145 -145
  121. package/src/blocks/__tests__/layout.test.ts +169 -169
  122. package/src/blocks/__tests__/validate.test.ts +260 -260
  123. package/src/blocks/index.ts +17 -17
  124. package/src/blocks/layout.ts +186 -186
  125. package/src/blocks/types.ts +62 -62
  126. package/src/blocks/validate.ts +206 -206
  127. package/src/capability-map/__tests__/validate.test.ts +238 -238
  128. package/src/capability-map/index.ts +8 -8
  129. package/src/capability-map/types.ts +29 -29
  130. package/src/capability-map/validate.ts +127 -127
  131. package/src/change/__tests__/validate.test.ts +43 -34
  132. package/src/change/index.ts +1 -0
  133. package/src/change/types.ts +5 -3
  134. package/src/change/validate.ts +75 -43
  135. package/src/compliance/__tests__/classify.test.ts +64 -27
  136. package/src/compliance/__tests__/confidence.test.ts +98 -98
  137. package/src/compliance/__tests__/coverage-metric.test.ts +300 -188
  138. package/src/compliance/__tests__/demo-scenario.test.ts +273 -273
  139. package/src/compliance/__tests__/example.test.ts +57 -57
  140. package/src/compliance/__tests__/gap-report.test.ts +128 -128
  141. package/src/compliance/__tests__/html.test.ts +141 -141
  142. package/src/compliance/__tests__/impact.test.ts +840 -481
  143. package/src/compliance/__tests__/markdown.test.ts +94 -94
  144. package/src/compliance/__tests__/views.test.ts +64 -64
  145. package/src/compliance/classify.ts +100 -84
  146. package/src/compliance/confidence.ts +69 -69
  147. package/src/compliance/coverage-metric.ts +404 -263
  148. package/src/compliance/gap-report.ts +94 -94
  149. package/src/compliance/html.ts +298 -295
  150. package/src/compliance/impact.ts +861 -677
  151. package/src/compliance/index.ts +57 -56
  152. package/src/compliance/markdown.ts +136 -135
  153. package/src/compliance/reverse-index.ts +33 -33
  154. package/src/compliance/types.ts +136 -120
  155. package/src/compliance/views.ts +46 -46
  156. package/src/compliance-matrix/__tests__/build.test.ts +200 -200
  157. package/src/compliance-matrix/__tests__/example.test.ts +57 -57
  158. package/src/compliance-matrix/index.ts +11 -11
  159. package/src/compliance-matrix/types.ts +102 -102
  160. package/src/edge-path.ts +50 -44
  161. package/src/factor/__tests__/validate.test.ts +80 -45
  162. package/src/factor/index.ts +3 -7
  163. package/src/factor/types.ts +14 -18
  164. package/src/factor/validate.ts +77 -51
  165. package/src/fgca/__tests__/chain-table.test.ts +172 -172
  166. package/src/fgca/__tests__/layout.test.ts +179 -179
  167. package/src/fgca/__tests__/parse-canonical.test.ts +248 -248
  168. package/src/fgca/__tests__/preview-layout.test.ts +124 -124
  169. package/src/fgca/__tests__/resolver.test.ts +209 -0
  170. package/src/fgca/__tests__/validate.test.ts +142 -142
  171. package/src/fgca/chain-table.ts +225 -225
  172. package/src/fgca/index.ts +47 -45
  173. package/src/fgca/layout.ts +204 -204
  174. package/src/fgca/nodeTypes.ts +11 -11
  175. package/src/fgca/nodes/FGCAActivityNode.tsx +65 -65
  176. package/src/fgca/nodes/FGCAChangeNode.tsx +64 -64
  177. package/src/fgca/nodes/FGCAFactorNode.tsx +64 -64
  178. package/src/fgca/nodes/FGCAGoalNode.tsx +72 -72
  179. package/src/fgca/parse-canonical.ts +400 -400
  180. package/src/fgca/preview-layout.ts +217 -217
  181. package/src/fgca/resolver.ts +157 -0
  182. package/src/fgca/types.ts +50 -50
  183. package/src/fgca/validate.ts +126 -126
  184. package/src/geometry.ts +9 -9
  185. package/src/goals/__tests__/layout.test.ts +175 -175
  186. package/src/goals/__tests__/mutations.test.ts +111 -111
  187. package/src/goals/__tests__/parse-canonical.test.ts +145 -145
  188. package/src/goals/__tests__/validate.test.ts +202 -202
  189. package/src/goals/index.ts +20 -21
  190. package/src/goals/layout.ts +172 -172
  191. package/src/goals/mutations.ts +123 -123
  192. package/src/goals/parse-canonical.ts +286 -286
  193. package/src/goals/types.ts +85 -85
  194. package/src/goals/validate.ts +136 -136
  195. package/src/index.ts +28 -23
  196. package/src/process-blueprint/__tests__/compliance-lane.test.ts +320 -320
  197. package/src/process-blueprint/__tests__/layout.test.ts +240 -195
  198. package/src/process-blueprint/__tests__/validate.test.ts +310 -310
  199. package/src/process-blueprint/index.ts +31 -31
  200. package/src/process-blueprint/layout.ts +535 -531
  201. package/src/process-blueprint/types.ts +263 -242
  202. package/src/process-blueprint/validate.ts +181 -181
  203. package/src/process-map/__tests__/validate.test.ts +223 -223
  204. package/src/process-map/index.ts +10 -10
  205. package/src/process-map/types.ts +36 -36
  206. package/src/process-map/validate.ts +135 -135
  207. package/src/products/__tests__/validate.test.ts +239 -239
  208. package/src/products/index.ts +9 -9
  209. package/src/products/types.ts +31 -31
  210. package/src/products/validate.ts +112 -112
  211. package/src/repo-validate/__tests__/validate-repo.test.ts +149 -149
  212. package/src/repo-validate/index.ts +2 -2
  213. package/src/repo-validate/types.ts +50 -50
  214. package/src/repo-validate/validate-repo.ts +196 -196
  215. package/src/requirement/__tests__/example.test.ts +24 -24
  216. package/src/requirement/__tests__/validate.test.ts +96 -96
  217. package/src/requirement/index.ts +4 -4
  218. package/src/requirement/types.ts +35 -35
  219. package/src/requirement/validate.ts +92 -92
  220. package/src/scenarios/__tests__/validate.test.ts +209 -209
  221. package/src/scenarios/index.ts +15 -15
  222. package/src/scenarios/types.ts +37 -37
  223. package/src/scenarios/validate.ts +122 -122
  224. package/src/schema-version.ts +16 -16
  225. package/src/scope.ts +46 -46
  226. package/src/stakeholder/__tests__/validate.test.ts +51 -47
  227. package/src/stakeholder/index.ts +1 -0
  228. package/src/stakeholder/types.ts +7 -7
  229. package/src/stakeholder/validate.ts +79 -52
  230. package/src/target-state/__tests__/validate.test.ts +48 -27
  231. package/src/target-state/index.ts +2 -0
  232. package/src/target-state/types.ts +11 -7
  233. package/src/target-state/validate.ts +76 -46
  234. package/src/theme/index.ts +4 -4
  235. package/src/theme/themes.ts +237 -237
  236. package/src/theme/tokens.ts +133 -133
  237. package/src/typed-id.ts +56 -56
  238. package/src/validation-types.ts +36 -36
  239. package/src/webview/__tests__/entry-notations.test.ts +77 -77
  240. package/src/webview/__tests__/entry.test.ts +64 -64
  241. package/src/webview/__tests__/render-goals.test.ts +72 -72
  242. package/src/webview/entry.ts +292 -292
  243. package/src/webview/render-activities.ts +5 -3
  244. package/src/webview/render-activity-card.ts +266 -234
  245. package/src/webview/render-fgca.ts +6 -5
  246. package/src/webview/render-goals.ts +111 -110
  247. package/src/webview/render-process-blueprint.ts +207 -207
  248. package/src/webview/styles.css +322 -323
  249. package/src/yaml-normalize.ts +56 -56
  250. package/dist/element-envelope.d.ts +0 -15
  251. package/dist/element-envelope.js +0 -55
  252. package/dist/issues/__tests__/layout.test.js +0 -62
  253. package/dist/issues/__tests__/validate.test.d.ts +0 -1
  254. package/dist/issues/__tests__/validate.test.js +0 -82
  255. package/dist/issues/index.d.ts +0 -4
  256. package/dist/issues/index.js +0 -2
  257. package/dist/issues/layout.d.ts +0 -10
  258. package/dist/issues/layout.js +0 -82
  259. package/dist/issues/types.d.ts +0 -61
  260. package/dist/issues/types.js +0 -6
  261. package/dist/issues/validate.d.ts +0 -3
  262. package/dist/issues/validate.js +0 -143
  263. package/dist/webview/transitrix-render.css +0 -323
  264. package/dist/webview/transitrix-render.js +0 -6452
  265. package/dist/webview/transitrix-render.js.map +0 -7
  266. package/src/element-envelope.ts +0 -76
  267. /package/dist/{issues/__tests__/layout.test.d.ts → fgca/__tests__/resolver.test.d.ts} +0 -0
@@ -15,59 +15,65 @@ function valid() {
15
15
  valid_to: null,
16
16
  };
17
17
  }
18
- const codes = (input) => validateChange(input).errors.map(e => e.code);
18
+ const codes = (input, opts) => validateChange(input, opts).errors.map(e => e.code);
19
19
  describe('validateChange — positive', () => {
20
- it('accepts the worked-example CHANGE-EU-CRM-1', () => {
20
+ it('accepts a well-formed change', () => {
21
21
  const r = validateChange(valid());
22
22
  expect(r.valid, JSON.stringify(r.errors)).toBe(true);
23
- expect(r.errors).toHaveLength(0);
24
23
  });
25
- it('accepts a minimal change (no goals, no parent)', () => {
26
- const r = valid();
27
- delete r.goals;
28
- expect(validateChange(r).valid).toBe(true);
24
+ it('accepts a minimal change with no optional fields', () => {
25
+ const c = valid();
26
+ delete c.goals;
27
+ delete c.description;
28
+ expect(validateChange(c).valid).toBe(true);
29
29
  });
30
30
  it('accepts a child change with a CHANGE parent', () => {
31
- expect(validateChange({ ...valid(), id: 'CHANGE-EU-CRM-DETAIL-1', parent: 'CHANGE-EU-CRM-1' }).valid).toBe(true);
31
+ const c = valid();
32
+ c.id = 'CHANGE-EU-CRM-2';
33
+ c.parent = 'CHANGE-EU-CRM-1';
34
+ expect(validateChange(c).valid).toBe(true);
32
35
  });
33
36
  });
34
- describe('validateChange — CHANGE-001 (envelope / shape)', () => {
35
- it('flags a non-object', () => {
36
- expect(codes(null)).toEqual(['CHANGE-001']);
37
+ describe('validateChange — CHANGE-001 (shape / id grammar)', () => {
38
+ it('flags a missing required envelope field', () => {
39
+ const c = valid();
40
+ delete c.name;
41
+ expect(codes(c)).toContain('CHANGE-001');
37
42
  });
38
- it('flags wrong notation tag', () => {
39
- expect(codes({ ...valid(), notation: 'gap' })).toContain('CHANGE-001');
43
+ it('flags an id that violates the grammar', () => {
44
+ expect(codes({ ...valid(), id: 'CHANGE-EU-CRM-001' })).toContain('CHANGE-001');
45
+ expect(codes({ ...valid(), id: 'CHG-1' })).toContain('CHANGE-001');
40
46
  });
41
- it('flags an id with the wrong TYPE prefix', () => {
42
- expect(codes({ ...valid(), id: 'ACTIVITY-DESIGN-1' })).toContain('CHANGE-001');
47
+ it('flags a wrong notation tag', () => {
48
+ expect(codes({ ...valid(), notation: 'gap' })).toContain('CHANGE-001');
43
49
  });
44
- it('flags a missing admission field', () => {
45
- const r = valid();
46
- delete r.admitted_at;
47
- expect(codes(r)).toContain('CHANGE-001');
50
+ it('flags a non-array goals', () => {
51
+ expect(codes({ ...valid(), goals: 'GOAL-EU-1' })).toContain('CHANGE-001');
48
52
  });
49
- it('flags a missing valid_to key', () => {
50
- const r = valid();
51
- delete r.valid_to;
52
- expect(codes(r)).toContain('CHANGE-001');
53
+ it('rejects a non-object', () => {
54
+ expect(codes(null)).toEqual(['CHANGE-001']);
53
55
  });
54
56
  });
55
- describe('validateChange — CHANGE-002 (goals refs)', () => {
56
- it('flags a non-GOAL typed id in goals', () => {
57
- expect(codes({ ...valid(), goals: ['CHANGE-EU-CRM-1'] })).toContain('CHANGE-002');
58
- });
59
- it('flags a malformed entry in goals', () => {
57
+ describe('validateChange — CHANGE-002 (goals references)', () => {
58
+ it('flags a malformed goals entry', () => {
60
59
  expect(codes({ ...valid(), goals: ['not an id'] })).toContain('CHANGE-002');
61
60
  });
62
- it('flags goals when it is not an array', () => {
63
- expect(codes({ ...valid(), goals: 'GOAL-EU-1' })).toContain('CHANGE-002');
61
+ it('flags a goals entry of the wrong TYPE', () => {
62
+ expect(codes({ ...valid(), goals: ['FACTOR-X-1'] })).toContain('CHANGE-002');
63
+ });
64
+ it('flags an unresolved id when a catalog is supplied', () => {
65
+ const catalog = { typeOf: () => undefined };
66
+ expect(codes(valid(), { catalog })).toContain('CHANGE-002');
64
67
  });
65
68
  });
66
- describe('validateChange — CHANGE-003 (parent ref)', () => {
67
- it('flags a non-CHANGE parent', () => {
68
- expect(codes({ ...valid(), parent: 'GOAL-EU-1' })).toContain('CHANGE-003');
69
+ describe('validateChange — CHANGE-003 (parent)', () => {
70
+ it('flags a parent of the wrong TYPE', () => {
71
+ expect(codes({ ...valid(), parent: 'GOAL-X-1' })).toContain('CHANGE-003');
69
72
  });
70
- it('flags self-parenting', () => {
73
+ it('flags a parent equal to self', () => {
71
74
  expect(codes({ ...valid(), parent: 'CHANGE-EU-CRM-1' })).toContain('CHANGE-003');
72
75
  });
76
+ it('flags a malformed parent', () => {
77
+ expect(codes({ ...valid(), parent: 'not an id' })).toContain('CHANGE-003');
78
+ });
73
79
  });
@@ -1,2 +1,3 @@
1
1
  export type { Change } from './types.js';
2
2
  export { validateChange } from './validate.js';
3
+ export type { ChangeValidateOptions } from './validate.js';
@@ -5,7 +5,7 @@ export interface Change {
5
5
  name: string;
6
6
  /** GOAL-… IDs this change delivers. */
7
7
  goals?: string[];
8
- /** CHANGE-… — the higher-scale change this one decomposes from. */
8
+ /** Higher-scale CHANGE this one decomposes from (recursive, §6.1). */
9
9
  parent?: string;
10
10
  description?: string;
11
11
  zone: 'canon';
@@ -1,3 +1,3 @@
1
- // CHANGE — implementation-layer gap primitive (ArchiMate Gap).
2
- // Schema: methodology notations/ELEMENT_PRIMITIVES.md §7.3 + envelope §3.
1
+ // CHANGE — implementation-layer Gap (ArchiMate). The BDN change layer.
2
+ // Schema: methodology notations/ELEMENT_PRIMITIVES.md §7.3.
3
3
  export {};
@@ -1,2 +1,7 @@
1
1
  import type { ValidationResult } from '../validation-types.js';
2
- export declare function validateChange(input: unknown): ValidationResult;
2
+ import { type CanonCatalog } from '../typed-id.js';
3
+ export interface ChangeValidateOptions {
4
+ /** When provided, enforces that each `goals` / `parent` id is admitted. */
5
+ catalog?: CanonCatalog;
6
+ }
7
+ export declare function validateChange(input: unknown, options?: ChangeValidateOptions): ValidationResult;
@@ -1,56 +1,83 @@
1
1
  // CHANGE validator — methodology notations/ELEMENT_PRIMITIVES.md §7.3.
2
2
  //
3
- // Implements CHANGE-001..003. Cross-cutting HDR-/LIFECYCLE- and the BDN
4
- // linkage rules across documents are owned elsewhere (CONTRACT.md §8) and
5
- // out of scope for this single-artefact validator.
6
- //
7
- // §7.3 defines no subtype vocabulary on `type`, so there is no enum gate here.
8
- import { typeOfId } from '../typed-id.js';
9
- import { validateElementEnvelope } from '../element-envelope.js';
10
- export function validateChange(input) {
3
+ // Codes:
4
+ // CHANGE-001 — shape / id grammar / required envelope fields.
5
+ // CHANGE-002 — `goals` entry malformed or wrong TYPE (must be GOAL-…).
6
+ // CHANGE-003 — `parent` malformed, wrong TYPE (must be CHANGE-…), or equals self.
7
+ import { typeOfId, isCanonicalIdOfType } from '../typed-id.js';
8
+ const REQUIRED_STRING_FIELDS = [
9
+ 'name', 'admitted_at', 'admitted_by', 'valid_from',
10
+ ];
11
+ export function validateChange(input, options = {}) {
11
12
  const errors = [];
12
13
  const warnings = [];
13
- const { record, errors: envErrors } = validateElementEnvelope(input, {
14
- notation: 'change',
15
- idType: 'CHANGE',
16
- code: 'CHANGE-001',
17
- });
18
- errors.push(...envErrors);
19
- if (!record)
14
+ if (input === null || typeof input !== 'object' || Array.isArray(input)) {
15
+ errors.push({ code: 'CHANGE-001', message: 'Change must be a YAML mapping.' });
20
16
  return { valid: false, errors, warnings };
21
- // CHANGE-002 — goals[] must be a list of GOAL typed ids.
22
- if (record.goals !== undefined) {
23
- if (!Array.isArray(record.goals)) {
24
- errors.push({ code: 'CHANGE-002', message: 'goals must be a list of typed GOAL IDs.', path: 'goals' });
17
+ }
18
+ const c = input;
19
+ // CHANGE-001 — id grammar + notation tag + envelope.
20
+ if (!isCanonicalIdOfType(c.id, 'CHANGE')) {
21
+ errors.push({ code: 'CHANGE-001', message: `id "${String(c.id)}" must match CHANGE-[<middle>-]<INTEGER>.`, path: 'id' });
22
+ }
23
+ if (c.notation !== 'change') {
24
+ errors.push({ code: 'CHANGE-001', message: 'notation must be the fixed value "change".', path: 'notation' });
25
+ }
26
+ if (c.zone !== undefined && c.zone !== 'canon') {
27
+ errors.push({ code: 'CHANGE-001', message: 'zone must be "canon" for a CHANGE.', path: 'zone' });
28
+ }
29
+ else if (c.zone === undefined) {
30
+ errors.push({ code: 'CHANGE-001', message: 'zone is required.', path: 'zone' });
31
+ }
32
+ for (const field of REQUIRED_STRING_FIELDS) {
33
+ const v = c[field];
34
+ if (typeof v !== 'string' || v.trim() === '') {
35
+ errors.push({ code: 'CHANGE-001', message: `${field} is required.`, path: field });
36
+ }
37
+ }
38
+ if (c.gate_checks === null || typeof c.gate_checks !== 'object') {
39
+ errors.push({ code: 'CHANGE-001', message: 'gate_checks is required and must be a mapping.', path: 'gate_checks' });
40
+ }
41
+ if (!('valid_to' in c) || !(typeof c.valid_to === 'string' || c.valid_to === null)) {
42
+ errors.push({ code: 'CHANGE-001', message: 'valid_to is required (an ISO date string or null).', path: 'valid_to' });
43
+ }
44
+ // CHANGE-002 — `goals` references.
45
+ if (c.goals !== undefined) {
46
+ if (!Array.isArray(c.goals)) {
47
+ errors.push({ code: 'CHANGE-001', message: 'goals must be a list of typed IDs.', path: 'goals' });
25
48
  }
26
49
  else {
27
- record.goals.forEach((ref, i) => {
28
- if (typeOfId(ref) !== 'GOAL') {
29
- errors.push({
30
- code: 'CHANGE-002',
31
- message: `goals[${i}] "${String(ref)}" must be a GOAL typed ID.`,
32
- path: `goals[${i}]`,
33
- });
50
+ c.goals.forEach((ref, i) => {
51
+ const type = typeOfId(ref);
52
+ if (!type) {
53
+ errors.push({ code: 'CHANGE-002', message: `goals[${i}] "${String(ref)}" is not a resolvable typed ID.`, path: `goals[${i}]` });
54
+ return;
55
+ }
56
+ if (type !== 'GOAL') {
57
+ errors.push({ code: 'CHANGE-002', message: `goals[${i}] TYPE "${type}" must be GOAL.`, path: `goals[${i}]` });
58
+ return;
59
+ }
60
+ if (options.catalog && options.catalog.typeOf(ref) === undefined) {
61
+ errors.push({ code: 'CHANGE-002', message: `goals[${i}] "${String(ref)}" does not resolve to an admitted artefact.`, path: `goals[${i}]` });
34
62
  }
35
63
  });
36
64
  }
37
65
  }
38
- // CHANGE-003 — parent must be a CHANGE typed id (multi-scale decomposition).
39
- if (record.parent !== undefined) {
40
- if (typeof record.parent !== 'string' || typeOfId(record.parent) !== 'CHANGE') {
41
- errors.push({
42
- code: 'CHANGE-003',
43
- message: `parent "${String(record.parent)}" must be a CHANGE typed ID.`,
44
- path: 'parent',
45
- });
66
+ // CHANGE-003 — `parent` (CHANGE-…; not self).
67
+ if (c.parent !== undefined) {
68
+ const p = c.parent;
69
+ const type = typeOfId(p);
70
+ if (typeof p !== 'string' || !type) {
71
+ errors.push({ code: 'CHANGE-003', message: `parent "${String(p)}" is not a resolvable typed ID.`, path: 'parent' });
46
72
  }
47
- else if (record.parent === record.id) {
48
- // A change cannot decompose from itself.
49
- errors.push({
50
- code: 'CHANGE-003',
51
- message: `parent "${record.parent}" must not equal id (a change cannot be its own parent).`,
52
- path: 'parent',
53
- });
73
+ else if (type !== 'CHANGE') {
74
+ errors.push({ code: 'CHANGE-003', message: `parent TYPE "${type}" must be CHANGE.`, path: 'parent' });
75
+ }
76
+ else if (typeof c.id === 'string' && p === c.id) {
77
+ errors.push({ code: 'CHANGE-003', message: `parent "${p}" must not equal the element's own id.`, path: 'parent' });
78
+ }
79
+ else if (options.catalog && options.catalog.typeOf(p) === undefined) {
80
+ errors.push({ code: 'CHANGE-003', message: `parent "${p}" does not resolve to an admitted CHANGE.`, path: 'parent' });
54
81
  }
55
82
  }
56
83
  return { valid: errors.length === 0, errors, warnings };
@@ -11,6 +11,39 @@ describe('ingestComplianceDoc', () => {
11
11
  expect(canon.requirements[0]).toMatchObject({ id: 'REQUIREMENT-1', severity: 'high', derived_from: ['LAW-1'] });
12
12
  expect(canon.assertions[0]).toMatchObject({ id: 'ASSERTION-1', status: 'compliant', evidenceCount: 1 });
13
13
  expect(canon.codex[0]).toMatchObject({ id: 'LAW-1', type: 'LAW', jurisdiction: 'ge' });
14
+ expect(canon.subjects).toEqual([]);
15
+ });
16
+ it('ingests capability, process, application, system into canon.subjects', () => {
17
+ const canon = emptyCanon();
18
+ expect(ingestComplianceDoc(canon, { notation: 'capability', id: 'CAPABILITY-V2', name: 'CRM' })).toBe('CAPABILITY-V2');
19
+ expect(ingestComplianceDoc(canon, { notation: 'process', id: 'PROCESS-CS-1', name: 'Support' })).toBe('PROCESS-CS-1');
20
+ expect(ingestComplianceDoc(canon, { notation: 'application', id: 'APP-ERP-1', name: 'ERP System' })).toBe('APP-ERP-1');
21
+ expect(ingestComplianceDoc(canon, { notation: 'system', id: 'SYSTEM-IAM-1', name: 'IAM' })).toBe('SYSTEM-IAM-1');
22
+ expect(canon.subjects).toEqual([
23
+ { id: 'CAPABILITY-V2', name: 'CRM' },
24
+ { id: 'PROCESS-CS-1', name: 'Support' },
25
+ { id: 'APP-ERP-1', name: 'ERP System' },
26
+ { id: 'SYSTEM-IAM-1', name: 'IAM' },
27
+ ]);
28
+ expect(canon.products).toEqual([]);
29
+ });
30
+ it('falls back to id when name is absent for subjects', () => {
31
+ const canon = emptyCanon();
32
+ ingestComplianceDoc(canon, { notation: 'capability', id: 'CAPABILITY-X-1' });
33
+ expect(canon.subjects[0]).toEqual({ id: 'CAPABILITY-X-1', name: 'CAPABILITY-X-1' });
34
+ });
35
+ it('captures owner_to_confirm on pending_owner assertions', () => {
36
+ const canon = emptyCanon();
37
+ ingestComplianceDoc(canon, {
38
+ notation: 'assertion', id: 'ASSERTION-PO-1',
39
+ about: 'REQUIREMENT-1', subject: 'PRODUCT-1',
40
+ status: 'pending_owner', owner_to_confirm: 'alice@example.com',
41
+ });
42
+ expect(canon.assertions[0]).toMatchObject({
43
+ id: 'ASSERTION-PO-1',
44
+ status: 'pending_owner',
45
+ owner_to_confirm: 'alice@example.com',
46
+ });
14
47
  });
15
48
  it('returns null for non-artefacts, missing id, and malformed assertions', () => {
16
49
  const canon = emptyCanon();
@@ -20,5 +53,6 @@ describe('ingestComplianceDoc', () => {
20
53
  expect(ingestComplianceDoc(canon, null)).toBeNull();
21
54
  expect(ingestComplianceDoc(canon, 'str')).toBeNull();
22
55
  expect(canon.products).toEqual([]);
56
+ expect(canon.subjects).toEqual([]);
23
57
  });
24
58
  });
@@ -8,17 +8,14 @@ describe('parseCoverageMetricConfig', () => {
8
8
  expect(r.ok).toBe(false);
9
9
  });
10
10
  it('rejects missing id', () => {
11
- const r = parseCoverageMetricConfig({ coverage_metric: { name: 'x' } });
11
+ const r = parseCoverageMetricConfig({ view: { name: 'x' } });
12
12
  expect(r.ok).toBe(false);
13
13
  if (!r.ok)
14
14
  expect(r.errors.some(e => e.includes('id'))).toBe(true);
15
15
  });
16
- it('parses minimal config', () => {
16
+ it('parses minimal config via view: wrapper', () => {
17
17
  const r = parseCoverageMetricConfig({
18
- coverage_metric: {
19
- id: 'CM-1',
20
- name: 'Test',
21
- },
18
+ view: { id: 'CM-1', name: 'Test' },
22
19
  });
23
20
  expect(r.ok).toBe(true);
24
21
  if (r.ok) {
@@ -26,47 +23,105 @@ describe('parseCoverageMetricConfig', () => {
26
23
  expect(r.config.name).toBe('Test');
27
24
  expect(r.config.thresholds.green).toBe(0.8);
28
25
  expect(r.config.thresholds.amber).toBe(0.5);
26
+ expect(r.config.regimes).toBeUndefined();
29
27
  }
30
28
  });
31
- it('parses full config with scope and thresholds', () => {
29
+ it('parses view: wrapper with regimes.include and subjects', () => {
32
30
  const r = parseCoverageMetricConfig({
33
- coverage_metric: {
31
+ view: {
34
32
  id: 'CM-2',
35
33
  name: 'EU Coverage',
36
34
  description: 'desc',
37
- scope: {
38
- jurisdictions: ['EU'],
39
- codex: ['LAW-GDPR-1'],
40
- subjects: { products: ['PRODUCT-1'] },
41
- },
35
+ regimes: { include: ['LAW-GDPR-1'] },
36
+ subjects: { products: ['PRODUCT-1'] },
42
37
  thresholds: { green: 0.9, amber: 0.6 },
43
38
  },
44
39
  });
45
40
  expect(r.ok).toBe(true);
46
41
  if (r.ok) {
47
- expect(r.config.scope.codex).toEqual(['LAW-GDPR-1']);
48
- expect(r.config.scope.subjects?.products).toEqual(['PRODUCT-1']);
42
+ expect(r.config.regimes?.include).toEqual(['LAW-GDPR-1']);
43
+ expect(r.config.subjects?.products).toEqual(['PRODUCT-1']);
49
44
  expect(r.config.thresholds.green).toBe(0.9);
50
45
  expect(r.config.thresholds.amber).toBe(0.6);
46
+ expect(r.config.warnings).toBeUndefined();
47
+ }
48
+ });
49
+ it('parses view: wrapper with regimes.filter', () => {
50
+ const r = parseCoverageMetricConfig({
51
+ view: {
52
+ id: 'CM-3',
53
+ name: 'EU Filter',
54
+ regimes: { filter: { jurisdiction: ['EU'], codex_type: ['regulation'] } },
55
+ },
56
+ });
57
+ expect(r.ok).toBe(true);
58
+ if (r.ok) {
59
+ expect(r.config.regimes?.filter?.jurisdiction).toEqual(['EU']);
60
+ expect(r.config.regimes?.filter?.codex_type).toEqual(['regulation']);
61
+ expect(r.config.regimes?.include).toBeUndefined();
62
+ }
63
+ });
64
+ it('warns COVMET-007 when both regimes.include and regimes.filter are set', () => {
65
+ const r = parseCoverageMetricConfig({
66
+ view: {
67
+ id: 'CM-4',
68
+ name: 'Both',
69
+ regimes: {
70
+ include: ['LAW-GDPR-1'],
71
+ filter: { jurisdiction: ['EU'] },
72
+ },
73
+ },
74
+ });
75
+ expect(r.ok).toBe(true);
76
+ if (r.ok) {
77
+ expect(r.config.warnings?.some(w => w.includes('COVMET-007'))).toBe(true);
51
78
  }
52
79
  });
53
- it('accepts bare (unwrapped) object', () => {
54
- const r = parseCoverageMetricConfig({ id: 'CM-3', name: 'Bare' });
80
+ it('accepts bare (unwrapped) object without wrapper warning', () => {
81
+ const r = parseCoverageMetricConfig({ id: 'CM-5', name: 'Bare' });
55
82
  expect(r.ok).toBe(true);
83
+ if (r.ok) {
84
+ expect(r.config.warnings?.some(w => w.includes('COVMET-DEPRECATED'))).toBeFalsy();
85
+ }
86
+ });
87
+ // ── Backward compatibility: deprecated coverage_metric: wrapper ──────────────
88
+ it('accepts deprecated coverage_metric: wrapper with COVMET-DEPRECATED warning', () => {
89
+ const r = parseCoverageMetricConfig({
90
+ coverage_metric: { id: 'CM-6', name: 'Legacy' },
91
+ });
92
+ expect(r.ok).toBe(true);
93
+ if (r.ok) {
94
+ expect(r.config.warnings?.some(w => w.includes('COVMET-DEPRECATED'))).toBe(true);
95
+ }
96
+ });
97
+ it('maps deprecated scope.codex to regimes.include', () => {
98
+ const r = parseCoverageMetricConfig({
99
+ coverage_metric: {
100
+ id: 'CM-7',
101
+ name: 'Legacy Scope',
102
+ scope: {
103
+ codex: ['LAW-GDPR-1', 'LAW-NIS2-1'],
104
+ subjects: { products: ['PRODUCT-1'] },
105
+ },
106
+ thresholds: { green: 0.9, amber: 0.6 },
107
+ },
108
+ });
109
+ expect(r.ok).toBe(true);
110
+ if (r.ok) {
111
+ expect(r.config.regimes?.include).toEqual(['LAW-GDPR-1', 'LAW-NIS2-1']);
112
+ expect(r.config.subjects?.products).toEqual(['PRODUCT-1']);
113
+ expect(r.config.thresholds.green).toBe(0.9);
114
+ }
56
115
  });
57
116
  });
58
117
  // ── buildCoverageMatrix ───────────────────────────────────────────────────────
59
118
  function makeCanon() {
60
119
  const canon = emptyCanon();
61
- // Codex: GDPR law
62
120
  ingestComplianceDoc(canon, { id: 'LAW-GDPR-1', name: 'GDPR', zone: 'codex', jurisdiction: 'EU' });
63
- // Requirements
64
121
  ingestComplianceDoc(canon, { notation: 'requirement', id: 'REQ-1', name: 'Lawful basis', derived_from: ['LAW-GDPR-1'] });
65
122
  ingestComplianceDoc(canon, { notation: 'requirement', id: 'REQ-2', name: 'Data minimisation', derived_from: ['LAW-GDPR-1'] });
66
123
  ingestComplianceDoc(canon, { notation: 'requirement', id: 'REQ-3', name: 'Breach notification', derived_from: ['LAW-GDPR-1'] });
67
- // Products
68
124
  ingestComplianceDoc(canon, { notation: 'product', id: 'PRODUCT-1', name: 'E-commerce' });
69
- // Assertions
70
125
  ingestComplianceDoc(canon, { notation: 'assertion', id: 'ASSERTION-1', about: 'REQ-1', subject: 'PRODUCT-1', status: 'compliant' });
71
126
  ingestComplianceDoc(canon, { notation: 'assertion', id: 'ASSERTION-2', about: 'REQ-2', subject: 'PRODUCT-1', status: 'partial' });
72
127
  // REQ-3 has no assertion → gap
@@ -76,19 +131,15 @@ function makeConfig(overrides) {
76
131
  return {
77
132
  id: 'CM-TEST-1',
78
133
  name: 'Test Coverage',
79
- scope: {
80
- codex: ['LAW-GDPR-1'],
81
- subjects: { products: ['PRODUCT-1'] },
82
- },
134
+ regimes: { include: ['LAW-GDPR-1'] },
135
+ subjects: { products: ['PRODUCT-1'] },
83
136
  thresholds: { green: 0.8, amber: 0.5 },
84
137
  ...overrides,
85
138
  };
86
139
  }
87
140
  describe('buildCoverageMatrix', () => {
88
141
  it('returns correct row counts', () => {
89
- const canon = makeCanon();
90
- const config = makeConfig();
91
- const matrix = buildCoverageMatrix(canon, config);
142
+ const matrix = buildCoverageMatrix(makeCanon(), makeConfig());
92
143
  expect(matrix.rows).toHaveLength(1);
93
144
  const row = matrix.rows[0];
94
145
  expect(row.codexId).toBe('LAW-GDPR-1');
@@ -99,18 +150,14 @@ describe('buildCoverageMatrix', () => {
99
150
  expect(row.coveredCount).toBe(2);
100
151
  });
101
152
  it('computes coverage percentage correctly', () => {
102
- const matrix = buildCoverageMatrix(makeCanon(), makeConfig());
103
- const row = matrix.rows[0];
104
- // 2 covered out of 3 → ~66.7%
153
+ const row = buildCoverageMatrix(makeCanon(), makeConfig()).rows[0];
105
154
  expect(row.coveragePct).toBeCloseTo(2 / 3, 5);
106
155
  });
107
156
  it('assigns amber RAG status for ~67% coverage with green=0.80, amber=0.50', () => {
108
- const matrix = buildCoverageMatrix(makeCanon(), makeConfig());
109
- expect(matrix.rows[0].ragStatus).toBe('amber');
157
+ expect(buildCoverageMatrix(makeCanon(), makeConfig()).rows[0].ragStatus).toBe('amber');
110
158
  });
111
159
  it('assigns green RAG when all requirements covered', () => {
112
160
  const canon = makeCanon();
113
- // Add assertion for REQ-3 so all 3 are covered
114
161
  ingestComplianceDoc(canon, { notation: 'assertion', id: 'ASSERTION-3', about: 'REQ-3', subject: 'PRODUCT-1', status: 'compliant' });
115
162
  const matrix = buildCoverageMatrix(canon, makeConfig());
116
163
  expect(matrix.rows[0].ragStatus).toBe('green');
@@ -118,38 +165,35 @@ describe('buildCoverageMatrix', () => {
118
165
  });
119
166
  it('assigns red RAG when coverage < amber threshold', () => {
120
167
  const matrix = buildCoverageMatrix(makeCanon(), makeConfig({ thresholds: { green: 0.9, amber: 0.8 } }));
121
- // ~67% < 80% → red
122
168
  expect(matrix.rows[0].ragStatus).toBe('red');
123
169
  });
124
170
  it('returns no_data when codex has no requirements', () => {
125
171
  const canon = emptyCanon();
126
172
  ingestComplianceDoc(canon, { id: 'LAW-EMPTY-1', name: 'Empty Law', zone: 'codex', jurisdiction: 'XX' });
127
- const config = makeConfig({ scope: { codex: ['LAW-EMPTY-1'] } });
128
- const matrix = buildCoverageMatrix(canon, config);
173
+ const matrix = buildCoverageMatrix(canon, makeConfig({ regimes: { include: ['LAW-EMPTY-1'] } }));
129
174
  expect(matrix.rows[0].ragStatus).toBe('no_data');
130
175
  expect(matrix.rows[0].totalRequirements).toBe(0);
131
176
  });
132
177
  it('includes jurisdiction from scanned canon', () => {
133
- const matrix = buildCoverageMatrix(makeCanon(), makeConfig());
134
- expect(matrix.rows[0].jurisdiction).toBe('EU');
178
+ expect(buildCoverageMatrix(makeCanon(), makeConfig()).rows[0].jurisdiction).toBe('EU');
135
179
  });
136
- it('handles multiple codex entries', () => {
180
+ it('handles multiple codex entries via regimes.include', () => {
137
181
  const canon = makeCanon();
138
182
  ingestComplianceDoc(canon, { id: 'LAW-NIS2-1', name: 'NIS2', zone: 'codex', jurisdiction: 'EU' });
139
183
  ingestComplianceDoc(canon, { notation: 'requirement', id: 'REQ-NIS-1', name: 'Incident reporting', derived_from: ['LAW-NIS2-1'] });
140
- const config = makeConfig({ scope: { codex: ['LAW-GDPR-1', 'LAW-NIS2-1'] } });
141
- const matrix = buildCoverageMatrix(canon, config);
184
+ const matrix = buildCoverageMatrix(canon, makeConfig({ regimes: { include: ['LAW-GDPR-1', 'LAW-NIS2-1'] } }));
142
185
  expect(matrix.rows).toHaveLength(2);
143
186
  expect(matrix.rows[1].codexId).toBe('LAW-NIS2-1');
144
187
  expect(matrix.rows[1].gap).toBe(1);
145
188
  });
146
189
  it('respects product scope — ignores assertions for other products', () => {
147
190
  const canon = makeCanon();
148
- // Add another product with compliant assertion for REQ-3
149
191
  ingestComplianceDoc(canon, { notation: 'product', id: 'PRODUCT-2', name: 'Support' });
150
192
  ingestComplianceDoc(canon, { notation: 'assertion', id: 'ASSERTION-OUT', about: 'REQ-3', subject: 'PRODUCT-2', status: 'compliant' });
151
- // config only scopes PRODUCT-1 → REQ-3 should still be a gap
152
- const matrix = buildCoverageMatrix(canon, makeConfig({ scope: { codex: ['LAW-GDPR-1'], subjects: { products: ['PRODUCT-1'] } } }));
193
+ const matrix = buildCoverageMatrix(canon, makeConfig({
194
+ regimes: { include: ['LAW-GDPR-1'] },
195
+ subjects: { products: ['PRODUCT-1'] },
196
+ }));
153
197
  expect(matrix.rows[0].gap).toBe(1);
154
198
  });
155
199
  it('counts non_compliant separately', () => {
@@ -158,7 +202,63 @@ describe('buildCoverageMatrix', () => {
158
202
  const matrix = buildCoverageMatrix(canon, makeConfig());
159
203
  expect(matrix.rows[0].non_compliant).toBe(1);
160
204
  expect(matrix.rows[0].gap).toBe(0);
161
- // non_compliant does not count as covered
162
205
  expect(matrix.rows[0].coveredCount).toBe(2);
163
206
  });
207
+ it('counts pending_owner separately and does not include it in coveredCount', () => {
208
+ const canon = makeCanon();
209
+ ingestComplianceDoc(canon, { notation: 'assertion', id: 'ASSERTION-PO', about: 'REQ-3', subject: 'PRODUCT-1', status: 'pending_owner', owner_to_confirm: 'alice' });
210
+ const matrix = buildCoverageMatrix(canon, makeConfig());
211
+ expect(matrix.rows[0].pending_owner).toBe(1);
212
+ expect(matrix.rows[0].gap).toBe(0);
213
+ expect(matrix.rows[0].coveredCount).toBe(2);
214
+ });
215
+ // ── regimes.filter ───────────────────────────────────────────────────────────
216
+ it('resolves codex via regimes.filter.jurisdiction', () => {
217
+ const canon = makeCanon();
218
+ ingestComplianceDoc(canon, { id: 'LAW-CCPA-1', name: 'CCPA', zone: 'codex', jurisdiction: 'US' });
219
+ // regimes.filter: only EU laws → should return LAW-GDPR-1 only
220
+ const matrix = buildCoverageMatrix(canon, makeConfig({
221
+ regimes: { filter: { jurisdiction: ['EU'] } },
222
+ }));
223
+ expect(matrix.rows.every(r => r.codexId !== 'LAW-CCPA-1')).toBe(true);
224
+ expect(matrix.rows.some(r => r.codexId === 'LAW-GDPR-1')).toBe(true);
225
+ });
226
+ it('resolves codex via regimes.filter.codex_type', () => {
227
+ const canon = emptyCanon();
228
+ ingestComplianceDoc(canon, { id: 'LAW-A', name: 'Regulation A', zone: 'codex', type: 'regulation' });
229
+ ingestComplianceDoc(canon, { id: 'LAW-B', name: 'Standard B', zone: 'codex', type: 'standard' });
230
+ const matrix = buildCoverageMatrix(canon, makeConfig({
231
+ regimes: { filter: { codex_type: ['regulation'] } },
232
+ }));
233
+ expect(matrix.rows.map(r => r.codexId)).toEqual(['LAW-A']);
234
+ });
235
+ // ── Zero-config: all canon codex entries ─────────────────────────────────────
236
+ it('zero-config (no regimes) enumerates all canon codex', () => {
237
+ const canon = makeCanon();
238
+ ingestComplianceDoc(canon, { id: 'LAW-NIS2-1', name: 'NIS2', zone: 'codex', jurisdiction: 'EU' });
239
+ const matrix = buildCoverageMatrix(canon, makeConfig({ regimes: undefined }));
240
+ expect(matrix.rows.map(r => r.codexId)).toContain('LAW-GDPR-1');
241
+ expect(matrix.rows.map(r => r.codexId)).toContain('LAW-NIS2-1');
242
+ });
243
+ // ── Backward compat: deprecated config still produces correct matrix ─────────
244
+ it('deprecated scope.codex via parseCoverageMetricConfig maps to correct matrix', () => {
245
+ const r = parseCoverageMetricConfig({
246
+ coverage_metric: {
247
+ id: 'CM-LEGACY',
248
+ name: 'Legacy',
249
+ scope: {
250
+ codex: ['LAW-GDPR-1'],
251
+ subjects: { products: ['PRODUCT-1'] },
252
+ },
253
+ thresholds: { green: 0.8, amber: 0.5 },
254
+ },
255
+ });
256
+ expect(r.ok).toBe(true);
257
+ if (r.ok) {
258
+ const matrix = buildCoverageMatrix(makeCanon(), r.config);
259
+ expect(matrix.rows).toHaveLength(1);
260
+ expect(matrix.rows[0].codexId).toBe('LAW-GDPR-1');
261
+ expect(matrix.rows[0].coveredCount).toBe(2);
262
+ }
263
+ });
164
264
  });
@@ -104,7 +104,7 @@ describe('renderImpactMatrixHtml (CV-6)', () => {
104
104
  ingestComplianceDoc(canon, { notation: 'product', id: 'PROD-1', name: 'Product One' });
105
105
  ingestComplianceDoc(canon, { notation: 'requirement', id: 'REQ-1', name: 'Req One', severity: 'high' });
106
106
  ingestComplianceDoc(canon, { notation: 'assertion', id: 'ASS-1', about: 'REQ-1', subject: 'PROD-1', status: 'compliant' });
107
- const matrix = buildImpactMatrix({ products: canon.products, requirements: canon.requirements, assertions: canon.assertions, codex: canon.codex }, { id: 'VIEW-1', name: 'My View', subjects: { products: ['PROD-1'] }, obligations: {} });
107
+ const matrix = buildImpactMatrix({ products: canon.products, requirements: canon.requirements, assertions: canon.assertions, codex: canon.codex, subjects: canon.subjects }, { id: 'VIEW-1', name: 'My View', subjects: { products: ['PROD-1'] }, obligations: {} });
108
108
  const html = renderImpactMatrixHtml(matrix, { today: '2026-06-09' });
109
109
  expect(html).toContain('<table>');
110
110
  expect(html).toContain('My View');
@@ -115,7 +115,7 @@ describe('renderImpactMatrixHtml (CV-6)', () => {
115
115
  const canon = emptyCanon();
116
116
  ingestComplianceDoc(canon, { notation: 'product', id: 'PROD-1', name: 'P1' });
117
117
  ingestComplianceDoc(canon, { notation: 'requirement', id: 'REQ-1', name: 'R1' });
118
- const matrix = buildImpactMatrix({ products: canon.products, requirements: canon.requirements, assertions: canon.assertions, codex: canon.codex }, { id: 'V2', name: 'V2', subjects: { products: ['PROD-1'] }, obligations: {} });
118
+ const matrix = buildImpactMatrix({ products: canon.products, requirements: canon.requirements, assertions: canon.assertions, codex: canon.codex, subjects: canon.subjects }, { id: 'V2', name: 'V2', subjects: { products: ['PROD-1'] }, obligations: {} });
119
119
  const html = renderImpactMatrixHtml(matrix);
120
120
  expect(html).toContain('No mapped obligation');
121
121
  });