@transitrix/diagrams 1.10.0 → 1.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/dist/activities/resolver.d.ts +3 -0
  2. package/dist/activities/resolver.js +39 -18
  3. package/dist/activities/validate.js +59 -0
  4. package/dist/canon-resolver-utils.d.ts +3 -0
  5. package/dist/canon-resolver-utils.js +16 -0
  6. package/dist/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 +3 -1
  21. package/dist/fgca/parse-canonical.js +13 -8
  22. package/dist/fgca/resolver.d.ts +6 -2
  23. package/dist/fgca/resolver.js +12 -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/render-fgca.js +1 -1
  41. package/dist/webview/render-process.d.ts +19 -1
  42. package/dist/webview/render-process.js +75 -42
  43. package/package.json +1 -1
  44. package/src/activities/resolver.ts +40 -20
  45. package/src/activities/validate.ts +58 -0
  46. package/src/canon-resolver-utils.ts +19 -0
  47. package/src/compliance/classify.ts +1 -0
  48. package/src/compliance/impact.ts +41 -12
  49. package/src/compliance/types.ts +6 -0
  50. package/src/compliance-verification-matrix/build.ts +84 -0
  51. package/src/compliance-verification-matrix/csv.ts +47 -0
  52. package/src/compliance-verification-matrix/index.ts +7 -0
  53. package/src/compliance-verification-matrix/types.ts +85 -0
  54. package/src/fgca/action-change-ids.ts +13 -0
  55. package/src/fgca/parse-canonical.ts +12 -8
  56. package/src/fgca/resolver.ts +15 -6
  57. package/src/fgca/validate.ts +2 -0
  58. package/src/index.ts +1 -0
  59. package/src/process-blueprint/index.ts +11 -0
  60. package/src/process-blueprint/layout.ts +48 -29
  61. package/src/process-blueprint/resolve-columns.ts +147 -0
  62. package/src/process-blueprint/types.ts +19 -5
  63. package/src/process-blueprint/validate.ts +107 -21
  64. package/src/repo-validate/check-process-parent.ts +184 -0
  65. package/src/repo-validate/index.ts +5 -0
  66. package/src/repo-validate/validate-repo.ts +3 -0
  67. package/src/webview/render-fgca.ts +1 -1
  68. package/src/webview/render-process.ts +94 -49
