@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.
- package/dist/activities/resolver.d.ts +3 -0
- package/dist/activities/resolver.js +39 -18
- package/dist/activities/validate.js +59 -0
- package/dist/canon-resolver-utils.d.ts +3 -0
- package/dist/canon-resolver-utils.js +16 -0
- package/dist/codex/index.d.ts +1 -1
- package/dist/codex/index.js +1 -1
- package/dist/codex/types.d.ts +9 -2
- package/dist/codex/types.js +10 -3
- package/dist/codex/validate.js +29 -3
- package/dist/compliance/classify.d.ts +11 -1
- package/dist/compliance/classify.js +31 -17
- package/dist/compliance/impact.d.ts +3 -1
- package/dist/compliance/impact.js +41 -13
- package/dist/compliance/types.d.ts +6 -0
- package/dist/compliance-matrix/build.js +0 -0
- package/dist/compliance-verification-matrix/build.d.ts +17 -0
- package/dist/compliance-verification-matrix/build.js +65 -0
- package/dist/compliance-verification-matrix/csv.d.ts +6 -0
- package/dist/compliance-verification-matrix/csv.js +41 -0
- package/dist/compliance-verification-matrix/index.d.ts +3 -0
- package/dist/compliance-verification-matrix/index.js +2 -0
- package/dist/compliance-verification-matrix/types.d.ts +75 -0
- package/dist/compliance-verification-matrix/types.js +4 -0
- package/dist/fgca/action-change-ids.d.ts +7 -0
- package/dist/fgca/action-change-ids.js +11 -0
- package/dist/fgca/parse-canonical.d.ts +3 -1
- package/dist/fgca/parse-canonical.js +13 -8
- package/dist/fgca/resolver.d.ts +6 -2
- package/dist/fgca/resolver.js +12 -6
- package/dist/fgca/validate.d.ts +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/process-blueprint/index.d.ts +3 -1
- package/dist/process-blueprint/index.js +1 -0
- package/dist/process-blueprint/layout.js +33 -29
- package/dist/process-blueprint/resolve-columns.d.ts +52 -0
- package/dist/process-blueprint/resolve-columns.js +138 -0
- package/dist/process-blueprint/types.d.ts +23 -5
- package/dist/process-blueprint/validate.d.ts +19 -1
- package/dist/process-blueprint/validate.js +73 -21
- package/dist/repo-validate/check-candidate-fields.d.ts +6 -0
- package/dist/repo-validate/check-candidate-fields.js +87 -0
- package/dist/repo-validate/check-process-parent.d.ts +8 -0
- package/dist/repo-validate/check-process-parent.js +172 -0
- package/dist/repo-validate/index.d.ts +1 -0
- package/dist/repo-validate/index.js +1 -0
- package/dist/repo-validate/validate-repo.js +7 -0
- package/dist/requirement/types.d.ts +1 -1
- package/dist/requirement/types.js +1 -1
- package/dist/webview/entry.js +11 -4
- package/dist/webview/render-fgca.js +1 -1
- package/dist/webview/render-process.d.ts +19 -1
- package/dist/webview/render-process.js +75 -42
- package/package.json +1 -1
- package/src/activities/resolver.ts +40 -20
- package/src/activities/validate.ts +58 -0
- package/src/canon-resolver-utils.ts +19 -0
- package/src/codex/index.ts +1 -1
- package/src/codex/types.ts +11 -3
- package/src/codex/validate.ts +30 -2
- package/src/compliance/classify.ts +40 -17
- package/src/compliance/impact.ts +41 -12
- package/src/compliance/types.ts +6 -0
- package/src/compliance-matrix/build.ts +0 -0
- package/src/compliance-verification-matrix/build.ts +84 -0
- package/src/compliance-verification-matrix/csv.ts +47 -0
- package/src/compliance-verification-matrix/index.ts +7 -0
- package/src/compliance-verification-matrix/types.ts +85 -0
- package/src/fgca/action-change-ids.ts +13 -0
- package/src/fgca/parse-canonical.ts +12 -8
- package/src/fgca/resolver.ts +15 -6
- package/src/fgca/validate.ts +2 -0
- package/src/index.ts +1 -0
- package/src/process-blueprint/index.ts +11 -0
- package/src/process-blueprint/layout.ts +48 -29
- package/src/process-blueprint/resolve-columns.ts +147 -0
- package/src/process-blueprint/types.ts +19 -5
- package/src/process-blueprint/validate.ts +107 -21
- package/src/repo-validate/check-candidate-fields.ts +93 -0
- package/src/repo-validate/check-process-parent.ts +184 -0
- package/src/repo-validate/index.ts +5 -0
- package/src/repo-validate/validate-repo.ts +7 -0
- package/src/requirement/types.ts +1 -1
- package/src/webview/entry.ts +13 -6
- package/src/webview/render-fgca.ts +1 -1
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|
package/src/compliance/impact.ts
CHANGED
|
@@ -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
|
|
654
|
-
*
|
|
655
|
-
* `
|
|
656
|
-
*
|
|
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(
|
|
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
|
-
|
|
664
|
-
|
|
665
|
-
|
|
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
|
|
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);
|
package/src/compliance/types.ts
CHANGED
|
@@ -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`, `
|
|
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`)
|
|
68
|
-
function
|
|
69
|
-
|
|
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 &&
|
|
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
|
|
257
|
-
// canonical
|
|
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
|
|
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 };
|
package/src/fgca/resolver.ts
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
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(
|
|
152
|
+
if (str(actionsConf['surface']) === 'all') {
|
|
145
153
|
selectedActivities = [...activityElems.values()];
|
|
146
154
|
} else {
|
|
147
155
|
selectedActivities = [...activityElems.values()].filter((a) =>
|
|
148
|
-
strArray(a[
|
|
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
|
}
|
package/src/fgca/validate.ts
CHANGED
|
@@ -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';
|