@transitrix/diagrams 1.8.21 → 1.10.0

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 (190) hide show
  1. package/dist/activity-card/types.js +1 -1
  2. package/dist/agreement.d.ts +15 -0
  3. package/dist/agreement.js +61 -0
  4. package/dist/applications/index.d.ts +2 -0
  5. package/dist/applications/index.js +1 -0
  6. package/dist/applications/resolve-maturity.d.ts +16 -0
  7. package/dist/applications/resolve-maturity.js +57 -0
  8. package/dist/applications/validate.js +20 -0
  9. package/dist/capability-map/CapabilityCardNode.d.ts +1 -1
  10. package/dist/capability-map/CapabilityRootNode.d.ts +1 -1
  11. package/dist/capability-map/index.d.ts +2 -0
  12. package/dist/capability-map/index.js +1 -0
  13. package/dist/capability-map/resolve-maturity.d.ts +20 -0
  14. package/dist/capability-map/resolve-maturity.js +72 -0
  15. package/dist/capability-map/validate.js +1 -2
  16. package/dist/codex/index.d.ts +1 -1
  17. package/dist/codex/index.js +1 -1
  18. package/dist/codex/types.d.ts +9 -2
  19. package/dist/codex/types.js +10 -3
  20. package/dist/codex/validate.js +29 -3
  21. package/dist/compliance/classify.d.ts +21 -2
  22. package/dist/compliance/classify.js +68 -13
  23. package/dist/compliance/confidence.js +1 -1
  24. package/dist/compliance/gap-report.d.ts +31 -1
  25. package/dist/compliance/gap-report.js +55 -3
  26. package/dist/compliance/html.js +14 -6
  27. package/dist/compliance/impact.js +1 -1
  28. package/dist/compliance/index.d.ts +3 -1
  29. package/dist/compliance/index.js +2 -0
  30. package/dist/compliance/markdown.js +26 -4
  31. package/dist/compliance/reverse-index.d.ts +6 -1
  32. package/dist/compliance/reverse-index.js +27 -3
  33. package/dist/compliance/trace.js +5 -1
  34. package/dist/compliance/types.d.ts +70 -0
  35. package/dist/compliance/types.js +1 -2
  36. package/dist/compliance/views.js +1 -1
  37. package/dist/compliance-matrix/build.js +0 -0
  38. package/dist/compliance-matrix/types.d.ts +1 -1
  39. package/dist/compliance-matrix/types.js +1 -1
  40. package/dist/confidence/score.js +1 -1
  41. package/dist/constraint/validate.js +3 -0
  42. package/dist/edge-path.js +1 -1
  43. package/dist/factor/validate.js +2 -2
  44. package/dist/fgca/chain-table.js +1 -1
  45. package/dist/fgca/nodeTypes.d.ts +4 -4
  46. package/dist/fgca/nodes/FGCAActivityNode.d.ts +1 -1
  47. package/dist/fgca/nodes/FGCAChangeNode.d.ts +1 -1
  48. package/dist/fgca/nodes/FGCAFactorNode.d.ts +1 -1
  49. package/dist/fgca/nodes/FGCAGoalNode.d.ts +1 -1
  50. package/dist/fgca/parse-canonical.d.ts +2 -2
  51. package/dist/fgca/parse-canonical.js +2 -2
  52. package/dist/fgca/preview-layout.d.ts +1 -1
  53. package/dist/goals/GoalNode.d.ts +1 -1
  54. package/dist/goals/layout.d.ts +1 -1
  55. package/dist/goals/layout.js +1 -1
  56. package/dist/goals/parse-canonical.d.ts +1 -1
  57. package/dist/goals/parse-canonical.js +1 -1
  58. package/dist/goals/types.d.ts +1 -1
  59. package/dist/goals/validate.js +2 -2
  60. package/dist/link-suspicion.d.ts +43 -0
  61. package/dist/link-suspicion.js +156 -0
  62. package/dist/metric/index.d.ts +4 -0
  63. package/dist/metric/index.js +2 -0
  64. package/dist/metric/types.d.ts +24 -0
  65. package/dist/metric/types.js +7 -0
  66. package/dist/metric/validate.d.ts +7 -0
  67. package/dist/metric/validate.js +90 -0
  68. package/dist/need/index.d.ts +3 -0
  69. package/dist/need/index.js +1 -0
  70. package/dist/need/types.d.ts +21 -0
  71. package/dist/need/types.js +3 -0
  72. package/dist/need/validate.d.ts +7 -0
  73. package/dist/need/validate.js +60 -0
  74. package/dist/repo-validate/check-candidate-fields.d.ts +6 -0
  75. package/dist/repo-validate/check-candidate-fields.js +87 -0
  76. package/dist/repo-validate/check-versioned-attributes.d.ts +4 -0
  77. package/dist/repo-validate/check-versioned-attributes.js +105 -0
  78. package/dist/repo-validate/validate-repo.js +7 -0
  79. package/dist/requirement/index.d.ts +2 -2
  80. package/dist/requirement/index.js +1 -1
  81. package/dist/requirement/types.d.ts +19 -1
  82. package/dist/requirement/types.js +3 -1
  83. package/dist/requirement/validate.js +22 -1
  84. package/dist/risk/index.d.ts +4 -0
  85. package/dist/risk/index.js +2 -0
  86. package/dist/risk/types.d.ts +25 -0
  87. package/dist/risk/types.js +3 -0
  88. package/dist/risk/validate.d.ts +7 -0
  89. package/dist/risk/validate.js +108 -0
  90. package/dist/schema-version.d.ts +9 -6
  91. package/dist/schema-version.js +9 -6
  92. package/dist/theme/themes.js +3 -2
  93. package/dist/validation/index.d.ts +4 -0
  94. package/dist/validation/index.js +2 -0
  95. package/dist/validation/types.d.ts +31 -0
  96. package/dist/validation/types.js +21 -0
  97. package/dist/validation/validate.d.ts +7 -0
  98. package/dist/validation/validate.js +106 -0
  99. package/dist/verification/index.d.ts +4 -0
  100. package/dist/verification/index.js +2 -0
  101. package/dist/verification/types.d.ts +31 -0
  102. package/dist/verification/types.js +12 -0
  103. package/dist/verification/validate.d.ts +7 -0
  104. package/dist/verification/validate.js +104 -0
  105. package/dist/versioned-attribute/index.d.ts +4 -0
  106. package/dist/versioned-attribute/index.js +2 -0
  107. package/dist/versioned-attribute/resolve.d.ts +15 -0
  108. package/dist/versioned-attribute/resolve.js +37 -0
  109. package/dist/versioned-attribute/types.d.ts +12 -0
  110. package/dist/versioned-attribute/types.js +6 -0
  111. package/dist/versioned-attribute/validate.d.ts +17 -0
  112. package/dist/versioned-attribute/validate.js +87 -0
  113. package/dist/webview/entry.js +11 -4
  114. package/dist/webview/render-applications.d.ts +15 -1
  115. package/dist/webview/render-applications.js +7 -2
  116. package/dist/webview/render-blocks.js +6 -4
  117. package/dist/webview/render-capability-map.d.ts +16 -1
  118. package/dist/webview/render-capability-map.js +4 -1
  119. package/dist/webview/render-process.d.ts +1 -1
  120. package/dist/webview/render-process.js +5 -5
  121. package/package.json +9 -6
  122. package/src/activity-card/types.ts +1 -1
  123. package/src/agreement.ts +71 -0
  124. package/src/applications/index.ts +3 -0
  125. package/src/applications/resolve-maturity.ts +66 -0
  126. package/src/applications/validate.ts +23 -0
  127. package/src/capability-map/index.ts +7 -0
  128. package/src/capability-map/resolve-maturity.ts +84 -0
  129. package/src/capability-map/validate.ts +1 -2
  130. package/src/codex/index.ts +1 -1
  131. package/src/codex/types.ts +11 -3
  132. package/src/codex/validate.ts +30 -2
  133. package/src/compliance/classify.ts +87 -14
  134. package/src/compliance/confidence.ts +1 -1
  135. package/src/compliance/gap-report.ts +91 -4
  136. package/src/compliance/html.ts +18 -6
  137. package/src/compliance/impact.ts +1 -1
  138. package/src/compliance/index.ts +6 -0
  139. package/src/compliance/markdown.ts +18 -4
  140. package/src/compliance/reverse-index.ts +35 -4
  141. package/src/compliance/trace.ts +7 -1
  142. package/src/compliance/types.ts +75 -2
  143. package/src/compliance/views.ts +1 -1
  144. package/src/compliance-matrix/build.ts +0 -0
  145. package/src/compliance-matrix/types.ts +2 -2
  146. package/src/confidence/score.ts +1 -1
  147. package/src/constraint/validate.ts +4 -0
  148. package/src/edge-path.ts +1 -1
  149. package/src/factor/validate.ts +2 -2
  150. package/src/fgca/chain-table.ts +1 -1
  151. package/src/fgca/parse-canonical.ts +2 -2
  152. package/src/fgca/preview-layout.ts +1 -1
  153. package/src/goals/layout.ts +1 -1
  154. package/src/goals/parse-canonical.ts +1 -1
  155. package/src/goals/types.ts +1 -1
  156. package/src/goals/validate.ts +2 -2
  157. package/src/link-suspicion.ts +181 -0
  158. package/src/metric/index.ts +4 -0
  159. package/src/metric/types.ts +38 -0
  160. package/src/metric/validate.ts +102 -0
  161. package/src/need/index.ts +3 -0
  162. package/src/need/types.ts +31 -0
  163. package/src/need/validate.ts +71 -0
  164. package/src/repo-validate/check-candidate-fields.ts +93 -0
  165. package/src/repo-validate/check-versioned-attributes.ts +112 -0
  166. package/src/repo-validate/validate-repo.ts +7 -0
  167. package/src/requirement/index.ts +2 -2
  168. package/src/requirement/types.ts +26 -1
  169. package/src/requirement/validate.ts +25 -1
  170. package/src/risk/index.ts +4 -0
  171. package/src/risk/types.ts +35 -0
  172. package/src/risk/validate.ts +121 -0
  173. package/src/schema-version.ts +9 -6
  174. package/src/theme/themes.ts +3 -2
  175. package/src/validation/index.ts +4 -0
  176. package/src/validation/types.ts +57 -0
  177. package/src/validation/validate.ts +113 -0
  178. package/src/verification/index.ts +4 -0
  179. package/src/verification/types.ts +48 -0
  180. package/src/verification/validate.ts +111 -0
  181. package/src/versioned-attribute/index.ts +4 -0
  182. package/src/versioned-attribute/resolve.ts +45 -0
  183. package/src/versioned-attribute/types.ts +19 -0
  184. package/src/versioned-attribute/validate.ts +109 -0
  185. package/src/webview/entry.ts +13 -6
  186. package/src/webview/render-applications.ts +25 -2
  187. package/src/webview/render-blocks.ts +8 -4
  188. package/src/webview/render-capability-map.ts +23 -1
  189. package/src/webview/render-process.ts +5 -5
  190. package/src/webview/styles.css +6 -0
