@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.
Files changed (71) hide show
  1. package/dist/activities/resolver.d.ts +3 -0
  2. package/dist/activities/resolver.js +39 -18
  3. package/dist/activities/validate.js +67 -4
  4. package/dist/canon-resolver-utils.d.ts +3 -0
  5. package/dist/canon-resolver-utils.js +16 -0
  6. package/dist/compliance/classify.js +1 -0
  7. package/dist/compliance/impact.d.ts +3 -1
  8. package/dist/compliance/impact.js +41 -13
  9. package/dist/compliance/types.d.ts +6 -0
  10. package/dist/compliance-verification-matrix/build.d.ts +17 -0
  11. package/dist/compliance-verification-matrix/build.js +65 -0
  12. package/dist/compliance-verification-matrix/csv.d.ts +6 -0
  13. package/dist/compliance-verification-matrix/csv.js +41 -0
  14. package/dist/compliance-verification-matrix/index.d.ts +3 -0
  15. package/dist/compliance-verification-matrix/index.js +2 -0
  16. package/dist/compliance-verification-matrix/types.d.ts +75 -0
  17. package/dist/compliance-verification-matrix/types.js +4 -0
  18. package/dist/fgca/action-change-ids.d.ts +7 -0
  19. package/dist/fgca/action-change-ids.js +11 -0
  20. package/dist/fgca/parse-canonical.d.ts +17 -3
  21. package/dist/fgca/parse-canonical.js +44 -16
  22. package/dist/fgca/resolver.d.ts +6 -2
  23. package/dist/fgca/resolver.js +28 -6
  24. package/dist/fgca/validate.d.ts +2 -0
  25. package/dist/index.d.ts +1 -0
  26. package/dist/index.js +1 -0
  27. package/dist/process-blueprint/index.d.ts +3 -1
  28. package/dist/process-blueprint/index.js +1 -0
  29. package/dist/process-blueprint/layout.js +33 -29
  30. package/dist/process-blueprint/resolve-columns.d.ts +52 -0
  31. package/dist/process-blueprint/resolve-columns.js +138 -0
  32. package/dist/process-blueprint/types.d.ts +23 -5
  33. package/dist/process-blueprint/validate.d.ts +19 -1
  34. package/dist/process-blueprint/validate.js +73 -21
  35. package/dist/repo-validate/check-process-parent.d.ts +8 -0
  36. package/dist/repo-validate/check-process-parent.js +172 -0
  37. package/dist/repo-validate/index.d.ts +1 -0
  38. package/dist/repo-validate/index.js +1 -0
  39. package/dist/repo-validate/validate-repo.js +3 -0
  40. package/dist/webview/entry.js +26 -4
  41. package/dist/webview/render-fgca.d.ts +2 -0
  42. package/dist/webview/render-fgca.js +9 -3
  43. package/dist/webview/render-process.d.ts +19 -1
  44. package/dist/webview/render-process.js +75 -42
  45. package/package.json +3 -3
  46. package/src/activities/resolver.ts +40 -20
  47. package/src/activities/validate.ts +66 -4
  48. package/src/canon-resolver-utils.ts +19 -0
  49. package/src/compliance/classify.ts +1 -0
  50. package/src/compliance/impact.ts +41 -12
  51. package/src/compliance/types.ts +6 -0
  52. package/src/compliance-verification-matrix/build.ts +84 -0
  53. package/src/compliance-verification-matrix/csv.ts +47 -0
  54. package/src/compliance-verification-matrix/index.ts +7 -0
  55. package/src/compliance-verification-matrix/types.ts +85 -0
  56. package/src/fgca/action-change-ids.ts +13 -0
  57. package/src/fgca/parse-canonical.ts +56 -15
  58. package/src/fgca/resolver.ts +39 -6
  59. package/src/fgca/validate.ts +2 -0
  60. package/src/index.ts +1 -0
  61. package/src/process-blueprint/index.ts +11 -0
  62. package/src/process-blueprint/layout.ts +48 -29
  63. package/src/process-blueprint/resolve-columns.ts +147 -0
  64. package/src/process-blueprint/types.ts +19 -5
  65. package/src/process-blueprint/validate.ts +107 -21
  66. package/src/repo-validate/check-process-parent.ts +184 -0
  67. package/src/repo-validate/index.ts +5 -0
  68. package/src/repo-validate/validate-repo.ts +3 -0
  69. package/src/webview/entry.ts +26 -4
  70. package/src/webview/render-fgca.ts +13 -2
  71. 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`, `activity.changes`, `activity.goals`.
11
+ * `change.goals`, `action.delivers_changes`, `action.goals`.
12
12
  * - Per-notation validation codes `FGCA-001 … FGCA-015`.
13
13
  *
14
14
  * Returns the validation result plus, on success, an internal `FGCADoc`
@@ -36,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`) may omit the changes layer. */
68
- function isChangesLayerOff(raw: Record<string, unknown>): boolean {
69
- const vc = raw['view_config'];
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(input: unknown): CanonicalFGCAResult {
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 && isChangesLayerOff(raw)) {
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 activity reference (via
257
- // canonical activity.changes)? Used to populate the internal `change.activity_ids`.
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 refIds = checkRefArray(el!, 'changes', changeIds, CHANGE_ID_RE, 'FGCA-010', `activities[${i}]`);
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 { valid: true, errors, warnings, parsed };
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(input: unknown): CanonicalFGAResult {
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
  }
@@ -7,6 +7,8 @@
7
7
  * string IDs. Filesystem-free — callable from unit tests without vscode.
8
8
  */
9
9
 
10
+ import { actionChangeLinkField } from './action-change-ids.js';
11
+
10
12
  export interface FGCAViewConfig {
11
13
  goals?: {
12
14
  filter?: 'all' | 'ids' | 'tags';
@@ -15,6 +17,8 @@ export interface FGCAViewConfig {
15
17
  };
16
18
  factors?: { surface?: 'derived' | 'all' };
17
19
  changes?: { surface?: 'derived' | 'all' };
20
+ /** Canonical projection key. `activities` remains a deprecated alias. */
21
+ actions?: { surface?: 'derived' | 'all' };
18
22
  activities?: { surface?: 'derived' | 'all' };
19
23
  display?: { depth?: number | null; collapsed?: string[] };
20
24
  }
@@ -69,8 +73,8 @@ export function isFGCAViewDoc(parsed: unknown): boolean {
69
73
  * to pass to `parseCanonicalFGCA`.
70
74
  *
71
75
  * Cross-reference fields (`goal.factors[]`, `change.goals[]`,
72
- * `action.changes[]`) are carried from the canon elements unchanged; the
73
- * resolver decides which elements to include, not how they are shaped.
76
+ * `action.delivers_changes[]`) are carried from the canon elements unchanged;
77
+ * the resolver decides which elements to include, not how they are shaped.
74
78
  */
75
79
  export function resolveFGCA(
76
80
  viewDoc: unknown,
@@ -84,7 +88,11 @@ export function resolveFGCA(
84
88
  const goalsConf = isObject(vc['goals']) ? vc['goals'] : {};
85
89
  const factorsConf = isObject(vc['factors']) ? vc['factors'] : {};
86
90
  const changesConf = isObject(vc['changes']) ? vc['changes'] : {};
87
- const activitiesConf = isObject(vc['activities']) ? vc['activities'] : {};
91
+ const actionsConf = isObject(vc['actions'])
92
+ ? vc['actions']
93
+ : isObject(vc['activities'])
94
+ ? vc['activities']
95
+ : {};
88
96
 
89
97
  const factorElemsLegacy = collectByNotation(sources.elements, 'factor');
90
98
  const factorElemsNew = collectByNotation(sources.elements, 'driver');
@@ -139,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 activities: derived = reference ≥1 selected change or goal (degenerate FGA link)
150
+ // 4. Select actions: derived = delivers ≥1 selected change, or (DGA) a selected goal
143
151
  let selectedActivities: Array<Record<string, unknown>>;
144
- if (str(activitiesConf['surface']) === 'all') {
152
+ if (str(actionsConf['surface']) === 'all') {
145
153
  selectedActivities = [...activityElems.values()];
146
154
  } else {
147
155
  selectedActivities = [...activityElems.values()].filter((a) =>
148
- strArray(a['changes']).some((cid) => selectedChangeIds.has(cid)) ||
156
+ strArray(a[actionChangeLinkField(a)]).some((cid) => selectedChangeIds.has(cid)) ||
149
157
  strArray(a['goals']).some((gid) => selectedGoalIds.has(gid)),
150
158
  );
151
159
  }
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
  }
@@ -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';