@transitrix/diagrams 1.9.18 → 1.11.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 (87) hide show
  1. package/dist/activities/resolver.d.ts +3 -0
  2. package/dist/activities/resolver.js +39 -18
  3. package/dist/activities/validate.js +59 -0
  4. package/dist/canon-resolver-utils.d.ts +3 -0
  5. package/dist/canon-resolver-utils.js +16 -0
  6. package/dist/codex/index.d.ts +1 -1
  7. package/dist/codex/index.js +1 -1
  8. package/dist/codex/types.d.ts +9 -2
  9. package/dist/codex/types.js +10 -3
  10. package/dist/codex/validate.js +29 -3
  11. package/dist/compliance/classify.d.ts +11 -1
  12. package/dist/compliance/classify.js +31 -17
  13. package/dist/compliance/impact.d.ts +3 -1
  14. package/dist/compliance/impact.js +41 -13
  15. package/dist/compliance/types.d.ts +6 -0
  16. package/dist/compliance-matrix/build.js +0 -0
  17. package/dist/compliance-verification-matrix/build.d.ts +17 -0
  18. package/dist/compliance-verification-matrix/build.js +65 -0
  19. package/dist/compliance-verification-matrix/csv.d.ts +6 -0
  20. package/dist/compliance-verification-matrix/csv.js +41 -0
  21. package/dist/compliance-verification-matrix/index.d.ts +3 -0
  22. package/dist/compliance-verification-matrix/index.js +2 -0
  23. package/dist/compliance-verification-matrix/types.d.ts +75 -0
  24. package/dist/compliance-verification-matrix/types.js +4 -0
  25. package/dist/fgca/action-change-ids.d.ts +7 -0
  26. package/dist/fgca/action-change-ids.js +11 -0
  27. package/dist/fgca/parse-canonical.d.ts +3 -1
  28. package/dist/fgca/parse-canonical.js +13 -8
  29. package/dist/fgca/resolver.d.ts +6 -2
  30. package/dist/fgca/resolver.js +12 -6
  31. package/dist/fgca/validate.d.ts +2 -0
  32. package/dist/index.d.ts +1 -0
  33. package/dist/index.js +1 -0
  34. package/dist/process-blueprint/index.d.ts +3 -1
  35. package/dist/process-blueprint/index.js +1 -0
  36. package/dist/process-blueprint/layout.js +33 -29
  37. package/dist/process-blueprint/resolve-columns.d.ts +52 -0
  38. package/dist/process-blueprint/resolve-columns.js +138 -0
  39. package/dist/process-blueprint/types.d.ts +23 -5
  40. package/dist/process-blueprint/validate.d.ts +19 -1
  41. package/dist/process-blueprint/validate.js +73 -21
  42. package/dist/repo-validate/check-candidate-fields.d.ts +6 -0
  43. package/dist/repo-validate/check-candidate-fields.js +87 -0
  44. package/dist/repo-validate/check-process-parent.d.ts +8 -0
  45. package/dist/repo-validate/check-process-parent.js +172 -0
  46. package/dist/repo-validate/index.d.ts +1 -0
  47. package/dist/repo-validate/index.js +1 -0
  48. package/dist/repo-validate/validate-repo.js +7 -0
  49. package/dist/requirement/types.d.ts +1 -1
  50. package/dist/requirement/types.js +1 -1
  51. package/dist/webview/entry.js +11 -4
  52. package/dist/webview/render-fgca.js +1 -1
  53. package/dist/webview/render-process.d.ts +19 -1
  54. package/dist/webview/render-process.js +75 -42
  55. package/package.json +1 -1
  56. package/src/activities/resolver.ts +40 -20
  57. package/src/activities/validate.ts +58 -0
  58. package/src/canon-resolver-utils.ts +19 -0
  59. package/src/codex/index.ts +1 -1
  60. package/src/codex/types.ts +11 -3
  61. package/src/codex/validate.ts +30 -2
  62. package/src/compliance/classify.ts +40 -17
  63. package/src/compliance/impact.ts +41 -12
  64. package/src/compliance/types.ts +6 -0
  65. package/src/compliance-matrix/build.ts +0 -0
  66. package/src/compliance-verification-matrix/build.ts +84 -0
  67. package/src/compliance-verification-matrix/csv.ts +47 -0
  68. package/src/compliance-verification-matrix/index.ts +7 -0
  69. package/src/compliance-verification-matrix/types.ts +85 -0
  70. package/src/fgca/action-change-ids.ts +13 -0
  71. package/src/fgca/parse-canonical.ts +12 -8
  72. package/src/fgca/resolver.ts +15 -6
  73. package/src/fgca/validate.ts +2 -0
  74. package/src/index.ts +1 -0
  75. package/src/process-blueprint/index.ts +11 -0
  76. package/src/process-blueprint/layout.ts +48 -29
  77. package/src/process-blueprint/resolve-columns.ts +147 -0
  78. package/src/process-blueprint/types.ts +19 -5
  79. package/src/process-blueprint/validate.ts +107 -21
  80. package/src/repo-validate/check-candidate-fields.ts +93 -0
  81. package/src/repo-validate/check-process-parent.ts +184 -0
  82. package/src/repo-validate/index.ts +5 -0
  83. package/src/repo-validate/validate-repo.ts +7 -0
  84. package/src/requirement/types.ts +1 -1
  85. package/src/webview/entry.ts +13 -6
  86. package/src/webview/render-fgca.ts +1 -1
  87. package/src/webview/render-process.ts +94 -49
