@waron97/prbot 3.2.0 → 3.2.1
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/agrippa-pb.md +33 -8
- package/package.json +1 -1
- package/src/agrippa/commands/diff.js +23 -10
- package/src/agrippa/commands/pb.js +16 -4
- package/src/agrippa/commands/pull.js +14 -14
- package/src/agrippa/commands/push.js +7 -8
- package/src/agrippa/index.js +6 -0
- package/src/agrippa/lib/pbEdit.js +80 -4
- package/src/agrippa/lib/pbLayout.js +23 -2
- package/src/agrippa/lib/pbModel.js +9 -6
- package/src/agrippa/lib/pbPreview.js +4 -2
- package/src/agrippa/lib/pbProject.js +12 -1
package/agrippa-pb.md
CHANGED
|
@@ -156,6 +156,7 @@ Node types: `startEvent`, `endEvent`, `boundaryEvent`, `exclusiveGateway`,
|
|
|
156
156
|
```
|
|
157
157
|
agrippa pb ls # discover node/edge ids
|
|
158
158
|
agrippa pb add / rm / connect / disconnect / set-default # change the graph
|
|
159
|
+
agrippa pb lint # check for structural issues before handing off
|
|
159
160
|
# STOP. Do NOT format. Do NOT push. Report back to the human (see Formatting below).
|
|
160
161
|
```
|
|
161
162
|
|
|
@@ -229,20 +230,27 @@ manifest entries. No dangling references left behind.
|
|
|
229
230
|
# plain sequence
|
|
230
231
|
agrippa pb connect --from ScriptTask_a --to ScriptTask_b --pb <document_id>
|
|
231
232
|
|
|
232
|
-
#
|
|
233
|
+
# named branch (required when the source has 2+ outgoing flows and this is non-default)
|
|
233
234
|
agrippa pb connect --from ExclusiveGateway_g --to ScriptTask_b \
|
|
234
|
-
--condition '${isAlive}' --pb <document_id>
|
|
235
|
+
--name "alive" --condition '${isAlive}' --pb <document_id>
|
|
235
236
|
|
|
236
|
-
# the gateway's fallback branch
|
|
237
|
+
# the gateway's fallback branch (default needs no name)
|
|
237
238
|
agrippa pb connect --from ExclusiveGateway_g --to EndEvent_err \
|
|
238
239
|
--default --pb <document_id>
|
|
239
240
|
```
|
|
240
241
|
|
|
241
242
|
A flow's id is printed. Conditions default to `xsi:type="tFormalExpression"`
|
|
242
|
-
(override with `--condition-type`). **
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
`--
|
|
243
|
+
(override with `--condition-type`). **Rules** (warned by this command and by `pb lint`):
|
|
244
|
+
|
|
245
|
+
- An `exclusiveGateway` with more than one outgoing flow must have **exactly one**
|
|
246
|
+
`--default` flow, and **every other** outgoing flow must carry a `--condition`.
|
|
247
|
+
- Any node with more than one outgoing flow: every **non-default** flow must have a
|
|
248
|
+
`--name` (operators need labels to distinguish branches at runtime).
|
|
249
|
+
- Only `exclusiveGateway` may have more than one **incoming** flow.
|
|
250
|
+
- An `exclusiveGateway` may not have both multiple incoming **and** multiple outgoing
|
|
251
|
+
flows — pick one direction (merging or splitting), not both.
|
|
252
|
+
|
|
253
|
+
Heed the `!` warnings `connect` prints.
|
|
246
254
|
|
|
247
255
|
#### `pb disconnect` — remove a flow
|
|
248
256
|
|
|
@@ -251,6 +259,22 @@ agrippa pb disconnect --id SequenceFlow_1lso8x0 --pb <document_id>
|
|
|
251
259
|
agrippa pb disconnect --from ScriptTask_a --to ScriptTask_b --pb <document_id>
|
|
252
260
|
```
|
|
253
261
|
|
|
262
|
+
#### `pb lint` — check for structural issues
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
agrippa pb lint --pb <document_id>
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Runs all diagram rules and prints any violations. Exits 1 if issues found. Run after
|
|
269
|
+
every batch of structural edits before handing off to a human. Rules checked:
|
|
270
|
+
|
|
271
|
+
- `exclusiveGateway` default/condition rule (2+ outgoing → one default, rest conditioned)
|
|
272
|
+
- Non-default outgoing flows must have a name when the source has 2+ outgoing flows
|
|
273
|
+
- Only `exclusiveGateway` may have multiple incoming flows
|
|
274
|
+
- `exclusiveGateway` may not have both multiple incoming and multiple outgoing flows
|
|
275
|
+
|
|
276
|
+
`pb format` and `pb connect` also surface the same warnings inline.
|
|
277
|
+
|
|
254
278
|
#### `pb set-default` — change a gateway's default flow
|
|
255
279
|
|
|
256
280
|
```bash
|
|
@@ -324,7 +348,8 @@ graph (`pb ls`) first if so.
|
|
|
324
348
|
|
|
325
349
|
```bash
|
|
326
350
|
agrippa pb add --type exclusiveGateway --name "alive?" --pb W # → ExclusiveGateway_g
|
|
327
|
-
agrippa pb connect --from ExclusiveGateway_g --to ScriptTask_ok
|
|
351
|
+
agrippa pb connect --from ExclusiveGateway_g --to ScriptTask_ok \
|
|
352
|
+
--name "alive" --condition '${isAlive}' --pb W
|
|
328
353
|
agrippa pb connect --from ExclusiveGateway_g --to EndEvent_err --default --pb W
|
|
329
354
|
```
|
|
330
355
|
|
package/package.json
CHANGED
|
@@ -47,19 +47,30 @@ async function diff(targetArg) {
|
|
|
47
47
|
const token = await getToken();
|
|
48
48
|
|
|
49
49
|
let diffCount = 0;
|
|
50
|
+
const chunks = [];
|
|
50
51
|
|
|
51
52
|
if (codeEntries.length) {
|
|
52
53
|
const remoteCodeMap = await fetchRemoteCode(token, process.env.RIP_URL, codeEntries);
|
|
53
|
-
diffCount
|
|
54
|
+
const { diffCount: c, chunks: cs } = diffCodeEntries(codeEntries, remoteCodeMap);
|
|
55
|
+
diffCount += c;
|
|
56
|
+
chunks.push(...cs);
|
|
54
57
|
}
|
|
55
58
|
|
|
56
59
|
for (const entry of pbEntries) {
|
|
57
|
-
|
|
60
|
+
const { hasDiff, chunk } = await diffPbEntry(token, entry);
|
|
61
|
+
if (hasDiff) {
|
|
62
|
+
diffCount++;
|
|
63
|
+
chunks.push(chunk);
|
|
64
|
+
}
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
if (diffCount === 0) {
|
|
61
68
|
console.log('No differences found — all tracked files match the remote.');
|
|
62
69
|
} else {
|
|
70
|
+
const combined = Buffer.concat(chunks);
|
|
71
|
+
const pager = process.env.PAGER || 'less';
|
|
72
|
+
const pagerArgs = pager === 'less' ? ['-R', '-F'] : [];
|
|
73
|
+
spawnSync(pager, pagerArgs, { input: combined, stdio: ['pipe', 'inherit', 'inherit'] });
|
|
63
74
|
console.log(`\n${diffCount} file(s) differ from remote.`);
|
|
64
75
|
}
|
|
65
76
|
}
|
|
@@ -68,6 +79,7 @@ async function diff(targetArg) {
|
|
|
68
79
|
// hand both sides to `git diff --no-index`.
|
|
69
80
|
function diffCodeEntries(entries, remoteCodeMap) {
|
|
70
81
|
let diffCount = 0;
|
|
82
|
+
const chunks = [];
|
|
71
83
|
const tmpFiles = [];
|
|
72
84
|
try {
|
|
73
85
|
for (const entry of entries) {
|
|
@@ -86,17 +98,17 @@ function diffCodeEntries(entries, remoteCodeMap) {
|
|
|
86
98
|
writeFileSync(tmpPath, (remoteCode ?? '').trim() + '\n', 'utf-8');
|
|
87
99
|
tmpFiles.push(tmpPath);
|
|
88
100
|
|
|
89
|
-
console.log(`\n=== ${entry.path} [${entry.name}] ===`);
|
|
90
|
-
|
|
91
101
|
const result = spawnSync(
|
|
92
102
|
'git',
|
|
93
103
|
['diff', '--no-index', '--color=always', tmpPath, entry.path],
|
|
94
|
-
{ stdio: ['ignore', '
|
|
104
|
+
{ stdio: ['ignore', 'pipe', 'pipe'] }
|
|
95
105
|
);
|
|
96
106
|
// exit code 1 means differences found (normal), 0 means identical, >1 means error
|
|
97
107
|
if (result.status !== null && result.status > 1) {
|
|
98
108
|
console.error(`git diff failed for ${entry.path}`);
|
|
99
109
|
}
|
|
110
|
+
const header = Buffer.from(`\n=== ${entry.path} [${entry.name}] ===\n`);
|
|
111
|
+
chunks.push(Buffer.concat([header, result.stdout ?? Buffer.alloc(0)]));
|
|
100
112
|
diffCount++;
|
|
101
113
|
}
|
|
102
114
|
} finally {
|
|
@@ -108,7 +120,7 @@ function diffCodeEntries(entries, remoteCodeMap) {
|
|
|
108
120
|
}
|
|
109
121
|
}
|
|
110
122
|
}
|
|
111
|
-
return diffCount;
|
|
123
|
+
return { diffCount, chunks };
|
|
112
124
|
}
|
|
113
125
|
|
|
114
126
|
// Process-builder wizard: decompose the upstream payload into a throwaway
|
|
@@ -136,18 +148,19 @@ async function diffPbEntry(token, entry) {
|
|
|
136
148
|
// and never matches the raw upstream xml byte-for-byte even when nothing
|
|
137
149
|
// structural changed, so it'd otherwise look like a permanent false diff.
|
|
138
150
|
if (localChecksum(projectReader(upstreamDir)) === localChecksum(projectReader(localDir)))
|
|
139
|
-
return false;
|
|
151
|
+
return { hasDiff: false };
|
|
140
152
|
|
|
141
|
-
console.log(`\n=== ${entry.path} [${entry.name}] (process-builder) ===`);
|
|
142
153
|
const result = spawnSync(
|
|
143
154
|
'git',
|
|
144
155
|
['diff', '--no-index', '--color=always', 'upstream', 'local'],
|
|
145
|
-
{ cwd: tmpRoot, stdio: ['ignore', '
|
|
156
|
+
{ cwd: tmpRoot, stdio: ['ignore', 'pipe', 'pipe'] }
|
|
146
157
|
);
|
|
147
158
|
if (result.status !== null && result.status > 1) {
|
|
148
159
|
console.error(`git diff failed for ${entry.path}`);
|
|
149
160
|
}
|
|
150
|
-
|
|
161
|
+
const header = Buffer.from(`\n=== ${entry.path} [${entry.name}] (process-builder) ===\n`);
|
|
162
|
+
const chunk = Buffer.concat([header, result.stdout ?? Buffer.alloc(0)]);
|
|
163
|
+
return { hasDiff: true, chunk };
|
|
151
164
|
} finally {
|
|
152
165
|
rmSync(tmpRoot, { recursive: true, force: true });
|
|
153
166
|
}
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
connect,
|
|
26
26
|
disconnect,
|
|
27
27
|
eachNode,
|
|
28
|
-
|
|
28
|
+
lintAll,
|
|
29
29
|
listGraph,
|
|
30
30
|
removeNode,
|
|
31
31
|
setDefault,
|
|
@@ -125,9 +125,9 @@ async function pbFormat(opts) {
|
|
|
125
125
|
console.log(
|
|
126
126
|
`Formatted ${dir} (${nodes} node(s) laid out${missing ? `, ${missing} without layout` : ''}).`
|
|
127
127
|
);
|
|
128
|
-
const issues =
|
|
128
|
+
const issues = lintAll(structure);
|
|
129
129
|
if (issues.length) {
|
|
130
|
-
console.warn('
|
|
130
|
+
console.warn('Diagram issues:');
|
|
131
131
|
for (const w of issues) console.warn(` ! ${w}`);
|
|
132
132
|
}
|
|
133
133
|
}
|
|
@@ -276,4 +276,16 @@ async function pbPreview(opts) {
|
|
|
276
276
|
console.log(`Wrote ${out} (${svg.length} bytes).`);
|
|
277
277
|
}
|
|
278
278
|
|
|
279
|
-
|
|
279
|
+
async function pbLint(opts) {
|
|
280
|
+
const dir = await resolveProjectPath(opts);
|
|
281
|
+
const { structure } = loadProject(dir);
|
|
282
|
+
const issues = lintAll(structure);
|
|
283
|
+
if (!issues.length) {
|
|
284
|
+
console.log('No issues.');
|
|
285
|
+
} else {
|
|
286
|
+
for (const w of issues) console.warn(` ! ${w}`);
|
|
287
|
+
process.exitCode = 1;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export { pbFormat, pbAdd, pbRemove, pbConnect, pbDisconnect, pbSetDefault, pbList, pbPreview, pbLint };
|
|
@@ -5,7 +5,7 @@ import { describeWorkflow, getPhasesByIds, getPhasesByWorkflow, listMfas } from
|
|
|
5
5
|
import { computeChecksum } from '../lib/checksum.js';
|
|
6
6
|
import { loadEffectiveEnv, readConfig, writeConfig } from '../lib/config.js';
|
|
7
7
|
import { getProcess } from '../lib/pbApi.js';
|
|
8
|
-
import { localChecksum } from '../lib/pbProject.js';
|
|
8
|
+
import { localChecksum, remoteChecksumPb } from '../lib/pbProject.js';
|
|
9
9
|
import { projectReader } from '../lib/pbWorkspace.js';
|
|
10
10
|
import {
|
|
11
11
|
fileExists,
|
|
@@ -63,13 +63,13 @@ async function pull() {
|
|
|
63
63
|
const localCode = readCodeFile(entry.path);
|
|
64
64
|
|
|
65
65
|
const remoteChecksum = computeChecksum(remoteCode ?? '');
|
|
66
|
-
const
|
|
66
|
+
const localChecksumVal = computeChecksum(localCode ?? '');
|
|
67
67
|
const pullChecksum = entry.checksum_at_pull;
|
|
68
68
|
|
|
69
69
|
let status;
|
|
70
|
-
if (
|
|
70
|
+
if (localChecksumVal === remoteChecksum) {
|
|
71
71
|
status = 'unchanged';
|
|
72
|
-
} else if (pullChecksum ===
|
|
72
|
+
} else if (pullChecksum === localChecksumVal) {
|
|
73
73
|
status = 'fast-forward';
|
|
74
74
|
} else {
|
|
75
75
|
status = 'conflict';
|
|
@@ -116,11 +116,10 @@ async function pull() {
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
// Refresh tracked wizards from upstream. Pull concern (inverted from push):
|
|
119
|
-
// overwriting *local* edits. Status per entry
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
// conflict remote advanced AND local diverged → overwrite loses local work
|
|
119
|
+
// overwriting *local* edits. Status per entry:
|
|
120
|
+
// unchanged local === remote semantically → nothing to do
|
|
121
|
+
// fast-forward remote changed, local untouched since pull → safe overwrite
|
|
122
|
+
// conflict local diverged from checksum_at_pull → overwrite loses local work
|
|
124
123
|
async function pullPbEntries(token, config) {
|
|
125
124
|
const entries = config.workspace.filter((e) => e.object_type === 'process_builder');
|
|
126
125
|
if (!entries.length) return;
|
|
@@ -140,12 +139,13 @@ async function pullPbEntries(token, config) {
|
|
|
140
139
|
console.warn(` ${entry.name}: could not fetch upstream, skipping`);
|
|
141
140
|
continue;
|
|
142
141
|
}
|
|
143
|
-
const
|
|
144
|
-
const
|
|
145
|
-
const
|
|
142
|
+
const localSemantic = localChecksum(projectReader(entry.path));
|
|
143
|
+
const remoteSemantic = remoteChecksumPb(upstream);
|
|
144
|
+
const pullChecksum = entry.checksum_at_pull;
|
|
146
145
|
let status;
|
|
147
|
-
if (
|
|
148
|
-
else
|
|
146
|
+
if (localSemantic === remoteSemantic) status = 'unchanged';
|
|
147
|
+
else if (pullChecksum === localSemantic) status = 'fast-forward';
|
|
148
|
+
else status = 'conflict';
|
|
149
149
|
classified.push({ ...entry, upstream, status });
|
|
150
150
|
}
|
|
151
151
|
|
|
@@ -6,7 +6,7 @@ import { updateMfa, updatePhase } from '../lib/api.js';
|
|
|
6
6
|
import { computeChecksum } from '../lib/checksum.js';
|
|
7
7
|
import { loadEffectiveEnv, readConfig, writeConfig } from '../lib/config.js';
|
|
8
8
|
import { getProcess } from '../lib/pbApi.js';
|
|
9
|
-
import { localChecksum } from '../lib/pbProject.js';
|
|
9
|
+
import { localChecksum, remoteChecksumPb } from '../lib/pbProject.js';
|
|
10
10
|
import { projectReader } from '../lib/pbWorkspace.js';
|
|
11
11
|
import { fileExists, readCodeFile } from '../lib/workspace.js';
|
|
12
12
|
import { fetchRemoteCode, selectEntries } from './pull.js';
|
|
@@ -71,12 +71,12 @@ async function push(opts = {}) {
|
|
|
71
71
|
const classified = config.workspace.map((entry) => {
|
|
72
72
|
if (entry.object_type === 'process_builder') {
|
|
73
73
|
const upstream = upstreamMap.get(entry.guid) ?? null;
|
|
74
|
-
const
|
|
75
|
-
const
|
|
76
|
-
const
|
|
74
|
+
const localSemantic = localChecksum(projectReader(entry.path));
|
|
75
|
+
const remoteSemantic = upstream ? remoteChecksumPb(upstream) : null;
|
|
76
|
+
const pullChecksum = entry.checksum_at_pull;
|
|
77
77
|
let status;
|
|
78
|
-
if (
|
|
79
|
-
else if (
|
|
78
|
+
if (localSemantic === remoteSemantic) status = 'unchanged';
|
|
79
|
+
else if (pullChecksum === remoteSemantic) status = 'fast-forward';
|
|
80
80
|
else status = 'conflict';
|
|
81
81
|
return { ...entry, upstream, status };
|
|
82
82
|
}
|
|
@@ -88,8 +88,7 @@ async function push(opts = {}) {
|
|
|
88
88
|
const localChecksumVal = computeChecksum(localCode ?? '');
|
|
89
89
|
const pullChecksum = entry.checksum_at_pull;
|
|
90
90
|
let status;
|
|
91
|
-
if (
|
|
92
|
-
status = 'unchanged';
|
|
91
|
+
if (localChecksumVal === remoteChecksum) status = 'unchanged';
|
|
93
92
|
else if (pullChecksum === remoteChecksum) status = 'fast-forward';
|
|
94
93
|
else status = 'conflict';
|
|
95
94
|
return { ...entry, remoteCode, localCode, status };
|
package/src/agrippa/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
pbConnect,
|
|
11
11
|
pbDisconnect,
|
|
12
12
|
pbFormat,
|
|
13
|
+
pbLint,
|
|
13
14
|
pbList,
|
|
14
15
|
pbPreview,
|
|
15
16
|
pbRemove,
|
|
@@ -171,6 +172,11 @@ pb.command('set-default')
|
|
|
171
172
|
.option('--pb <document_id>', 'Target wizard')
|
|
172
173
|
.action((opts) => pbSetDefault(opts).catch(die));
|
|
173
174
|
|
|
175
|
+
pb.command('lint')
|
|
176
|
+
.description('Check diagram for structural issues (edge names, incoming-edge rules, gateway rules)')
|
|
177
|
+
.option('--pb <document_id>', 'Target wizard')
|
|
178
|
+
.action((opts) => pbLint(opts).catch(die));
|
|
179
|
+
|
|
174
180
|
pb.command('ls')
|
|
175
181
|
.description('List nodes and edges (discover ids without reading the YAML)')
|
|
176
182
|
.option('--pb <document_id>', 'Target wizard')
|
|
@@ -44,6 +44,20 @@ const PREFIX = {
|
|
|
44
44
|
|
|
45
45
|
const CONTAINER = new Set(['subProcess', 'transaction']);
|
|
46
46
|
|
|
47
|
+
// Default attrs injected at node creation so the editor accepts them without manual edits.
|
|
48
|
+
const DEFAULT_ATTRS = {
|
|
49
|
+
scriptTask: {
|
|
50
|
+
scriptFormat: 'javascript',
|
|
51
|
+
'activiti:async': 'false',
|
|
52
|
+
'activiti:exclusive': 'false',
|
|
53
|
+
'activiti:autoStoreVariables': 'false',
|
|
54
|
+
},
|
|
55
|
+
serviceTask: {
|
|
56
|
+
'activiti:async': 'false',
|
|
57
|
+
'activiti:exclusive': 'false',
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
47
61
|
// Depth-first visit of every node in the nested graph, with its parent node.
|
|
48
62
|
function eachNode(nodes, parent, fn) {
|
|
49
63
|
for (const n of nodes || []) {
|
|
@@ -161,6 +175,8 @@ function addNode(structure, manifest, opts, ctx = {}) {
|
|
|
161
175
|
node.nodes = [];
|
|
162
176
|
}
|
|
163
177
|
|
|
178
|
+
if (DEFAULT_ATTRS[type]) node.attrs = { ...DEFAULT_ATTRS[type] };
|
|
179
|
+
|
|
164
180
|
if (parentId) {
|
|
165
181
|
const f = findNode(structure, parentId);
|
|
166
182
|
if (!f) throw new Error(`parent not found: ${parentId}`);
|
|
@@ -334,10 +350,9 @@ function connect(structure, { from, to, name, condition, conditionType, makeDefa
|
|
|
334
350
|
warnings.push(`replaced previous default flow ${prev} on ${from}.`);
|
|
335
351
|
}
|
|
336
352
|
}
|
|
337
|
-
// Re-check
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
}
|
|
353
|
+
// Re-check all structural invariants after the edit, scoped to affected nodes.
|
|
354
|
+
const allIssues = lintAll(structure);
|
|
355
|
+
warnings.push(...allIssues.filter((w) => w.startsWith(from) || w.startsWith(to)));
|
|
341
356
|
|
|
342
357
|
return { writes: {}, deletes: [], result: { id, from, to, warnings } };
|
|
343
358
|
}
|
|
@@ -407,6 +422,64 @@ function lintGateways(structure) {
|
|
|
407
422
|
return issues;
|
|
408
423
|
}
|
|
409
424
|
|
|
425
|
+
// Flag non-default outgoing edges that are missing a name when a node has 2+
|
|
426
|
+
// outgoing flows. In Activiti diagrams these labels are mandatory so operators
|
|
427
|
+
// can distinguish branches at runtime.
|
|
428
|
+
function lintEdgeNames(structure) {
|
|
429
|
+
const issues = [];
|
|
430
|
+
eachNode(structure.nodes, null, (n) => {
|
|
431
|
+
const outs = n.edges || [];
|
|
432
|
+
if (outs.length < 2) return;
|
|
433
|
+
const def = n.attrs?.default;
|
|
434
|
+
for (const e of outs) {
|
|
435
|
+
if (e.id === def) continue;
|
|
436
|
+
if (!e.name) {
|
|
437
|
+
issues.push(
|
|
438
|
+
`${n.id} → ${e.target} (${e.id}): non-default flow has no name.`
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
});
|
|
443
|
+
return issues;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Flag incoming-edge violations:
|
|
447
|
+
// - Only exclusiveGateway may have >1 incoming flows.
|
|
448
|
+
// - An exclusiveGateway may not have both >1 incoming AND >1 outgoing (pick one
|
|
449
|
+
// direction for merging or splitting, not both at once).
|
|
450
|
+
function lintIncomingEdges(structure) {
|
|
451
|
+
const inCount = {};
|
|
452
|
+
const outCount = {};
|
|
453
|
+
eachNode(structure.nodes, null, (n) => {
|
|
454
|
+
for (const e of n.edges || []) {
|
|
455
|
+
inCount[e.target] = (inCount[e.target] || 0) + 1;
|
|
456
|
+
outCount[n.id] = (outCount[n.id] || 0) + 1;
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
const issues = [];
|
|
461
|
+
eachNode(structure.nodes, null, (n) => {
|
|
462
|
+
const inc = inCount[n.id] || 0;
|
|
463
|
+
const out = outCount[n.id] || 0;
|
|
464
|
+
if (inc > 1 && n.type !== 'exclusiveGateway') {
|
|
465
|
+
issues.push(
|
|
466
|
+
`${n.id} (${n.name || n.type}): only exclusiveGateway may have multiple incoming flows (has ${inc}).`
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
if (n.type === 'exclusiveGateway' && inc > 1 && out > 1) {
|
|
470
|
+
issues.push(
|
|
471
|
+
`${n.id} (${n.name || 'gateway'}): exclusiveGateway may not have both multiple incoming (${inc}) and multiple outgoing (${out}) flows.`
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
return issues;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Run all lint rules and return combined issues.
|
|
479
|
+
function lintAll(structure) {
|
|
480
|
+
return [...lintGateways(structure), ...lintEdgeNames(structure), ...lintIncomingEdges(structure)];
|
|
481
|
+
}
|
|
482
|
+
|
|
410
483
|
// Remove an edge by id, or by --from/--to pair.
|
|
411
484
|
function disconnect(structure, { id, from, to }) {
|
|
412
485
|
let removed = 0;
|
|
@@ -464,4 +537,7 @@ export {
|
|
|
464
537
|
setDefault,
|
|
465
538
|
listGraph,
|
|
466
539
|
lintGateways,
|
|
540
|
+
lintEdgeNames,
|
|
541
|
+
lintIncomingEdges,
|
|
542
|
+
lintAll,
|
|
467
543
|
};
|
|
@@ -37,7 +37,13 @@ const ROOT_OPTS = {
|
|
|
37
37
|
'elk.layered.spacing.edgeEdgeBetweenLayers': '20',
|
|
38
38
|
'elk.layered.spacing.edgeNodeBetweenLayers': '20',
|
|
39
39
|
};
|
|
40
|
-
const CONTAINER_OPTS = {
|
|
40
|
+
const CONTAINER_OPTS = {
|
|
41
|
+
'elk.padding': '[top=50.0,left=30.0,bottom=50.0,right=30.0]',
|
|
42
|
+
'elk.layered.spacing.nodeNodeBetweenLayers': '60',
|
|
43
|
+
'elk.spacing.nodeNode': '80',
|
|
44
|
+
'elk.spacing.edgeNode': '20',
|
|
45
|
+
'elk.layered.spacing.edgeNodeBetweenLayers': '20',
|
|
46
|
+
};
|
|
41
47
|
|
|
42
48
|
function sizeOf(n) {
|
|
43
49
|
if (SIZE[n.type]) return SIZE[n.type];
|
|
@@ -147,6 +153,9 @@ async function autoLayout(structure) {
|
|
|
147
153
|
sources: [source],
|
|
148
154
|
targets: [e.target],
|
|
149
155
|
sourcePort,
|
|
156
|
+
labels: e.name
|
|
157
|
+
? [{ id: `${e.id}__lbl`, text: e.name, width: Math.min(e.name.length * 6, 140), height: 14 }]
|
|
158
|
+
: [],
|
|
150
159
|
layoutOptions: {
|
|
151
160
|
'elk.layered.priority.straightness': isHappy ? '10' : 1,
|
|
152
161
|
'elk.layered.priority.shortness': isHappy ? '10' : 1,
|
|
@@ -237,8 +246,20 @@ async function autoLayout(structure) {
|
|
|
237
246
|
height: round(p.height),
|
|
238
247
|
};
|
|
239
248
|
for (const e of n.edges || []) {
|
|
240
|
-
const
|
|
249
|
+
const off = lcaOffset(n.id, e.target);
|
|
250
|
+
const wp = waypointsFor(e.id, off);
|
|
241
251
|
if (wp) e.waypoints = wp;
|
|
252
|
+
if (e.name) {
|
|
253
|
+
const lbl = elkEdges[e.id]?.labels?.[0];
|
|
254
|
+
if (lbl) {
|
|
255
|
+
e.labelPos = {
|
|
256
|
+
x: round(lbl.x + off.x),
|
|
257
|
+
y: round(lbl.y + off.y),
|
|
258
|
+
width: round(lbl.width),
|
|
259
|
+
height: round(lbl.height),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
}
|
|
242
263
|
}
|
|
243
264
|
});
|
|
244
265
|
|
|
@@ -353,8 +353,9 @@ function boundsAttrs(b) {
|
|
|
353
353
|
}
|
|
354
354
|
|
|
355
355
|
// Rebuild <bpmndi> purely from structure.yaml geometry (model graph + geo maps).
|
|
356
|
-
// DI element ids are derived (`<id>_di`); the manifest is never consulted.
|
|
357
|
-
// are omitted (renderers auto-place them);
|
|
356
|
+
// DI element ids are derived (`<id>_di`); the manifest is never consulted. Node
|
|
357
|
+
// labels are omitted (renderers auto-place them); edge labels use ELK-computed
|
|
358
|
+
// bounds from geo.labelPos when present — only positions, sizes, waypoints and
|
|
358
359
|
// subprocess expand-state are authoritative — see compareDiagram.
|
|
359
360
|
function buildDiagram(model, geo) {
|
|
360
361
|
if (!geo) return null;
|
|
@@ -373,10 +374,12 @@ function buildDiagram(model, geo) {
|
|
|
373
374
|
const pushEdge = (id) => {
|
|
374
375
|
const wps = geo.waypoints[id];
|
|
375
376
|
if (!wps) return;
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
)
|
|
379
|
-
|
|
377
|
+
const children = [];
|
|
378
|
+
const lp = geo.labelPos?.[id];
|
|
379
|
+
for (const [x, y] of wps)
|
|
380
|
+
children.push(el('omgdi:waypoint', { '@_x': String(x), '@_y': String(y) }, []));
|
|
381
|
+
if (lp) children.push(el('bpmndi:BPMNLabel', {}, [el('omgdc:Bounds', boundsAttrs(lp), [])]));
|
|
382
|
+
edgeEls.push(el('bpmndi:BPMNEdge', { '@_id': `${id}_di`, '@_bpmnElement': id }, children));
|
|
380
383
|
};
|
|
381
384
|
for (const e of model.edges) pushEdge(e.id);
|
|
382
385
|
for (const a of model.associations || []) pushEdge(a.id);
|
|
@@ -91,9 +91,11 @@ function toSvg(structure) {
|
|
|
91
91
|
`<polyline points="${pts}" fill="none" stroke="#444" stroke-width="1.5" marker-end="url(#arrow)"/>`
|
|
92
92
|
);
|
|
93
93
|
if (e.name) {
|
|
94
|
-
const
|
|
94
|
+
const lp = e.labelPos;
|
|
95
|
+
const lx = lp ? lp.x + lp.width / 2 : e.waypoints[Math.floor(e.waypoints.length / 2)][0];
|
|
96
|
+
const ly = lp ? lp.y + lp.height / 2 : e.waypoints[Math.floor(e.waypoints.length / 2)][1] - 4;
|
|
95
97
|
out.push(
|
|
96
|
-
`<text x="${
|
|
98
|
+
`<text x="${lx + ox}" y="${ly + oy}" text-anchor="middle" fill="#666" font-size="9">${esc(e.name)}</text>`
|
|
97
99
|
);
|
|
98
100
|
}
|
|
99
101
|
}
|
|
@@ -169,6 +169,7 @@ function flattenNodes(structNodes, parentId, model, read, geo) {
|
|
|
169
169
|
if (se.documentation !== undefined) e.documentation = se.documentation;
|
|
170
170
|
if (parentId) e.parent = parentId;
|
|
171
171
|
if (se.waypoints) geo.waypoints[se.id] = se.waypoints;
|
|
172
|
+
if (se.labelPos) geo.labelPos[se.id] = se.labelPos;
|
|
172
173
|
model.edges.push(e);
|
|
173
174
|
}
|
|
174
175
|
if (sn.nodes) flattenNodes(sn.nodes, sn.id, model, read, geo);
|
|
@@ -264,7 +265,7 @@ function recompose(read) {
|
|
|
264
265
|
annotations: structure.annotations || [],
|
|
265
266
|
associations: structure.associations || [],
|
|
266
267
|
};
|
|
267
|
-
const geo = { bounds: {}, waypoints: {}, expanded: {} };
|
|
268
|
+
const geo = { bounds: {}, waypoints: {}, expanded: {}, labelPos: {} };
|
|
268
269
|
flattenNodes(structure.nodes, null, model, read, geo);
|
|
269
270
|
// annotation/association geometry (top-level lists carry it inline)
|
|
270
271
|
for (const a of model.annotations) if (a.layout) geo.bounds[a.id] = a.layout;
|
|
@@ -342,6 +343,15 @@ function localChecksum(read) {
|
|
|
342
343
|
return computeChecksum(stableStringify(recompose(read)));
|
|
343
344
|
}
|
|
344
345
|
|
|
346
|
+
// Semantic checksum of a remote payload. Decompose → recompose normalises field
|
|
347
|
+
// order and non-semantic server-side differences, same pipeline as localChecksum
|
|
348
|
+
// so the two are directly comparable.
|
|
349
|
+
function remoteChecksumPb(payload) {
|
|
350
|
+
const { files } = decompose(payload);
|
|
351
|
+
const read = (p) => files[p] ?? '';
|
|
352
|
+
return computeChecksum(stableStringify(recompose(read)));
|
|
353
|
+
}
|
|
354
|
+
|
|
345
355
|
// Enumerate the wizard's pages from the pages/*.yml files (the authoritative
|
|
346
356
|
// local set). Each page links to its userTask via `page._id.stepkey`; the
|
|
347
357
|
// manifest supplies the upstream page guid (absent => a locally-added page that
|
|
@@ -379,6 +389,7 @@ export {
|
|
|
379
389
|
verifyRoundTrip,
|
|
380
390
|
stableStringify,
|
|
381
391
|
localChecksum,
|
|
392
|
+
remoteChecksumPb,
|
|
382
393
|
enumeratePages,
|
|
383
394
|
stringifyStructure,
|
|
384
395
|
pad,
|