@@ -0,0 +1,71 @@
1
+ // Agreement axis — methodology CONTRACT.md §6.3 / §6.3.1 (AGREE-001..003).
2
+ //
3
+ // A closed three-value axis (`draft` | `agreed` | `disputed`) carried by
4
+ // REQUIREMENT, CONSTRAINT, and NEED — independent of admission (§6.1),
5
+ // recording whether the accountable party has committed to the statement.
6
+ // Absent ⇒ `agreed` (back-compat; a human-authored element that predates
7
+ // this axis needs no change).
8
+ //
9
+ // Ported from methodology's reference implementation
10
+ // (`scripts/check-agreement.mjs`'s `checkAgreement`/`looksLikeTool`) so a
11
+ // repo carrying the field validates identically here — same closed
12
+ // vocabulary, same tool-id heuristic, same "AGREE-003 wins over AGREE-002
13
+ // when both would fire" precedence.
14
+
15
+ import type { ValidationError } from './validation-types.js';
16
+
17
+ export const AGREEMENT_VALUES = ['draft', 'agreed', 'disputed'] as const;
18
+ export type AgreementValue = (typeof AGREEMENT_VALUES)[number];
19
+
20
+ /** Same convention as `ADMIT-007`'s footgun-catcher: an npm-scoped name, or
21
+ * a hyphenated `*-cli` / `*-reviewer[-…]` / `*-bot` / `*-scanner` id, reads
22
+ * as a tool; anything else reads as a human handle. Not a security
23
+ * boundary — catches the common mistake of a tool defaulting or
24
+ * copy-pasting `agreed`. */
25
+ const TOOL_ID_RE = /^@|-cli$|-reviewer(?:-\w+)?$|-bot$|-scanner$/i;
26
+
27
+ export function looksLikeTool(id: unknown): boolean {
28
+ return TOOL_ID_RE.test(String(id ?? ''));
29
+ }
30
+
31
+ /**
32
+ * AGREE-001..003 over an element's already-extracted `agreement` /
33
+ * `agreed_by` fields. Returns an empty list when `agreement` is absent
34
+ * (back-compat — absent ⇒ `agreed`, nothing to check) or when the fields
35
+ * are well-formed.
36
+ *
37
+ * AGREE-002 and AGREE-003 can both describe an `agreement: agreed` record
38
+ * with no `agreed_by` — this reports AGREE-003 only, the same choice
39
+ * `check-agreement.mjs` makes (CONTRACT.md §6.3.1 permits either or both).
40
+ */
41
+ export function checkAgreement(record: Record<string, unknown>): ValidationError[] {
42
+ const agreement = record.agreement;
43
+ if (agreement === undefined) return [];
44
+
45
+ if (typeof agreement !== 'string' || !(AGREEMENT_VALUES as readonly string[]).includes(agreement)) {
46
+ return [{
47
+ code: 'AGREE-001',
48
+ message: `agreement "${String(agreement)}" must be one of ${AGREEMENT_VALUES.join(', ')}.`,
49
+ path: 'agreement',
50
+ }];
51
+ }
52
+
53
+ const agreedBy = record.agreed_by;
54
+ if (typeof agreedBy !== 'string' || agreedBy.trim() === '') {
55
+ return [{
56
+ code: 'AGREE-003',
57
+ message: 'agreed_by is required whenever agreement is present.',
58
+ path: 'agreed_by',
59
+ }];
60
+ }
61
+
62
+ if (agreement === 'agreed' && looksLikeTool(agreedBy)) {
63
+ return [{
64
+ code: 'AGREE-002',
65
+ message: `agreement: agreed but agreed_by "${agreedBy}" identifies a tool — a tool must never write "agreed".`,
66
+ path: 'agreed_by',
67
+ }];
68
+ }
69
+
70
+ return [];
71
+ }
@@ -9,3 +9,6 @@ export type {
9
9
  } from './types.js';