@@ -39,6 +39,9 @@ export interface ActionCanonSources {
39
39
  * to each element's inline `goals[]`. */
40
40
  relations?: unknown[];
41
41
  }
42
+ /** In-memory bag of ACTION ids dropped by `root_action` scoping. Consumed by
43
+ * `validateActivities` as `ACT-021`. Not part of the notation. */
44
+ export declare const ACT_021_OMITTED_KEY = "__act021_omitted";
42
45
  /**
43
46
  * Returns true when the parsed view document uses the canon-projection form
44
47
  * (`view_config` key present, no inline `actions[]`). Used by the preview and
@@ -11,7 +11,10 @@
11
11
  * field name; `type` is the canonical element field per elements/24-action.md
12
12
  * §2). Filesystem-free — callable from unit tests without vscode.
13
13
  */
14
- import { isObject, str, strArray, descendantsOf, stripEnvelope } from '../canon-resolver-utils.js';
14
+ import { isObject, str, strArray, stripEnvelope, reachesRootViaParent } from '../canon-resolver-utils.js';
15
+ /** In-memory bag of ACTION ids dropped by `root_action` scoping. Consumed by
16
+ * `validateActivities` as `ACT-021`. Not part of the notation. */
17
+ export const ACT_021_OMITTED_KEY = '__act021_omitted';
15
18
  /** ACTION elements by id. `notation: action` is canonical; `notation: activity`
16
19
  * is the deprecated pre-2026-06-25 alias (elements/24-action.md §"Deprecated
17
20
  * alias") — accepted the same way `collectByNotation` callers elsewhere do. */
@@ -93,39 +96,32 @@ export function resolveAction(viewDoc, sources) {
93
96
  const relGoals = activeActionGoalRelations(relations, id);
94
97
  return relGoals.length > 0 ? relGoals : strArray(el['goals']);
95
98
  }
96
- // 1. Scope by root_action (this action + its descendants), or the full
97
- // catalogue when omitted (§5.1: "Omit to include elements from the full
98
- // catalogue (or use goals/type_filter for other scoping)").
99
- const rootAction = str(scope['root_action']);
100
- let candidateIds;
101
- if (rootAction) {
102
- candidateIds = allActions.has(rootAction) ? descendantsOf(rootAction, allActions) : new Set();
103
- }
104
- else {
105
- candidateIds = new Set(allActions.keys());
106
- }
107
- // 2. Narrow by goals — only actions linked to at least one listed GOAL
99
+ // 1. Other scope fields first — the set the view would otherwise select
100
+ // (07-action.md §5.1). `root_action` then omits (and warns) rather than
101
+ // silently intersecting first.
102
+ let otherwiseIds = new Set(allActions.keys());
103
+ // Narrow by goals — only actions linked to at least one listed GOAL
108
104
  // (via an active action_goal relation, or inline goals[] when no relation
109
105
  // exists for that action).
110
106
  const goalsFilter = strArray(scope['goals']);
111
107
  if (goalsFilter.length > 0) {
112
- candidateIds = new Set([...candidateIds].filter((id) => effectiveGoals(id, allActions.get(id)).some((g) => goalsFilter.includes(g))));
108
+ otherwiseIds = new Set([...otherwiseIds].filter((id) => effectiveGoals(id, allActions.get(id)).some((g) => goalsFilter.includes(g))));
113
109
  }
114
- // 3. Narrow by type_filter — only actions of a listed type.
110
+ // Narrow by type_filter — only actions of a listed type.
115
111
  const typeFilter = strArray(scope['type_filter']);
