@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
|
@@ -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,10 +36,13 @@ 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. */
|
|
42
43
|
parsed?: FGCADoc;
|
|
44
|
+
/** True when a goal-scoped projection should show the scope caption. */
|
|
45
|
+
scopeCaption?: boolean;
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
// Canonical ID grammars per IDS_AND_REFERENCES.md §1. The TYPE prefix
|
|
@@ -64,9 +67,10 @@ function asObjectArray(v: unknown): Array<Record<string, unknown>> | null {
|
|
|
64
67
|
return v.map((el) => (el && typeof el === 'object' ? (el as Record<string, unknown>) : null) as Record<string, unknown>);
|
|
65
68
|
}
|
|
66
69
|
|
|
67
|
-
/** DGA mode (`view_config.layers.changes: off`)
|
|
68
|
-
function
|
|
69
|
-
|
|
70
|
+
/** DGA mode (`view_config.layers.changes: off`) omits the Changes column. */
|
|
71
|
+
export function isDgcaChangesLayerOff(input: unknown): boolean {
|
|
72
|
+
if (!input || typeof input !== 'object') return false;
|
|
73
|
+
const vc = (input as Record<string, unknown>)['view_config'];
|
|
70
74
|
if (!vc || typeof vc !== 'object') return false;
|
|
71
75
|
const layers = (vc as Record<string, unknown>)['layers'];
|
|
72
76
|
if (!layers || typeof layers !== 'object') return false;
|
|
@@ -77,8 +81,20 @@ function isChangesLayerOff(raw: Record<string, unknown>): boolean {
|
|
|
77
81
|
/**
|
|
78
82
|
* Validate canonical FGCA YAML and, on success, return an internal-form
|
|
79
83
|
* `FGCADoc` ready for `buildFGCALayout`.
|
|
84
|
+
*
|
|
85
|
+
* @param input The FGCA document to parse
|
|
86
|
+
* @param outOfScopeGoalIds When a goal-scoped projection omits goals that exist
|
|
87
|
+
* in the catalogue, pass their IDs here to suppress FGCA-011/FGCA-009 errors
|
|
88
|
+
* for references to out-of-scope goals. When provided and non-empty, and at least
|
|
89
|
+
* one action or change references an out-of-scope goal, `scopeCaption` is set true.
|
|
90
|
+
* @param outOfScopeFactorIds When a goal-scoped projection omits factors that exist
|
|
91
|
+
* in the catalogue, pass their IDs here to suppress FGCA-008 errors.
|
|
80
92
|
*/
|
|
81
|
-
export function parseCanonicalFGCA(
|
|
93
|
+
export function parseCanonicalFGCA(
|
|
94
|
+
input: unknown,
|
|
95
|
+
outOfScopeGoalIds?: Set<string> | null,
|
|
96
|
+
outOfScopeFactorIds?: Set<string> | null,
|
|
97
|
+
): CanonicalFGCAResult {
|
|
82
98
|
const errors: ValidationError[] = [];
|
|
83
99
|
const warnings: ValidationWarning[] = [];
|
|
84
100
|
|
|
@@ -125,7 +141,7 @@ export function parseCanonicalFGCA(input: unknown): CanonicalFGCAResult {
|
|
|
125
141
|
const factorsRaw = asObjectArray(raw['factors']);
|
|
126
142
|
const goalsRaw = asObjectArray(raw['goals']);
|
|
127
143
|
let changesRaw = asObjectArray(raw['changes']);
|
|
128
|
-
if (changesRaw === null && raw['changes'] === undefined &&
|
|
144
|
+
if (changesRaw === null && raw['changes'] === undefined && isDgcaChangesLayerOff(raw)) {
|
|
129
145
|
changesRaw = [];
|
|
130
146
|
}
|
|
131
147
|
const activitiesRaw = asObjectArray(raw['actions']);
|
|
@@ -202,6 +218,9 @@ export function parseCanonicalFGCA(input: unknown): CanonicalFGCAResult {
|
|
|
202
218
|
|
|
203
219
|
if (errors.length > 0) return { valid: false, errors, warnings };
|
|
204
220
|
|
|
221
|
+
// Track if any out-of-scope goals/factors are referenced (for scope caption).
|
|
222
|
+
let hasOutOfScopeRef = false;
|
|
223
|
+
|
|
205
224
|
// Cross-refs.
|
|
206
225
|
function checkRefArray(
|
|
207
226
|
el: Record<string, unknown>,
|
|
@@ -210,6 +229,7 @@ export function parseCanonicalFGCA(input: unknown): CanonicalFGCAResult {
|
|
|
210
229
|
refRe: RegExp,
|
|
211
230
|
code: string,
|
|
212
231
|
path: string,
|
|
232
|
+
outOfScope?: Set<string> | null,
|
|
213
233
|
): string[] {
|
|
214
234
|
const ref = el[field];
|
|
215
235
|
if (ref === undefined || ref === null) return [];
|
|
@@ -227,6 +247,12 @@ export function parseCanonicalFGCA(input: unknown): CanonicalFGCAResult {
|
|
|
227
247
|
errors.push({ code: 'FGCA-007', message: `${path}.${field}[] entry "${r}" does not match the expected grammar`, path });
|
|
228
248
|
continue;
|
|
229
249
|
}
|
|
250
|
+
if (outOfScope?.has(r)) {
|
|
251
|
+
// Reference is out-of-scope: suppress error and mark that we found an out-of-scope reference (for caption).
|
|
252
|
+
hasOutOfScopeRef = true;
|
|
253
|
+
out.push(r);
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
230
256
|
if (!targets.has(r)) {
|
|
231
257
|
errors.push({ code, message: `${path}.${field}[] references undeclared element "${r}"`, path });
|
|
232
258
|
continue;
|
|
@@ -245,7 +271,7 @@ export function parseCanonicalFGCA(input: unknown): CanonicalFGCAResult {
|
|
|
245
271
|
}));
|
|
246
272
|
|
|
247
273
|
const internalGoals: GoalItem[] = goals.map((el, i) => {
|
|
248
|
-
const refIds = checkRefArray(el!, 'factors', factorIds, FACTOR_OR_DRIVER_ID_RE, 'FGCA-008', `goals[${i}]
|
|
274
|
+
const refIds = checkRefArray(el!, 'factors', factorIds, FACTOR_OR_DRIVER_ID_RE, 'FGCA-008', `goals[${i}]`, outOfScopeFactorIds);
|
|
249
275
|
return {
|
|
250
276
|
id: String(el!['id']),
|
|
251
277
|
name: String(el!['name'] ?? ''),
|
|
@@ -253,11 +279,12 @@ export function parseCanonicalFGCA(input: unknown): CanonicalFGCAResult {
|
|
|
253
279
|
};
|
|
254
280
|
});
|
|
255
281
|
|
|
256
|
-
// Build a reverse mapping: which changes does each
|
|
257
|
-
// canonical
|
|
282
|
+
// Build a reverse mapping: which changes does each action deliver (via
|
|
283
|
+
// canonical `delivers_changes`, or the pre-rename `changes` alias)?
|
|
258
284
|
const activityChangesByCanonicalActivityId = new Map<string, string[]>();
|
|
259
285
|
activities.forEach((el, i) => {
|
|
260
|
-
const
|
|
286
|
+
const field = actionChangeLinkField(el!);
|
|
287
|
+
const refIds = checkRefArray(el!, field, changeIds, CHANGE_ID_RE, 'FGCA-010', `activities[${i}]`);
|
|
261
288
|
activityChangesByCanonicalActivityId.set(String(el!['id']), refIds);
|
|
262
289
|
});
|
|
263
290
|
|
|
@@ -272,7 +299,7 @@ export function parseCanonicalFGCA(input: unknown): CanonicalFGCAResult {
|
|
|
272
299
|
}
|
|
273
300
|
|
|
274
301
|
const internalChanges: BdnChangeWithActivities[] = changes.map((el, i) => {
|
|
275
|
-
const goalRefs = checkRefArray(el!, 'goals', goalIds, GOAL_ID_RE, 'FGCA-009', `changes[${i}]
|
|
302
|
+
const goalRefs = checkRefArray(el!, 'goals', goalIds, GOAL_ID_RE, 'FGCA-009', `changes[${i}]`, outOfScopeGoalIds);
|
|
276
303
|
// Internal `change.goal_id` is singular; canonical `change.goals` is plural.
|
|
277
304
|
// First goal wins for the singular field; warn (not error) if multiple.
|
|
278
305
|
if (goalRefs.length > 1) {
|
|
@@ -293,7 +320,7 @@ export function parseCanonicalFGCA(input: unknown): CanonicalFGCAResult {
|
|
|
293
320
|
});
|
|
294
321
|
|
|
295
322
|
const internalActivities: ActivityItem[] = activities.map((el, i) => {
|
|
296
|
-
const goalRefs = checkRefArray(el!, 'goals', goalIds, GOAL_ID_RE, 'FGCA-011', `actions[${i}]
|
|
323
|
+
const goalRefs = checkRefArray(el!, 'goals', goalIds, GOAL_ID_RE, 'FGCA-011', `actions[${i}]`, outOfScopeGoalIds);
|
|
297
324
|
const typeVal = typeof el!['type'] === 'string' ? el!['type'] : undefined;
|
|
298
325
|
return {
|
|
299
326
|
id: String(el!['id']),
|
|
@@ -331,9 +358,16 @@ export function parseCanonicalFGCA(input: unknown): CanonicalFGCAResult {
|
|
|
331
358
|
goals: internalGoals,
|
|
332
359
|
changes: internalChanges,
|
|
333
360
|
activities: internalActivities,
|
|
361
|
+
hideChanges: isDgcaChangesLayerOff(raw),
|
|
334
362
|
};
|
|
335
363
|
|
|
336
|
-
return {
|
|
364
|
+
return {
|
|
365
|
+
valid: true,
|
|
366
|
+
errors,
|
|
367
|
+
warnings,
|
|
368
|
+
parsed,
|
|
369
|
+
scopeCaption: (outOfScopeGoalIds?.size ?? 0) > 0 && hasOutOfScopeRef,
|
|
370
|
+
};
|
|
337
371
|
}
|
|
338
372
|
|
|
339
373
|
const FGA_DOC_ID_RE = /^(FGA|DGA)(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
|
|
@@ -341,6 +375,8 @@ const FGA_DOC_ID_RE = /^(FGA|DGA)(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
|
|
|
341
375
|
/** On success, the internal-form FGCADoc derived from canonical FGA input. */
|
|
342
376
|
export interface CanonicalFGAResult extends ValidationResult {
|
|
343
377
|
parsed?: FGCADoc;
|
|
378
|
+
/** True when a goal-scoped projection should show the scope caption. */
|
|
379
|
+
scopeCaption?: boolean;
|
|
344
380
|
}
|
|
345
381
|
|
|
346
382
|
/**
|
|
@@ -360,7 +396,11 @@ export interface CanonicalFGAResult extends ValidationResult {
|
|
|
360
396
|
* the canonical FGA path is unit-testable and the library owns the single
|
|
361
397
|
* canon shape — flat form only, no `fga:` wrapper.
|
|
362
398
|
*/
|
|
363
|
-
export function parseCanonicalFGA(
|
|
399
|
+
export function parseCanonicalFGA(
|
|
400
|
+
input: unknown,
|
|
401
|
+
outOfScopeGoalIds?: Set<string> | null,
|
|
402
|
+
outOfScopeFactorIds?: Set<string> | null,
|
|
403
|
+
): CanonicalFGAResult {
|
|
364
404
|
if (!input || typeof input !== 'object') {
|
|
365
405
|
return { valid: false, errors: [{ code: 'FGA-001', message: 'document root is not an object' }], warnings: [] };
|
|
366
406
|
}
|
|
@@ -388,7 +428,7 @@ export function parseCanonicalFGA(input: unknown): CanonicalFGAResult {
|
|
|
388
428
|
// codes are remapped on the way out. FGCA-009 / 010 / 014 (changes-related)
|
|
389
429
|
// are unreachable here because `changes` is empty.
|
|
390
430
|
const synth = { ...raw, notation: 'dgca', id: 'DGCA-FROM-DGA-1', changes: [] };
|
|
391
|
-
const r = parseCanonicalFGCA(synth);
|
|
431
|
+
const r = parseCanonicalFGCA(synth, outOfScopeGoalIds, outOfScopeFactorIds);
|
|
392
432
|
const remap: Record<string, string> = {
|
|
393
433
|
'FGCA-001': 'FGA-001',
|
|
394
434
|
'FGCA-002': 'FGA-002',
|
|
@@ -408,5 +448,6 @@ export function parseCanonicalFGA(input: unknown): CanonicalFGAResult {
|
|
|
408
448
|
errors: r.errors.map((e) => ({ ...e, code: remapCode(e.code) })),
|
|
409
449
|
warnings: r.warnings.map((w) => ({ ...w, code: remapCode(w.code) })),
|
|
410
450
|
parsed: r.parsed,
|
|
451
|
+
scopeCaption: r.scopeCaption,
|
|
411
452
|
};
|
|
412
453
|
}
|
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,17 +147,39 @@ 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
|
}
|
|
152
160
|
|
|
161
|
+
// Collect out-of-scope goals (exist in catalogue but not in selected set).
|
|
162
|
+
// Used by the validator to suppress errors for references to goals that exist
|
|
163
|
+
// but are outside the scope.
|
|
164
|
+
let outOfScopeGoalIds: Set<string> | undefined;
|
|
165
|
+
if (goalsFilter !== 'all') {
|
|
166
|
+
// Goal scope is restricted; collect goals that exist but aren't selected.
|
|
167
|
+
outOfScopeGoalIds = new Set(
|
|
168
|
+
[...goalElems.keys()].filter((id) => !selectedGoalIds.has(id)),
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Collect out-of-scope factors (exist in catalogue but not in selected set).
|
|
173
|
+
const selectedFactorIds = new Set(
|
|
174
|
+
selectedFactors.map((f) => str(f['id'])).filter((x): x is string => x !== undefined),
|
|
175
|
+
);
|
|
176
|
+
let outOfScopeFactorIds: Set<string> | undefined;
|
|
177
|
+
if (goalsFilter !== 'all') {
|
|
178
|
+
outOfScopeFactorIds = new Set(
|
|
179
|
+
[...factorElems.keys()].filter((id) => !selectedFactorIds.has(id)),
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
153
183
|
return {
|
|
154
184
|
notation: 'dgca',
|
|
155
185
|
id: viewDoc['id'],
|
|
@@ -159,5 +189,8 @@ export function resolveFGCA(
|
|
|
159
189
|
goals: selectedGoals,
|
|
160
190
|
changes: selectedChanges,
|
|
161
191
|
actions: selectedActivities,
|
|
192
|
+
view_config: viewDoc['view_config'],
|
|
193
|
+
__outOfScopeGoalIds: outOfScopeGoalIds,
|
|
194
|
+
__outOfScopeFactorIds: outOfScopeFactorIds,
|
|
162
195
|
};
|
|
163
196
|
}
|
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';
|