10
10
 
11
11
  export { validateApplicationsCatalogue } from './validate.js';
12
+
13
+ export type { ResolvedApplicationAttributes } from './resolve-maturity.js';
14
+ export { resolveApplicationAttributes, withResolvedAttributes } from './resolve-maturity.js';
@@ -0,0 +1,66 @@
1
+ // Render-time resolution of the applications-catalogue's sidecar-bound
2
+ // time_varying fields (owner_role, vendor, maturity — notations/views/
3
+ // 10-applications.md §5a, CONTRACT.md §9.4). VERSIONED-004 rejects these
4
+ // inline on an admitted catalogue entry, so a compliant document never
5
+ // carries them — a renderer that only reads the document straight shows
6
+ // blank cells for every field the maturity decision moved out.
7
+ //
8
+ // Pure: no I/O, no repo walk. The host (VS Code extension, IntelliJ/JCEF,
9
+ // CLI) already owns loading canon/elements/** (canon-loader.ts's shared FS
10
+ // layer) — this module only knows how to pick sidecars out of that pool and
11
+ // resolve them at a date.
12
+ //
13
+ // Per the 2026-08-05 hub decision (proposal #1022): this is a read model,
14
+ // never storage. Callers render an augmented, ephemeral copy of the
15
+ // application row and must never serialise it back — the sidecar stays the
16
+ // only place these fields are written.
17
+
18
+ import { parseSidecar, resolveAttributes } from '../versioned-attribute/index.js';
19
+ import type { Application } from './types.js';
20
+
21
+ export interface ResolvedApplicationAttributes {
22
+ owner_role?: string;
23
+ vendor?: string;
24
+ maturity?: number;
25
+ }
26
+
27
+ /** Pick every sidecar out of a raw canon/elements/** doc pool and resolve
28
+ * owner_role/vendor/maturity at `atDate`, keyed by the sidecar's target id
29
+ * (== an application's `app_id`, per the notation's element-file pairing). */
30
+ export function resolveApplicationAttributes(
31
+ rawElementDocs: unknown[],
32
+ atDate: string,
33
+ ): Map<string, ResolvedApplicationAttributes> {
34
+ const byAppId = new Map<string, ResolvedApplicationAttributes>();
35
+ for (const doc of rawElementDocs) {
36
+ if (!doc || typeof doc !== 'object' || Array.isArray(doc)) continue;
37
+ const sidecar = parseSidecar(doc as Record<string, unknown>);
38
+ if (!sidecar) continue;
39
+ const resolved = resolveAttributes(sidecar, atDate);
40
+ const entry: ResolvedApplicationAttributes = {};
41
+ if (typeof resolved['owner_role'] === 'string') entry.owner_role = resolved['owner_role'];
42
+ if (typeof resolved['vendor'] === 'string') entry.vendor = resolved['vendor'];
43
+ if (typeof resolved['maturity'] === 'number') entry.maturity = resolved['maturity'];
44
+ if (Object.keys(entry).length > 0) byAppId.set(sidecar.target, entry);
45
+ }
46
+ return byAppId;
47
+ }
48
+
49
+ /** Merge resolved sidecar values into a rendering copy of an application row.
50
+ * An inline value would win if present, but VERSIONED-004 already forbids
51
+ * that on an admitted document — this is a display fallback, not a
52
+ * migration tool. The returned object is a plain copy; nothing here writes
53
+ * it anywhere. */
54
+ export function withResolvedAttributes(
55
+ app: Application,
56
+ resolved: Map<string, ResolvedApplicationAttributes>,
57
+ ): Application {
58
+ const r = resolved.get(app.app_id);
59
+ if (!r) return app;
60
+ return {
61
+ ...app,
62
+ owner_role: app.owner_role ?? r.owner_role,
63
+ vendor: app.vendor ?? r.vendor,
64
+ maturity: app.maturity ?? r.maturity,
65
+ };
66
+ }
@@ -12,6 +12,17 @@ const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
12
12
 
