@transitrix/diagrams 1.10.0 → 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 (68) 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/compliance/classify.js +1 -0
  7. package/dist/compliance/impact.d.ts +3 -1
  8. package/dist/compliance/impact.js +41 -13
  9. package/dist/compliance/types.d.ts +6 -0
  10. package/dist/compliance-verification-matrix/build.d.ts +17 -0
  11. package/dist/compliance-verification-matrix/build.js +65 -0
  12. package/dist/compliance-verification-matrix/csv.d.ts +6 -0
  13. package/dist/compliance-verification-matrix/csv.js +41 -0
  14. package/dist/compliance-verification-matrix/index.d.ts +3 -0
  15. package/dist/compliance-verification-matrix/index.js +2 -0
  16. package/dist/compliance-verification-matrix/types.d.ts +75 -0
  17. package/dist/compliance-verification-matrix/types.js +4 -0
  18. package/dist/fgca/action-change-ids.d.ts +7 -0
  19. package/dist/fgca/action-change-ids.js +11 -0
  20. package/dist/fgca/parse-canonical.d.ts +3 -1
  21. package/dist/fgca/parse-canonical.js +13 -8
  22. package/dist/fgca/resolver.d.ts +6 -2
  23. package/dist/fgca/resolver.js +12 -6
  24. package/dist/fgca/validate.d.ts +2 -0
  25. package/dist/index.d.ts +1 -0
  26. package/dist/index.js +1 -0
  27. package/dist/process-blueprint/index.d.ts +3 -1
  28. package/dist/process-blueprint/index.js +1 -0
  29. package/dist/process-blueprint/layout.js +33 -29
  30. package/dist/process-blueprint/resolve-columns.d.ts +52 -0
  31. package/dist/process-blueprint/resolve-columns.js +138 -0
  32. package/dist/process-blueprint/types.d.ts +23 -5
  33. package/dist/process-blueprint/validate.d.ts +19 -1
  34. package/dist/process-blueprint/validate.js +73 -21
  35. package/dist/repo-validate/check-process-parent.d.ts +8 -0
  36. package/dist/repo-validate/check-process-parent.js +172 -0
  37. package/dist/repo-validate/index.d.ts +1 -0
  38. package/dist/repo-validate/index.js +1 -0
  39. package/dist/repo-validate/validate-repo.js +3 -0
  40. package/dist/webview/render-fgca.js +1 -1
  41. package/dist/webview/render-process.d.ts +19 -1
  42. package/dist/webview/render-process.js +75 -42
  43. package/package.json +1 -1
  44. package/src/activities/resolver.ts +40 -20
  45. package/src/activities/validate.ts +58 -0
  46. package/src/canon-resolver-utils.ts +19 -0
  47. package/src/compliance/classify.ts +1 -0
  48. package/src/compliance/impact.ts +41 -12
  49. package/src/compliance/types.ts +6 -0
  50. package/src/compliance-verification-matrix/build.ts +84 -0
  51. package/src/compliance-verification-matrix/csv.ts +47 -0
  52. package/src/compliance-verification-matrix/index.ts +7 -0
  53. package/src/compliance-verification-matrix/types.ts +85 -0
  54. package/src/fgca/action-change-ids.ts +13 -0
  55. package/src/fgca/parse-canonical.ts +12 -8
  56. package/src/fgca/resolver.ts +15 -6
  57. package/src/fgca/validate.ts +2 -0
  58. package/src/index.ts +1 -0
  59. package/src/process-blueprint/index.ts +11 -0
  60. package/src/process-blueprint/layout.ts +48 -29
  61. package/src/process-blueprint/resolve-columns.ts +147 -0
  62. package/src/process-blueprint/types.ts +19 -5
  63. package/src/process-blueprint/validate.ts +107 -21
  64. package/src/repo-validate/check-process-parent.ts +184 -0
  65. package/src/repo-validate/index.ts +5 -0
  66. package/src/repo-validate/validate-repo.ts +3 -0
  67. package/src/webview/render-fgca.ts +1 -1
  68. 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. */
