@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
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
const ID_GRAMMAR_RE = /^[A-Z][A-Z_]*(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
|
|
2
2
|
const PROCESS_BLUEPRINT_ID_RE = /^PROCESS_BLUEPRINT(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
|
|
3
3
|
const STAGE_ID_RE = /^STAGE(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
|
|
4
|
+
const PROCESS_ID_RE = /^PROCESS(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
|
|
4
5
|
const APPLICATION_ID_RE = /^APPLICATION(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
|
|
5
6
|
const ROLE_ID_RE = /^ROLE(-[A-Z0-9][A-Z0-9_]*)*-\d+$/;
|
|
6
7
|
const ASPECT_CATEGORIES = ['systems', 'actors', 'equipment', 'information_entities'];
|
|
8
|
+
const RESTATED_COLUMN_FIELDS = ['name', 'goal', 'result'];
|
|
7
9
|
function isNonEmptyString(v) {
|
|
8
10
|
return typeof v === 'string' && v.trim().length > 0;
|
|
9
11
|
}
|
|
10
|
-
|
|
12
|
+
function isProcessColumnId(id) {
|
|
13
|
+
return PROCESS_ID_RE.test(id);
|
|
14
|
+
}
|
|
15
|
+
function isStageColumnId(id) {
|
|
16
|
+
return STAGE_ID_RE.test(id);
|
|
17
|
+
}
|
|
18
|
+
function isColumnId(id) {
|
|
19
|
+
return isStageColumnId(id) || isProcessColumnId(id);
|
|
20
|
+
}
|
|
21
|
+
function fieldIsPresent(record, key) {
|
|
22
|
+
return Object.prototype.hasOwnProperty.call(record, key);
|
|
23
|
+
}
|
|
24
|
+
export function validateProcessBlueprint(input, options = {}) {
|
|
11
25
|
const errors = [];
|
|
12
26
|
const warnings = [];
|
|
13
27
|
if (!input || typeof input !== 'object') {
|
|
@@ -47,6 +61,7 @@ export function validateProcessBlueprint(input) {
|
|
|
47
61
|
return { valid: false, errors, warnings };
|
|
48
62
|
}
|
|
49
63
|
const stageIds = new Set();
|
|
64
|
+
const processColumnIds = [];
|
|
50
65
|
for (let i = 0; i < stagesRaw.length; i++) {
|
|
51
66
|
const s = stagesRaw[i];
|
|
52
67
|
const path = `stages[${i}]`;
|
|
@@ -55,31 +70,68 @@ export function validateProcessBlueprint(input) {
|
|
|
55
70
|
continue;
|
|
56
71
|
}
|
|
57
72
|
if (!isNonEmptyString(s['id'])) {
|
|
58
|
-
errors.push({ code: 'BP-
|
|
73
|
+
errors.push({ code: 'BP-006', message: `${path}.id is required` });
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const sid = s['id'].trim();
|
|
77
|
+
if (stageIds.has(sid)) {
|
|
78
|
+
errors.push({ code: 'BP-006', message: `Duplicate stage id: "${sid}"` });
|
|
59
79
|
}
|
|
60
80
|
else {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
81
|
+
stageIds.add(sid);
|
|
82
|
+
}
|
|
83
|
+
if (!isColumnId(sid)) {
|
|
84
|
+
errors.push({
|
|
85
|
+
code: 'BP-006',
|
|
86
|
+
message: `${path}.id "${sid}" must match STAGE-[<middle>-]<INTEGER> or PROCESS-[<middle>-]<INTEGER>`,
|
|
87
|
+
});
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (isProcessColumnId(sid)) {
|
|
91
|
+
processColumnIds.push(sid);
|
|
92
|
+
for (const field of RESTATED_COLUMN_FIELDS) {
|
|
93
|
+
if (fieldIsPresent(s, field)) {
|
|
94
|
+
errors.push({
|
|
95
|
+
code: 'BP-013',
|
|
96
|
+
message: `${path}.${field} must not be restated on a PROCESS-… column — derive it from the process element`,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
67
99
|
}
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
100
|
+
if (options.catalog) {
|
|
101
|
+
const resolved = options.catalog.typeOf(sid);
|
|
102
|
+
if (resolved !== 'PROCESS') {
|
|
103
|
+
errors.push({
|
|
104
|
+
code: 'BP-012',
|
|
105
|
+
message: `${path}.id "${sid}" must resolve to an admitted PROCESS element`,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
73
108
|
}
|
|
74
109
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
110
|
+
else {
|
|
111
|
+
if (!isNonEmptyString(s['name'])) {
|
|
112
|
+
errors.push({ code: 'BP-005', message: `${path}.name is required` });
|
|
113
|
+
}
|
|
114
|
+
if (!isNonEmptyString(s['goal'])) {
|
|
115
|
+
errors.push({ code: 'BP-005', message: `${path}.goal is required` });
|
|
116
|
+
}
|
|
117
|
+
if (!isNonEmptyString(s['result'])) {
|
|
118
|
+
errors.push({ code: 'BP-005', message: `${path}.result is required` });
|
|
119
|
+
}
|
|
80
120
|
}
|
|
81
|
-
|
|
82
|
-
|
|
121
|
+
}
|
|
122
|
+
const parentProcess = isNonEmptyString(pb['process']) ? pb['process'].trim() : undefined;
|
|
123
|
+
if (parentProcess && options.processParentEdges && processColumnIds.length > 0) {
|
|
124
|
+
const linked = new Set(options.processParentEdges
|
|
125
|
+
.filter((e) => e.to === parentProcess)
|
|
126
|
+
.map((e) => e.from));
|
|
127
|
+
for (const child of processColumnIds) {
|
|
128
|
+
if (!linked.has(child)) {
|
|
129
|
+
warnings.push({
|
|
130
|
+
code: 'BP-014',
|
|
131
|
+
message: `Column "${child}" is a PROCESS-… id and process_blueprint.process is "${parentProcess}", ` +
|
|
132
|
+
`but no in-effect process_parent REL links that column to that parent`,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
83
135
|
}
|
|
84
136
|
}
|
|
85
137
|
const usedStageIds = new Set();
|
|
@@ -105,7 +157,7 @@ export function validateProcessBlueprint(input) {
|
|
|
105
157
|
if (!Array.isArray(entryStages) || entryStages.length === 0) {
|
|
106
158
|
errors.push({
|
|
107
159
|
code: 'BP-007',
|
|
108
|
-
message: `${path}.stages must be a non-empty array of
|
|
160
|
+
message: `${path}.stages must be a non-empty array of declared column ids`,
|
|
109
161
|
});
|
|
110
162
|
}
|
|
111
163
|
else {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ProcessParentEdge } from '../process-blueprint/validate.js';
|
|
2
|
+
import type { RepoFinding, RepoModelInput } from './types.js';
|
|
3
|
+
/** Inclusive window: in effect at `asOf` iff valid_from ≤ asOf and (valid_to is null or asOf ≤ valid_to). */
|
|
4
|
+
export declare function relationInEffectAt(data: Record<string, unknown>, asOf: string): boolean;
|
|
5
|
+
export declare function todayIsoDate(now?: Date): string;
|
|
6
|
+
/** In-effect `process_parent` edges (child → parent) among loaded REL files. */
|
|
7
|
+
export declare function collectInEffectProcessParentEdges(input: RepoModelInput, asOf?: string): ProcessParentEdge[];
|
|
8
|
+
export declare function checkProcessParentSemantics(input: RepoModelInput, findings: RepoFinding[]): void;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// process_parent relation checks — 17-relations.md §3 / §5 (REL-007, REL-008)
|
|
2
|
+
// and endpoint TYPE (PROCESS → PROCESS), plus the in-effect edge collector
|
|
3
|
+
// that process-blueprint BP-014 consumes.
|
|
4
|
+
//
|
|
5
|
+
// REL-007 is single-file (from === to). REL-008 is cross-cutting over the
|
|
6
|
+
// admitted process_parent graph and is a warning, parallel to REL-006.
|
|
7
|
+
import { docId, endpointId } from './validate-repo.js';
|
|
8
|
+
const PScope = 'repo';
|
|
9
|
+
function isProcessNotation(n) {
|
|
10
|
+
return n === 'process';
|
|
11
|
+
}
|
|
12
|
+
function isoDate(value) {
|
|
13
|
+
return typeof value === 'string' && /^\d{4}-\d{2}-\d{2}/.test(value) ? value.slice(0, 10) : undefined;
|
|
14
|
+
}
|
|
15
|
+
/** Inclusive window: in effect at `asOf` iff valid_from ≤ asOf and (valid_to is null or asOf ≤ valid_to). */
|
|
16
|
+
export function relationInEffectAt(data, asOf) {
|
|
17
|
+
const from = isoDate(data['valid_from']);
|
|
18
|
+
if (from && from > asOf)
|
|
19
|
+
return false;
|
|
20
|
+
const to = data['valid_to'];
|
|
21
|
+
if (to === null || to === undefined)
|
|
22
|
+
return true;
|
|
23
|
+
const toDate = isoDate(to);
|
|
24
|
+
if (!toDate)
|
|
25
|
+
return true;
|
|
26
|
+
return asOf <= toDate;
|
|
27
|
+
}
|
|
28
|
+
export function todayIsoDate(now = new Date()) {
|
|
29
|
+
return now.toISOString().slice(0, 10);
|
|
30
|
+
}
|
|
31
|
+
/** In-effect `process_parent` edges (child → parent) among loaded REL files. */
|
|
32
|
+
export function collectInEffectProcessParentEdges(input, asOf = todayIsoDate()) {
|
|
33
|
+
const edges = [];
|
|
34
|
+
for (const doc of input.relations) {
|
|
35
|
+
if (!doc.data)
|
|
36
|
+
continue;
|
|
37
|
+
if (doc.data['type'] !== 'process_parent')
|
|
38
|
+
continue;
|
|
39
|
+
if (!relationInEffectAt(doc.data, asOf))
|
|
40
|
+
continue;
|
|
41
|
+
const fromId = endpointId(doc.data['from']) ?? endpointId(doc.data['source']);
|
|
42
|
+
const toId = endpointId(doc.data['to']) ?? endpointId(doc.data['target']);
|
|
43
|
+
if (!fromId || !toId)
|
|
44
|
+
continue;
|
|
45
|
+
edges.push({ from: fromId, to: toId });
|
|
46
|
+
}
|
|
47
|
+
return edges;
|
|
48
|
+
}
|
|
49
|
+
function processParentRels(input) {
|
|
50
|
+
const out = [];
|
|
51
|
+
for (const doc of input.relations) {
|
|
52
|
+
if (!doc.data)
|
|
53
|
+
continue;
|
|
54
|
+
if (doc.data['type'] !== 'process_parent')
|
|
55
|
+
continue;
|
|
56
|
+
const fromId = endpointId(doc.data['from']) ?? endpointId(doc.data['source']);
|
|
57
|
+
const toId = endpointId(doc.data['to']) ?? endpointId(doc.data['target']);
|
|
58
|
+
if (!fromId || !toId)
|
|
59
|
+
continue;
|
|
60
|
+
out.push({ relId: docId(doc) ?? '', fromId, toId });
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
/** REL-007 — process_parent self-reference. */
|
|
65
|
+
function checkSelfReference(rels, findings) {
|
|
66
|
+
for (const rel of rels) {
|
|
67
|
+
if (rel.fromId === rel.toId) {
|
|
68
|
+
findings.push({
|
|
69
|
+
scope: PScope,
|
|
70
|
+
id: rel.relId,
|
|
71
|
+
ruleId: 'REL-007',
|
|
72
|
+
message: `REL-007: process_parent relation '${rel.relId}' has from equal to to ('${rel.fromId}').`,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** REL-008 — cycle in the process_parent graph (warning). */
|
|
78
|
+
function checkCycle(rels, findings) {
|
|
79
|
+
const childrenOf = new Map();
|
|
80
|
+
const relByChildParent = new Map();
|
|
81
|
+
const nodes = new Set();
|
|
82
|
+
for (const rel of rels) {
|
|
83
|
+
if (rel.fromId === rel.toId)
|
|
84
|
+
continue;
|
|
85
|
+
nodes.add(rel.fromId);
|
|
86
|
+
nodes.add(rel.toId);
|
|
87
|
+
const list = childrenOf.get(rel.fromId);
|
|
88
|
+
if (list)
|
|
89
|
+
list.push(rel.toId);
|
|
90
|
+
else
|
|
91
|
+
childrenOf.set(rel.fromId, [rel.toId]);
|
|
92
|
+
relByChildParent.set(`${rel.fromId}\0${rel.toId}`, rel.relId);
|
|
93
|
+
}
|
|
94
|
+
const UNVISITED = 0;
|
|
95
|
+
const VISITING = 1;
|
|
96
|
+
const DONE = 2;
|
|
97
|
+
const state = new Map();
|
|
98
|
+
let hit;
|
|
99
|
+
function walk(id) {
|
|
100
|
+
const s = state.get(id) ?? UNVISITED;
|
|
101
|
+
if (s === DONE)
|
|
102
|
+
return false;
|
|
103
|
+
if (s === VISITING) {
|
|
104
|
+
hit = id;
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
state.set(id, VISITING);
|
|
108
|
+
for (const parent of childrenOf.get(id) ?? []) {
|
|
109
|
+
if (nodes.has(parent) && walk(parent))
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
state.set(id, DONE);
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
for (const id of nodes) {
|
|
116
|
+
if (walk(id))
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
if (!hit)
|
|
120
|
+
return;
|
|
121
|
+
const relId = [...relByChildParent.entries()].find(([key]) => key.startsWith(`${hit}\0`))?.[1] ?? hit;
|
|
122
|
+
findings.push({
|
|
123
|
+
scope: PScope,
|
|
124
|
+
id: relId,
|
|
125
|
+
ruleId: 'REL-008',
|
|
126
|
+
severity: 'warning',
|
|
127
|
+
message: `REL-008: process_parent graph contains a cycle involving '${hit}'.`,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Endpoint TYPE for process_parent: both ends must be PROCESS when they
|
|
132
|
+
* resolve. Unknown endpoints stay with referential integrity; this pass only
|
|
133
|
+
* fires when the element exists and is the wrong notation.
|
|
134
|
+
*/
|
|
135
|
+
function checkEndpointTypes(input, rels, findings) {
|
|
136
|
+
const elementById = new Map();
|
|
137
|
+
for (const doc of input.elements) {
|
|
138
|
+
const id = docId(doc);
|
|
139
|
+
if (id && doc.data)
|
|
140
|
+
elementById.set(id, doc.data);
|
|
141
|
+
}
|
|
142
|
+
for (const rel of rels) {
|
|
143
|
+
const from = elementById.get(rel.fromId);
|
|
144
|
+
if (from && !isProcessNotation(from['notation'])) {
|
|
145
|
+
findings.push({
|
|
146
|
+
scope: PScope,
|
|
147
|
+
id: rel.relId,
|
|
148
|
+
ruleId: 'REL-002',
|
|
149
|
+
message: `Layer-semantics: '${rel.relId}' type 'process_parent' requires from to be ` +
|
|
150
|
+
`PROCESS; got notation='${from['notation']}'.`,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
const to = elementById.get(rel.toId);
|
|
154
|
+
if (to && !isProcessNotation(to['notation'])) {
|
|
155
|
+
findings.push({
|
|
156
|
+
scope: PScope,
|
|
157
|
+
id: rel.relId,
|
|
158
|
+
ruleId: 'REL-002',
|
|
159
|
+
message: `Layer-semantics: '${rel.relId}' type 'process_parent' requires to to be ` +
|
|
160
|
+
`PROCESS; got notation='${to['notation']}'.`,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
export function checkProcessParentSemantics(input, findings) {
|
|
166
|
+
const rels = processParentRels(input);
|
|
167
|
+
if (rels.length === 0)
|
|
168
|
+
return;
|
|
169
|
+
checkSelfReference(rels, findings);
|
|
170
|
+
checkCycle(rels, findings);
|
|
171
|
+
checkEndpointTypes(input, rels, findings);
|
|
172
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { validateRepoModel } from './validate-repo.js';
|
|
2
2
|
export { resolveRepoModel } from './resolve-model.js';
|
|
3
|
+
export { checkProcessParentSemantics, collectInEffectProcessParentEdges, todayIsoDate, } from './check-process-parent.js';
|
|
3
4
|
export type { RepoDoc, RepoFinding, RepoModelInput, ResolvedElementRecord, ResolvedRelationRecord, ResolvedRepoModel } from './types.js';
|
|
@@ -30,6 +30,7 @@ import { checkStrategyChainSemantics } from './check-strategy-chain.js';
|
|
|
30
30
|
import { checkElementHygiene } from './check-element-hygiene.js';
|
|
31
31
|
import { checkVersionedAttributes } from './check-versioned-attributes.js';
|
|
32
32
|
import { checkCandidateFields } from './check-candidate-fields.js';
|
|
33
|
+
import { checkProcessParentSemantics } from './check-process-parent.js';
|
|
33
34
|
const PScope = 'repo';
|
|
34
35
|
/** Statuses that, per lint.py's policy check, require an owner. */
|
|
35
36
|
const OWNER_REQUIRED_STATUSES = new Set(['Active', 'Production']);
|
|
@@ -154,6 +155,7 @@ function checkReferentialIntegrity(input, findings) {
|
|
|
154
155
|
* realizes — BUSINESS_SERVICE → CAPABILITY (25-business-services.md)
|
|
155
156
|
* hosts — NODE → TECHNOLOGY_SERVICE (25-nodes.md / 26-technology-services.md)
|
|
156
157
|
* uses — APPLICATION → TECHNOLOGY_SERVICE (26-technology-services.md)
|
|
158
|
+
* process_parent — PROCESS → PROCESS (17-relations.md §3; REL-007/008)
|
|
157
159
|
*
|
|
158
160
|
* Implemented element checks:
|
|
159
161
|
* TSVC-003 — TECHNOLOGY_SERVICE.node must resolve to NODE notation
|
|
@@ -407,6 +409,7 @@ export function validateRepoModel(input) {
|
|
|
407
409
|
checkAtomicity(input, findings);
|
|
408
410
|
checkReferentialIntegrity(input, findings);
|
|
409
411
|
checkLayerSemantics(input, findings);
|
|
412
|
+
checkProcessParentSemantics(input, findings);
|
|
410
413
|
checkPolicy(input, findings);
|
|
411
414
|
// Phase 7 — strategy-chain semantic rules ported from DSM's Go Validate*
|
|
412
415
|
// functions (GOALS-010, ACT-006..009, FGCA-008..011).
|
|
@@ -67,7 +67,7 @@ export function renderFgcaBody(columns, nodes, edges, curvature, entryCurvature,
|
|
|
67
67
|
}
|
|
68
68
|
export function renderFgcaSvg(doc, options = {}) {
|
|
69
69
|
const { variant = 'dgca', title = '', curvature = DEFAULT_EDGE_CURVATURE, entryCurvature, nodeSizePreset = 'normal', layoutOptions, } = options;
|
|
70
|
-
const hideChanges = variant === 'dga';
|
|
70
|
+
const hideChanges = variant === 'dga' || doc.hideChanges === true;
|
|
71
71
|
const nodeSize = resolveDgcaNodeSize(parseNodeSizePreset(nodeSizePreset));
|
|
72
72
|
const { nodes, edges, columns, width, height } = layoutFGCAPreview(doc, {
|
|
73
73
|
hideChanges,
|
|
@@ -64,13 +64,31 @@ export interface ProcessDiagramLayout {
|
|
|
64
64
|
flows: ProcessFlow[];
|
|
65
65
|
associations: ProcessAssociation[];
|
|
66
66
|
}
|
|
67
|
+
export interface BpmnTypography {
|
|
68
|
+
taskFontPx: number;
|
|
69
|
+
headerFontPx: number;
|
|
70
|
+
labelFontPx: number;
|
|
71
|
+
taskChars: number;
|
|
72
|
+
taskLineH: number;
|
|
73
|
+
labelChars: number;
|
|
74
|
+
labelLineH: number;
|
|
75
|
+
headerCharW: number;
|
|
76
|
+
}
|
|
77
|
+
/** Interactive-preview / default-export type sizes. */
|
|
78
|
+
export declare const DEFAULT_BPMN_TYPOGRAPHY: BpmnTypography;
|
|
79
|
+
/** Presentation-export type sizes (20 px floor, wrap tuned to default node boxes). */
|
|
80
|
+
export declare const PRESENTATION_BPMN_TYPOGRAPHY: BpmnTypography;
|
|
67
81
|
export interface RenderProcessOptions {
|
|
68
82
|
/** Raw SVG injected after `<style>` — a title line or full title block. */
|
|
69
83
|
title?: string;
|
|
70
84
|
/** Extra vertical space (px) reserved above the pool for a title block. */
|
|
71
85
|
topInset?: number;
|
|
86
|
+
/** Label metrics. Omit for the interactive-preview defaults. */
|
|
87
|
+
typography?: BpmnTypography;
|
|
72
88
|
}
|
|
73
|
-
export declare
|
|
89
|
+
export declare function bpmnProcessCss(t?: BpmnTypography): string;
|
|
90
|
+
/** Default-preview CSS (11 px / 10 px). Presentation export builds its own via `bpmnProcessCss`. */
|
|
91
|
+
export declare const BPMN_PROCESS_CSS: string;
|
|
74
92
|
/**
|
|
75
93
|
* Canonical body emitter, shared by every host (VS Code, JCEF, static export).
|
|
76
94
|
*
|
|
@@ -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
|
}
|