13
13
  const IFACE_CONDITIONAL = ['protocol', 'payload_class', 'sensitivity', 'directionality'] as const;
14
14
 
15
+ // VERSIONED-004 (CONTRACT.md §9.3) — `owner_role`, `vendor`, `maturity` are
16
+ // declared `time_varying` by notations/views/10-applications.md §5/§5a and
17
+ // belong in the sidecar `<app_id>.history.yaml`, not inline on the catalogue
18
+ // entry. Unlike capability's VERSIONED-004 (packages/diagrams/src/
19
+ // repo-validate/check-versioned-attributes.ts), which walks standalone
20
+ // element files under canon/elements/**, an applications-catalogue entry has
21
+ // no file of its own to check inline-vs-sidecar on — the entry lives in this
22
+ // view document — so the check runs here, at the per-file notation
23
+ // validator, rather than in the repo-scope element sweep.
24
+ const TIME_VARYING_FIELDS = ['owner_role', 'vendor', 'maturity'] as const;
25
+
15
26
  export function validateApplicationsCatalogue(input: unknown): ValidationResult {
16
27
  const errors: ValidationError[] = [];
17
28
  const warnings: ValidationWarning[] = [];
@@ -114,6 +125,18 @@ export function validateApplicationsCatalogue(input: unknown): ValidationResult
114
125
  }
115
126
  }
116
127
 
128
+ // VERSIONED-004: time_varying fields must live in the sidecar, not inline.
129
+ for (const field of TIME_VARYING_FIELDS) {
130
+ if (a[field] !== undefined) {
131
+ errors.push({
132
+ code: 'VERSIONED-004',
133
+ message:
134
+ `${idx}: '${field}' is time_varying (CONTRACT.md §9) and belongs in the sidecar ` +
135
+ `'${typeof a['app_id'] === 'string' ? a['app_id'] : idx}.history.yaml', not inline on the catalogue entry.`,
136
+ });
137
+ }
138
+ }
139
+
117
140
  // APP-009: integrations[].direction enum
