@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
|
@@ -14,6 +14,28 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { generateSvgEmbedCss } from '../theme/index.js';
|
|
16
16
|
import { escXml } from './render-util.js';
|
|
17
|
+
/** Interactive-preview / default-export type sizes. */
|
|
18
|
+
export const DEFAULT_BPMN_TYPOGRAPHY = {
|
|
19
|
+
taskFontPx: 11,
|
|
20
|
+
headerFontPx: 11,
|
|
21
|
+
labelFontPx: 10,
|
|
22
|
+
taskChars: 14,
|
|
23
|
+
taskLineH: 14,
|
|
24
|
+
labelChars: 18,
|
|
25
|
+
labelLineH: 13,
|
|
26
|
+
headerCharW: 6.5,
|
|
27
|
+
};
|
|
28
|
+
/** Presentation-export type sizes (20 px floor, wrap tuned to default node boxes). */
|
|
29
|
+
export const PRESENTATION_BPMN_TYPOGRAPHY = {
|
|
30
|
+
taskFontPx: 20,
|
|
31
|
+
headerFontPx: 20,
|
|
32
|
+
labelFontPx: 20,
|
|
33
|
+
taskChars: 9,
|
|
34
|
+
taskLineH: 22,
|
|
35
|
+
labelChars: 14,
|
|
36
|
+
labelLineH: 22,
|
|
37
|
+
headerCharW: 11.5,
|
|
38
|
+
};
|
|
17
39
|
// ---------------------------------------------------------------------------
|
|
18
40
|
// Constants — match DEFAULT_LAYOUT_DIAGRAM_OPTIONS in src/layout-options.ts
|
|
19
41
|
// ---------------------------------------------------------------------------
|
|
@@ -21,43 +43,49 @@ import { escXml } from './render-util.js';
|
|
|
21
43
|
const LANE_LABEL_BAND = 44;
|
|
22
44
|
/** End margin (px) along the header height axis for rotated pool/lane captions. */
|
|
23
45
|
const HEADER_LABEL_AXIS_PAD = 32;
|
|
24
|
-
/**
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
46
|
+
/** Active wrap/font metrics for the current render call (sync, restored after). */
|
|
47
|
+
let typo = DEFAULT_BPMN_TYPOGRAPHY;
|
|
48
|
+
function runWithTypography(t, fn) {
|
|
49
|
+
const prev = typo;
|
|
50
|
+
typo = t;
|
|
51
|
+
try {
|
|
52
|
+
return fn();
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
typo = prev;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
34
58
|
// ---------------------------------------------------------------------------
|
|
35
59
|
// BPMN-specific CSS — no font-style:italic per CLAUDE.md design rule
|
|
36
60
|
// ---------------------------------------------------------------------------
|
|
37
|
-
export
|
|
61
|
+
export function bpmnProcessCss(t = DEFAULT_BPMN_TYPOGRAPHY) {
|
|
62
|
+
return `
|
|
38
63
|
.bpmn-pool { fill: var(--ts-bg-surface,#f8fafc); stroke: var(--ts-node-stroke,#004d67); stroke-width: 1.5; }
|
|
39
64
|
.bpmn-pool-name { fill: var(--ts-bg-elevated,#f1f5f9); stroke: var(--ts-node-stroke,#004d67); stroke-width: 0.75; }
|
|
40
65
|
.bpmn-lane { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); stroke-width: 0.75; }
|
|
41
66
|
.bpmn-lane-header { fill: var(--ts-bg-elevated,#f1f5f9); stroke: var(--ts-node-stroke,#004d67); stroke-width: 0.75; }
|
|
42
|
-
.bpmn-pool-label { fill: var(--ts-text-primary,#0d2b35); font-family: var(--vscode-font-family, system-ui, -apple-system, sans-serif); font-size:
|
|
43
|
-
.bpmn-lane-label { fill: var(--ts-text-primary,#0d2b35); font-family: var(--vscode-font-family, system-ui, -apple-system, sans-serif); font-size:
|
|
67
|
+
.bpmn-pool-label { fill: var(--ts-text-primary,#0d2b35); font-family: var(--vscode-font-family, system-ui, -apple-system, sans-serif); font-size: ${t.headerFontPx}px; font-weight: 700; }
|
|
68
|
+
.bpmn-lane-label { fill: var(--ts-text-primary,#0d2b35); font-family: var(--vscode-font-family, system-ui, -apple-system, sans-serif); font-size: ${t.headerFontPx}px; font-weight: 600; }
|
|
44
69
|
.bpmn-task { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); stroke-width: 1.5; }
|
|
45
|
-
.bpmn-task-name { fill: var(--ts-text-primary,#0d2b35); font-family: var(--vscode-font-family, system-ui, -apple-system, sans-serif); font-size:
|
|
70
|
+
.bpmn-task-name { fill: var(--ts-text-primary,#0d2b35); font-family: var(--vscode-font-family, system-ui, -apple-system, sans-serif); font-size: ${t.taskFontPx}px; text-anchor: middle; }
|
|
46
71
|
.bpmn-event { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); }
|
|
47
72
|
.bpmn-event-start { stroke-width: 1.5; }
|
|
48
73
|
.bpmn-event-end { stroke-width: 4; }
|
|
49
|
-
.bpmn-event-label { fill: var(--ts-text-secondary,#516970); font-family: var(--vscode-font-family, system-ui, -apple-system, sans-serif); font-size:
|
|
74
|
+
.bpmn-event-label { fill: var(--ts-text-secondary,#516970); font-family: var(--vscode-font-family, system-ui, -apple-system, sans-serif); font-size: ${t.labelFontPx}px; text-anchor: middle; }
|
|
50
75
|
.bpmn-gateway { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); stroke-width: 1.5; }
|
|
51
76
|
.bpmn-gateway-marker { stroke: var(--ts-node-stroke,#004d67); stroke-width: 2.5; stroke-linecap: round; fill: none; }
|
|
52
|
-
.bpmn-gateway-label { fill: var(--ts-text-secondary,#516970); font-family: var(--vscode-font-family, system-ui, -apple-system, sans-serif); font-size:
|
|
77
|
+
.bpmn-gateway-label { fill: var(--ts-text-secondary,#516970); font-family: var(--vscode-font-family, system-ui, -apple-system, sans-serif); font-size: ${t.labelFontPx}px; text-anchor: middle; }
|
|
53
78
|
.bpmn-seq-flow { fill: none; stroke: var(--ts-edge-stroke,#004d67); stroke-width: 1.5; }
|
|
54
79
|
.bpmn-default-mark { stroke: var(--ts-edge-stroke,#004d67); stroke-width: 1.5; stroke-linecap: round; }
|
|
55
80
|
.bpmn-assoc { fill: none; stroke: var(--ts-edge-stroke,#004d67); stroke-width: 1; stroke-dasharray: 4 2; }
|
|
56
81
|
.bpmn-data-obj { fill: var(--ts-bg,#ffffff); stroke: var(--ts-node-stroke,#004d67); stroke-width: 1; }
|
|
57
|
-
.bpmn-data-obj-label { fill: var(--ts-text-secondary,#516970); font-size:
|
|
82
|
+
.bpmn-data-obj-label { fill: var(--ts-text-secondary,#516970); font-size: ${t.labelFontPx}px; text-anchor: middle; }
|
|
58
83
|
.bpmn-event-intermediate { fill: none; stroke: var(--ts-node-stroke,#004d67); stroke-width: 1; }
|
|
59
84
|
.bpmn-event-icon { fill: none; stroke: var(--ts-node-stroke,#004d67); stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; }
|
|
60
85
|
`;
|
|
86
|
+
}
|
|
87
|
+
/** Default-preview CSS (11 px / 10 px). Presentation export builds its own via `bpmnProcessCss`. */
|
|
88
|
+
export const BPMN_PROCESS_CSS = bpmnProcessCss(DEFAULT_BPMN_TYPOGRAPHY);
|
|
61
89
|
// ---------------------------------------------------------------------------
|
|
62
90
|
// Internal helpers
|
|
63
91
|
// ---------------------------------------------------------------------------
|
|
@@ -73,28 +101,29 @@ function wordTruncate(text, maxChars) {
|
|
|
73
101
|
return (lastSpace > maxChars / 2 ? cut.slice(0, lastSpace) : cut) + '…';
|
|
74
102
|
}
|
|
75
103
|
function fitRotatedHeaderText(name, spanPx) {
|
|
76
|
-
const maxChars = Math.max(4, Math.floor(spanPx /
|
|
104
|
+
const maxChars = Math.max(4, Math.floor(spanPx / typo.headerCharW));
|
|
77
105
|
return wordTruncate(name, maxChars);
|
|
78
106
|
}
|
|
79
107
|
function wrapTaskName(name) {
|
|
80
108
|
if (!name)
|
|
81
109
|
return [];
|
|
110
|
+
const limit = typo.taskChars;
|
|
82
111
|
const words = name.split(/\s+/);
|
|
83
112
|
const lines = [];
|
|
84
113
|
let cur = '';
|
|
85
114
|
for (const w of words) {
|
|
86
|
-
const token = w.length >
|
|
115
|
+
const token = w.length > limit ? w.slice(0, limit - 1) + '…' : w;
|
|
87
116
|
if (!cur) {
|
|
88
117
|
cur = token;
|
|
89
118
|
}
|
|
90
|
-
else if (cur.length + 1 + token.length <=
|
|
119
|
+
else if (cur.length + 1 + token.length <= limit) {
|
|
91
120
|
cur += ' ' + token;
|
|
92
121
|
}
|
|
93
122
|
else {
|
|
94
123
|
lines.push(cur);
|
|
95
124
|
if (lines.length === 3) {
|
|
96
|
-
if (lines[2].length >
|
|
97
|
-
lines[2] = lines[2].slice(0,
|
|
125
|
+
if (lines[2].length > limit - 1)
|
|
126
|
+
lines[2] = lines[2].slice(0, limit - 1) + '…';
|
|
98
127
|
return lines;
|
|
99
128
|
}
|
|
100
129
|
cur = token;
|
|
@@ -103,28 +132,29 @@ function wrapTaskName(name) {
|
|
|
103
132
|
if (cur && lines.length < 3)
|
|
104
133
|
lines.push(cur);
|
|
105
134
|
if (lines.length === 0)
|
|
106
|
-
lines.push(name.slice(0,
|
|
135
|
+
lines.push(name.slice(0, limit));
|
|
107
136
|
return lines;
|
|
108
137
|
}
|
|
109
138
|
function taskNameSvg(name, cx, cy) {
|
|
110
139
|
if (!name)
|
|
111
140
|
return '';
|
|
112
141
|
const lines = wrapTaskName(name);
|
|
113
|
-
const totalH = lines.length *
|
|
114
|
-
const startY = cy - totalH / 2 +
|
|
142
|
+
const totalH = lines.length * typo.taskLineH;
|
|
143
|
+
const startY = cy - totalH / 2 + typo.taskLineH * 0.8;
|
|
115
144
|
const tspans = lines
|
|
116
|
-
.map((ln, i) => `<tspan x="${r(cx)}" y="${r(startY + i *
|
|
145
|
+
.map((ln, i) => `<tspan x="${r(cx)}" y="${r(startY + i * typo.taskLineH)}">${escXml(ln)}</tspan>`)
|
|
117
146
|
.join('');
|
|
118
147
|
return `<text class="bpmn-task-name">${tspans}</text>`;
|
|
119
148
|
}
|
|
120
149
|
function wrapBelowLabel(name) {
|
|
150
|
+
const limit = typo.labelChars;
|
|
121
151
|
const words = name.split(/\s+/).filter(Boolean);
|
|
122
152
|
const allLines = [];
|
|
123
153
|
let cur = '';
|
|
124
154
|
for (const word of words) {
|
|
125
|
-
const tok = word.length >
|
|
155
|
+
const tok = word.length > limit ? word.slice(0, limit - 1) + '…' : word;
|
|
126
156
|
const test = cur ? `${cur} ${tok}` : tok;
|
|
127
|
-
if (test.length <=
|
|
157
|
+
if (test.length <= limit) {
|
|
128
158
|
cur = test;
|
|
129
159
|
}
|
|
130
160
|
else {
|
|
@@ -136,12 +166,12 @@ function wrapBelowLabel(name) {
|
|
|
136
166
|
if (cur)
|
|
137
167
|
allLines.push(cur);
|
|
138
168
|
if (allLines.length === 0)
|
|
139
|
-
return [name.slice(0,
|
|
169
|
+
return [name.slice(0, limit)];
|
|
140
170
|
if (allLines.length <= 3)
|
|
141
171
|
return allLines;
|
|
142
172
|
const lines = allLines.slice(0, 3);
|
|
143
173
|
const last = lines[2];
|
|
144
|
-
lines[2] = last.length + 1 <=
|
|
174
|
+
lines[2] = last.length + 1 <= limit ? last + '…' : last.slice(0, limit - 1) + '…';
|
|
145
175
|
return lines;
|
|
146
176
|
}
|
|
147
177
|
function belowLabelSvg(name, cls, cx, belowY, minX = 0) {
|
|
@@ -155,7 +185,7 @@ function belowLabelSvg(name, cls, cx, belowY, minX = 0) {
|
|
|
155
185
|
return `<text class="${cls}" x="${r(textX)}" y="${startY}">${escXml(lines[0])}</text>`;
|
|
156
186
|
}
|
|
157
187
|
const tspans = lines
|
|
158
|
-
.map((ln, i) => `<tspan x="${r(textX)}" dy="${i === 0 ? 0 :
|
|
188
|
+
.map((ln, i) => `<tspan x="${r(textX)}" dy="${i === 0 ? 0 : typo.labelLineH}">${escXml(ln)}</tspan>`)
|
|
159
189
|
.join('');
|
|
160
190
|
return `<text class="${cls}" x="${r(textX)}" y="${startY}">${tspans}</text>`;
|
|
161
191
|
}
|
|
@@ -378,15 +408,18 @@ export function renderProcessBody(layout, ox, oy) {
|
|
|
378
408
|
*/
|
|
379
409
|
export function renderProcessLayoutSvg(layout, options = {}) {
|
|
380
410
|
const { title = '', topInset = title ? 32 : 0 } = options;
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
(
|
|
390
|
-
|
|
391
|
-
|
|
411
|
+
const t = options.typography ?? DEFAULT_BPMN_TYPOGRAPHY;
|
|
412
|
+
return runWithTypography(t, () => {
|
|
413
|
+
const pb = layout.poolBounds;
|
|
414
|
+
const PAD = 16;
|
|
415
|
+
const svgW = r(pb.x + pb.width + PAD);
|
|
416
|
+
const svgH = r(pb.y + pb.height + PAD + topInset);
|
|
417
|
+
const embedCss = generateSvgEmbedCss('transitrix') + bpmnProcessCss(t);
|
|
418
|
+
const body = renderProcessBody(layout, 0, topInset);
|
|
419
|
+
return (`<svg xmlns="http://www.w3.org/2000/svg" width="${svgW}" height="${svgH}" viewBox="0 0 ${svgW} ${svgH}">\n` +
|
|
420
|
+
`<style>${embedCss}</style>\n` +
|
|
421
|
+
(title ? `${title}\n` : '') +
|
|
422
|
+
`${body}\n` +
|
|
423
|
+
`</svg>`);
|
|
424
|
+
});
|
|
392
425
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@transitrix/diagrams",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Shared rendering, validation, and pure mutations for Transitrix custom diagram formats.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -55,12 +55,12 @@
|
|
|
55
55
|
"@testing-library/dom": "^10.4.1",
|
|
56
56
|
"@testing-library/react": "^16.3.2",
|
|
57
57
|
"@types/react": "^19.2.17",
|
|
58
|
-
"@types/react-dom": "^19.2.
|
|
58
|
+
"@types/react-dom": "^19.2.5",
|
|
59
59
|
"jsdom": "^29.1.1",
|
|
60
60
|
"react": "^19.2.8",
|
|
61
61
|
"react-dom": "^19.2.8",
|
|
62
62
|
"reactflow": "^11.11.4",
|
|
63
63
|
"typescript": "^5.0.0",
|
|
64
|
-
"vitest": "^4.1.
|
|
64
|
+
"vitest": "^4.1.11"
|
|
65
65
|
}
|
|
66
66
|
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* §2). Filesystem-free — callable from unit tests without vscode.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { isObject, str, strArray,
|
|
15
|
+
import { isObject, str, strArray, stripEnvelope, reachesRootViaParent } from '../canon-resolver-utils.js';
|
|
16
16
|
|
|
17
17
|
export interface ActionViewConfig {
|
|
18
18
|
scope?: {
|
|
@@ -44,6 +44,10 @@ export interface ActionCanonSources {
|
|
|
44
44
|
relations?: unknown[];
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
+
/** In-memory bag of ACTION ids dropped by `root_action` scoping. Consumed by
|
|
48
|
+
* `validateActivities` as `ACT-021`. Not part of the notation. */
|
|
49
|
+
export const ACT_021_OMITTED_KEY = '__act021_omitted';
|
|
50
|
+
|
|
47
51
|
/** ACTION elements by id. `notation: action` is canonical; `notation: activity`
|
|
48
52
|
* is the deprecated pre-2026-06-25 alias (elements/24-action.md §"Deprecated
|
|
49
53
|
* alias") — accepted the same way `collectByNotation` callers elsewhere do. */
|
|
@@ -123,32 +127,26 @@ export function resolveAction(
|
|
|
123
127
|
return relGoals.length > 0 ? relGoals : strArray(el['goals']);
|
|
124
128
|
}
|
|
125
129
|
|
|
126
|
-
// 1.
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
let candidateIds: Set<string>;
|
|
131
|
-
if (rootAction) {
|
|
132
|
-
candidateIds = allActions.has(rootAction) ? descendantsOf(rootAction, allActions) : new Set();
|
|
133
|
-
} else {
|
|
134
|
-
candidateIds = new Set(allActions.keys());
|
|
135
|
-
}
|
|
130
|
+
// 1. Other scope fields first — the set the view would otherwise select
|
|
131
|
+
// (07-action.md §5.1). `root_action` then omits (and warns) rather than
|
|
132
|
+
// silently intersecting first.
|
|
133
|
+
let otherwiseIds: Set<string> = new Set(allActions.keys());
|
|
136
134
|
|
|
137
|
-
//
|
|
135
|
+
// Narrow by goals — only actions linked to at least one listed GOAL
|
|
138
136
|
// (via an active action_goal relation, or inline goals[] when no relation
|
|
139
137
|
// exists for that action).
|
|
140
138
|
const goalsFilter = strArray(scope['goals']);
|
|
141
139
|
if (goalsFilter.length > 0) {
|
|
142
|
-
|
|
143
|
-
[...
|
|
140
|
+
otherwiseIds = new Set(
|
|
141
|
+
[...otherwiseIds].filter((id) => effectiveGoals(id, allActions.get(id)!).some((g) => goalsFilter.includes(g))),
|
|
144
142
|
);
|
|
145
143
|
}
|
|
146
144
|
|
|
147
|
-
//
|
|
145
|
+
// Narrow by type_filter — only actions of a listed type.
|
|
148
146
|
const typeFilter = strArray(scope['type_filter']);
|
|
149
147
|
if (typeFilter.length > 0) {
|
|
150
|
-
|
|
151
|
-
[...
|
|
148
|
+
otherwiseIds = new Set(
|
|
149
|
+
[...otherwiseIds].filter((id) => {
|
|
152
150
|
const el = allActions.get(id);
|
|
153
151
|
const t = str(el?.['type']) ?? str(el?.['activity_type']);
|
|
154
152
|
return t !== undefined && typeFilter.includes(t);
|
|
@@ -156,13 +154,13 @@ export function resolveAction(
|
|
|
156
154
|
);
|
|
157
155
|
}
|
|
158
156
|
|
|
159
|
-
//
|
|
157
|
+
// Narrow by valid_at — only actions in effect on that date. Actions with
|
|
160
158
|
// no valid_from are not lifecycle-tracked and are kept (permissive default,
|
|
161
159
|
// matching the resolver's general "missing = not excluded" stance).
|
|
162
160
|
const validAt = str(scope['valid_at']);
|
|
163
161
|
if (validAt) {
|
|
164
|
-
|
|
165
|
-
[...
|
|
162
|
+
otherwiseIds = new Set(
|
|
163
|
+
[...otherwiseIds].filter((id) => {
|
|
166
164
|
const el = allActions.get(id);
|
|
167
165
|
const from = str(el?.['valid_from']);
|
|
168
166
|
const to = str(el?.['valid_to']);
|
|
@@ -173,6 +171,27 @@ export function resolveAction(
|
|
|
173
171
|
);
|
|
174
172
|
}
|
|
175
173
|
|
|
174
|
+
const rootAction = str(scope['root_action']);
|
|
175
|
+
let candidateIds: Set<string>;
|
|
176
|
+
const omittedOutsideRoot: string[] = [];
|
|
177
|
+
if (rootAction) {
|
|
178
|
+
if (!allActions.has(rootAction)) {
|
|
179
|
+
candidateIds = new Set();
|
|
180
|
+
} else {
|
|
181
|
+
candidateIds = new Set();
|
|
182
|
+
for (const id of otherwiseIds) {
|
|
183
|
+
if (reachesRootViaParent(id, rootAction, allActions)) {
|
|
184
|
+
candidateIds.add(id);
|
|
185
|
+
} else {
|
|
186
|
+
omittedOutsideRoot.push(id);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
omittedOutsideRoot.sort();
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
candidateIds = otherwiseIds;
|
|
193
|
+
}
|
|
194
|
+
|
|
176
195
|
// Element fields map onto Activity fields unchanged, except the canonical
|
|
177
196
|
// `type` (elements/24-action.md §2) which becomes the internal `activity_type`
|
|
178
197
|
// (types.ts `Activity.activity_type`), and `goals` which is resolved via
|
|
@@ -204,5 +223,6 @@ export function resolveAction(
|
|
|
204
223
|
actions: selectedActions,
|
|
205
224
|
};
|
|
206
225
|
if (Object.keys(project).length > 0) out['project'] = project;
|
|
226
|
+
if (omittedOutsideRoot.length > 0) out[ACT_021_OMITTED_KEY] = { root: rootAction, ids: omittedOutsideRoot };
|
|
207
227
|
return out;
|
|
208
228
|
}
|
|
@@ -5,10 +5,62 @@ import type {
|
|
|
5
5
|
ActivityValidationError,
|
|
6
6
|
ActivityValidationWarning,
|
|
7
7
|
} from './types.js';
|
|
8
|
+
import { isObject, str, reachesRootViaParent } from '../canon-resolver-utils.js';
|
|
9
|
+
import { ACT_021_OMITTED_KEY } from './resolver.js';
|
|
8
10
|
|
|
9
11
|
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
10
12
|
const VALID_WEEKDAYS = new Set(['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']);
|
|
11
13
|
|
|
14
|
+
function act021Message(actionId: string, rootId: string): string {
|
|
15
|
+
return `ACTION "${actionId}" is not "${rootId}" and is not reachable from it via parent`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function collectAct021FromProjection(raw: Record<string, unknown>): ActivityValidationWarning[] {
|
|
19
|
+
const bag = raw[ACT_021_OMITTED_KEY];
|
|
20
|
+
delete raw[ACT_021_OMITTED_KEY];
|
|
21
|
+
if (!isObject(bag)) return [];
|
|
22
|
+
const root = str(bag['root']);
|
|
23
|
+
const ids = Array.isArray(bag['ids']) ? bag['ids'] : [];
|
|
24
|
+
if (!root) return [];
|
|
25
|
+
const out: ActivityValidationWarning[] = [];
|
|
26
|
+
for (const id of ids) {
|
|
27
|
+
if (typeof id !== 'string' || !id.trim()) continue;
|
|
28
|
+
out.push({
|
|
29
|
+
code: 'ACT-021',
|
|
30
|
+
message: act021Message(id, root),
|
|
31
|
+
path: 'view_config.scope.root_action',
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function collectAct021FromAuthored(
|
|
38
|
+
raw: Record<string, unknown>,
|
|
39
|
+
actions: unknown[],
|
|
40
|
+
): ActivityValidationWarning[] {
|
|
41
|
+
const vc = isObject(raw['view_config']) ? raw['view_config'] : undefined;
|
|
42
|
+
const scope = vc && isObject(vc['scope']) ? vc['scope'] : undefined;
|
|
43
|
+
const root = scope ? str(scope['root_action']) : undefined;
|
|
44
|
+
if (!root) return [];
|
|
45
|
+
const byId = new Map<string, Record<string, unknown>>();
|
|
46
|
+
for (const el of actions) {
|
|
47
|
+
if (!isObject(el)) continue;
|
|
48
|
+
const id = str(el['id']);
|
|
49
|
+
if (id) byId.set(id, el);
|
|
50
|
+
}
|
|
51
|
+
if (!byId.has(root)) return [];
|
|
52
|
+
const out: ActivityValidationWarning[] = [];
|
|
53
|
+
for (const id of byId.keys()) {
|
|
54
|
+
if (reachesRootViaParent(id, root, byId)) continue;
|
|
55
|
+
out.push({
|
|
56
|
+
code: 'ACT-021',
|
|
57
|
+
message: act021Message(id, root),
|
|
58
|
+
path: 'view_config.scope.root_action',
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
12
64
|
export function validateActivities(input: unknown): ActivityValidationResult {
|
|
13
65
|
const errors: ActivityValidationError[] = [];
|
|
14
66
|
const warnings: ActivityValidationWarning[] = [];
|
|
@@ -46,6 +98,12 @@ export function validateActivities(input: unknown): ActivityValidationResult {
|
|
|
46
98
|
// Normalise onto raw.activities for the rest of this function (avoids touching downstream code)
|
|
47
99
|
if (!Array.isArray(raw.activities)) raw.activities = raw.actions;
|
|
48
100
|
|
|
101
|
+
const fromProjection = collectAct021FromProjection(raw);
|
|
102
|
+
warnings.push(...fromProjection);
|
|
103
|
+
if (fromProjection.length === 0) {
|
|
104
|
+
warnings.push(...collectAct021FromAuthored(raw, rawArray));
|
|
105
|
+
}
|
|
106
|
+
|
|
49
107
|
// ACT-014 / ACT-015: project.calendar validation (run before per-activity loop;
|
|
50
108
|
// surfaces clearly even when activities also have issues).
|
|
51
109
|
if (raw.project && typeof raw.project === 'object') {
|
|
@@ -158,8 +216,9 @@ export function validateActivities(input: unknown): ActivityValidationResult {
|
|
|
158
216
|
}
|
|
159
217
|
|
|
160
218
|
// ACT-011: warn if no duration (duration_days is accepted as an alias)
|
|
219
|
+
// Network view is unaffected; this is only about CPM/Gantt rendering.
|
|
161
220
|
if ((act.duration === undefined || act.duration === null) && (act.duration_days === undefined || act.duration_days === null)) {
|
|
162
|
-
warnings.push({ code: 'ACT-011', message: `Activity "${id}" has no duration —
|
|
221
|
+
warnings.push({ code: 'ACT-011', message: `Activity "${id}" has no duration — Gantt view and CPM analysis will exclude it. Network view is unaffected.`, path });
|
|
163
222
|
}
|
|
164
223
|
}
|
|
165
224
|
|
|
@@ -234,7 +293,9 @@ export function validateActivities(input: unknown): ActivityValidationResult {
|
|
|
234
293
|
}
|
|
235
294
|
}
|
|
236
295
|
|
|
237
|
-
// ACT-013: orphan activities (warning only)
|
|
296
|
+
// ACT-013: orphan activities (warning only). An activity is structurally orphan
|
|
297
|
+
// if it has no connections to the rest of the graph. Valid connections are:
|
|
298
|
+
// predecessors, successors, parent, or goals (direct or via delivers_changes).
|
|
238
299
|
const successorIds = new Set<string>();
|
|
239
300
|
for (const a of doc.activities) {
|
|
240
301
|
for (const pred of (a.predecessors ?? [])) successorIds.add(pred);
|
|
@@ -244,10 +305,11 @@ export function validateActivities(input: unknown): ActivityValidationResult {
|
|
|
244
305
|
const hasSuccessors = successorIds.has(a.id);
|
|
245
306
|
const hasGoals = Array.isArray(a.goals) && a.goals.length > 0;
|
|
246
307
|
const hasPredecessors = Array.isArray(a.predecessors) && a.predecessors.length > 0;
|
|
247
|
-
|
|
308
|
+
const hasParent = typeof a.parent === 'string' && a.parent.length > 0;
|
|
309
|
+
if (doc.activities.length > 1 && !hasSuccessors && !hasGoals && !hasPredecessors && !hasParent) {
|
|
248
310
|
warnings.push({
|
|
249
311
|
code: 'ACT-013',
|
|
250
|
-
message: `Activity "${a.id}" appears to be structurally orphan (no predecessors, successors, or goals)`,
|
|
312
|
+
message: `Activity "${a.id}" appears to be structurally orphan (no predecessors, successors, parent, or goals)`,
|
|
251
313
|
path: `activities[${i}]`,
|
|
252
314
|
});
|
|
253
315
|
}
|
|
@@ -48,6 +48,25 @@ export function descendantsOf(
|
|
|
48
48
|
return result;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/** Walk `parent` from `id` toward its ancestors until `rootId` or the chain
|
|
52
|
+
* ends. Cycle-safe: a cycle is not a path to the root. */
|
|
53
|
+
export function reachesRootViaParent(
|
|
54
|
+
id: string,
|
|
55
|
+
rootId: string,
|
|
56
|
+
all: Map<string, Record<string, unknown>>,
|
|
57
|
+
): boolean {
|
|
58
|
+
const seen = new Set<string>();
|
|
59
|
+
let current: string | undefined = id;
|
|
60
|
+
while (current) {
|
|
61
|
+
if (current === rootId) return true;
|
|
62
|
+
if (seen.has(current)) return false;
|
|
63
|
+
seen.add(current);
|
|
64
|
+
const el = all.get(current);
|
|
65
|
+
current = el ? str(el['parent']) : undefined;
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
51
70
|
/** Depth of `id` in its `parent` chain within `all` (root = 0, each step up
|
|
52
71
|
* adds 1). Cycle-safe (a cycle resolves the entering node to depth 0) and
|
|
53
72
|
* memoized across calls via the shared `memo` map. */
|
|
@@ -166,6 +166,7 @@ export function ingestComplianceDoc(canon: ComplianceCanon, doc: unknown): strin
|
|
|
166
166
|
if (!verifies || !method || !outcome) return null;
|
|
167
167
|
const ok = pushUnique(canon, canon.verifications, {
|
|
168
168
|
id, verifies, method, outcome,
|
|
169
|
+
protocol: str(d.protocol),
|
|
169
170
|
performed_at: str(d.performed_at),
|
|
170
171
|
evidenceCount: Array.isArray(d.evidence) ? d.evidence.length : 0,
|
|
171
172
|
admitted_at: str(d.admitted_at),
|
package/src/compliance/impact.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type { ComplianceCanon } from './classify.js';
|
|
|
12
12
|
import { buildComplianceIndex } from './reverse-index.js';
|
|
13
13
|
import type { CanonCatalog } from '../typed-id.js';
|
|
14
14
|
import type { ComplianceIndex, IndexAssertion, IndexRequirement, ObjectDetailInput, ObjectDetailDef, DeadlineStatus } from './types.js';
|
|
15
|
+
import { isStageColumnId } from '../process-blueprint/resolve-columns.js';
|
|
15
16
|
|
|
16
17
|
/** Filter selecting REQUIREMENTs by codex source (jurisdiction / regime keys
|
|
17
18
|
* are accepted for forward compatibility but not yet honoured — the canon
|
|
@@ -649,22 +650,48 @@ function buildTaskColumns(
|
|
|
649
650
|
*
|
|
650
651
|
* At `'product'` grain: `a.subject` must match `col.subjectId`.
|
|
651
652
|
* At `'product-stage'` grain: `a.subject` must match AND either
|
|
652
|
-
* `a.realised_via` is absent (claim covers the whole subject) OR
|
|
653
|
-
* `col.stageId
|
|
654
|
-
*
|
|
655
|
-
* `
|
|
656
|
-
*
|
|
657
|
-
* in that stage.
|
|
653
|
+
* `a.realised_via` is absent (claim covers the whole subject) OR the token
|
|
654
|
+
* pins this column: a `PROCESS-…` id matching `col.stageId`, or a STEP whose
|
|
655
|
+
* home process is that column. Sketch `STAGE-…` ids are never a join key.
|
|
656
|
+
* At `'product-stage-task'` grain: a task-id hit covers that task column; a
|
|
657
|
+
* process-level claim covers every task column of that process.
|
|
658
658
|
*/
|
|
659
|
-
function assertionMatchesColumn(
|
|
659
|
+
function assertionMatchesColumn(
|
|
660
|
+
a: IndexAssertion,
|
|
661
|
+
col: ImpactColumn,
|
|
662
|
+
stepHomeProcess?: ReadonlyMap<string, string>,
|
|
663
|
+
): boolean {
|
|
660
664
|
if (a.subject !== col.subjectId) return false;
|
|
661
665
|
if (!col.stageId) return true;
|
|
662
666
|
if (!a.realised_via || a.realised_via.length === 0) return true;
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
667
|
+
|
|
668
|
+
const stageId = col.stageId;
|
|
669
|
+
if (isStageColumnId(stageId)) {
|
|
670
|
+
// Sketch column: only a task-id hit counts — never the STAGE- key.
|
|
671
|
+
return col.taskId != null && a.realised_via.includes(col.taskId);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
for (const ref of a.realised_via) {
|
|
675
|
+
if (typeof ref !== 'string') continue;
|
|
676
|
+
if (isStageColumnId(ref)) continue;
|
|
677
|
+
if (col.taskId && ref === col.taskId) return true;
|
|
678
|
+
if (ref === stageId) return true;
|
|
679
|
+
if (!col.taskId && stepHomeProcess?.get(ref) === stageId) return true;
|
|
666
680
|
}
|
|
667
|
-
return
|
|
681
|
+
return false;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function stepHomeFromObjectDetails(objectDetails?: ObjectDetailInput[]): Map<string, string> {
|
|
685
|
+
const out = new Map<string, string>();
|
|
686
|
+
if (!objectDetails) return out;
|
|
687
|
+
for (const od of objectDetails) {
|
|
688
|
+
for (const d of od.details ?? []) {
|
|
689
|
+
for (const t of d.tasks ?? []) {
|
|
690
|
+
if (typeof t?.id === 'string' && t.id.length > 0) out.set(t.id, d.id);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
return out;
|
|
668
695
|
}
|
|
669
696
|
|
|
670
697
|
/**
|
|
@@ -691,6 +718,7 @@ export function buildImpactMatrix(
|
|
|
691
718
|
canon: ComplianceCanon,
|
|
692
719
|
config: ImpactViewConfig,
|
|
693
720
|
objectDetails?: ObjectDetailInput[],
|
|
721
|
+
join?: { stepHomeProcess?: ReadonlyMap<string, string> },
|
|
694
722
|
): ImpactMatrix {
|
|
695
723
|
const index = buildComplianceIndex({
|
|
696
724
|
requirements: canon.requirements,
|
|
@@ -755,6 +783,7 @@ export function buildImpactMatrix(
|
|
|
755
783
|
|
|
756
784
|
const rowIndex = new Set(obligations.map(r => r.id));
|
|
757
785
|
const cells: ImpactCell[][] = obligations.map(() => columns.map(() => emptyCell()));
|
|
786
|
+
const stepHome = join?.stepHomeProcess ?? stepHomeFromObjectDetails(objectDetails);
|
|
758
787
|
|
|
759
788
|
for (let c = 0; c < columns.length; c++) {
|
|
760
789
|
const col = columns[c];
|
|
@@ -762,7 +791,7 @@ export function buildImpactMatrix(
|
|
|
762
791
|
for (const a of subjectAssertions) {
|
|
763
792
|
if (!rowIndex.has(a.about)) continue;
|
|
764
793
|
if (!allowedStatuses.has(a.status)) continue;
|
|
765
|
-
if (!assertionMatchesColumn(a, col)) continue;
|
|
794
|
+
if (!assertionMatchesColumn(a, col, stepHome)) continue;
|
|
766
795
|
const rowIdx = obligations.findIndex(r => r.id === a.about);
|
|
767
796
|
if (rowIdx < 0) continue;
|
|
768
797
|
cells[rowIdx][c].assertions.push(a);
|
package/src/compliance/types.ts
CHANGED
|
@@ -128,6 +128,12 @@ export interface IndexVerification {
|
|
|
128
128
|
verifies: string;
|
|
129
129
|
method: VerificationMethod;
|
|
130
130
|
outcome: VerificationOutcome;
|
|
131
|
+
/**
|
|
132
|
+
* The verification procedure (27-verification.md §2). VERIFICATION carries
|
|
133
|
+
* no `name` field of its own; `protocol` is the closest human-readable
|
|
134
|
+
* description and is what a matrix/list view shows as the row's label.
|
|
135
|
+
*/
|
|
136
|
+
protocol?: string;
|
|
131
137
|
performed_at?: string;
|
|
132
138
|
/** Number of evidence entries — feeds the VERIF-006 gap. */
|
|
133
139
|
evidenceCount?: number;
|