@@ -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`
@@ -23,6 +23,7 @@
23
23
  * unify the internal types with the canonical form across all sister
24
24
  * modules.
25
25
  */
26
+ import { actionChangeLinkField } from './action-change-ids.js';
26
27
  // Canonical ID grammars per IDS_AND_REFERENCES.md §1. The TYPE prefix
27
28
  // is fixed per layer; middle segments are optional; the terminal is a
28
29
  // positive integer with no leading zeros.
@@ -43,9 +44,11 @@ function asObjectArray(v) {
43
44
  return null;
44
45
  return v.map((el) => (el && typeof el === 'object' ? el : null));
45
46
  }
46
- /** DGA mode (`view_config.layers.changes: off`) may omit the changes layer. */
47
- function isChangesLayerOff(raw) {
48
- const vc = raw['view_config'];
47
+ /** DGA mode (`view_config.layers.changes: off`) omits the Changes column. */
48
+ export function isDgcaChangesLayerOff(input) {
49
+ if (!input || typeof input !== 'object')
50
+ return false;
51
+ const vc = input['view_config'];
49
52
  if (!vc || typeof vc !== 'object')
50
53
  return false;
51
54
  const layers = vc['layers'];
@@ -99,7 +102,7 @@ export function parseCanonicalFGCA(input) {
99
102
  const factorsRaw = asObjectArray(raw['factors']);
100
103
  const goalsRaw = asObjectArray(raw['goals']);
101
104
  let changesRaw = asObjectArray(raw['changes']);
102
- if (changesRaw === null && raw['changes'] === undefined && isChangesLayerOff(raw)) {
105
+ if (changesRaw === null && raw['changes'] === undefined && isDgcaChangesLayerOff(raw)) {
103
106
  changesRaw = [];
104
107
  }
105
108
  const activitiesRaw = asObjectArray(raw['actions']);
@@ -212,11 +215,12 @@ export function parseCanonicalFGCA(input) {
212
215
  factor: refIds.map((r) => ({ id: r })),
213
216
  };
214
217
  });
215
- // Build a reverse mapping: which changes does each activity reference (via
216
- // canonical activity.changes)? Used to populate the internal `change.activity_ids`.
218
+ // Build a reverse mapping: which changes does each action deliver (via
219
+ // canonical `delivers_changes`, or the pre-rename `changes` alias)?
217
220
  const activityChangesByCanonicalActivityId = new Map();
218
221
  activities.forEach((el, i) => {
219
- const refIds = checkRefArray(el, 'changes', changeIds, CHANGE_ID_RE, 'FGCA-010', `activities[${i}]`);
222
+ const field = actionChangeLinkField(el);
223
+ const refIds = checkRefArray(el, field, changeIds, CHANGE_ID_RE, 'FGCA-010', `activities[${i}]`);
220
224
  activityChangesByCanonicalActivityId.set(String(el['id']), refIds);
221
225
  });
222
226
  // For each change, find the activities that reference it.
@@ -286,6 +290,7 @@ export function parseCanonicalFGCA(input) {
286
290
  goals: internalGoals,
287
291
  changes: internalChanges,
288
292
  activities: internalActivities,
293
+ hideChanges: isDgcaChangesLayerOff(raw),
289
294
  };
290
295
  return { valid: true, errors, warnings, parsed };
291
296
  }
@@ -18,6 +18,10 @@ export interface FGCAViewConfig {
18
18
  changes?: {
19
19
  surface?: 'derived' | 'all';
20
20
  };
21
+ /** Canonical projection key. `activities` remains a deprecated alias. */
22
+ actions?: {
23
+ surface?: 'derived' | 'all';
24
+ };
21
25
  activities?: {
22
26
  surface?: 'derived' | 'all';
23
27
  };
@@ -43,7 +47,7 @@ export declare function isFGCAViewDoc(parsed: unknown): boolean;
43
47
  * to pass to `parseCanonicalFGCA`.
44
48
  *
45
49
  * Cross-reference fields (`goal.factors[]`, `change.goals[]`,
46
- * `action.changes[]`) are carried from the canon elements unchanged; the
47
- * resolver decides which elements to include, not how they are shaped.
50
+ * `action.delivers_changes[]`) are carried from the canon elements unchanged;
51
+ * the resolver decides which elements to include, not how they are shaped.
48
52
  */
49
53
  export declare function resolveFGCA(viewDoc: unknown, sources: FGCACanonSources): Record<string, unknown>;
@@ -6,6 +6,7 @@
6
6
  * top-level `factors[]`, `goals[]`, `changes[]`, `actions[]` with typed
7
7
  * string IDs. Filesystem-free — callable from unit tests without vscode.
8
8
  */
9
+ import { actionChangeLinkField } from './action-change-ids.js';
9
10
  function isObject(v) {
10
11
  return !!v && typeof v === 'object' && !Array.isArray(v);
11
12
  }
@@ -49,8 +50,8 @@ export function isFGCAViewDoc(parsed) {
49
50
  * to pass to `parseCanonicalFGCA`.
50
51
  *
51
52
  * Cross-reference fields (`goal.factors[]`, `change.goals[]`,
52
- * `action.changes[]`) are carried from the canon elements unchanged; the
53
- * resolver decides which elements to include, not how they are shaped.
53
+ * `action.delivers_changes[]`) are carried from the canon elements unchanged;
54
+ * the resolver decides which elements to include, not how they are shaped.
54
55
  */
55
56
  export function resolveFGCA(viewDoc, sources) {
56
57
  if (!isObject(viewDoc)) {
@@ -60,7 +61,11 @@ export function resolveFGCA(viewDoc, sources) {
60
61
  const goalsConf = isObject(vc['goals']) ? vc['goals'] : {};
61
62
  const factorsConf = isObject(vc['factors']) ? vc['factors'] : {};
62
63
  const changesConf = isObject(vc['changes']) ? vc['changes'] : {};
63
- const activitiesConf = isObject(vc['activities']) ? vc['activities'] : {};
64
+ const actionsConf = isObject(vc['actions'])
65
+ ? vc['actions']
66
+ : isObject(vc['activities'])
67
+ ? vc['activities']
68
+ : {};
64
69
  const factorElemsLegacy = collectByNotation(sources.elements, 'factor');
65
70
  const factorElemsNew = collectByNotation(sources.elements, 'driver');
66
71
  const factorElems = new Map([...factorElemsLegacy, ...factorElemsNew]);
@@ -108,13 +113,13 @@ export function resolveFGCA(viewDoc, sources) {
108
113
  selectedChanges = [...changeElems.values()].filter((c) => strArray(c['goals']).some((gid) => selectedGoalIds.has(gid)));
109
114
  }
110
115
  const selectedChangeIds = new Set(selectedChanges.map((c) => str(c['id'])).filter((x) => x !== undefined));
111
- // 4. Select activities: derived = reference ≥1 selected change or goal (degenerate FGA link)
116
+ // 4. Select actions: derived = delivers ≥1 selected change, or (DGA) a selected goal
112
117
  let selectedActivities;
113
- if (str(activitiesConf['surface']) === 'all') {
118
+ if (str(actionsConf['surface']) === 'all') {
114
119
  selectedActivities = [...activityElems.values()];
115
120
  }
116
121
  else {
117
- selectedActivities = [...activityElems.values()].filter((a) => strArray(a['changes']).some((cid) => selectedChangeIds.has(cid)) ||
122
+ selectedActivities = [...activityElems.values()].filter((a) => strArray(a[actionChangeLinkField(a)]).some((cid) => selectedChangeIds.has(cid)) ||
118
123
  strArray(a['goals']).some((gid) => selectedGoalIds.has(gid)));
119
124
  }
120
125
  return {
@@ -126,5 +131,6 @@ export function resolveFGCA(viewDoc, sources) {
126
131
  goals: selectedGoals,
127
132
  changes: selectedChanges,
128
133
  actions: selectedActivities,
134
+ view_config: viewDoc['view_config'],
129
135
  };
130
136
  }
@@ -10,5 +10,7 @@ export interface FGCADoc {
10
10
  goals: GoalItem[];
11
11
  changes: BdnChangeWithActivities[];
12
12
  activities: ActivityItem[];
13
+ /** DGA mode: omit the Changes column (`view_config.layers.changes: off`). */
14
+ hideChanges?: boolean;
13
15
  }
14
16
  export declare function validateFGCADoc(input: unknown): FGCAValidationResult;
package/dist/index.d.ts CHANGED
@@ -11,6 +11,7 @@ export { validateCapabilityMap, layoutCapabilityTree, TREE_NODE_WIDTH, TREE_NODE
11
11
  export * from "./change/index";
12
12
  export * from "./compliance/index";
13
13
  export * from "./compliance-matrix/index";
14
+ export * from "./compliance-verification-matrix/index";
14
15
  export * from "./confidence/index";
15
16
  export * from "./factor/index";
16
17
  export * from "./fgca/index";
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ export { validateCapabilityMap, layoutCapabilityTree, TREE_NODE_WIDTH, TREE_NODE
10
10
  export * from "./change/index.js";
11
11
  export * from "./compliance/index.js";
12
12
  export * from "./compliance-matrix/index.js";
13
+ export * from "./compliance-verification-matrix/index.js";
13
14
  export * from "./confidence/index.js";
14
15
  export * from "./factor/index.js";
15
16
  export * from "./fgca/index.js";
@@ -1,4 +1,6 @@
1
1
  export type { AspectCategory, RowId, Stage, AspectEntry, LaneConfig, ProcessBlueprintHeader, ProcessBlueprintFile, ProcessBlueprintLayoutOptions, LegendCell, StageHeaderCell, StageTextCell, AspectPill, AspectRow, ProcessBlueprintLayout, ComplianceDecoration, ComplianceChip, ComplianceRow, ComplianceLaneConfig, ComplianceLaneAssertion, ComplianceLaneRequirement, ComplianceLaneInput, } from './types.js';
2
2
  export { validateProcessBlueprint } from './validate.js';
3
- export type { ValidationError as ProcessBlueprintValidationError, ValidationWarning as ProcessBlueprintValidationWarning, ValidationResult as ProcessBlueprintValidationResult, } from './validate.js';
3
+ export type { ValidationError as ProcessBlueprintValidationError, ValidationWarning as ProcessBlueprintValidationWarning, ValidationResult as ProcessBlueprintValidationResult, ProcessBlueprintValidateOptions, ProcessParentEdge, } from './validate.js';
4
4
  export { layoutProcessBlueprint } from './layout.js';
5
+ export { collectProcessColumnRecords, collectStepHomeProcess, columnIndexesForRealisedVia, isProcessColumnId, isStageColumnId, resolveColumnDisplay, } from './resolve-columns.js';
6
+ export type { ProcessColumnRecord } from './resolve-columns.js';
@@ -1,2 +1,3 @@
1
1
  export { validateProcessBlueprint } from './validate.js';
2
2
  export { layoutProcessBlueprint } from './layout.js';
3
+ export { collectProcessColumnRecords, collectStepHomeProcess, columnIndexesForRealisedVia, isProcessColumnId, isStageColumnId, resolveColumnDisplay, } from './resolve-columns.js';
@@ -1,4 +1,5 @@
1
1
  import { wrapTextLines } from '../webview/entity-text-layout.js';
2
+ import { columnIndexesForRealisedVia, resolveColumnDisplay } from './resolve-columns.js';
2
3
  const ASPECT_CATEGORIES = ['systems', 'actors', 'equipment', 'information_entities'];
3
4
  const ASPECT_LABEL = {
4
5
  systems: 'Systems',
@@ -181,29 +182,31 @@ function deriveComplianceRow(stages, stageIndexById, input, opts, yStart) {
181
182
  for (const stageRef of realisedVia) {
182
183
  if (typeof stageRef !== 'string')
183
184
  continue;
184
- const idx = stageIndexById.get(stageRef.trim());
185
- if (idx === undefined)
185
+ const idxs = columnIndexesForRealisedVia(stageRef, stageIndexById, opts.stepHomeProcess);
186
+ if (idxs.length === 0)
186
187
  continue;
187
- for (const lawId of lawIds) {
188
- // Apply jurisdiction filter.
189
- if (filterJurisdictions) {
190
- const jur = input.codexJurisdictions?.[lawId];
191
- if (!jur || !filterJurisdictions.has(jur))
192
- continue;
193
- }
194
- if (!stageAccum.has(idx))
195
- stageAccum.set(idx, new Map());
196
- const lawMap = stageAccum.get(idx);
197
- const existing = lawMap.get(lawId);
198
- const isGap = a.status === 'non_compliant' || a.status === 'partial';
199
- if (!existing) {
200
- lawMap.set(lawId, { gap: isGap, deadline: req.deadline });
201
- }
202
- else {
203
- existing.gap = existing.gap || isGap;
204
- // Keep the earlier deadline (more urgent).
205
- if (req.deadline && (!existing.deadline || req.deadline < existing.deadline)) {
206
- existing.deadline = req.deadline;
188
+ for (const idx of idxs) {
189
+ for (const lawId of lawIds) {
190
+ // Apply jurisdiction filter.
191
+ if (filterJurisdictions) {
192
+ const jur = input.codexJurisdictions?.[lawId];
193
+ if (!jur || !filterJurisdictions.has(jur))
194
+ continue;
195
+ }
196
+ if (!stageAccum.has(idx))
197
+ stageAccum.set(idx, new Map());
198
+ const lawMap = stageAccum.get(idx);
199
+ const existing = lawMap.get(lawId);
200
+ const isGap = a.status === 'non_compliant' || a.status === 'partial';
201
+ if (!existing) {
202
+ lawMap.set(lawId, { gap: isGap, deadline: req.deadline });
203
+ }
204
+ else {
205
+ existing.gap = existing.gap || isGap;
206
+ // Keep the earlier deadline (more urgent).
207
+ if (req.deadline && (!existing.deadline || req.deadline < existing.deadline)) {
208
+ existing.deadline = req.deadline;
209
+ }
207
210
  }
208
211
  }
209
212
  }
@@ -253,6 +256,7 @@ export function layoutProcessBlueprint(file, options) {
253
256
  ? allStages.filter(s => s?.id != null && opts.visibleStages.includes(s.id))
254
257
  : allStages;
255
258
  const stageIndexById = buildStageIndex(stages);
259
+ const display = stages.map(s => resolveColumnDisplay(s, opts.processCatalog));
256
260
  // Row visibility from visibleRows
257
261
  const visRows = opts.visibleRows;
258
262
  const showGoal = !visRows || visRows.includes('goal');
@@ -266,7 +270,7 @@ export function layoutProcessBlueprint(file, options) {
266
270
  const stageHeaders = stages.map((s, i) => ({
267
271
  stageIndex: i,
268
272
  id: s?.id ?? '',
269
- name: s?.name ?? '',
273
+ name: display[i].name,
270
274
  x: opts.legendColumnWidth + i * opts.stageColumnWidth,
271
275
  y: 0,
272
276
  width: opts.stageColumnWidth,
@@ -280,8 +284,8 @@ export function layoutProcessBlueprint(file, options) {
280
284
  const maxLines = Math.max(1, ...lineCounts);
281
285
  return Math.max(minHeight, maxLines * opts.textLineHeight + 2 * opts.cellTextPadY);
282
286
  };
283
- const goalLines = stages.map(s => wrap(s?.goal ?? ''));
284
- const resultLines = stages.map(s => wrap(s?.result ?? ''));
287
+ const goalLines = display.map(d => wrap(d.goal));
288
+ const resultLines = display.map(d => wrap(d.result));
285
289
  let cursorY = opts.stageHeaderHeight;
286
290
  let goalRowY = 0, goalRowHeight = 0;
287
291
  let resultRowY = 0, resultRowHeight = 0;
@@ -290,8 +294,8 @@ export function layoutProcessBlueprint(file, options) {
290
294
  if (showGoal) {
291
295
  goalRowY = cursorY;
292
296
  goalRowHeight = rowHeightFor(goalLines.map(l => l.length), opts.goalRowHeight);
293
- goalCells.push(...stages.map((s, i) => ({
294
- stageIndex: i, text: s?.goal ?? '', lines: goalLines[i],
297
+ goalCells.push(...display.map((d, i) => ({
298
+ stageIndex: i, text: d.goal, lines: goalLines[i],
295
299
  x: opts.legendColumnWidth + i * opts.stageColumnWidth, y: goalRowY,
296
300
  width: opts.stageColumnWidth, height: goalRowHeight,
297
301
  })));
@@ -300,8 +304,8 @@ export function layoutProcessBlueprint(file, options) {
300
304
  if (showResult) {
301
305
  resultRowY = cursorY;
302
306
  resultRowHeight = rowHeightFor(resultLines.map(l => l.length), opts.resultRowHeight);
303
- resultCells.push(...stages.map((s, i) => ({
304
- stageIndex: i, text: s?.result ?? '', lines: resultLines[i],
307
+ resultCells.push(...display.map((d, i) => ({
308
+ stageIndex: i, text: d.result, lines: resultLines[i],
305
309
  x: opts.legendColumnWidth + i * opts.stageColumnWidth, y: resultRowY,
306
310
  width: opts.stageColumnWidth, height: resultRowHeight,
307
311
  })));
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Render-time resolution of Process Blueprint column headers and the
3
+ * compliance-lane join key.
4
+ *
5
+ * A `PROCESS-…` column is the process: `name` / `goal` / `result` come from
6
+ * the child PROCESS element (ELEMENT_PRIMITIVES §7.5), never from restated
7
+ * view fields. A `STAGE-…` column keeps the authored sketch copy.
8
+ *
9
+ * Pure: no I/O. The host (VS Code preview, JCEF, CLI) already owns loading
10
+ * canon/elements/** — this module only knows how to pick PROCESS / STEP
11
+ * records out of that pool.
12
+ *
13
+ * This is a read model, never storage. Callers render an ephemeral display
14
+ * copy and must never serialise it back onto the view.
15
+ */
16
+ import type { Stage } from './types.js';
17
+ export declare function isProcessColumnId(id: string): boolean;
18
+ export declare function isStageColumnId(id: string): boolean;
19
+ /** Display fields taken from a `notation: process` element. */
20
+ export interface ProcessColumnRecord {
21
+ name: string;
22
+ goal?: string;
23
+ result?: string;
24
+ }
25
+ /**
26
+ * Collect admitted PROCESS elements from a raw canon/elements/** doc pool,
27
+ * keyed by id. Malformed documents are skipped.
28
+ */
29
+ export declare function collectProcessColumnRecords(docs: readonly unknown[]): Map<string, ProcessColumnRecord>;
30
+ /**
31
+ * Map each STEP id onto the PROCESS that contains it — either inline in
32
+ * `PROCESS.flow.steps[]` or a promoted `{ notation: step, process }` element.
33
+ * First writer wins; a later duplicate is ignored.
34
+ */
35
+ export declare function collectStepHomeProcess(docs: readonly unknown[]): Map<string, string>;
36
+ /**
37
+ * Display copy for one column. PROCESS- columns ignore any restated view
38
+ * fields; missing catalogue falls back to the id (header) and empty
39
+ * goal/result. STAGE- columns keep authored name/goal/result.
40
+ */
41
+ export declare function resolveColumnDisplay(stage: Stage, catalog?: ReadonlyMap<string, ProcessColumnRecord>): {
42
+ name: string;
43
+ goal: string;
44
+ result: string;
45
+ };
46
+ /**
47
+ * Column indexes a `realised_via` token pins on this blueprint.
48
+ *
49
+ * `STAGE-…` is never a join key (ASSERT-004). A `PROCESS-…` token pins that
50
+ * column. A STEP whose home process is a column pins that column.
51
+ */
52
+ export declare function columnIndexesForRealisedVia(ref: string, stageIndexById: ReadonlyMap<string, number>, stepHomeProcess?: ReadonlyMap<string, string>): number[];
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Render-time resolution of Process Blueprint column headers and the
3
+ * compliance-lane join key.
4
+ *
5
+ * A `PROCESS-…` column is the process: `name` / `goal` / `result` come from
6
+ * the child PROCESS element (ELEMENT_PRIMITIVES §7.5), never from restated
7
+ * view fields. A `STAGE-…` column keeps the authored sketch copy.
8
+ *
9
+ * Pure: no I/O. The host (VS Code preview, JCEF, CLI) already owns loading
10
+ * canon/elements/** — this module only knows how to pick PROCESS / STEP
11
+ * records out of that pool.
12
+ *
13
+ * This is a read model, never storage. Callers render an ephemeral display
14
+ * copy and must never serialise it back onto the view.
15
+ */
16
+ const PROCESS_ID_RE = /^PROCESS(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
17
+ const STAGE_ID_RE = /^STAGE(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
18
+ export function isProcessColumnId(id) {
19
+ return PROCESS_ID_RE.test(id);
20
+ }
21
+ export function isStageColumnId(id) {
22
+ return STAGE_ID_RE.test(id);
23
+ }
24
+ function isRecord(v) {
25
+ return v !== null && typeof v === 'object' && !Array.isArray(v);
26
+ }
27
+ function optionalString(v) {
28
+ return typeof v === 'string' && v.trim().length > 0 ? v.trim() : undefined;
29
+ }
30
+ /**
31
+ * Collect admitted PROCESS elements from a raw canon/elements/** doc pool,
32
+ * keyed by id. Malformed documents are skipped.
33
+ */
34
+ export function collectProcessColumnRecords(docs) {
35
+ const out = new Map();
36
+ for (const doc of docs) {
37
+ if (!isRecord(doc))
38
+ continue;
39
+ if (doc['notation'] !== 'process')
40
+ continue;
41
+ const id = optionalString(doc['id']);
42
+ if (!id || !isProcessColumnId(id))
43
+ continue;
44
+ const name = optionalString(doc['name']);
45
+ if (!name)
46
+ continue;
47
+ const rec = { name };
48
+ const goal = optionalString(doc['goal']);
49
+ const result = optionalString(doc['result']);
50
+ if (goal)
51
+ rec.goal = goal;
52
+ if (result)
53
+ rec.result = result;
54
+ out.set(id, rec);
55
+ }
56
+ return out;
57
+ }
58
+ /**
59
+ * Map each STEP id onto the PROCESS that contains it — either inline in
60
+ * `PROCESS.flow.steps[]` or a promoted `{ notation: step, process }` element.
61
+ * First writer wins; a later duplicate is ignored.
62
+ */
63
+ export function collectStepHomeProcess(docs) {
64
+ const out = new Map();
65
+ for (const doc of docs) {
66
+ if (!isRecord(doc))
67
+ continue;
68
+ if (doc['notation'] === 'process') {
69
+ const processId = optionalString(doc['id']);
70
+ if (!processId || !isProcessColumnId(processId))
71
+ continue;
72
+ const flow = isRecord(doc['flow']) ? doc['flow'] : undefined;
73
+ const steps = flow && Array.isArray(flow['steps']) ? flow['steps'] : [];
74
+ for (const step of steps) {
75
+ if (!isRecord(step))
76
+ continue;
77
+ const stepId = optionalString(step['id']);
78
+ if (!stepId || out.has(stepId))
79
+ continue;
80
+ out.set(stepId, processId);
81
+ }
82
+ continue;
83
+ }
84
+ if (doc['notation'] === 'step') {
85
+ const stepId = optionalString(doc['id']);
86
+ const processId = optionalString(doc['process']);
87
+ if (!stepId || !processId || !isProcessColumnId(processId))
88
+ continue;
89
+ if (!out.has(stepId))
90
+ out.set(stepId, processId);
91
+ }
92
+ }
93
+ return out;
94
+ }
95
+ /**
96
+ * Display copy for one column. PROCESS- columns ignore any restated view
97
+ * fields; missing catalogue falls back to the id (header) and empty
98
+ * goal/result. STAGE- columns keep authored name/goal/result.
99
+ */
100
+ export function resolveColumnDisplay(stage, catalog) {
101
+ const id = typeof stage.id === 'string' ? stage.id.trim() : '';
102
+ if (isProcessColumnId(id)) {
103
+ const rec = catalog?.get(id);
104
+ return {
105
+ name: rec?.name ?? id,
106
+ goal: rec?.goal ?? '',
107
+ result: rec?.result ?? '',
108
+ };
109
+ }
110
+ return {
111
+ name: typeof stage.name === 'string' ? stage.name : '',
112
+ goal: typeof stage.goal === 'string' ? stage.goal : '',
113
+ result: typeof stage.result === 'string' ? stage.result : '',
114
+ };
115
+ }
116
+ /**
117
+ * Column indexes a `realised_via` token pins on this blueprint.
118
+ *
119
+ * `STAGE-…` is never a join key (ASSERT-004). A `PROCESS-…` token pins that
120
+ * column. A STEP whose home process is a column pins that column.
121
+ */
122
+ export function columnIndexesForRealisedVia(ref, stageIndexById, stepHomeProcess) {
123
+ const token = ref.trim();
124
+ if (token.length === 0)
125
+ return [];
126
+ if (isStageColumnId(token))
127
+ return [];
128
+ const direct = stageIndexById.get(token);
129
+ if (direct !== undefined && isProcessColumnId(token))
130
+ return [direct];
131
+ const home = stepHomeProcess?.get(token);
132
+ if (home && isProcessColumnId(home)) {
133
+ const idx = stageIndexById.get(home);
134
+ if (idx !== undefined)
135
+ return [idx];
136
+ }
137
+ return [];
138
+ }
@@ -27,8 +27,9 @@ export interface ComplianceLaneAssertion {
27
27
  /** Compliance realisation status. */
28
28
  status: 'compliant' | 'partial' | 'non_compliant' | 'under_review' | 'pending_owner' | 'n_a';
29
29
  /**
30
- * Typed IDs of stages / process steps where the requirement is realised.
31
- * When empty/absent, the assertion covers the entire subject, not a specific stage.
30
+ * Typed IDs of processes or steps where the requirement is realised.
31
+ * Sketch `STAGE-…` ids are not a join key. When empty/absent, the assertion
32
+ * covers the entire subject, not a specific column.
32
33
  */
33
34
  realised_via?: string[];
34
35
  }
@@ -89,9 +90,12 @@ export interface ComplianceRow {
89
90
  }
90
91
  export interface Stage {
91
92
  id: string;
92
- name: string;
93
- goal: string;
94
- result: string;
93
+ /** Required on `STAGE-…` sketch columns (`BP-005`); omitted on `PROCESS-…`. */
94
+ name?: string;
95
+ /** Required on `STAGE-…` sketch columns (`BP-005`); omitted on `PROCESS-…`. */
96
+ goal?: string;
97
+ /** Required on `STAGE-…` sketch columns (`BP-005`); omitted on `PROCESS-…`. */
98
+ result?: string;
95
99
  description?: string;
96
100
  }
97
101
  export interface AspectEntry {
@@ -188,6 +192,20 @@ export interface ProcessBlueprintLayoutOptions {
188
192
  * When absent, all stages are shown.
189
193
  */
190
194
  visibleStages?: string[];
195
+ /**
196
+ * Catalogue of `PROCESS-…` column records (id → name / optional goal / result).
197
+ * Required to header catalogued columns; ignored for `STAGE-…` sketches.
198
+ */
199
+ processCatalog?: ReadonlyMap<string, {
200
+ name: string;
201
+ goal?: string;
202
+ result?: string;
203
+ }>;
204
+ /**
205
+ * STEP id → home PROCESS id (contained in `PROCESS.flow` or promoted).
206
+ * Used by the compliance-lane join; omit to skip STEP-level pins.
207
+ */
208
+ stepHomeProcess?: ReadonlyMap<string, string>;
191
209
  }
192
210
  import type { LayoutBounds } from '../geometry.js';
193
211
  export type { LayoutBounds };
@@ -1,3 +1,21 @@
1
1
  import type { ValidationError, ValidationWarning, ValidationResult } from '../validation-types.js';
2
+ import type { CanonCatalog } from '../typed-id.js';
2
3
  export type { ValidationError, ValidationWarning, ValidationResult };
3
- export declare function validateProcessBlueprint(input: unknown): ValidationResult;
4
+ export interface ProcessParentEdge {
5
+ /** Child process id (`from`). */
6
+ from: string;
7
+ /** Parent process id (`to`). */
8
+ to: string;
9
+ }
10
+ export interface ProcessBlueprintValidateOptions {
11
+ /** When provided, BP-012 resolves `PROCESS-…` column ids against admitted canon. */
12
+ catalog?: CanonCatalog;
13
+ /**
14
+ * In-effect `process_parent` edges (child → parent). When provided, BP-014
15
+ * warns if `process_blueprint.process` is set and a `PROCESS-…` column has
16
+ * no such edge to that parent. Omit to skip BP-014 (file-scope without the
17
+ * relation catalogue).
18
+ */
19
+ processParentEdges?: readonly ProcessParentEdge[];
20
+ }
21
+ export declare function validateProcessBlueprint(input: unknown, options?: ProcessBlueprintValidateOptions): ValidationResult;