116
112
  if (typeFilter.length > 0) {
117
- candidateIds = new Set([...candidateIds].filter((id) => {
113
+ otherwiseIds = new Set([...otherwiseIds].filter((id) => {
118
114
  const el = allActions.get(id);
119
115
  const t = str(el?.['type']) ?? str(el?.['activity_type']);
120
116
  return t !== undefined && typeFilter.includes(t);
121
117
  }));
122
118
  }
123
- // 4. Narrow by valid_at — only actions in effect on that date. Actions with
119
+ // Narrow by valid_at — only actions in effect on that date. Actions with
124
120
  // no valid_from are not lifecycle-tracked and are kept (permissive default,
125
121
  // matching the resolver's general "missing = not excluded" stance).
126
122
  const validAt = str(scope['valid_at']);
127
123
  if (validAt) {
128
- candidateIds = new Set([...candidateIds].filter((id) => {
124
+ otherwiseIds = new Set([...otherwiseIds].filter((id) => {
129
125
  const el = allActions.get(id);
130
126
  const from = str(el?.['valid_from']);
131
127
  const to = str(el?.['valid_to']);
@@ -136,6 +132,29 @@ export function resolveAction(viewDoc, sources) {
136
132
  return true;
137
133
  }));
138
134
  }
135
+ const rootAction = str(scope['root_action']);
136
+ let candidateIds;
137
+ const omittedOutsideRoot = [];
138
+ if (rootAction) {
139
+ if (!allActions.has(rootAction)) {
140
+ candidateIds = new Set();
141
+ }
142
+ else {
143
+ candidateIds = new Set();
144
+ for (const id of otherwiseIds) {
145
+ if (reachesRootViaParent(id, rootAction, allActions)) {
146
+ candidateIds.add(id);
147
+ }
148
+ else {
149
+ omittedOutsideRoot.push(id);
150
+ }
151
+ }
152
+ omittedOutsideRoot.sort();
153
+ }
154
+ }
155
+ else {
156
+ candidateIds = otherwiseIds;
157
+ }
139
158
  // Element fields map onto Activity fields unchanged, except the canonical
140
159
  // `type` (elements/24-action.md §2) which becomes the internal `activity_type`
141
160
  // (types.ts `Activity.activity_type`), and `goals` which is resolved via
@@ -168,5 +187,7 @@ export function resolveAction(viewDoc, sources) {
168
187
  };
169
188
  if (Object.keys(project).length > 0)
170
189
  out['project'] = project;
190
+ if (omittedOutsideRoot.length > 0)
191
+ out[ACT_021_OMITTED_KEY] = { root: rootAction, ids: omittedOutsideRoot };
171
192
  return out;
172
193
  }
@@ -1,5 +1,59 @@
1
+ import { isObject, str, reachesRootViaParent } from '../canon-resolver-utils.js';
2
+ import { ACT_021_OMITTED_KEY } from './resolver.js';
1
3
  const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
2
4
  const VALID_WEEKDAYS = new Set(['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']);
5
+ function act021Message(actionId, rootId) {
6
+ return `ACTION "${actionId}" is not "${rootId}" and is not reachable from it via parent`;
7
+ }
8
+ function collectAct021FromProjection(raw) {
9
+ const bag = raw[ACT_021_OMITTED_KEY];
10
+ delete raw[ACT_021_OMITTED_KEY];
11
+ if (!isObject(bag))
12
+ return [];
13
+ const root = str(bag['root']);
14
+ const ids = Array.isArray(bag['ids']) ? bag['ids'] : [];
15
+ if (!root)
16
+ return [];
17
+ const out = [];
18
+ for (const id of ids) {
19
+ if (typeof id !== 'string' || !id.trim())
20
+ continue;
21
+ out.push({
22
+ code: 'ACT-021',
23
+ message: act021Message(id, root),
24
+ path: 'view_config.scope.root_action',
25
+ });
26
+ }
27
+ return out;
28
+ }
29
+ function collectAct021FromAuthored(raw, actions) {
30
+ const vc = isObject(raw['view_config']) ? raw['view_config'] : undefined;
31
+ const scope = vc && isObject(vc['scope']) ? vc['scope'] : undefined;
32
+ const root = scope ? str(scope['root_action']) : undefined;
33
+ if (!root)
34
+ return [];
35
+ const byId = new Map();
36
+ for (const el of actions) {
37
+ if (!isObject(el))
38
+ continue;
39
+ const id = str(el['id']);
40
+ if (id)
41
+ byId.set(id, el);
42
+ }
43
+ if (!byId.has(root))
44
+ return [];
45
+ const out = [];
46
+ for (const id of byId.keys()) {
47
+ if (reachesRootViaParent(id, root, byId))
48
+ continue;
49
+ out.push({
50
+ code: 'ACT-021',
51
+ message: act021Message(id, root),
52
+ path: 'view_config.scope.root_action',
53
+ });
54
+ }
55
+ return out;
56
+ }
3
57
  export function validateActivities(input) {
4
58
  const errors = [];
5
59
  const warnings = [];
@@ -33,6 +87,11 @@ export function validateActivities(input) {
33
87
  // Normalise onto raw.activities for the rest of this function (avoids touching downstream code)
34
88
  if (!Array.isArray(raw.activities))
35
89
  raw.activities = raw.actions;
90
+ const fromProjection = collectAct021FromProjection(raw);
91
+ warnings.push(...fromProjection);
92
+ if (fromProjection.length === 0) {
93
+ warnings.push(...collectAct021FromAuthored(raw, rawArray));
94
+ }
36
95
  // ACT-014 / ACT-015: project.calendar validation (run before per-activity loop;
37
96
  // surfaces clearly even when activities also have issues).
38
97
  if (raw.project && typeof raw.project === 'object') {
@@ -13,6 +13,9 @@ export declare function strArray(v: unknown): string[];
13
13
  * per the element map's own `parent` field. Used to scope a projection to a
14
14
  * root element and its transitive descendants (WBS / goal hierarchy / etc). */
15
15
  export declare function descendantsOf(rootId: string, all: Map<string, Record<string, unknown>>): Set<string>;
16
+ /** Walk `parent` from `id` toward its ancestors until `rootId` or the chain
17
+ * ends. Cycle-safe: a cycle is not a path to the root. */
18
+ export declare function reachesRootViaParent(id: string, rootId: string, all: Map<string, Record<string, unknown>>): boolean;
16
19
  /** Depth of `id` in its `parent` chain within `all` (root = 0, each step up
17
20
  * adds 1). Cycle-safe (a cycle resolves the entering node to depth 0) and
18
21
  * memoized across calls via the shared `memo` map. */
@@ -45,6 +45,22 @@ export function descendantsOf(rootId, all) {
45
45
  }
46
46
  return result;
47
47
  }
48
+ /** Walk `parent` from `id` toward its ancestors until `rootId` or the chain
49
+ * ends. Cycle-safe: a cycle is not a path to the root. */
50
+ export function reachesRootViaParent(id, rootId, all) {
51
+ const seen = new Set();
52
+ let current = id;
53
+ while (current) {
54
+ if (current === rootId)
55
+ return true;
56
+ if (seen.has(current))
57
+ return false;
58
+ seen.add(current);
59
+ const el = all.get(current);
60
+ current = el ? str(el['parent']) : undefined;
61
+ }
62
+ return false;
63
+ }
48
64
  /** Depth of `id` in its `parent` chain within `all` (root = 0, each step up
49
65
  * adds 1). Cycle-safe (a cycle resolves the entering node to depth 0) and
50
66
  * memoized across calls via the shared `memo` map. */
@@ -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,2 +1,2 @@
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 { validateCodex, isCodexDoc, folderJurisdictionFromPath } from './validate.js';
@@ -1,7 +1,14 @@
1
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"];
2
+ export declare const CODEX_ARTEFACT_TYPES: readonly ["LAW", "REGULATION", "POLICY", "INTERNAL_STANDARD", "PRINCIPLE"];
3
3
  export type CodexArtefactType = (typeof CODEX_ARTEFACT_TYPES)[number];
4
4
  /** External codex artefacts (codex/external/<jurisdiction>/). */
5
5
  export declare const EXTERNAL_CODEX_TYPES: readonly CodexArtefactType[];
6
- /** Internal codex artefacts (codex/internal/). */
6
+ /** Internal codex artefacts (codex/internal/) that carry a named issuing authority (§4). */
7
7
  export declare const INTERNAL_CODEX_TYPES: readonly CodexArtefactType[];
8
+ /**
9
+ * Internal codex artefacts (codex/internal/) with the `PRINCIPLE` shape (§4.1) —
10
+ * `statement` + `rationale` required, `issuing_authority`/`effective_date`/`established_by`
11
+ * optional. Kept distinct from {@link INTERNAL_CODEX_TYPES} because the required-field set
12
+ * differs; both share the same file location.
13
+ */
14
+ export declare const PRINCIPLE_CODEX_TYPES: readonly CodexArtefactType[];
@@ -1,8 +1,15 @@
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
  /** TYPE prefixes admitted in the codex zone (REQ-003 / 14-codex.md). */
4
- export const CODEX_ARTEFACT_TYPES = ['LAW', 'REGULATION', 'POLICY', 'INTERNAL_STANDARD'];
4
+ export const CODEX_ARTEFACT_TYPES = ['LAW', 'REGULATION', 'POLICY', 'INTERNAL_STANDARD', 'PRINCIPLE'];
5
5
  /** External codex artefacts (codex/external/<jurisdiction>/). */
6
6
  export const EXTERNAL_CODEX_TYPES = ['LAW', 'REGULATION'];
7
- /** Internal codex artefacts (codex/internal/). */
7
+ /** Internal codex artefacts (codex/internal/) that carry a named issuing authority (§4). */
8
8
  export const INTERNAL_CODEX_TYPES = ['POLICY', 'INTERNAL_STANDARD'];
9
+ /**
10
+ * Internal codex artefacts (codex/internal/) with the `PRINCIPLE` shape (§4.1) —
11
+ * `statement` + `rationale` required, `issuing_authority`/`effective_date`/`established_by`
12
+ * optional. Kept distinct from {@link INTERNAL_CODEX_TYPES} because the required-field set
13
+ * differs; both share the same file location.
14
+ */
15
+ export const PRINCIPLE_CODEX_TYPES = ['PRINCIPLE'];
@@ -3,11 +3,22 @@
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
  import { isCanonicalId, typeOfId } from '../typed-id.js';
10
- import { CODEX_ARTEFACT_TYPES, EXTERNAL_CODEX_TYPES, INTERNAL_CODEX_TYPES, } from './types.js';
21
+ import { CODEX_ARTEFACT_TYPES, EXTERNAL_CODEX_TYPES, INTERNAL_CODEX_TYPES, PRINCIPLE_CODEX_TYPES, } from './types.js';
11
22
  function isCodexType(type) {
12
23
  return type !== null && CODEX_ARTEFACT_TYPES.includes(type);
13
24
  }
@@ -31,7 +42,7 @@ export function validateCodex(input, options = {}) {
31
42
  if (!isCanonicalId(c.id) || !isCodexType(idType)) {
32
43
  errors.push({
33
44
  code: 'CODEX-001',
34
- message: `id "${String(c.id)}" must be a typed codex id (LAW, REGULATION, POLICY, or INTERNAL_STANDARD).`,
45
+ message: `id "${String(c.id)}" must be a typed codex id (LAW, REGULATION, POLICY, INTERNAL_STANDARD, or PRINCIPLE).`,
35
46
  path: 'id',
36
47
  });
37
48
  }
@@ -84,6 +95,21 @@ export function validateCodex(input, options = {}) {
84
95
  errors.push({ code: 'CODEX-004', message: 'effective_date is required for internal codex artefacts.', path: 'effective_date' });
85
96
  }
86
97
  }
98
+ if (artefactType && PRINCIPLE_CODEX_TYPES.includes(artefactType)) {
99
+ if (typeof c.statement !== 'string' || c.statement.trim() === '') {
100
+ errors.push({ code: 'CODEX-004', message: 'statement is required for a PRINCIPLE artefact.', path: 'statement' });
101
+ }
102
+ if (typeof c.rationale !== 'string' || c.rationale.trim() === '') {
103
+ errors.push({ code: 'CODEX-004', message: 'rationale is required for a PRINCIPLE artefact.', path: 'rationale' });
104
+ }
105
+ if (typeof c.established_by !== 'string' || c.established_by.trim() === '') {
106
+ warnings.push({
107
+ code: 'CODEX-006',
108
+ message: 'established_by is not declared on this PRINCIPLE artefact — admissible, but a review finding for whoever admits it.',
109
+ path: 'established_by',
110
+ });
111
+ }
112
+ }
87
113
  return { valid: errors.length === 0, errors, warnings };
88
114
  }
89
115
  /** True when a parsed document is a codex-zone artefact candidate. */
@@ -30,6 +30,14 @@ export interface ComplianceCanon {
30
30
  /** VALIDATION artefacts (28-validation.md) — the validation-domain peer of
31
31
  * `verifications`, anchored on NEED instead of REQUIREMENT. */
32
32
  validations: IndexValidation[];
33
+ /**
34
+ * Ids rejected by `ingestComplianceDoc` because that id was already present
35
+ * in the same bucket (two documents claiming one id — transitrix-hq#218).
36
+ * The second and any later document is dropped rather than silently
37
+ * duplicating a matrix column; callers that scan a filesystem can surface
38
+ * this list as a diagnostic.
39
+ */
40
+ duplicateIds: string[];
33
41
  }
34
42
  export declare function emptyCanon(): ComplianceCanon;
35
43
  /**
@@ -37,6 +45,8 @@ export declare function emptyCanon(): ComplianceCanon;
37
45
  * pushes its projection into `canon`. Products / requirements / assertions are
38
46
  * identified by their `notation` tag; codex source documents by `zone: codex`.
39
47
  * Returns the artefact id when ingested (so the caller can record its path), or
40
- * null when the document is not a (well-formed) compliance artefact.
48
+ * null when the document is not a (well-formed) compliance artefact, or when
49
+ * its id duplicates one already ingested into the same bucket (see
50
+ * `canon.duplicateIds`).
41
51
  */
42
52
  export declare function ingestComplianceDoc(canon: ComplianceCanon, doc: unknown): string | null;
@@ -12,16 +12,33 @@ export function emptyCanon() {
12
12
  subjects: [],
13
13
  needs: [],
14
14
  validations: [],
15
+ duplicateIds: [],
15
16
  };
16
17
  }
17
18
  const str = (v) => (typeof v === 'string' ? v : undefined);
18
19
  const strArray = (v) => Array.isArray(v) ? v.filter((x) => typeof x === 'string') : undefined;
20
+ /**
21
+ * Pushes `item` onto `list` unless `list` already holds an entry with the same
22
+ * id, in which case the id is recorded in `canon.duplicateIds` and the item is
23
+ * dropped — two documents claiming one id is a defect (transitrix-hq#218), not
24
+ * a second matrix column.
25
+ */
26
+ function pushUnique(canon, list, item) {
27
+ if (list.some(existing => existing.id === item.id)) {
28
+ canon.duplicateIds.push(item.id);
29
+ return false;
30
+ }
31
+ list.push(item);
32
+ return true;
33
+ }
19
34
  /**
20
35
  * Classifies one parsed YAML document and, if it is a compliance artefact,
21
36
  * pushes its projection into `canon`. Products / requirements / assertions are
22
37
  * identified by their `notation` tag; codex source documents by `zone: codex`.
23
38
  * Returns the artefact id when ingested (so the caller can record its path), or
24
- * null when the document is not a (well-formed) compliance artefact.
39
+ * null when the document is not a (well-formed) compliance artefact, or when
40
+ * its id duplicates one already ingested into the same bucket (see
41
+ * `canon.duplicateIds`).
25
42
  */
26
43
  export function ingestComplianceDoc(canon, doc) {
27
44
  if (doc === null || typeof doc !== 'object' || Array.isArray(doc))
@@ -31,19 +48,17 @@ export function ingestComplianceDoc(canon, doc) {
31
48
  if (!id)
32
49
  return null;
33
50
  if (d.notation === 'product') {
34
- canon.products.push({ id, name: str(d.name) ?? id });
35
- return id;
51
+ return pushUnique(canon, canon.products, { id, name: str(d.name) ?? id }) ? id : null;
36
52
  }
37
53
  if (d.notation === 'capability' ||
38
54
  d.notation === 'process' ||
39
55
  d.notation === 'application' ||
40
56
  d.notation === 'system') {
41
- canon.subjects.push({ id, name: str(d.name) ?? id });
42
- return id;
57
+ return pushUnique(canon, canon.subjects, { id, name: str(d.name) ?? id }) ? id : null;
43
58
  }
44
59
  if (d.notation === 'requirement' || d.notation === 'constraint') {
45
60
  const origin = str(d.origin);
46
- canon.requirements.push({
61
+ const ok = pushUnique(canon, canon.requirements, {
47
62
  id,
48
63
  name: str(d.name) ?? id,
49
64
  severity: str(d.severity),
@@ -57,11 +72,10 @@ export function ingestComplianceDoc(canon, doc) {
57
72
  next_review_at: str(d.next_review_at),
58
73
  serves: str(d.serves),
59
74
  });
60
- return id;
75
+ return ok ? id : null;
61
76
  }
62
77
  if (d.notation === 'need') {
63
- canon.needs.push({ id, name: str(d.name) ?? id });
64
- return id;
78
+ return pushUnique(canon, canon.needs, { id, name: str(d.name) ?? id }) ? id : null;
65
79
  }
66
80
  if (d.notation === 'validation') {
67
81
  const validates = str(d.validates);
@@ -69,13 +83,13 @@ export function ingestComplianceDoc(canon, doc) {
69
83
  const outcome = str(d.outcome);
70
84
  if (!validates || !method || !outcome)
71
85
  return null;
72
- canon.validations.push({
86
+ const ok = pushUnique(canon, canon.validations, {
73
87
  id, validates, method, outcome,
74
88
  performed_at: str(d.performed_at),
75
89
  evidenceCount: Array.isArray(d.evidence) ? d.evidence.length : 0,
76
90
  admitted_at: str(d.admitted_at),
77
91
  });
78
- return id;
92
+ return ok ? id : null;
79
93
  }
80
94
  if (d.notation === 'assertion') {
81
95
  const about = str(d.about);
@@ -83,7 +97,7 @@ export function ingestComplianceDoc(canon, doc) {
83
97
  const status = str(d.status);
84
98
  if (!about || !subject || !status)
85
99
  return null;
86
- canon.assertions.push({
100
+ const ok = pushUnique(canon, canon.assertions, {
87
101
  id, about, subject, status,
88
102
  assessed_at: str(d.assessed_at),
89
103
  next_review_at: str(d.next_review_at),
@@ -92,7 +106,7 @@ export function ingestComplianceDoc(canon, doc) {
92
106
  realised_via: strArray(d.realised_via),
93
107
  owner_to_confirm: str(d.owner_to_confirm),
94
108
  });
95
- return id;
109
+ return ok ? id : null;
96
110
  }
97
111
  if (d.notation === 'verification') {
98
112
  const verifies = str(d.verifies);
@@ -100,17 +114,17 @@ export function ingestComplianceDoc(canon, doc) {
100
114
  const outcome = str(d.outcome);
101
115
  if (!verifies || !method || !outcome)
102
116
  return null;
103
- canon.verifications.push({
117
+ const ok = pushUnique(canon, canon.verifications, {
104
118
  id, verifies, method, outcome,
119
+ protocol: str(d.protocol),
105
120
  performed_at: str(d.performed_at),
106
121
  evidenceCount: Array.isArray(d.evidence) ? d.evidence.length : 0,
107
122
  admitted_at: str(d.admitted_at),
108
123
  });
109
- return id;
124
+ return ok ? id : null;
110
125
  }
111
126
  if (d.zone === 'codex') {
112
- canon.codex.push({ id, name: str(d.name) ?? id, type: str(d.type), jurisdiction: str(d.jurisdiction) });
113
- return id;
127
+ return pushUnique(canon, canon.codex, { id, name: str(d.name) ?? id, type: str(d.type), jurisdiction: str(d.jurisdiction) }) ? id : null;
114
128
  }
115
129
  return null;
116
130
  }
@@ -277,7 +277,9 @@ export declare function computeDeadlineStatus(deadline: string | undefined, toda
277
277
  * and `config.subjects.capabilities` (in that order). Subjects with no binding
278
278
  * obligation still appear as columns so the gap is visible.
279
279
  */
280
- export declare function buildImpactMatrix(canon: ComplianceCanon, config: ImpactViewConfig, objectDetails?: ObjectDetailInput[]): ImpactMatrix;
280
+ export declare function buildImpactMatrix(canon: ComplianceCanon, config: ImpactViewConfig, objectDetails?: ObjectDetailInput[], join?: {
281
+ stepHomeProcess?: ReadonlyMap<string, string>;
282
+ }): ImpactMatrix;
281
283
  /**
282
284
  * Extract stage-level `ObjectDetailInput` from a raw (YAML-parsed)
283
285
  * process-blueprint document.
@@ -7,6 +7,7 @@
7
7
  // (`product` columns × `obligation` rows). Stage / task grouping is planned
8
8
  // for CV-3a and intentionally out of scope here.
9
9
  import { buildComplianceIndex } from './reverse-index.js';
10
+ import { isStageColumnId } from '../process-blueprint/resolve-columns.js';
10
11
  /**
11
12
  * Repo-scope cross-reference checks for a parsed compliance-impact view config
12
13
  * (#518 Phase C3). Surfaces unresolved subject, obligation, and regime ids before
@@ -390,25 +391,51 @@ function buildTaskColumns(config, objectDetails, isCombined) {
390
391
  *
391
392
  * At `'product'` grain: `a.subject` must match `col.subjectId`.
392
393
  * At `'product-stage'` grain: `a.subject` must match AND either
393
- * `a.realised_via` is absent (claim covers the whole subject) OR
394
- * `col.stageId` ∈ `a.realised_via`.
395
- * At `'product-stage-task'` grain: as above for stage, plus `col.taskId` ∈
396
- * `a.realised_via` counts as a match (task-level claim covers this task column).
397
- * A stage-level claim (`col.stageId` ∈ `a.realised_via`) covers all task columns
398
- * in that stage.
394
+ * `a.realised_via` is absent (claim covers the whole subject) OR the token
395
+ * pins this column: a `PROCESS-…` id matching `col.stageId`, or a STEP whose
396
+ * home process is that column. Sketch `STAGE-…` ids are never a join key.
397
+ * At `'product-stage-task'` grain: a task-id hit covers that task column; a
398
+ * process-level claim covers every task column of that process.
399
399
  */
400
- function assertionMatchesColumn(a, col) {
400
+ function assertionMatchesColumn(a, col, stepHomeProcess) {
401
401
  if (a.subject !== col.subjectId)
402
402
  return false;
403
403
  if (!col.stageId)
404
404
  return true;
405
405
  if (!a.realised_via || a.realised_via.length === 0)
406
406
  return true;
407
- if (col.taskId) {
408
- // Task grain: match on taskId OR on stageId (stage claim covers all tasks).
409
- return a.realised_via.includes(col.taskId) || a.realised_via.includes(col.stageId);
407
+ const stageId = col.stageId;
408
+ if (isStageColumnId(stageId)) {
409
+ // Sketch column: only a task-id hit counts — never the STAGE- key.
410
+ return col.taskId != null && a.realised_via.includes(col.taskId);
410
411
  }
411
- return a.realised_via.includes(col.stageId);
412
+ for (const ref of a.realised_via) {
413
+ if (typeof ref !== 'string')
414
+ continue;
415
+ if (isStageColumnId(ref))
416
+ continue;
417
+ if (col.taskId && ref === col.taskId)
418
+ return true;
419
+ if (ref === stageId)
420
+ return true;
421
+ if (!col.taskId && stepHomeProcess?.get(ref) === stageId)
422
+ return true;
423
+ }
424
+ return false;
425
+ }
426
+ function stepHomeFromObjectDetails(objectDetails) {
427
+ const out = new Map();
428
+ if (!objectDetails)
429
+ return out;
430
+ for (const od of objectDetails) {
431
+ for (const d of od.details ?? []) {
432
+ for (const t of d.tasks ?? []) {
433
+ if (typeof t?.id === 'string' && t.id.length > 0)
434
+ out.set(t.id, d.id);
435
+ }
436
+ }
437
+ }
438
+ return out;
412
439
  }
413
440
  /**
414
441
  * Build the matrix per §5.2.
@@ -430,7 +457,7 @@ function assertionMatchesColumn(a, col) {
430
457
  * and `config.subjects.capabilities` (in that order). Subjects with no binding
431
458
  * obligation still appear as columns so the gap is visible.
432
459
  */
433
- export function buildImpactMatrix(canon, config, objectDetails) {
460
+ export function buildImpactMatrix(canon, config, objectDetails, join) {
434
461
  const index = buildComplianceIndex({
435
462
  requirements: canon.requirements,
436
463
  assertions: canon.assertions,
@@ -485,6 +512,7 @@ export function buildImpactMatrix(canon, config, objectDetails) {
485
512
  const allowedStatuses = new Set(config.status_display?.show ?? ['compliant', 'partial', 'non_compliant', 'under_review', 'pending_owner', 'n_a']);
486
513
  const rowIndex = new Set(obligations.map(r => r.id));
487
514
  const cells = obligations.map(() => columns.map(() => emptyCell()));
515
+ const stepHome = join?.stepHomeProcess ?? stepHomeFromObjectDetails(objectDetails);
488
516
  for (let c = 0; c < columns.length; c++) {
489
517
  const col = columns[c];
490
518
  const subjectAssertions = index.assertionsBySubject.get(col.subjectId) ?? [];
@@ -493,7 +521,7 @@ export function buildImpactMatrix(canon, config, objectDetails) {
493
521
  continue;
494
522
  if (!allowedStatuses.has(a.status))
495
523
  continue;
496
- if (!assertionMatchesColumn(a, col))
524
+ if (!assertionMatchesColumn(a, col, stepHome))
497
525
  continue;
498
526
  const rowIdx = obligations.findIndex(r => r.id === a.about);
499
527
  if (rowIdx < 0)
@@ -116,6 +116,12 @@ export interface IndexVerification {
116
116
  verifies: string;
117
117
  method: VerificationMethod;
118
118
  outcome: VerificationOutcome;
119
+ /**
120
+ * The verification procedure (27-verification.md §2). VERIFICATION carries
121
+ * no `name` field of its own; `protocol` is the closest human-readable
122
+ * description and is what a matrix/list view shows as the row's label.
123
+ */
124
+ protocol?: string;
119
125
  performed_at?: string;
120
126
  /** Number of evidence entries — feeds the VERIF-006 gap. */
121
127
  evidenceCount?: number;
Binary file
@@ -0,0 +1,17 @@
1
+ import type { ComplianceIndex } from '../compliance/types.js';
2
+ import type { RequirementVerificationMatrix, RequirementVerificationMatrixOptions } from './types.js';
3
+ /**
4
+ * Builds the repository-wide requirement -> verification matrix.
5
+ *
6
+ * Pure projection over an already-built `ComplianceIndex`
7
+ * (`compliance/reverse-index.ts`) — no I/O, no revalidation. Reuses
8
+ * `buildGapReport`'s `REQ-VERIF-COVERAGE-001`/`-002` verdicts verbatim rather
9
+ * than recomputing a competing coverage status.
10
+ *
11
+ * Ordering: requirements sorted by id; within a requirement, its verification
12
+ * rows sorted by verification id. Both are explicit sorts over the index's
13
+ * values, so the result is independent of the Map's insertion order (which —
14
+ * for a caller that built the index from a filesystem walk — would otherwise
15
+ * track directory enumeration order).
16
+ */
17
+ export declare function buildRequirementVerificationMatrix(index: ComplianceIndex, options?: RequirementVerificationMatrixOptions): RequirementVerificationMatrix;