@@ -116,6 +116,7 @@ export function ingestComplianceDoc(canon, doc) {
116
116
  return null;
117
117
  const ok = pushUnique(canon, canon.verifications, {
118
118
  id, verifies, method, outcome,
119
+ protocol: str(d.protocol),
119
120
  performed_at: str(d.performed_at),
120
121
  evidenceCount: Array.isArray(d.evidence) ? d.evidence.length : 0,
121
122
  admitted_at: str(d.admitted_at),
@@ -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;
@@ -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;
@@ -0,0 +1,65 @@
1
+ // Requirement -> verification matrix builder.
2
+ import { buildGapReport } from '../compliance/gap-report.js';
3
+ function byId(a, b) {
4
+ return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
5
+ }
6
+ /**
7
+ * Builds the repository-wide requirement -> verification matrix.
8
+ *
9
+ * Pure projection over an already-built `ComplianceIndex`
10
+ * (`compliance/reverse-index.ts`) — no I/O, no revalidation. Reuses
11
+ * `buildGapReport`'s `REQ-VERIF-COVERAGE-001`/`-002` verdicts verbatim rather
12
+ * than recomputing a competing coverage status.
13
+ *
14
+ * Ordering: requirements sorted by id; within a requirement, its verification
15
+ * rows sorted by verification id. Both are explicit sorts over the index's
16
+ * values, so the result is independent of the Map's insertion order (which —
17
+ * for a caller that built the index from a filesystem walk — would otherwise
18
+ * track directory enumeration order).
19
+ */
20
+ export function buildRequirementVerificationMatrix(index, options = {}) {
21
+ const gapReport = buildGapReport(index, options);
22
+ const noVerification = new Set(gapReport.requirementsWithoutVerification.map(r => r.id));
23
+ const unresolvedVerification = new Set(gapReport.requirementsWithUnresolvedVerification.map(r => r.id));
24
+ const requirements = [...index.requirementById.values()].sort(byId);
25
+ const rows = [];
26
+ let verificationRowCount = 0;
27
+ let gapRequirementCount = 0;
28
+ for (const requirement of requirements) {
29
+ const parentId = requirement.parent;
30
+ const parentLabel = parentId ? index.requirementById.get(parentId)?.name : undefined;
31
+ const base = {
32
+ requirementId: requirement.id,
33
+ requirementLabel: requirement.name,
34
+ ...(parentId !== undefined ? { parentId } : {}),
35
+ ...(parentLabel !== undefined ? { parentLabel } : {}),
36
+ };
37
+ if (noVerification.has(requirement.id)) {
38
+ gapRequirementCount++;
39
+ rows.push({ ...base, coverageGap: 'REQ-VERIF-COVERAGE-001' });
40
+ continue;
41
+ }
42
+ const gap = unresolvedVerification.has(requirement.id) ? 'REQ-VERIF-COVERAGE-002' : undefined;
43
+ if (gap)
44
+ gapRequirementCount++;
45
+ const verifications = [...(index.verificationsByRequirement.get(requirement.id) ?? [])].sort(byId);
46
+ for (const verification of verifications) {
47
+ verificationRowCount++;
48
+ rows.push({
49
+ ...base,
50
+ verificationId: verification.id,
51
+ verificationLabel: verification.protocol ?? verification.id,
52
+ verificationOutcome: verification.outcome,
53
+ ...(gap ? { coverageGap: gap } : {}),
54
+ });
55
+ }
56
+ }
57
+ return {
58
+ rows,
59
+ summary: {
60
+ requirements: requirements.length,
61
+ verifications: verificationRowCount,
62
+ gaps: gapRequirementCount,
63
+ },
64
+ };
65
+ }
@@ -0,0 +1,6 @@
1
+ import type { RequirementVerificationMatrix } from './types.js';
2
+ /**
3
+ * Serialise the matrix to CSV. Identical input yields byte-identical output;
4
+ * the row order is the builder's stable order.
5
+ */
6
+ export declare function renderRequirementVerificationMatrixCsv(matrix: RequirementVerificationMatrix): string;
@@ -0,0 +1,41 @@
1
+ // Deterministic CSV serialiser for the requirement-verification matrix.
2
+ // Same row order as `buildRequirementVerificationMatrix` (requirement id,
3
+ // then verification id). CRLF line endings, RFC-4180 quoting — the same
4
+ // convention as the Gap Dashboard export.
5
+ const HEADER = [
6
+ 'requirement_id',
7
+ 'requirement_label',
8
+ 'parent_id',
9
+ 'parent_label',
10
+ 'related_test_result_id',
11
+ 'related_test_result_label',
12
+ 'outcome',
13
+ 'coverage_gap',
14
+ ];
15
+ function csvCell(value) {
16
+ const v = value ?? '';
17
+ return `"${v.replace(/"/g, '""')}"`;
18
+ }
19
+ function rowCells(row) {
20
+ return [
21
+ row.requirementId,
22
+ row.requirementLabel,
23
+ row.parentId ?? '',
24
+ row.parentLabel ?? '',
25
+ row.verificationId ?? '',
26
+ row.verificationLabel ?? '',
27
+ row.verificationOutcome ?? '',
28
+ row.coverageGap ?? '',
29
+ ];
30
+ }
31
+ /**
32
+ * Serialise the matrix to CSV. Identical input yields byte-identical output;
33
+ * the row order is the builder's stable order.
34
+ */
35
+ export function renderRequirementVerificationMatrixCsv(matrix) {
36
+ const lines = [HEADER.map(h => csvCell(h)).join(',')];
37
+ for (const row of matrix.rows) {
38
+ lines.push(rowCells(row).map(csvCell).join(','));
39
+ }
40
+ return lines.join('\r\n') + '\r\n';
41
+ }
@@ -0,0 +1,3 @@
1
+ export { buildRequirementVerificationMatrix } from './build.js';
2
+ export { renderRequirementVerificationMatrixCsv } from './csv.js';
3
+ export type { RequirementVerificationMatrix, RequirementVerificationMatrixOptions, RequirementVerificationRow, } from './types.js';
@@ -0,0 +1,2 @@
1
+ export { buildRequirementVerificationMatrix } from './build.js';
2
+ export { renderRequirementVerificationMatrixCsv } from './csv.js';
@@ -0,0 +1,75 @@
1
+ import type { VerificationOutcome } from '../verification/types.js';
2
+ /**
3
+ * One row of the requirement-verification matrix.
4
+ *
5
+ * A requirement with N admitted VERIFICATION records produces N rows, one per
6
+ * verification. A requirement with none still produces exactly one row,
7
+ * carrying the `REQ-VERIF-COVERAGE-001` gap. A requirement whose
8
+ * verifications exist but never closed (pass/fail) produces one row per
9
+ * verification — each carrying the `REQ-VERIF-COVERAGE-002` gap alongside its
10
+ * (still-open) outcome; the open verification is real data and is never
11
+ * replaced by a placeholder gap row.
12
+ *
13
+ * Coverage comes only from `VERIFICATION.verifies` (27-verification.md §2). A
14
+ * child requirement's verification never covers its `parent`
15
+ * (15-requirement.md §2.4 — `parent` records structure only, no roll-up).
16
+ */
17
+ export interface RequirementVerificationRow {
18
+ requirementId: string;
19
+ requirementLabel: string;
20
+ /**
21
+ * Direct `REQUIREMENT.parent` id, verbatim, present whenever the field is
22
+ * set — whether or not it resolves. `parent` resolution is not currently a
23
+ * validator concern (15-requirement.md §2.4); a dangling parent must still
24
+ * be visible on the row, not silently dropped.
25
+ */
26
+ parentId?: string;
27
+ /**
28
+ * The resolved parent's `name`, present only when `parentId` names an
29
+ * admitted requirement in the index. Absent when `parentId` is set but
30
+ * unresolved (dangling), or when the requirement has no parent.
31
+ */
32
+ parentLabel?: string;
33
+ /** The verification filling this row, when one exists. */
34
+ verificationId?: string;
35
+ /**
36
+ * VERIFICATION carries no `name`/label field of its own
37
+ * (27-verification.md §2); `protocol` — the procedure description — is the
38
+ * closest human-readable text and is what this row shows as its label.
39
+ */
40
+ verificationLabel?: string;
41
+ verificationOutcome?: VerificationOutcome;
42
+ /**
43
+ * `REQ-VERIF-COVERAGE-001` / `-002` (`compliance/gap-report.ts`), reused
44
+ * verbatim — this builder never recomputes a competing coverage verdict.
45
+ * Absent once the requirement has at least one closed (pass/fail)
46
+ * verification.
47
+ */
48
+ coverageGap?: 'REQ-VERIF-COVERAGE-001' | 'REQ-VERIF-COVERAGE-002';
49
+ }
50
+ export interface RequirementVerificationMatrixOptions {
51
+ /**
52
+ * Forwarded to `buildGapReport`. Neither coverage gap this matrix surfaces
53
+ * (`REQ-VERIF-COVERAGE-001`/`-002`) depends on `today` — accepted only for
54
+ * call-site symmetry with the other compliance-view builders.
55
+ */
56
+ today?: string;
57
+ }
58
+ export interface RequirementVerificationMatrix {
59
+ /**
60
+ * One row per (requirement, verification) pair, plus one gap row per
61
+ * requirement with zero verifications.
62
+ *
63
+ * Ordering (stable, independent of filesystem/Map enumeration order):
64
+ * requirements sorted by id; within a requirement, its verification rows
65
+ * sorted by verification id.
66
+ */
67
+ rows: RequirementVerificationRow[];
68
+ summary: {
69
+ requirements: number;
70
+ /** Rows carrying a real verification (excludes -001 gap-only rows). */
71
+ verifications: number;
72
+ /** Requirements carrying a REQ-VERIF-COVERAGE-001 or -002 gap. */
73
+ gaps: number;
74
+ };
75
+ }
@@ -0,0 +1,4 @@
1
+ // Requirement -> verification matrix — deterministic, repository-wide
2
+ // projection over the shared compliance reverse-index. Pure data; the
3
+ // consumer (a VS Code table, an export) renders it.
4
+ export {};
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Action → Change linkage on an element record.
3
+ *
4
+ * Canonical field is `delivers_changes` (Action element spec). `changes` is
5
+ * the pre-rename alias still present on older fixtures and inline docs.
6
+ */
7
+ export declare function actionChangeLinkField(el: Record<string, unknown>): 'delivers_changes' | 'changes';
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Action → Change linkage on an element record.
3
+ *
4
+ * Canonical field is `delivers_changes` (Action element spec). `changes` is
5
+ * the pre-rename alias still present on older fixtures and inline docs.
6
+ */
7
+ export function actionChangeLinkField(el) {
8
+ return Object.prototype.hasOwnProperty.call(el, 'delivers_changes')
9
+ ? 'delivers_changes'
10
+ : 'changes';
11
+ }
@@ -8,7 +8,7 @@
8
8
  * - Typed string IDs per `IDS_AND_REFERENCES.md` (`FACTOR-1`,
9
9
  * `GOAL-RET-1`, `CHANGE-1`, `ACTION-ONBOARD-1` (legacy `ACTIVITY-*` accepted).
10
10
  * - Plural canonical-direction cross-references: `goal.factors`,
11
- * `change.goals`, `activity.changes`, `activity.goals`.
11
+ * `change.goals`, `action.delivers_changes`, `action.goals`.
12
12
  * - Per-notation validation codes `FGCA-001 … FGCA-015`.
13
13
  *
14
14
  * Returns the validation result plus, on success, an internal `FGCADoc`
@@ -29,6 +29,8 @@ export interface CanonicalFGCAResult extends ValidationResult {
29
29
  /** On success, the internal-form FGCADoc derived from the canonical input. */
30
30
  parsed?: FGCADoc;
31
31
  }
32
+ /** DGA mode (`view_config.layers.changes: off`) omits the Changes column. */
33
+ export declare function isDgcaChangesLayerOff(input: unknown): boolean;
32
34
  /**
33
35
  * Validate canonical FGCA YAML and, on success, return an internal-form
34
36
  * `FGCADoc` ready for `buildFGCALayout`.