@transitrix/diagrams 1.10.0 → 1.12.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 +67 -4
- package/dist/canon-resolver-utils.d.ts +3 -0
- package/dist/canon-resolver-utils.js +16 -0
- package/dist/compliance/classify.js +1 -0
- 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-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 +17 -3
- package/dist/fgca/parse-canonical.js +44 -16
- package/dist/fgca/resolver.d.ts +6 -2
- package/dist/fgca/resolver.js +28 -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-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 +3 -0
- package/dist/webview/entry.js +26 -4
- package/dist/webview/render-fgca.d.ts +2 -0
- package/dist/webview/render-fgca.js +9 -3
- package/dist/webview/render-process.d.ts +19 -1
- package/dist/webview/render-process.js +75 -42
- package/package.json +3 -3
- package/src/activities/resolver.ts +40 -20
- package/src/activities/validate.ts +66 -4
- package/src/canon-resolver-utils.ts +19 -0
- package/src/compliance/classify.ts +1 -0
- package/src/compliance/impact.ts +41 -12
- package/src/compliance/types.ts +6 -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 +56 -15
- package/src/fgca/resolver.ts +39 -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-process-parent.ts +184 -0
- package/src/repo-validate/index.ts +5 -0
- package/src/repo-validate/validate-repo.ts +3 -0
- package/src/webview/entry.ts +26 -4
- package/src/webview/render-fgca.ts +13 -2
- 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,
|
|
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.
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
108
|
+
otherwiseIds = new Set([...otherwiseIds].filter((id) => effectiveGoals(id, allActions.get(id)).some((g) => goalsFilter.includes(g))));
|
|
113
109
|
}
|
|
114
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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') {
|
|
@@ -140,8 +199,9 @@ export function validateActivities(input) {
|
|
|
140
199
|
}
|
|
141
200
|
}
|
|
142
201
|
// ACT-011: warn if no duration (duration_days is accepted as an alias)
|
|
202
|
+
// Network view is unaffected; this is only about CPM/Gantt rendering.
|
|
143
203
|
if ((act.duration === undefined || act.duration === null) && (act.duration_days === undefined || act.duration_days === null)) {
|
|
144
|
-
warnings.push({ code: 'ACT-011', message: `Activity "${id}" has no duration —
|
|
204
|
+
warnings.push({ code: 'ACT-011', message: `Activity "${id}" has no duration — Gantt view and CPM analysis will exclude it. Network view is unaffected.`, path });
|
|
145
205
|
}
|
|
146
206
|
}
|
|
147
207
|
if (errors.length > 0)
|
|
@@ -210,7 +270,9 @@ export function validateActivities(input) {
|
|
|
210
270
|
}
|
|
211
271
|
}
|
|
212
272
|
}
|
|
213
|
-
// ACT-013: orphan activities (warning only)
|
|
273
|
+
// ACT-013: orphan activities (warning only). An activity is structurally orphan
|
|
274
|
+
// if it has no connections to the rest of the graph. Valid connections are:
|
|
275
|
+
// predecessors, successors, parent, or goals (direct or via delivers_changes).
|
|
214
276
|
const successorIds = new Set();
|
|
215
277
|
for (const a of doc.activities) {
|
|
216
278
|
for (const pred of (a.predecessors ?? []))
|
|
@@ -221,10 +283,11 @@ export function validateActivities(input) {
|
|
|
221
283
|
const hasSuccessors = successorIds.has(a.id);
|
|
222
284
|
const hasGoals = Array.isArray(a.goals) && a.goals.length > 0;
|
|
223
285
|
const hasPredecessors = Array.isArray(a.predecessors) && a.predecessors.length > 0;
|
|
224
|
-
|
|
286
|
+
const hasParent = typeof a.parent === 'string' && a.parent.length > 0;
|
|
287
|
+
if (doc.activities.length > 1 && !hasSuccessors && !hasGoals && !hasPredecessors && !hasParent) {
|
|
225
288
|
warnings.push({
|
|
226
289
|
code: 'ACT-013',
|
|
227
|
-
message: `Activity "${a.id}" appears to be structurally orphan (no predecessors, successors, or goals)`,
|
|
290
|
+
message: `Activity "${a.id}" appears to be structurally orphan (no predecessors, successors, parent, or goals)`,
|
|
228
291
|
path: `activities[${i}]`,
|
|
229
292
|
});
|
|
230
293
|
}
|
|
@@ -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[]
|
|
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
|
|
395
|
-
*
|
|
396
|
-
* `
|
|
397
|
-
*
|
|
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
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
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,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,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
|
+
}
|