@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.
- package/dist/activities/resolver.d.ts +3 -0
- package/dist/activities/resolver.js +39 -18
- package/dist/activities/validate.js +59 -0
- package/dist/canon-resolver-utils.d.ts +3 -0
- package/dist/canon-resolver-utils.js +16 -0
- package/dist/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 +3 -1
- package/dist/fgca/parse-canonical.js +13 -8
- package/dist/fgca/resolver.d.ts +6 -2
- package/dist/fgca/resolver.js +12 -6
- package/dist/fgca/validate.d.ts +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/process-blueprint/index.d.ts +3 -1
- package/dist/process-blueprint/index.js +1 -0
- package/dist/process-blueprint/layout.js +33 -29
- package/dist/process-blueprint/resolve-columns.d.ts +52 -0
- package/dist/process-blueprint/resolve-columns.js +138 -0
- package/dist/process-blueprint/types.d.ts +23 -5
- package/dist/process-blueprint/validate.d.ts +19 -1
- package/dist/process-blueprint/validate.js +73 -21
- package/dist/repo-validate/check-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/render-fgca.js +1 -1
- package/dist/webview/render-process.d.ts +19 -1
- package/dist/webview/render-process.js +75 -42
- package/package.json +1 -1
- package/src/activities/resolver.ts +40 -20
- package/src/activities/validate.ts +58 -0
- package/src/canon-resolver-utils.ts +19 -0
- package/src/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 +12 -8
- package/src/fgca/resolver.ts +15 -6
- package/src/fgca/validate.ts +2 -0
- package/src/index.ts +1 -0
- package/src/process-blueprint/index.ts +11 -0
- package/src/process-blueprint/layout.ts +48 -29
- package/src/process-blueprint/resolve-columns.ts +147 -0
- package/src/process-blueprint/types.ts +19 -5
- package/src/process-blueprint/validate.ts +107 -21
- package/src/repo-validate/check-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/render-fgca.ts +1 -1
- package/src/webview/render-process.ts +94 -49
package/src/fgca/resolver.ts
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* string IDs. Filesystem-free — callable from unit tests without vscode.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import { actionChangeLinkField } from './action-change-ids.js';
|
|
11
|
+
|
|
10
12
|
export interface FGCAViewConfig {
|
|
11
13
|
goals?: {
|
|
12
14
|
filter?: 'all' | 'ids' | 'tags';
|
|
@@ -15,6 +17,8 @@ export interface FGCAViewConfig {
|
|
|
15
17
|
};
|
|
16
18
|
factors?: { surface?: 'derived' | 'all' };
|
|
17
19
|
changes?: { surface?: 'derived' | 'all' };
|
|
20
|
+
/** Canonical projection key. `activities` remains a deprecated alias. */
|
|
21
|
+
actions?: { surface?: 'derived' | 'all' };
|
|
18
22
|
activities?: { surface?: 'derived' | 'all' };
|
|
19
23
|
display?: { depth?: number | null; collapsed?: string[] };
|
|
20
24
|
}
|
|
@@ -69,8 +73,8 @@ export function isFGCAViewDoc(parsed: unknown): boolean {
|
|
|
69
73
|
* to pass to `parseCanonicalFGCA`.
|
|
70
74
|
*
|
|
71
75
|
* Cross-reference fields (`goal.factors[]`, `change.goals[]`,
|
|
72
|
-
* `action.
|
|
73
|
-
* resolver decides which elements to include, not how they are shaped.
|
|
76
|
+
* `action.delivers_changes[]`) are carried from the canon elements unchanged;
|
|
77
|
+
* the resolver decides which elements to include, not how they are shaped.
|
|
74
78
|
*/
|
|
75
79
|
export function resolveFGCA(
|
|
76
80
|
viewDoc: unknown,
|
|
@@ -84,7 +88,11 @@ export function resolveFGCA(
|
|
|
84
88
|
const goalsConf = isObject(vc['goals']) ? vc['goals'] : {};
|
|
85
89
|
const factorsConf = isObject(vc['factors']) ? vc['factors'] : {};
|
|
86
90
|
const changesConf = isObject(vc['changes']) ? vc['changes'] : {};
|
|
87
|
-
const
|
|
91
|
+
const actionsConf = isObject(vc['actions'])
|
|
92
|
+
? vc['actions']
|
|
93
|
+
: isObject(vc['activities'])
|
|
94
|
+
? vc['activities']
|
|
95
|
+
: {};
|
|
88
96
|
|
|
89
97
|
const factorElemsLegacy = collectByNotation(sources.elements, 'factor');
|
|
90
98
|
const factorElemsNew = collectByNotation(sources.elements, 'driver');
|
|
@@ -139,13 +147,13 @@ export function resolveFGCA(
|
|
|
139
147
|
selectedChanges.map((c) => str(c['id'])).filter((x): x is string => x !== undefined),
|
|
140
148
|
);
|
|
141
149
|
|
|
142
|
-
// 4. Select
|
|
150
|
+
// 4. Select actions: derived = delivers ≥1 selected change, or (DGA) a selected goal
|
|
143
151
|
let selectedActivities: Array<Record<string, unknown>>;
|
|
144
|
-
if (str(
|
|
152
|
+
if (str(actionsConf['surface']) === 'all') {
|
|
145
153
|
selectedActivities = [...activityElems.values()];
|
|
146
154
|
} else {
|
|
147
155
|
selectedActivities = [...activityElems.values()].filter((a) =>
|
|
148
|
-
strArray(a[
|
|
156
|
+
strArray(a[actionChangeLinkField(a)]).some((cid) => selectedChangeIds.has(cid)) ||
|
|
149
157
|
strArray(a['goals']).some((gid) => selectedGoalIds.has(gid)),
|
|
150
158
|
);
|
|
151
159
|
}
|
|
@@ -159,5 +167,6 @@ export function resolveFGCA(
|
|
|
159
167
|
goals: selectedGoals,
|
|
160
168
|
changes: selectedChanges,
|
|
161
169
|
actions: selectedActivities,
|
|
170
|
+
view_config: viewDoc['view_config'],
|
|
162
171
|
};
|
|
163
172
|
}
|
package/src/fgca/validate.ts
CHANGED
|
@@ -19,6 +19,8 @@ export interface FGCADoc {
|
|
|
19
19
|
goals: GoalItem[];
|
|
20
20
|
changes: BdnChangeWithActivities[];
|
|
21
21
|
activities: ActivityItem[];
|
|
22
|
+
/** DGA mode: omit the Changes column (`view_config.layers.changes: off`). */
|
|
23
|
+
hideChanges?: boolean;
|
|
22
24
|
}
|
|
23
25
|
|
|
24
26
|
export function validateFGCADoc(input: unknown): FGCAValidationResult {
|
package/src/index.ts
CHANGED
|
@@ -38,6 +38,7 @@ export {
|
|
|
38
38
|
export * from "./change/index";
|
|
39
39
|
export * from "./compliance/index";
|
|
40
40
|
export * from "./compliance-matrix/index";
|
|
41
|
+
export * from "./compliance-verification-matrix/index";
|
|
41
42
|
export * from "./confidence/index";
|
|
42
43
|
export * from "./factor/index";
|
|
43
44
|
export * from "./fgca/index";
|
|
@@ -27,6 +27,17 @@ export type {
|
|
|
27
27
|
ValidationError as ProcessBlueprintValidationError,
|
|
28
28
|
ValidationWarning as ProcessBlueprintValidationWarning,
|
|
29
29
|
ValidationResult as ProcessBlueprintValidationResult,
|
|
30
|
+
ProcessBlueprintValidateOptions,
|
|
31
|
+
ProcessParentEdge,
|
|
30
32
|
} from './validate.js';
|
|
31
33
|
|
|
32
34
|
export { layoutProcessBlueprint } from './layout.js';
|
|
35
|
+
export {
|
|
36
|
+
collectProcessColumnRecords,
|
|
37
|
+
collectStepHomeProcess,
|
|
38
|
+
columnIndexesForRealisedVia,
|
|
39
|
+
isProcessColumnId,
|
|
40
|
+
isStageColumnId,
|
|
41
|
+
resolveColumnDisplay,
|
|
42
|
+
} from './resolve-columns.js';
|
|
43
|
+
export type { ProcessColumnRecord } from './resolve-columns.js';
|
|
@@ -19,6 +19,7 @@ import type {
|
|
|
19
19
|
StageTextCell,
|
|
20
20
|
} from './types.js';
|
|
21
21
|
import { wrapTextLines } from '../webview/entity-text-layout.js';
|
|
22
|
+
import { columnIndexesForRealisedVia, resolveColumnDisplay } from './resolve-columns.js';
|
|
22
23
|
|
|
23
24
|
const ASPECT_CATEGORIES: AspectCategory[] = ['systems', 'actors', 'equipment', 'information_entities'];
|
|
24
25
|
|
|
@@ -33,7 +34,13 @@ const ASPECT_LABEL: Record<AspectCategory, string> = {
|
|
|
33
34
|
// (absent = feature disabled) and must stay optional in the resolved type.
|
|
34
35
|
type SizingOptionKeys = Exclude<
|
|
35
36
|
keyof ProcessBlueprintLayoutOptions,
|
|
36
|
-
|
|
37
|
+
| 'complianceLane'
|
|
38
|
+
| 'complianceInput'
|
|
39
|
+
| 'visibleAspects'
|
|
40
|
+
| 'visibleRows'
|
|
41
|
+
| 'visibleStages'
|
|
42
|
+
| 'processCatalog'
|
|
43
|
+
| 'stepHomeProcess'
|
|
37
44
|
>;
|
|
38
45
|
|
|
39
46
|
/**
|
|
@@ -41,7 +48,16 @@ type SizingOptionKeys = Exclude<
|
|
|
41
48
|
* the opt-in feature fields stay optional (undefined = feature disabled).
|
|
42
49
|
*/
|
|
43
50
|
type ResolvedLayoutOptions = Required<Pick<ProcessBlueprintLayoutOptions, SizingOptionKeys>> &
|
|
44
|
-
Pick<
|
|
51
|
+
Pick<
|
|
52
|
+
ProcessBlueprintLayoutOptions,
|
|
53
|
+
| 'complianceLane'
|
|
54
|
+
| 'complianceInput'
|
|
55
|
+
| 'visibleAspects'
|
|
56
|
+
| 'visibleRows'
|
|
57
|
+
| 'visibleStages'
|
|
58
|
+
| 'processCatalog'
|
|
59
|
+
| 'stepHomeProcess'
|
|
60
|
+
>;
|
|
45
61
|
|
|
46
62
|
const DEFAULTS = {
|
|
47
63
|
legendColumnWidth: 140,
|
|
@@ -258,27 +274,29 @@ function deriveComplianceRow(
|
|
|
258
274
|
|
|
259
275
|
for (const stageRef of realisedVia) {
|
|
260
276
|
if (typeof stageRef !== 'string') continue;
|
|
261
|
-
const
|
|
262
|
-
if (
|
|
263
|
-
|
|
264
|
-
for (const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
277
|
+
const idxs = columnIndexesForRealisedVia(stageRef, stageIndexById, opts.stepHomeProcess);
|
|
278
|
+
if (idxs.length === 0) continue;
|
|
279
|
+
|
|
280
|
+
for (const idx of idxs) {
|
|
281
|
+
for (const lawId of lawIds) {
|
|
282
|
+
// Apply jurisdiction filter.
|
|
283
|
+
if (filterJurisdictions) {
|
|
284
|
+
const jur = input.codexJurisdictions?.[lawId];
|
|
285
|
+
if (!jur || !filterJurisdictions.has(jur)) continue;
|
|
286
|
+
}
|
|
270
287
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
288
|
+
if (!stageAccum.has(idx)) stageAccum.set(idx, new Map());
|
|
289
|
+
const lawMap = stageAccum.get(idx)!;
|
|
290
|
+
const existing = lawMap.get(lawId);
|
|
291
|
+
const isGap = a.status === 'non_compliant' || a.status === 'partial';
|
|
292
|
+
if (!existing) {
|
|
293
|
+
lawMap.set(lawId, { gap: isGap, deadline: req.deadline });
|
|
294
|
+
} else {
|
|
295
|
+
existing.gap = existing.gap || isGap;
|
|
296
|
+
// Keep the earlier deadline (more urgent).
|
|
297
|
+
if (req.deadline && (!existing.deadline || req.deadline < existing.deadline)) {
|
|
298
|
+
existing.deadline = req.deadline;
|
|
299
|
+
}
|
|
282
300
|
}
|
|
283
301
|
}
|
|
284
302
|
}
|
|
@@ -338,6 +356,7 @@ export function layoutProcessBlueprint(
|
|
|
338
356
|
? allStages.filter(s => s?.id != null && opts.visibleStages!.includes(s.id))
|
|
339
357
|
: allStages;
|
|
340
358
|
const stageIndexById = buildStageIndex(stages);
|
|
359
|
+
const display = stages.map(s => resolveColumnDisplay(s, opts.processCatalog));
|
|
341
360
|
|
|
342
361
|
// Row visibility from visibleRows
|
|
343
362
|
const visRows = opts.visibleRows;
|
|
@@ -354,7 +373,7 @@ export function layoutProcessBlueprint(
|
|
|
354
373
|
const stageHeaders: StageHeaderCell[] = stages.map((s, i) => ({
|
|
355
374
|
stageIndex: i,
|
|
356
375
|
id: s?.id ?? '',
|
|
357
|
-
name:
|
|
376
|
+
name: display[i].name,
|
|
358
377
|
x: opts.legendColumnWidth + i * opts.stageColumnWidth,
|
|
359
378
|
y: 0,
|
|
360
379
|
width: opts.stageColumnWidth,
|
|
@@ -373,8 +392,8 @@ export function layoutProcessBlueprint(
|
|
|
373
392
|
return Math.max(minHeight, maxLines * opts.textLineHeight + 2 * opts.cellTextPadY);
|
|
374
393
|
};
|
|
375
394
|
|
|
376
|
-
const goalLines =
|
|
377
|
-
const resultLines =
|
|
395
|
+
const goalLines = display.map(d => wrap(d.goal));
|
|
396
|
+
const resultLines = display.map(d => wrap(d.result));
|
|
378
397
|
|
|
379
398
|
let cursorY = opts.stageHeaderHeight;
|
|
380
399
|
let goalRowY = 0, goalRowHeight = 0;
|
|
@@ -385,8 +404,8 @@ export function layoutProcessBlueprint(
|
|
|
385
404
|
if (showGoal) {
|
|
386
405
|
goalRowY = cursorY;
|
|
387
406
|
goalRowHeight = rowHeightFor(goalLines.map(l => l.length), opts.goalRowHeight);
|
|
388
|
-
goalCells.push(...
|
|
389
|
-
stageIndex: i, text:
|
|
407
|
+
goalCells.push(...display.map((d, i) => ({
|
|
408
|
+
stageIndex: i, text: d.goal, lines: goalLines[i],
|
|
390
409
|
x: opts.legendColumnWidth + i * opts.stageColumnWidth, y: goalRowY,
|
|
391
410
|
width: opts.stageColumnWidth, height: goalRowHeight,
|
|
392
411
|
})));
|
|
@@ -395,8 +414,8 @@ export function layoutProcessBlueprint(
|
|
|
395
414
|
if (showResult) {
|
|
396
415
|
resultRowY = cursorY;
|
|
397
416
|
resultRowHeight = rowHeightFor(resultLines.map(l => l.length), opts.resultRowHeight);
|
|
398
|
-
resultCells.push(...
|
|
399
|
-
stageIndex: i, text:
|
|
417
|
+
resultCells.push(...display.map((d, i) => ({
|
|
418
|
+
stageIndex: i, text: d.result, lines: resultLines[i],
|
|
400
419
|
x: opts.legendColumnWidth + i * opts.stageColumnWidth, y: resultRowY,
|
|
401
420
|
width: opts.stageColumnWidth, height: resultRowHeight,
|
|
402
421
|
})));
|
|
@@ -0,0 +1,147 @@
|
|
|
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
|
+
|
|
17
|
+
import type { Stage } from './types.js';
|
|
18
|
+
|
|
19
|
+
const PROCESS_ID_RE = /^PROCESS(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
|
|
20
|
+
const STAGE_ID_RE = /^STAGE(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
|
|
21
|
+
|
|
22
|
+
export function isProcessColumnId(id: string): boolean {
|
|
23
|
+
return PROCESS_ID_RE.test(id);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function isStageColumnId(id: string): boolean {
|
|
27
|
+
return STAGE_ID_RE.test(id);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Display fields taken from a `notation: process` element. */
|
|
31
|
+
export interface ProcessColumnRecord {
|
|
32
|
+
name: string;
|
|
33
|
+
goal?: string;
|
|
34
|
+
result?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isRecord(v: unknown): v is Record<string, unknown> {
|
|
38
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function optionalString(v: unknown): string | undefined {
|
|
42
|
+
return typeof v === 'string' && v.trim().length > 0 ? v.trim() : undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Collect admitted PROCESS elements from a raw canon/elements/** doc pool,
|
|
47
|
+
* keyed by id. Malformed documents are skipped.
|
|
48
|
+
*/
|
|
49
|
+
export function collectProcessColumnRecords(docs: readonly unknown[]): Map<string, ProcessColumnRecord> {
|
|
50
|
+
const out = new Map<string, ProcessColumnRecord>();
|
|
51
|
+
for (const doc of docs) {
|
|
52
|
+
if (!isRecord(doc)) continue;
|
|
53
|
+
if (doc['notation'] !== 'process') continue;
|
|
54
|
+
const id = optionalString(doc['id']);
|
|
55
|
+
if (!id || !isProcessColumnId(id)) continue;
|
|
56
|
+
const name = optionalString(doc['name']);
|
|
57
|
+
if (!name) continue;
|
|
58
|
+
const rec: ProcessColumnRecord = { name };
|
|
59
|
+
const goal = optionalString(doc['goal']);
|
|
60
|
+
const result = optionalString(doc['result']);
|
|
61
|
+
if (goal) rec.goal = goal;
|
|
62
|
+
if (result) rec.result = result;
|
|
63
|
+
out.set(id, rec);
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Map each STEP id onto the PROCESS that contains it — either inline in
|
|
70
|
+
* `PROCESS.flow.steps[]` or a promoted `{ notation: step, process }` element.
|
|
71
|
+
* First writer wins; a later duplicate is ignored.
|
|
72
|
+
*/
|
|
73
|
+
export function collectStepHomeProcess(docs: readonly unknown[]): Map<string, string> {
|
|
74
|
+
const out = new Map<string, string>();
|
|
75
|
+
for (const doc of docs) {
|
|
76
|
+
if (!isRecord(doc)) continue;
|
|
77
|
+
if (doc['notation'] === 'process') {
|
|
78
|
+
const processId = optionalString(doc['id']);
|
|
79
|
+
if (!processId || !isProcessColumnId(processId)) continue;
|
|
80
|
+
const flow = isRecord(doc['flow']) ? doc['flow'] : undefined;
|
|
81
|
+
const steps = flow && Array.isArray(flow['steps']) ? flow['steps'] : [];
|
|
82
|
+
for (const step of steps) {
|
|
83
|
+
if (!isRecord(step)) continue;
|
|
84
|
+
const stepId = optionalString(step['id']);
|
|
85
|
+
if (!stepId || out.has(stepId)) continue;
|
|
86
|
+
out.set(stepId, processId);
|
|
87
|
+
}
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (doc['notation'] === 'step') {
|
|
91
|
+
const stepId = optionalString(doc['id']);
|
|
92
|
+
const processId = optionalString(doc['process']);
|
|
93
|
+
if (!stepId || !processId || !isProcessColumnId(processId)) continue;
|
|
94
|
+
if (!out.has(stepId)) out.set(stepId, processId);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Display copy for one column. PROCESS- columns ignore any restated view
|
|
102
|
+
* fields; missing catalogue falls back to the id (header) and empty
|
|
103
|
+
* goal/result. STAGE- columns keep authored name/goal/result.
|
|
104
|
+
*/
|
|
105
|
+
export function resolveColumnDisplay(
|
|
106
|
+
stage: Stage,
|
|
107
|
+
catalog?: ReadonlyMap<string, ProcessColumnRecord>,
|
|
108
|
+
): { name: string; goal: string; result: string } {
|
|
109
|
+
const id = typeof stage.id === 'string' ? stage.id.trim() : '';
|
|
110
|
+
if (isProcessColumnId(id)) {
|
|
111
|
+
const rec = catalog?.get(id);
|
|
112
|
+
return {
|
|
113
|
+
name: rec?.name ?? id,
|
|
114
|
+
goal: rec?.goal ?? '',
|
|
115
|
+
result: rec?.result ?? '',
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
name: typeof stage.name === 'string' ? stage.name : '',
|
|
120
|
+
goal: typeof stage.goal === 'string' ? stage.goal : '',
|
|
121
|
+
result: typeof stage.result === 'string' ? stage.result : '',
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Column indexes a `realised_via` token pins on this blueprint.
|
|
127
|
+
*
|
|
128
|
+
* `STAGE-…` is never a join key (ASSERT-004). A `PROCESS-…` token pins that
|
|
129
|
+
* column. A STEP whose home process is a column pins that column.
|
|
130
|
+
*/
|
|
131
|
+
export function columnIndexesForRealisedVia(
|
|
132
|
+
ref: string,
|
|
133
|
+
stageIndexById: ReadonlyMap<string, number>,
|
|
134
|
+
stepHomeProcess?: ReadonlyMap<string, string>,
|
|
135
|
+
): number[] {
|
|
136
|
+
const token = ref.trim();
|
|
137
|
+
if (token.length === 0) return [];
|
|
138
|
+
if (isStageColumnId(token)) return [];
|
|
139
|
+
const direct = stageIndexById.get(token);
|
|
140
|
+
if (direct !== undefined && isProcessColumnId(token)) return [direct];
|
|
141
|
+
const home = stepHomeProcess?.get(token);
|
|
142
|
+
if (home && isProcessColumnId(home)) {
|
|
143
|
+
const idx = stageIndexById.get(home);
|
|
144
|
+
if (idx !== undefined) return [idx];
|
|
145
|
+
}
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
@@ -33,8 +33,9 @@ export interface ComplianceLaneAssertion {
|
|
|
33
33
|
/** Compliance realisation status. */
|
|
34
34
|
status: 'compliant' | 'partial' | 'non_compliant' | 'under_review' | 'pending_owner' | 'n_a';
|
|
35
35
|
/**
|
|
36
|
-
* Typed IDs of
|
|
37
|
-
*
|
|
36
|
+
* Typed IDs of processes or steps where the requirement is realised.
|
|
37
|
+
* Sketch `STAGE-…` ids are not a join key. When empty/absent, the assertion
|
|
38
|
+
* covers the entire subject, not a specific column.
|
|
38
39
|
*/
|
|
39
40
|
realised_via?: string[];
|
|
40
41
|
}
|
|
@@ -100,9 +101,12 @@ export interface ComplianceRow {
|
|
|
100
101
|
|
|
101
102
|
export interface Stage {
|
|
102
103
|
id: string;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
/** Required on `STAGE-…` sketch columns (`BP-005`); omitted on `PROCESS-…`. */
|
|
105
|
+
name?: string;
|
|
106
|
+
/** Required on `STAGE-…` sketch columns (`BP-005`); omitted on `PROCESS-…`. */
|
|
107
|
+
goal?: string;
|
|
108
|
+
/** Required on `STAGE-…` sketch columns (`BP-005`); omitted on `PROCESS-…`. */
|
|
109
|
+
result?: string;
|
|
106
110
|
description?: string;
|
|
107
111
|
}
|
|
108
112
|
|
|
@@ -204,6 +208,16 @@ export interface ProcessBlueprintLayoutOptions {
|
|
|
204
208
|
* When absent, all stages are shown.
|
|
205
209
|
*/
|
|
206
210
|
visibleStages?: string[];
|
|
211
|
+
/**
|
|
212
|
+
* Catalogue of `PROCESS-…` column records (id → name / optional goal / result).
|
|
213
|
+
* Required to header catalogued columns; ignored for `STAGE-…` sketches.
|
|
214
|
+
*/
|
|
215
|
+
processCatalog?: ReadonlyMap<string, { name: string; goal?: string; result?: string }>;
|
|
216
|
+
/**
|
|
217
|
+
* STEP id → home PROCESS id (contained in `PROCESS.flow` or promoted).
|
|
218
|
+
* Used by the compliance-lane join; omit to skip STEP-level pins.
|
|
219
|
+
*/
|
|
220
|
+
stepHomeProcess?: ReadonlyMap<string, string>;
|
|
207
221
|
}
|
|
208
222
|
|
|
209
223
|
import type { LayoutBounds } from '../geometry.js';
|
|
@@ -1,21 +1,63 @@
|
|
|
1
1
|
import type { AspectCategory } from './types.js';
|
|
2
2
|
import type { ValidationError, ValidationWarning, ValidationResult } from '../validation-types.js';
|
|
3
|
+
import type { CanonCatalog } from '../typed-id.js';
|
|
3
4
|
|
|
4
5
|
export type { ValidationError, ValidationWarning, ValidationResult };
|
|
5
6
|
|
|
6
7
|
const ID_GRAMMAR_RE = /^[A-Z][A-Z_]*(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
|
|
7
8
|
const PROCESS_BLUEPRINT_ID_RE = /^PROCESS_BLUEPRINT(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
|
|
8
9
|
const STAGE_ID_RE = /^STAGE(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
|
|
10
|
+
const PROCESS_ID_RE = /^PROCESS(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
|
|
9
11
|
const APPLICATION_ID_RE = /^APPLICATION(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
|
|
10
12
|
const ROLE_ID_RE = /^ROLE(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
|
|
11
13
|
|
|
12
14
|
const ASPECT_CATEGORIES: AspectCategory[] = ['systems', 'actors', 'equipment', 'information_entities'];
|
|
13
15
|
|
|
16
|
+
const RESTATED_COLUMN_FIELDS = ['name', 'goal', 'result'] as const;
|
|
17
|
+
|
|
18
|
+
export interface ProcessParentEdge {
|
|
19
|
+
/** Child process id (`from`). */
|
|
20
|
+
from: string;
|
|
21
|
+
/** Parent process id (`to`). */
|
|
22
|
+
to: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ProcessBlueprintValidateOptions {
|
|
26
|
+
/** When provided, BP-012 resolves `PROCESS-…` column ids against admitted canon. */
|
|
27
|
+
catalog?: CanonCatalog;
|
|
28
|
+
/**
|
|
29
|
+
* In-effect `process_parent` edges (child → parent). When provided, BP-014
|
|
30
|
+
* warns if `process_blueprint.process` is set and a `PROCESS-…` column has
|
|
31
|
+
* no such edge to that parent. Omit to skip BP-014 (file-scope without the
|
|
32
|
+
* relation catalogue).
|
|
33
|
+
*/
|
|
34
|
+
processParentEdges?: readonly ProcessParentEdge[];
|
|
35
|
+
}
|
|
36
|
+
|
|
14
37
|
function isNonEmptyString(v: unknown): v is string {
|
|
15
38
|
return typeof v === 'string' && v.trim().length > 0;
|
|
16
39
|
}
|
|
17
40
|
|
|
18
|
-
|
|
41
|
+
function isProcessColumnId(id: string): boolean {
|
|
42
|
+
return PROCESS_ID_RE.test(id);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isStageColumnId(id: string): boolean {
|
|
46
|
+
return STAGE_ID_RE.test(id);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function isColumnId(id: string): boolean {
|
|
50
|
+
return isStageColumnId(id) || isProcessColumnId(id);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function fieldIsPresent(record: Record<string, unknown>, key: string): boolean {
|
|
54
|
+
return Object.prototype.hasOwnProperty.call(record, key);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function validateProcessBlueprint(
|
|
58
|
+
input: unknown,
|
|
59
|
+
options: ProcessBlueprintValidateOptions = {},
|
|
60
|
+
): ValidationResult {
|
|
19
61
|
const errors: ValidationError[] = [];
|
|
20
62
|
const warnings: ValidationWarning[] = [];
|
|
21
63
|
|
|
@@ -63,6 +105,7 @@ export function validateProcessBlueprint(input: unknown): ValidationResult {
|
|
|
63
105
|
}
|
|
64
106
|
|
|
65
107
|
const stageIds = new Set<string>();
|
|
108
|
+
const processColumnIds: string[] = [];
|
|
66
109
|
for (let i = 0; i < stagesRaw.length; i++) {
|
|
67
110
|
const s = stagesRaw[i] as Record<string, unknown> | undefined;
|
|
68
111
|
const path = `stages[${i}]`;
|
|
@@ -71,29 +114,72 @@ export function validateProcessBlueprint(input: unknown): ValidationResult {
|
|
|
71
114
|
continue;
|
|
72
115
|
}
|
|
73
116
|
if (!isNonEmptyString(s['id'])) {
|
|
74
|
-
errors.push({ code: 'BP-
|
|
117
|
+
errors.push({ code: 'BP-006', message: `${path}.id is required` });
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const sid = s['id'].trim();
|
|
122
|
+
if (stageIds.has(sid)) {
|
|
123
|
+
errors.push({ code: 'BP-006', message: `Duplicate stage id: "${sid}"` });
|
|
75
124
|
} else {
|
|
76
|
-
|
|
77
|
-
if (stageIds.has(sid)) {
|
|
78
|
-
errors.push({ code: 'BP-006', message: `Duplicate stage id: "${sid}"` });
|
|
79
|
-
} else {
|
|
80
|
-
stageIds.add(sid);
|
|
81
|
-
}
|
|
82
|
-
if (!STAGE_ID_RE.test(sid)) {
|
|
83
|
-
errors.push({
|
|
84
|
-
code: 'BP-006',
|
|
85
|
-
message: `${path}.id "${sid}" must match STAGE-[<middle>-]<INTEGER>`,
|
|
86
|
-
});
|
|
87
|
-
}
|
|
125
|
+
stageIds.add(sid);
|
|
88
126
|
}
|
|
89
|
-
if (!
|
|
90
|
-
errors.push({
|
|
127
|
+
if (!isColumnId(sid)) {
|
|
128
|
+
errors.push({
|
|
129
|
+
code: 'BP-006',
|
|
130
|
+
message: `${path}.id "${sid}" must match STAGE-[<middle>-]<INTEGER> or PROCESS-[<middle>-]<INTEGER>`,
|
|
131
|
+
});
|
|
132
|
+
continue;
|
|
91
133
|
}
|
|
92
|
-
|
|
93
|
-
|
|
134
|
+
|
|
135
|
+
if (isProcessColumnId(sid)) {
|
|
136
|
+
processColumnIds.push(sid);
|
|
137
|
+
for (const field of RESTATED_COLUMN_FIELDS) {
|
|
138
|
+
if (fieldIsPresent(s, field)) {
|
|
139
|
+
errors.push({
|
|
140
|
+
code: 'BP-013',
|
|
141
|
+
message: `${path}.${field} must not be restated on a PROCESS-… column — derive it from the process element`,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (options.catalog) {
|
|
146
|
+
const resolved = options.catalog.typeOf(sid);
|
|
147
|
+
if (resolved !== 'PROCESS') {
|
|
148
|
+
errors.push({
|
|
149
|
+
code: 'BP-012',
|
|
150
|
+
message: `${path}.id "${sid}" must resolve to an admitted PROCESS element`,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
if (!isNonEmptyString(s['name'])) {
|
|
156
|
+
errors.push({ code: 'BP-005', message: `${path}.name is required` });
|
|
157
|
+
}
|
|
158
|
+
if (!isNonEmptyString(s['goal'])) {
|
|
159
|
+
errors.push({ code: 'BP-005', message: `${path}.goal is required` });
|
|
160
|
+
}
|
|
161
|
+
if (!isNonEmptyString(s['result'])) {
|
|
162
|
+
errors.push({ code: 'BP-005', message: `${path}.result is required` });
|
|
163
|
+
}
|
|
94
164
|
}
|
|
95
|
-
|
|
96
|
-
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const parentProcess = isNonEmptyString(pb['process']) ? pb['process'].trim() : undefined;
|
|
168
|
+
if (parentProcess && options.processParentEdges && processColumnIds.length > 0) {
|
|
169
|
+
const linked = new Set(
|
|
170
|
+
options.processParentEdges
|
|
171
|
+
.filter((e) => e.to === parentProcess)
|
|
172
|
+
.map((e) => e.from),
|
|
173
|
+
);
|
|
174
|
+
for (const child of processColumnIds) {
|
|
175
|
+
if (!linked.has(child)) {
|
|
176
|
+
warnings.push({
|
|
177
|
+
code: 'BP-014',
|
|
178
|
+
message:
|
|
179
|
+
`Column "${child}" is a PROCESS-… id and process_blueprint.process is "${parentProcess}", ` +
|
|
180
|
+
`but no in-effect process_parent REL links that column to that parent`,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
97
183
|
}
|
|
98
184
|
}
|
|
99
185
|
|
|
@@ -123,7 +209,7 @@ export function validateProcessBlueprint(input: unknown): ValidationResult {
|
|
|
123
209
|
if (!Array.isArray(entryStages) || entryStages.length === 0) {
|
|
124
210
|
errors.push({
|
|
125
211
|
code: 'BP-007',
|
|
126
|
-
message: `${path}.stages must be a non-empty array of
|
|
212
|
+
message: `${path}.stages must be a non-empty array of declared column ids`,
|
|
127
213
|
});
|
|
128
214
|
} else {
|
|
129
215
|
for (let j = 0; j < entryStages.length; j++) {
|