118
141
  if (Array.isArray(a['integrations'])) {
119
142
  const integrations = a['integrations'] as unknown[];
@@ -7,6 +7,13 @@ export type {
7
7
 
8
8
  export { validateCapabilityMap } from './validate.js';
9
9
 
10
+ export type { ResolvedCapabilityAttributes } from './resolve-maturity.js';
11
+ export {
12
+ resolveCapabilityAttributes,
13
+ withResolvedAttributes,
14
+ withResolvedCapabilityMap,
15
+ } from './resolve-maturity.js';
16
+
10
17
  export type {
11
18
  CapabilityTreeNode,
12
19
  CapabilityTreeEdge,
@@ -0,0 +1,84 @@
1
+ // Render-time resolution of a nested capability-map entry's sidecar-bound
2
+ // time_varying fields (current_maturity, target_maturity, owner_role,
3
+ // target_date — 05-capability-map.md §13/§14, CONTRACT.md §9.4).
4
+ // check-versioned-attributes.ts's VERSIONED-004 already rejects these inline
5
+ // on the standalone CAPABILITY-*.yaml primitive; a nested capability-map
6
+ // entry references that same primitive by id (the 2026-08-05 packages
7
+ // decision — no separate per-nested-entry sidecar convention), so this
8
+ // resolves the same way applications/resolve-maturity.ts does for the
9
+ // applications catalogue.
10
+ //
11
+ // Pure: no I/O, no repo walk. The host (VS Code extension, IntelliJ/JCEF,
12
+ // CLI) already owns loading canon/elements/** — this module only knows how
13
+ // to pick sidecars out of that pool and resolve them at a date.
14
+ //
15
+ // Per the 2026-08-05 packages decision: this is a read model, never
16
+ // storage. Callers render an augmented, ephemeral copy of the capability
17
+ // tree and must never serialise it back — the sidecar stays the only place
18
+ // these fields are written.
19
+
20
+ import { parseSidecar, resolveAttributes } from '../versioned-attribute/index.js';
21
+ import type { CapabilityMapHeader, CapabilityNode } from './types.js';
22
+
23
+ export interface ResolvedCapabilityAttributes {
24
+ current_maturity?: number;
25
+ target_maturity?: number;
26
+ owner_role?: string;
27
+ target_date?: string;
28
+ }
29
+
30
+ /** Pick every sidecar out of a raw canon/elements/** doc pool and resolve
31
+ * current_maturity/target_maturity/owner_role/target_date at `atDate`,
32
+ * keyed by the sidecar's target id (== a capability entry's `id`, per the
33
+ * CAPABILITY-* primitive that entry references). */
34
+ export function resolveCapabilityAttributes(
35
+ rawElementDocs: unknown[],
36
+ atDate: string,
37
+ ): Map<string, ResolvedCapabilityAttributes> {
38
+ const byId = new Map<string, ResolvedCapabilityAttributes>();
39
+ for (const doc of rawElementDocs) {
40
+ if (!doc || typeof doc !== 'object' || Array.isArray(doc)) continue;
41
+ const sidecar = parseSidecar(doc as Record<string, unknown>);
42
+ if (!sidecar) continue;
43
+ const resolved = resolveAttributes(sidecar, atDate);
44
+ const entry: ResolvedCapabilityAttributes = {};
45
+ if (typeof resolved['current_maturity'] === 'number') entry.current_maturity = resolved['current_maturity'];
46
+ if (typeof resolved['target_maturity'] === 'number') entry.target_maturity = resolved['target_maturity'];
47
+ if (typeof resolved['owner_role'] === 'string') entry.owner_role = resolved['owner_role'];
48
+ if (typeof resolved['target_date'] === 'string') entry.target_date = resolved['target_date'];
49
+ if (Object.keys(entry).length > 0) byId.set(sidecar.target, entry);
50
+ }
51
+ return byId;
52
+ }
53
+
54
+ /** Merge resolved sidecar values into a rendering copy of a capability node
55
+ * and its descendants. An inline value always wins if present — this is a
56
+ * display fallback for fields the document doesn't carry inline, not a
57
+ * migration tool. Returns a new node tree; nothing here writes it
58
+ * anywhere. */
59
+ export function withResolvedAttributes(
60
+ node: CapabilityNode,
61
+ resolved: Map<string, ResolvedCapabilityAttributes>,
62
+ ): CapabilityNode {
63
+ const r = resolved.get(node.id);
64
+ const merged: CapabilityNode = r
65
+ ? {
66
+ ...node,
67
+ current_maturity: node.current_maturity ?? r.current_maturity,
68
+ target_maturity: node.target_maturity ?? r.target_maturity,
69
+ owner_role: node.owner_role ?? r.owner_role,
70
+ target_date: node.target_date ?? r.target_date,
71
+ }
72
+ : node;
73
+ if (!node.children || node.children.length === 0) return merged;
74
+ return { ...merged, children: node.children.map((c) => withResolvedAttributes(c, resolved)) };
75
+ }
76
+
77
+ /** Same merge, applied across a capability map's whole top-level tree. */
78
+ export function withResolvedCapabilityMap(
79
+ map: CapabilityMapHeader,
80
+ resolved: Map<string, ResolvedCapabilityAttributes>,
81
+ ): CapabilityMapHeader {
82
+ if (resolved.size === 0) return map;
83
+ return { ...map, capabilities: map.capabilities.map((c) => withResolvedAttributes(c, resolved)) };
84
+ }
@@ -8,8 +8,7 @@ const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
8
8
  // Canonical form per `IDS_AND_REFERENCES.md` §2: `CAPABILITY-V…` /
9
9
  // `CAPABILITY-H…` with the dotted address (`V1`, `V1.2.1`, `H1`). The bare
10
10
  // legacy form (`V1`, `H1`) is still accepted so existing maps without the
11
- // canonical prefix do not break — migration is tracked under
12
- // vkgeorgia/strategy#36.
11
+ // canonical prefix do not break; migration is tracked separately.
13
12
  const CAP_ID_RE = /^(CAPABILITY-)?(V|H)\d+(\.\d+)*$/;
14
13
 
15
14
  export function validateCapabilityMap(input: unknown): ValidationResult {
@@ -1,4 +1,4 @@
1
- export { CODEX_ARTEFACT_TYPES, EXTERNAL_CODEX_TYPES, INTERNAL_CODEX_TYPES } from './types.js';
1
+ export { CODEX_ARTEFACT_TYPES, EXTERNAL_CODEX_TYPES, INTERNAL_CODEX_TYPES, PRINCIPLE_CODEX_TYPES } from './types.js';
2
2
  export type { CodexArtefactType } from './types.js';
3
3
  export { validateCodex, isCodexDoc, folderJurisdictionFromPath } from './validate.js';
4
4
  export type { CodexValidateOptions } from './validate.js';
@@ -1,13 +1,21 @@
1
- // Codex artefact — external laws/regulations and internal policies/standards.
1
+ // Codex artefact — external laws/regulations and internal policies/standards/principles.
2
2
  // Schema: methodology notations/elements/14-codex.md §3–§4.
3
3
 
4
4
  /** TYPE prefixes admitted in the codex zone (REQ-003 / 14-codex.md). */
5
- export const CODEX_ARTEFACT_TYPES = ['LAW', 'REGULATION', 'POLICY', 'INTERNAL_STANDARD'] as const;
5
+ export const CODEX_ARTEFACT_TYPES = ['LAW', 'REGULATION', 'POLICY', 'INTERNAL_STANDARD', 'PRINCIPLE'] as const;
6
6
 
7
7
  export type CodexArtefactType = (typeof CODEX_ARTEFACT_TYPES)[number];
8
8
 
9
9
  /** External codex artefacts (codex/external/<jurisdiction>/). */
10
10
  export const EXTERNAL_CODEX_TYPES: readonly CodexArtefactType[] = ['LAW', 'REGULATION'];
11
11
 
12
- /** Internal codex artefacts (codex/internal/). */
12
+ /** Internal codex artefacts (codex/internal/) that carry a named issuing authority (§4). */
13
13
  export const INTERNAL_CODEX_TYPES: readonly CodexArtefactType[] = ['POLICY', 'INTERNAL_STANDARD'];
14
+
15
+ /**
16
+ * Internal codex artefacts (codex/internal/) with the `PRINCIPLE` shape (§4.1) —
17
+ * `statement` + `rationale` required, `issuing_authority`/`effective_date`/`established_by`
18
+ * optional. Kept distinct from {@link INTERNAL_CODEX_TYPES} because the required-field set
19
+ * differs; both share the same file location.
20
+ */
21
+ export const PRINCIPLE_CODEX_TYPES: readonly CodexArtefactType[] = ['PRINCIPLE'];
@@ -3,9 +3,20 @@
3
3
  // CODEX-001 — shape, zone, id grammar, admission envelope.
4
4
  // CODEX-002 — `type` field matches the id TYPE prefix when present.
5
5
  // CODEX-003 — external artefact frontmatter (jurisdiction, effective_date).
6
- // CODEX-004 — internal artefact frontmatter (issuing_authority, effective_date).
6
+ // CODEX-004 — internal artefact frontmatter (issuing_authority, effective_date for
7
+ // POLICY/INTERNAL_STANDARD; statement, rationale for PRINCIPLE — §4.1).
7
8
  // CODEX-005 — jurisdiction must match the parent folder under codex/external/
8
9
  // (when `folderJurisdiction` is supplied by the repo sweep).
10
+ // CODEX-006 — a PRINCIPLE artefact does not declare `established_by` (§4.1). The spec
11
+ // marks this "info" severity; this module has no info tier, so it is
12
+ // surfaced as a warning — the nearest existing severity that does not fail
13
+ // `valid`.
14
+ //
15
+ // Note: this file's CODEX-00x numbering predates the spec's own v0.2 renumbering
16
+ // (14-codex.md §6, §8.2) and does not track it code-for-code — e.g. the spec's current
17
+ // CODEX-001 is a folder/jurisdiction check this file assigns to CODEX-005. Realigning the
18
+ // full scheme is out of scope here; this change only adds the purely-additive PRINCIPLE
19
+ // TYPE (§4.1) under the existing local numbering.
9
20
 
10
21
  import type { ValidationError, ValidationWarning, ValidationResult } from '../validation-types.js';
11
22
  import { isCanonicalId, typeOfId } from '../typed-id.js';
@@ -13,6 +24,7 @@ import {
13
24
  CODEX_ARTEFACT_TYPES,
14
25
  EXTERNAL_CODEX_TYPES,
15
26
  INTERNAL_CODEX_TYPES,
27
+ PRINCIPLE_CODEX_TYPES,
16
28
  type CodexArtefactType,
17
29
  } from './types.js';
18
30
 
@@ -48,7 +60,7 @@ export function validateCodex(input: unknown, options: CodexValidateOptions = {}
48
60
  if (!isCanonicalId(c.id) || !isCodexType(idType)) {
49
61
  errors.push({
50
62
  code: 'CODEX-001',
51
- message: `id "${String(c.id)}" must be a typed codex id (LAW, REGULATION, POLICY, or INTERNAL_STANDARD).`,
63
+ message: `id "${String(c.id)}" must be a typed codex id (LAW, REGULATION, POLICY, INTERNAL_STANDARD, or PRINCIPLE).`,
52
64
  path: 'id',
53
65
  });
54
66
  }
@@ -106,6 +118,22 @@ export function validateCodex(input: unknown, options: CodexValidateOptions = {}
106
118
  }
107
119
  }
108
120
 
121
+ if (artefactType && (PRINCIPLE_CODEX_TYPES as readonly string[]).includes(artefactType)) {
122
+ if (typeof c.statement !== 'string' || c.statement.trim() === '') {
123
+ errors.push({ code: 'CODEX-004', message: 'statement is required for a PRINCIPLE artefact.', path: 'statement' });
124
+ }
125
+ if (typeof c.rationale !== 'string' || c.rationale.trim() === '') {
126
+ errors.push({ code: 'CODEX-004', message: 'rationale is required for a PRINCIPLE artefact.', path: 'rationale' });
127
+ }
128
+ if (typeof c.established_by !== 'string' || c.established_by.trim() === '') {
129
+ warnings.push({
130
+ code: 'CODEX-006',
131
+ message: 'established_by is not declared on this PRINCIPLE artefact — admissible, but a review finding for whoever admits it.',
132
+ path: 'established_by',
133
+ });
134
+ }
135
+ }
136
+
109
137
  return { valid: errors.length === 0, errors, warnings };
110
138
  }
111
139
 
@@ -1,10 +1,12 @@
1
- // Canon-artefact classification (vkgeorgia/strategy#84). The single authority
1
+ // Canon-artefact classification. The single authority
2
2
  // for "what is a compliance artefact" — used by both the Studio extension scan
3
3
  // (webview previews) and the CLI scan (`export-compliance`), so the recognition
4
4
  // rules live once. Pure: takes a parsed YAML document, no IO.
5
5
 
6
6
  import type { AssertionStatus } from '../assertion/types.js';
7
- import type { IndexAssertion, IndexRequirement } from './types.js';
7
+ import type { VerificationMethod, VerificationOutcome } from '../verification/types.js';
8
+ import type { ValidationMethod, ValidationOutcome } from '../validation/types.js';
9
+ import type { IndexAssertion, IndexRequirement, IndexVerification, IndexNeed, IndexValidation } from './types.js';
8
10
 
9
11
  export interface ComplianceProduct {
10
12
  id: string;
@@ -22,6 +24,8 @@ export interface ComplianceCanon {
22
24
  products: ComplianceProduct[];
23
25
  requirements: IndexRequirement[];
24
26
  assertions: IndexAssertion[];
27
+ /** VERIFICATION artefacts (27-verification.md) — the engineering V&V peer of `assertions`. */
28
+ verifications: IndexVerification[];
25
29
  codex: ComplianceCodexDoc[];
26
30
  /**
27
31
  * Named elements that can serve as assertion subjects beyond `products`:
@@ -29,22 +33,64 @@ export interface ComplianceCanon {
29
33
  * `ingestComplianceDoc` for the corresponding notation values.
30
34
  */
31
35
  subjects: ComplianceProduct[];
36
+ /** NEED elements (ELEMENT_PRIMITIVES.md §7.28) — upstream of `requirements`.
37
+ * Ingested so NEED-COVERAGE-001 / NEED-VALIDATION-COVERAGE-001..002 can
38
+ * compute coverage over the full needs catalogue. */
39
+ needs: IndexNeed[];
40
+ /** VALIDATION artefacts (28-validation.md) — the validation-domain peer of
41
+ * `verifications`, anchored on NEED instead of REQUIREMENT. */
42
+ validations: IndexValidation[];
43
+ /**
44
+ * Ids rejected by `ingestComplianceDoc` because that id was already present
45
+ * in the same bucket (two documents claiming one id — transitrix-hq#218).
46
+ * The second and any later document is dropped rather than silently
47
+ * duplicating a matrix column; callers that scan a filesystem can surface
48
+ * this list as a diagnostic.
49
+ */
50
+ duplicateIds: string[];
32
51
  }
33
52
 
34
53
  export function emptyCanon(): ComplianceCanon {
35
- return { products: [], requirements: [], assertions: [], codex: [], subjects: [] };
54
+ return {
55
+ products: [],
56
+ requirements: [],
57
+ assertions: [],
58
+ verifications: [],
59
+ codex: [],
60
+ subjects: [],
61
+ needs: [],
62
+ validations: [],
63
+ duplicateIds: [],
64
+ };
36
65
  }
37
66
 
38
67
  const str = (v: unknown): string | undefined => (typeof v === 'string' ? v : undefined);
39
68
  const strArray = (v: unknown): string[] | undefined =>
40
69
  Array.isArray(v) ? v.filter((x): x is string => typeof x === 'string') : undefined;
41
70
 
71
+ /**
72
+ * Pushes `item` onto `list` unless `list` already holds an entry with the same
73
+ * id, in which case the id is recorded in `canon.duplicateIds` and the item is
74
+ * dropped — two documents claiming one id is a defect (transitrix-hq#218), not
75
+ * a second matrix column.
76
+ */
77
+ function pushUnique<T extends { id: string }>(canon: ComplianceCanon, list: T[], item: T): boolean {
78
+ if (list.some(existing => existing.id === item.id)) {
79
+ canon.duplicateIds.push(item.id);
80
+ return false;
81
+ }
82
+ list.push(item);
83
+ return true;
84
+ }
85
+
42
86
  /**
43
87
  * Classifies one parsed YAML document and, if it is a compliance artefact,
44
88
  * pushes its projection into `canon`. Products / requirements / assertions are
45
89
  * identified by their `notation` tag; codex source documents by `zone: codex`.
46
90
  * Returns the artefact id when ingested (so the caller can record its path), or
47
- * null when the document is not a (well-formed) compliance artefact.
91
+ * null when the document is not a (well-formed) compliance artefact, or when
92
+ * its id duplicates one already ingested into the same bucket (see
93
+ * `canon.duplicateIds`).
48
94
  */
49
95
  export function ingestComplianceDoc(canon: ComplianceCanon, doc: unknown): string | null {
50
96
  if (doc === null || typeof doc !== 'object' || Array.isArray(doc)) return null;
@@ -53,8 +99,7 @@ export function ingestComplianceDoc(canon: ComplianceCanon, doc: unknown): strin
53
99
  if (!id) return null;
54
100
 
55
101
  if (d.notation === 'product') {
56
- canon.products.push({ id, name: str(d.name) ?? id });
57
- return id;
102
+ return pushUnique(canon, canon.products, { id, name: str(d.name) ?? id }) ? id : null;
58
103
  }
59
104
  if (
60
105
  d.notation === 'capability' ||
@@ -62,12 +107,11 @@ export function ingestComplianceDoc(canon: ComplianceCanon, doc: unknown): strin
62
107
  d.notation === 'application' ||
63
108
  d.notation === 'system'
64
109
  ) {
65
- canon.subjects.push({ id, name: str(d.name) ?? id });
66
- return id;
110
+ return pushUnique(canon, canon.subjects, { id, name: str(d.name) ?? id }) ? id : null;
67
111
  }
68
112
  if (d.notation === 'requirement' || d.notation === 'constraint') {
69
113
  const origin = str(d.origin);
70
- canon.requirements.push({
114
+ const ok = pushUnique(canon, canon.requirements, {
71
115
  id,
72
116
  name: str(d.name) ?? id,
73
117
  severity: str(d.severity),
@@ -79,15 +123,32 @@ export function ingestComplianceDoc(canon: ComplianceCanon, doc: unknown): strin
79
123
  element_kind: d.notation,
80
124
  description: str(d.description),
81
125
  next_review_at: str(d.next_review_at),
126
+ serves: str(d.serves),
127
+ });
128
+ return ok ? id : null;
129
+ }
130
+ if (d.notation === 'need') {
131
+ return pushUnique(canon, canon.needs, { id, name: str(d.name) ?? id }) ? id : null;
132
+ }
133
+ if (d.notation === 'validation') {
134
+ const validates = str(d.validates);
135
+ const method = str(d.method) as ValidationMethod | undefined;
136
+ const outcome = str(d.outcome) as ValidationOutcome | undefined;
137
+ if (!validates || !method || !outcome) return null;
138
+ const ok = pushUnique(canon, canon.validations, {
139
+ id, validates, method, outcome,
140
+ performed_at: str(d.performed_at),
141
+ evidenceCount: Array.isArray(d.evidence) ? d.evidence.length : 0,
142
+ admitted_at: str(d.admitted_at),
82
143
  });
83
- return id;
144
+ return ok ? id : null;
84
145
  }
85
146
  if (d.notation === 'assertion') {
86
147
  const about = str(d.about);
87
148
  const subject = str(d.subject);
88
149
  const status = str(d.status) as AssertionStatus | undefined;
89
150
  if (!about || !subject || !status) return null;
90
- canon.assertions.push({
151
+ const ok = pushUnique(canon, canon.assertions, {
91
152
  id, about, subject, status,
92
153
  assessed_at: str(d.assessed_at),
93
154
  next_review_at: str(d.next_review_at),
@@ -96,11 +157,23 @@ export function ingestComplianceDoc(canon: ComplianceCanon, doc: unknown): strin
96
157
  realised_via: strArray(d.realised_via),
97
158
  owner_to_confirm: str(d.owner_to_confirm),
98
159
  });
99
- return id;
160
+ return ok ? id : null;
161
+ }
162
+ if (d.notation === 'verification') {
163
+ const verifies = str(d.verifies);
164
+ const method = str(d.method) as VerificationMethod | undefined;
165
+ const outcome = str(d.outcome) as VerificationOutcome | undefined;
166
+ if (!verifies || !method || !outcome) return null;
167
+ const ok = pushUnique(canon, canon.verifications, {
168
+ id, verifies, method, outcome,
169
+ performed_at: str(d.performed_at),
170
+ evidenceCount: Array.isArray(d.evidence) ? d.evidence.length : 0,
171
+ admitted_at: str(d.admitted_at),
172
+ });
173
+ return ok ? id : null;
100
174
  }
101
175
  if (d.zone === 'codex') {
102
- canon.codex.push({ id, name: str(d.name) ?? id, type: str(d.type), jurisdiction: str(d.jurisdiction) });
103
- return id;
176
+ return pushUnique(canon, canon.codex, { id, name: str(d.name) ?? id, type: str(d.type), jurisdiction: str(d.jurisdiction) }) ? id : null;
104
177
  }
105
178
  return null;
106
179
  }
@@ -1,4 +1,4 @@
1
- // Confidence bridge for the compliance views (vkgeorgia/strategy#162 — DQ-2).
1
+ // Confidence bridge for the compliance views (DQ-2).
2
2
  // Maps the compliance index projections (IndexRequirement / IndexAssertion) onto
3
3
  // the DQ-1 ScoringElement shape, scores the view, and renders the §11.6 header
4
4
  // line. Lives in @transitrix/diagrams so Studio and DSM share one bridge and