@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
@@ -40,6 +40,14 @@ export interface ComplianceCanon {
40
40
  /** VALIDATION artefacts (28-validation.md) — the validation-domain peer of
41
41
  * `verifications`, anchored on NEED instead of REQUIREMENT. */
42
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[];
43
51
  }
44
52
 
45
53
  export function emptyCanon(): ComplianceCanon {
@@ -52,6 +60,7 @@ export function emptyCanon(): ComplianceCanon {
52
60
  subjects: [],
53
61
  needs: [],
54
62
  validations: [],
63
+ duplicateIds: [],
55
64
  };
56
65
  }
57
66
 
@@ -59,12 +68,29 @@ const str = (v: unknown): string | undefined => (typeof v === 'string' ? v : und
59
68
  const strArray = (v: unknown): string[] | undefined =>
60
69
  Array.isArray(v) ? v.filter((x): x is string => typeof x === 'string') : undefined;
61
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
+
62
86
  /**
63
87
  * Classifies one parsed YAML document and, if it is a compliance artefact,
64
88
  * pushes its projection into `canon`. Products / requirements / assertions are
65
89
  * identified by their `notation` tag; codex source documents by `zone: codex`.
66
90
  * Returns the artefact id when ingested (so the caller can record its path), or
67
- * 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`).
68
94
  */
69
95
  export function ingestComplianceDoc(canon: ComplianceCanon, doc: unknown): string | null {
70
96
  if (doc === null || typeof doc !== 'object' || Array.isArray(doc)) return null;
@@ -73,8 +99,7 @@ export function ingestComplianceDoc(canon: ComplianceCanon, doc: unknown): strin
73
99
  if (!id) return null;
74
100
 
75
101
  if (d.notation === 'product') {
76
- canon.products.push({ id, name: str(d.name) ?? id });
77
- return id;
102
+ return pushUnique(canon, canon.products, { id, name: str(d.name) ?? id }) ? id : null;
78
103
  }
79
104
  if (
80
105
  d.notation === 'capability' ||
@@ -82,12 +107,11 @@ export function ingestComplianceDoc(canon: ComplianceCanon, doc: unknown): strin
82
107
  d.notation === 'application' ||
83
108
  d.notation === 'system'
84
109
  ) {
85
- canon.subjects.push({ id, name: str(d.name) ?? id });
86
- return id;
110
+ return pushUnique(canon, canon.subjects, { id, name: str(d.name) ?? id }) ? id : null;
87
111
  }
88
112
  if (d.notation === 'requirement' || d.notation === 'constraint') {
89
113
  const origin = str(d.origin);
90
- canon.requirements.push({
114
+ const ok = pushUnique(canon, canon.requirements, {
91
115
  id,
92
116
  name: str(d.name) ?? id,
93
117
  severity: str(d.severity),
@@ -101,31 +125,30 @@ export function ingestComplianceDoc(canon: ComplianceCanon, doc: unknown): strin
101
125
  next_review_at: str(d.next_review_at),
102
126
  serves: str(d.serves),
103
127
  });
104
- return id;
128
+ return ok ? id : null;
105
129
  }
106
130
  if (d.notation === 'need') {
107
- canon.needs.push({ id, name: str(d.name) ?? id });
108
- return id;
131
+ return pushUnique(canon, canon.needs, { id, name: str(d.name) ?? id }) ? id : null;
109
132
  }
110
133
  if (d.notation === 'validation') {
111
134
  const validates = str(d.validates);
112
135
  const method = str(d.method) as ValidationMethod | undefined;
113
136
  const outcome = str(d.outcome) as ValidationOutcome | undefined;
114
137
  if (!validates || !method || !outcome) return null;
115
- canon.validations.push({
138
+ const ok = pushUnique(canon, canon.validations, {
116
139
  id, validates, method, outcome,
117
140
  performed_at: str(d.performed_at),
118
141
  evidenceCount: Array.isArray(d.evidence) ? d.evidence.length : 0,
119
142
  admitted_at: str(d.admitted_at),
120
143
  });
121
- return id;
144
+ return ok ? id : null;
122
145
  }
123
146
  if (d.notation === 'assertion') {
124
147
  const about = str(d.about);
125
148
  const subject = str(d.subject);
126
149
  const status = str(d.status) as AssertionStatus | undefined;
127
150
  if (!about || !subject || !status) return null;
128
- canon.assertions.push({
151
+ const ok = pushUnique(canon, canon.assertions, {
129
152
  id, about, subject, status,
130
153
  assessed_at: str(d.assessed_at),
131
154
  next_review_at: str(d.next_review_at),
@@ -134,24 +157,24 @@ export function ingestComplianceDoc(canon: ComplianceCanon, doc: unknown): strin
134
157
  realised_via: strArray(d.realised_via),
135
158
  owner_to_confirm: str(d.owner_to_confirm),
136
159
  });
137
- return id;
160
+ return ok ? id : null;
138
161
  }
139
162
  if (d.notation === 'verification') {
140
163
  const verifies = str(d.verifies);
141
164
  const method = str(d.method) as VerificationMethod | undefined;
142
165
  const outcome = str(d.outcome) as VerificationOutcome | undefined;
143
166
  if (!verifies || !method || !outcome) return null;
144
- canon.verifications.push({
167
+ const ok = pushUnique(canon, canon.verifications, {
145
168
  id, verifies, method, outcome,
169
+ protocol: str(d.protocol),
146
170
  performed_at: str(d.performed_at),
147
171
  evidenceCount: Array.isArray(d.evidence) ? d.evidence.length : 0,
148
172
  admitted_at: str(d.admitted_at),
149
173
  });
150
- return id;
174
+ return ok ? id : null;
151
175
  }
152
176
  if (d.zone === 'codex') {
153
- canon.codex.push({ id, name: str(d.name) ?? id, type: str(d.type), jurisdiction: str(d.jurisdiction) });
154
- return id;
177
+ return pushUnique(canon, canon.codex, { id, name: str(d.name) ?? id, type: str(d.type), jurisdiction: str(d.jurisdiction) }) ? id : null;
155
178
  }
156
179
  return null;
157
180
  }
@@ -12,6 +12,7 @@ import type { ComplianceCanon } from './classify.js';
12
12
  import { buildComplianceIndex } from './reverse-index.js';
13
13
  import type { CanonCatalog } from '../typed-id.js';
14
14
  import type { ComplianceIndex, IndexAssertion, IndexRequirement, ObjectDetailInput, ObjectDetailDef, DeadlineStatus } from './types.js';
15
+ import { isStageColumnId } from '../process-blueprint/resolve-columns.js';
15
16
 
16
17
  /** Filter selecting REQUIREMENTs by codex source (jurisdiction / regime keys
17
18
  * are accepted for forward compatibility but not yet honoured — the canon
@@ -649,22 +650,48 @@ function buildTaskColumns(
649
650
  *
650
651
  * At `'product'` grain: `a.subject` must match `col.subjectId`.
651
652
  * At `'product-stage'` grain: `a.subject` must match AND either
652
- * `a.realised_via` is absent (claim covers the whole subject) OR
653
- * `col.stageId` ∈ `a.realised_via`.
654
- * At `'product-stage-task'` grain: as above for stage, plus `col.taskId` ∈
655
- * `a.realised_via` counts as a match (task-level claim covers this task column).
656
- * A stage-level claim (`col.stageId` ∈ `a.realised_via`) covers all task columns
657
- * in that stage.
653
+ * `a.realised_via` is absent (claim covers the whole subject) OR the token
654
+ * pins this column: a `PROCESS-…` id matching `col.stageId`, or a STEP whose
655
+ * home process is that column. Sketch `STAGE-…` ids are never a join key.
656
+ * At `'product-stage-task'` grain: a task-id hit covers that task column; a
657
+ * process-level claim covers every task column of that process.
658
658
  */
659
- function assertionMatchesColumn(a: IndexAssertion, col: ImpactColumn): boolean {
659
+ function assertionMatchesColumn(
660
+ a: IndexAssertion,
661
+ col: ImpactColumn,
662
+ stepHomeProcess?: ReadonlyMap<string, string>,
663
+ ): boolean {
660
664
  if (a.subject !== col.subjectId) return false;
661
665
  if (!col.stageId) return true;
662
666
  if (!a.realised_via || a.realised_via.length === 0) return true;
663
- if (col.taskId) {
664
- // Task grain: match on taskId OR on stageId (stage claim covers all tasks).
665
- return a.realised_via.includes(col.taskId) || a.realised_via.includes(col.stageId);
667
+
668
+ const stageId = col.stageId;
669
+ if (isStageColumnId(stageId)) {
670
+ // Sketch column: only a task-id hit counts — never the STAGE- key.
671
+ return col.taskId != null && a.realised_via.includes(col.taskId);
672
+ }
673
+
674
+ for (const ref of a.realised_via) {
675
+ if (typeof ref !== 'string') continue;
676
+ if (isStageColumnId(ref)) continue;
677
+ if (col.taskId && ref === col.taskId) return true;
678
+ if (ref === stageId) return true;
679
+ if (!col.taskId && stepHomeProcess?.get(ref) === stageId) return true;
666
680
  }
667
- return a.realised_via.includes(col.stageId);
681
+ return false;
682
+ }
683
+
684
+ function stepHomeFromObjectDetails(objectDetails?: ObjectDetailInput[]): Map<string, string> {
685
+ const out = new Map<string, string>();
686
+ if (!objectDetails) return out;
687
+ for (const od of objectDetails) {
688
+ for (const d of od.details ?? []) {
689
+ for (const t of d.tasks ?? []) {
690
+ if (typeof t?.id === 'string' && t.id.length > 0) out.set(t.id, d.id);
691
+ }
692
+ }
693
+ }
694
+ return out;
668
695
  }
669
696
 
670
697
  /**
@@ -691,6 +718,7 @@ export function buildImpactMatrix(
691
718
  canon: ComplianceCanon,
692
719
  config: ImpactViewConfig,
693
720
  objectDetails?: ObjectDetailInput[],
721
+ join?: { stepHomeProcess?: ReadonlyMap<string, string> },
694
722
  ): ImpactMatrix {
695
723
  const index = buildComplianceIndex({
696
724
  requirements: canon.requirements,
@@ -755,6 +783,7 @@ export function buildImpactMatrix(
755
783
 
756
784
  const rowIndex = new Set(obligations.map(r => r.id));
757
785
  const cells: ImpactCell[][] = obligations.map(() => columns.map(() => emptyCell()));
786
+ const stepHome = join?.stepHomeProcess ?? stepHomeFromObjectDetails(objectDetails);
758
787
 
759
788
  for (let c = 0; c < columns.length; c++) {
760
789
  const col = columns[c];
@@ -762,7 +791,7 @@ export function buildImpactMatrix(
762
791
  for (const a of subjectAssertions) {
763
792
  if (!rowIndex.has(a.about)) continue;
764
793
  if (!allowedStatuses.has(a.status)) continue;
765
- if (!assertionMatchesColumn(a, col)) continue;
794
+ if (!assertionMatchesColumn(a, col, stepHome)) continue;
766
795
  const rowIdx = obligations.findIndex(r => r.id === a.about);
767
796
  if (rowIdx < 0) continue;
768
797
  cells[rowIdx][c].assertions.push(a);
@@ -128,6 +128,12 @@ export interface IndexVerification {
128
128
  verifies: string;
129
129
  method: VerificationMethod;
130
130
  outcome: VerificationOutcome;
131
+ /**
132
+ * The verification procedure (27-verification.md §2). VERIFICATION carries
133
+ * no `name` field of its own; `protocol` is the closest human-readable
134
+ * description and is what a matrix/list view shows as the row's label.
135
+ */
136
+ protocol?: string;
131
137
  performed_at?: string;
132
138
  /** Number of evidence entries — feeds the VERIF-006 gap. */
133
139
  evidenceCount?: number;
Binary file
@@ -0,0 +1,84 @@
1
+ // Requirement -> verification matrix builder.
2
+
3
+ import type { ComplianceIndex } from '../compliance/types.js';
4
+ import { buildGapReport } from '../compliance/gap-report.js';
5
+ import type {
6
+ RequirementVerificationMatrix,
7
+ RequirementVerificationMatrixOptions,
8
+ RequirementVerificationRow,
9
+ } from './types.js';
10
+
11
+ function byId<T extends { id: string }>(a: T, b: T): number {
12
+ return a.id < b.id ? -1 : a.id > b.id ? 1 : 0;
13
+ }
14
+
15
+ /**
16
+ * Builds the repository-wide requirement -> verification matrix.
17
+ *
18
+ * Pure projection over an already-built `ComplianceIndex`
19
+ * (`compliance/reverse-index.ts`) — no I/O, no revalidation. Reuses
20
+ * `buildGapReport`'s `REQ-VERIF-COVERAGE-001`/`-002` verdicts verbatim rather
21
+ * than recomputing a competing coverage status.
22
+ *
23
+ * Ordering: requirements sorted by id; within a requirement, its verification
24
+ * rows sorted by verification id. Both are explicit sorts over the index's
25
+ * values, so the result is independent of the Map's insertion order (which —
26
+ * for a caller that built the index from a filesystem walk — would otherwise
27
+ * track directory enumeration order).
28
+ */
29
+ export function buildRequirementVerificationMatrix(
30
+ index: ComplianceIndex,
31
+ options: RequirementVerificationMatrixOptions = {},
32
+ ): RequirementVerificationMatrix {
33
+ const gapReport = buildGapReport(index, options);
34
+ const noVerification = new Set(gapReport.requirementsWithoutVerification.map(r => r.id));
35
+ const unresolvedVerification = new Set(gapReport.requirementsWithUnresolvedVerification.map(r => r.id));
36
+
37
+ const requirements = [...index.requirementById.values()].sort(byId);
38
+
39
+ const rows: RequirementVerificationRow[] = [];
40
+ let verificationRowCount = 0;
41
+ let gapRequirementCount = 0;
42
+
43
+ for (const requirement of requirements) {
44
+ const parentId = requirement.parent;
45
+ const parentLabel = parentId ? index.requirementById.get(parentId)?.name : undefined;
46
+
47
+ const base: Pick<RequirementVerificationRow, 'requirementId' | 'requirementLabel' | 'parentId' | 'parentLabel'> = {
48
+ requirementId: requirement.id,
49
+ requirementLabel: requirement.name,
50
+ ...(parentId !== undefined ? { parentId } : {}),
51
+ ...(parentLabel !== undefined ? { parentLabel } : {}),
52
+ };
53
+
54
+ if (noVerification.has(requirement.id)) {
55
+ gapRequirementCount++;
56
+ rows.push({ ...base, coverageGap: 'REQ-VERIF-COVERAGE-001' });
57
+ continue;
58
+ }
59
+
60
+ const gap = unresolvedVerification.has(requirement.id) ? ('REQ-VERIF-COVERAGE-002' as const) : undefined;
61
+ if (gap) gapRequirementCount++;
62
+
63
+ const verifications = [...(index.verificationsByRequirement.get(requirement.id) ?? [])].sort(byId);
64
+ for (const verification of verifications) {
65
+ verificationRowCount++;
66
+ rows.push({
67
+ ...base,
68
+ verificationId: verification.id,
69
+ verificationLabel: verification.protocol ?? verification.id,
70
+ verificationOutcome: verification.outcome,
71
+ ...(gap ? { coverageGap: gap } : {}),
72
+ });
73
+ }
74
+ }
75
+
76
+ return {
77
+ rows,
78
+ summary: {
79
+ requirements: requirements.length,
80
+ verifications: verificationRowCount,
81
+ gaps: gapRequirementCount,
82
+ },
83
+ };
84
+ }
@@ -0,0 +1,47 @@
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
+
6
+ import type { RequirementVerificationMatrix, RequirementVerificationRow } from './types.js';
7
+
8
+ const HEADER = [
9
+ 'requirement_id',
10
+ 'requirement_label',
11
+ 'parent_id',
12
+ 'parent_label',
13
+ 'related_test_result_id',
14
+ 'related_test_result_label',
15
+ 'outcome',
16
+ 'coverage_gap',
17
+ ] as const;
18
+
19
+ function csvCell(value: string | undefined): string {
20
+ const v = value ?? '';
21
+ return `"${v.replace(/"/g, '""')}"`;
22
+ }
23
+
24
+ function rowCells(row: RequirementVerificationRow): string[] {
25
+ return [
26
+ row.requirementId,
27
+ row.requirementLabel,
28
+ row.parentId ?? '',
29
+ row.parentLabel ?? '',
30
+ row.verificationId ?? '',
31
+ row.verificationLabel ?? '',
32
+ row.verificationOutcome ?? '',
33
+ row.coverageGap ?? '',
34
+ ];
35
+ }
36
+
37
+ /**
38
+ * Serialise the matrix to CSV. Identical input yields byte-identical output;
39
+ * the row order is the builder's stable order.
40
+ */
41
+ export function renderRequirementVerificationMatrixCsv(matrix: RequirementVerificationMatrix): string {
42
+ const lines = [HEADER.map(h => csvCell(h)).join(',')];
43
+ for (const row of matrix.rows) {
44
+ lines.push(rowCells(row).map(csvCell).join(','));
45
+ }
46
+ return lines.join('\r\n') + '\r\n';
47
+ }
@@ -0,0 +1,7 @@
1
+ export { buildRequirementVerificationMatrix } from './build.js';
2
+ export { renderRequirementVerificationMatrixCsv } from './csv.js';
3
+ export type {
4
+ RequirementVerificationMatrix,
5
+ RequirementVerificationMatrixOptions,
6
+ RequirementVerificationRow,
7
+ } from './types.js';
@@ -0,0 +1,85 @@
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
+
5
+ import type { VerificationOutcome } from '../verification/types.js';
6
+
7
+ /**
8
+ * One row of the requirement-verification matrix.
9
+ *
10
+ * A requirement with N admitted VERIFICATION records produces N rows, one per
11
+ * verification. A requirement with none still produces exactly one row,
12
+ * carrying the `REQ-VERIF-COVERAGE-001` gap. A requirement whose
13
+ * verifications exist but never closed (pass/fail) produces one row per
14
+ * verification — each carrying the `REQ-VERIF-COVERAGE-002` gap alongside its
15
+ * (still-open) outcome; the open verification is real data and is never
16
+ * replaced by a placeholder gap row.
17
+ *
18
+ * Coverage comes only from `VERIFICATION.verifies` (27-verification.md §2). A
19
+ * child requirement's verification never covers its `parent`
20
+ * (15-requirement.md §2.4 — `parent` records structure only, no roll-up).
21
+ */
22
+ export interface RequirementVerificationRow {
23
+ requirementId: string;
24
+ requirementLabel: string;
25
+
26
+ /**
27
+ * Direct `REQUIREMENT.parent` id, verbatim, present whenever the field is
28
+ * set — whether or not it resolves. `parent` resolution is not currently a
29
+ * validator concern (15-requirement.md §2.4); a dangling parent must still
30
+ * be visible on the row, not silently dropped.
31
+ */
32
+ parentId?: string;
33
+ /**
34
+ * The resolved parent's `name`, present only when `parentId` names an
35
+ * admitted requirement in the index. Absent when `parentId` is set but
36
+ * unresolved (dangling), or when the requirement has no parent.
37
+ */
38
+ parentLabel?: string;
39
+
40
+ /** The verification filling this row, when one exists. */
41
+ verificationId?: string;
42
+ /**
43
+ * VERIFICATION carries no `name`/label field of its own
44
+ * (27-verification.md §2); `protocol` — the procedure description — is the
45
+ * closest human-readable text and is what this row shows as its label.
46
+ */
47
+ verificationLabel?: string;
48
+ verificationOutcome?: VerificationOutcome;
49
+
50
+ /**
51
+ * `REQ-VERIF-COVERAGE-001` / `-002` (`compliance/gap-report.ts`), reused
52
+ * verbatim — this builder never recomputes a competing coverage verdict.
53
+ * Absent once the requirement has at least one closed (pass/fail)
54
+ * verification.
55
+ */
56
+ coverageGap?: 'REQ-VERIF-COVERAGE-001' | 'REQ-VERIF-COVERAGE-002';
57
+ }
58
+
59
+ export interface RequirementVerificationMatrixOptions {
60
+ /**
61
+ * Forwarded to `buildGapReport`. Neither coverage gap this matrix surfaces
62
+ * (`REQ-VERIF-COVERAGE-001`/`-002`) depends on `today` — accepted only for
63
+ * call-site symmetry with the other compliance-view builders.
64
+ */
65
+ today?: string;
66
+ }
67
+
68
+ export interface RequirementVerificationMatrix {
69
+ /**
70
+ * One row per (requirement, verification) pair, plus one gap row per
71
+ * requirement with zero verifications.
72
+ *
73
+ * Ordering (stable, independent of filesystem/Map enumeration order):
74
+ * requirements sorted by id; within a requirement, its verification rows
75
+ * sorted by verification id.
76
+ */
77
+ rows: RequirementVerificationRow[];
78
+ summary: {
79
+ requirements: number;
80
+ /** Rows carrying a real verification (excludes -001 gap-only rows). */
81
+ verifications: number;
82
+ /** Requirements carrying a REQ-VERIF-COVERAGE-001 or -002 gap. */
83
+ gaps: number;
84
+ };
85
+ }
@@ -0,0 +1,13 @@
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(
8
+ el: Record<string, unknown>,
9
+ ): 'delivers_changes' | 'changes' {
10
+ return Object.prototype.hasOwnProperty.call(el, 'delivers_changes')
11
+ ? 'delivers_changes'
12
+ : 'changes';
13
+ }
@@ -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`
@@ -36,6 +36,7 @@ import type {
36
36
  ValidationWarning,
37
37
  ValidationResult,
38
38
  } from '../validation-types.js';
39
+ import { actionChangeLinkField } from './action-change-ids.js';
39
40
 
40
41
  export interface CanonicalFGCAResult extends ValidationResult {
41
42
  /** On success, the internal-form FGCADoc derived from the canonical input. */
@@ -64,9 +65,10 @@ function asObjectArray(v: unknown): Array<Record<string, unknown>> | null {
64
65
  return v.map((el) => (el && typeof el === 'object' ? (el as Record<string, unknown>) : null) as Record<string, unknown>);
65
66
  }
66
67
 
67
- /** DGA mode (`view_config.layers.changes: off`) may omit the changes layer. */
68
- function isChangesLayerOff(raw: Record<string, unknown>): boolean {
69
- const vc = raw['view_config'];
68
+ /** DGA mode (`view_config.layers.changes: off`) omits the Changes column. */
69
+ export function isDgcaChangesLayerOff(input: unknown): boolean {
70
+ if (!input || typeof input !== 'object') return false;
71
+ const vc = (input as Record<string, unknown>)['view_config'];
70
72
  if (!vc || typeof vc !== 'object') return false;
71
73
  const layers = (vc as Record<string, unknown>)['layers'];
72
74
  if (!layers || typeof layers !== 'object') return false;
@@ -125,7 +127,7 @@ export function parseCanonicalFGCA(input: unknown): CanonicalFGCAResult {
125
127
  const factorsRaw = asObjectArray(raw['factors']);
126
128
  const goalsRaw = asObjectArray(raw['goals']);
127
129
  let changesRaw = asObjectArray(raw['changes']);
128
- if (changesRaw === null && raw['changes'] === undefined && isChangesLayerOff(raw)) {
130
+ if (changesRaw === null && raw['changes'] === undefined && isDgcaChangesLayerOff(raw)) {
129
131
  changesRaw = [];
130
132
  }
131
133
  const activitiesRaw = asObjectArray(raw['actions']);
@@ -253,11 +255,12 @@ export function parseCanonicalFGCA(input: unknown): CanonicalFGCAResult {
253
255
  };
254
256
  });
255
257
 
256
- // Build a reverse mapping: which changes does each activity reference (via
257
- // canonical activity.changes)? Used to populate the internal `change.activity_ids`.
258
+ // Build a reverse mapping: which changes does each action deliver (via
259
+ // canonical `delivers_changes`, or the pre-rename `changes` alias)?
258
260
  const activityChangesByCanonicalActivityId = new Map<string, string[]>();
259
261
  activities.forEach((el, i) => {
260
- const refIds = checkRefArray(el!, 'changes', changeIds, CHANGE_ID_RE, 'FGCA-010', `activities[${i}]`);
262
+ const field = actionChangeLinkField(el!);
263
+ const refIds = checkRefArray(el!, field, changeIds, CHANGE_ID_RE, 'FGCA-010', `activities[${i}]`);
261
264
  activityChangesByCanonicalActivityId.set(String(el!['id']), refIds);
262
265
  });
263
266
 
@@ -331,6 +334,7 @@ export function parseCanonicalFGCA(input: unknown): CanonicalFGCAResult {
331
334
  goals: internalGoals,
332
335
  changes: internalChanges,
333
336
  activities: internalActivities,
337
+ hideChanges: isDgcaChangesLayerOff(raw),
334
338
  };
335
339
 
336
340
  return { valid: true, errors, warnings, parsed };
@@ -7,6 +7,8 @@
7
7
  * string IDs. Filesystem-free — callable from unit tests without vscode.
8
8
  */
9
9
 
10
+ import { actionChangeLinkField } from './action-change-ids.js';
11
+
10
12
  export interface FGCAViewConfig {
11
13
  goals?: {
12
14
  filter?: 'all' | 'ids' | 'tags';
@@ -15,6 +17,8 @@ export interface FGCAViewConfig {
15
17
  };
16
18
  factors?: { surface?: 'derived' | 'all' };
17
19
  changes?: { surface?: 'derived' | 'all' };
20
+ /** Canonical projection key. `activities` remains a deprecated alias. */
21
+ actions?: { surface?: 'derived' | 'all' };
18
22
  activities?: { surface?: 'derived' | 'all' };
19
23
  display?: { depth?: number | null; collapsed?: string[] };
20
24
  }
@@ -69,8 +73,8 @@ export function isFGCAViewDoc(parsed: unknown): boolean {
69
73
  * to pass to `parseCanonicalFGCA`.
70
74
  *
71
75
  * Cross-reference fields (`goal.factors[]`, `change.goals[]`,
72
- * `action.changes[]`) are carried from the canon elements unchanged; the
73
- * resolver decides which elements to include, not how they are shaped.
76
+ * `action.delivers_changes[]`) are carried from the canon elements unchanged;
77
+ * the resolver decides which elements to include, not how they are shaped.
74
78
  */
75
79
  export function resolveFGCA(
76
80
  viewDoc: unknown,
@@ -84,7 +88,11 @@ export function resolveFGCA(
84
88
  const goalsConf = isObject(vc['goals']) ? vc['goals'] : {};
85
89
  const factorsConf = isObject(vc['factors']) ? vc['factors'] : {};
86
90
  const changesConf = isObject(vc['changes']) ? vc['changes'] : {};
87
- const activitiesConf = isObject(vc['activities']) ? vc['activities'] : {};
91
+ const actionsConf = isObject(vc['actions'])
92
+ ? vc['actions']
93
+ : isObject(vc['activities'])
94
+ ? vc['activities']
95
+ : {};
88
96
 
89
97
  const factorElemsLegacy = collectByNotation(sources.elements, 'factor');
90
98
  const factorElemsNew = collectByNotation(sources.elements, 'driver');
@@ -139,13 +147,13 @@ export function resolveFGCA(
139
147
  selectedChanges.map((c) => str(c['id'])).filter((x): x is string => x !== undefined),
140
148
  );
141
149
 
142
- // 4. Select activities: derived = reference ≥1 selected change or goal (degenerate FGA link)
150
+ // 4. Select actions: derived = delivers ≥1 selected change, or (DGA) a selected goal
143
151
  let selectedActivities: Array<Record<string, unknown>>;
144
- if (str(activitiesConf['surface']) === 'all') {
152
+ if (str(actionsConf['surface']) === 'all') {
145
153
  selectedActivities = [...activityElems.values()];
146
154
  } else {
147
155
  selectedActivities = [...activityElems.values()].filter((a) =>
148
- strArray(a['changes']).some((cid) => selectedChangeIds.has(cid)) ||
156
+ strArray(a[actionChangeLinkField(a)]).some((cid) => selectedChangeIds.has(cid)) ||
149
157
  strArray(a['goals']).some((gid) => selectedGoalIds.has(gid)),
150
158
  );
151
159
  }
@@ -159,5 +167,6 @@ export function resolveFGCA(
159
167
  goals: selectedGoals,
160
168
  changes: selectedChanges,
161
169
  actions: selectedActivities,
170
+ view_config: viewDoc['view_config'],
162
171
  };
163
172
  }
@@ -19,6 +19,8 @@ export interface FGCADoc {
19
19
  goals: GoalItem[];
20
20
  changes: BdnChangeWithActivities[];
21
21
  activities: ActivityItem[];
22
+ /** DGA mode: omit the Changes column (`view_config.layers.changes: off`). */
23
+ hideChanges?: boolean;
22
24
  }
23
25
 
24
26
  export function validateFGCADoc(input: unknown): FGCAValidationResult {
package/src/index.ts CHANGED
@@ -38,6 +38,7 @@ export {
38
38
  export * from "./change/index";
39
39
  export * from "./compliance/index";
40
40
  export * from "./compliance-matrix/index";
41
+ export * from "./compliance-verification-matrix/index";
41
42
  export * from "./confidence/index";
42
43
  export * from "./factor/index";
43
44
  export * from "./fgca/index";
@@ -27,6 +27,17 @@ export type {
27
27
  ValidationError as ProcessBlueprintValidationError,
28
28
  ValidationWarning as ProcessBlueprintValidationWarning,
29
29
  ValidationResult as ProcessBlueprintValidationResult,
30
+ ProcessBlueprintValidateOptions,
31
+ ProcessParentEdge,
30
32
  } from './validate.js';
31
33
 
32
34
  export { layoutProcessBlueprint } from './layout.js';
35
+ export {
36
+ collectProcessColumnRecords,
37
+ collectStepHomeProcess,
38
+ columnIndexesForRealisedVia,
39
+ isProcessColumnId,
40
+ isStageColumnId,
41
+ resolveColumnDisplay,
42
+ } from './resolve-columns.js';
43
+ export type { ProcessColumnRecord } from './resolve-columns.js';