@waron97/prbot 3.1.4 → 3.2.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/README.md +50 -11
- package/agrippa-pb.md +350 -0
- package/package.json +7 -3
- package/src/agrippa/commands/clone.js +29 -13
- package/src/agrippa/commands/clonePb.js +107 -0
- package/src/agrippa/commands/diff.js +94 -18
- package/src/agrippa/commands/init.js +66 -12
- package/src/agrippa/commands/initPhase.js +16 -17
- package/src/agrippa/commands/pb.js +279 -0
- package/src/agrippa/commands/pull.js +119 -13
- package/src/agrippa/commands/pullPb.js +54 -0
- package/src/agrippa/commands/push.js +112 -47
- package/src/agrippa/commands/pushPb.js +87 -0
- package/src/agrippa/commands/repair.js +3 -1
- package/src/agrippa/index.js +138 -14
- package/src/agrippa/lib/api.js +17 -3
- package/src/agrippa/lib/checksum.js +3 -1
- package/src/agrippa/lib/config.js +2 -2
- package/src/agrippa/lib/pbApi.js +71 -0
- package/src/agrippa/lib/pbEdit.js +467 -0
- package/src/agrippa/lib/pbLayout.js +283 -0
- package/src/agrippa/lib/pbModel.js +698 -0
- package/src/agrippa/lib/pbPreview.js +151 -0
- package/src/agrippa/lib/pbProject.js +390 -0
- package/src/agrippa/lib/pbWorkspace.js +30 -0
- package/src/agrippa/lib/workspace.js +23 -3
- package/src/commands/autopr.js +5 -2
- package/src/commands/changelog.js +4 -1
- package/src/commands/export.js +3 -3
- package/src/commands/exportEmailTemplates.js +25 -15
- package/src/commands/exportImperex.js +4 -4
- package/src/commands/exportLrp.js +10 -7
- package/src/commands/exportPb.js +4 -5
- package/src/commands/exportWorkflow.js +27 -14
- package/src/commands/init.js +7 -0
- package/src/commands/routine.js +7 -5
- package/src/index.js +24 -7
- package/src/lib/premigrate.js +3 -3
- package/src/lib/updateCheck.js +5 -2
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// Quick-and-dirty SVG render of a decomposed wizard's diagram, for eyeballing
|
|
2
|
+
// the output of `pb format` during development. Not byte-faithful to the BPMN
|
|
3
|
+
// renderer — it just draws each node by type (event=circle, gateway=diamond,
|
|
4
|
+
// task=rounded rect, container=rect) and each edge as a waypoint polyline, from
|
|
5
|
+
// the geometry in structure.yaml.
|
|
6
|
+
|
|
7
|
+
import { CONTAINER, eachNode } from './pbEdit.js';
|
|
8
|
+
|
|
9
|
+
const EVENT = new Set(['startEvent', 'endEvent', 'boundaryEvent']);
|
|
10
|
+
|
|
11
|
+
function esc(s) {
|
|
12
|
+
return String(s ?? '')
|
|
13
|
+
.replace(/&/g, '&')
|
|
14
|
+
.replace(/</g, '<')
|
|
15
|
+
.replace(/>/g, '>');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function toSvg(structure) {
|
|
19
|
+
const leaves = [];
|
|
20
|
+
const containers = [];
|
|
21
|
+
const edges = [];
|
|
22
|
+
eachNode(structure.nodes, null, (n) => {
|
|
23
|
+
if (!n.layout) return;
|
|
24
|
+
if (CONTAINER.has(n.type)) containers.push(n);
|
|
25
|
+
else leaves.push(n);
|
|
26
|
+
for (const e of n.edges || []) if (e.waypoints?.length) edges.push(e);
|
|
27
|
+
});
|
|
28
|
+
const anns = (structure.annotations || []).filter((a) => a.layout);
|
|
29
|
+
const assocs = (structure.associations || []).filter((a) => a.waypoints?.length);
|
|
30
|
+
|
|
31
|
+
let minX = Infinity;
|
|
32
|
+
let minY = Infinity;
|
|
33
|
+
let maxX = -Infinity;
|
|
34
|
+
let maxY = -Infinity;
|
|
35
|
+
const ext = (x, y) => {
|
|
36
|
+
minX = Math.min(minX, x);
|
|
37
|
+
minY = Math.min(minY, y);
|
|
38
|
+
maxX = Math.max(maxX, x);
|
|
39
|
+
maxY = Math.max(maxY, y);
|
|
40
|
+
};
|
|
41
|
+
for (const n of [...leaves, ...containers, ...anns]) {
|
|
42
|
+
const l = n.layout;
|
|
43
|
+
ext(l.x, l.y);
|
|
44
|
+
ext(l.x + l.width, l.y + l.height);
|
|
45
|
+
}
|
|
46
|
+
for (const e of [...edges, ...assocs]) for (const [x, y] of e.waypoints) ext(x, y);
|
|
47
|
+
if (!Number.isFinite(minX)) {
|
|
48
|
+
minX = 0;
|
|
49
|
+
minY = 0;
|
|
50
|
+
maxX = 100;
|
|
51
|
+
maxY = 100;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const pad = 40;
|
|
55
|
+
const W = Math.round(maxX - minX + pad * 2);
|
|
56
|
+
const H = Math.round(maxY - minY + pad * 2);
|
|
57
|
+
const ox = pad - minX;
|
|
58
|
+
const oy = pad - minY;
|
|
59
|
+
|
|
60
|
+
const out = [];
|
|
61
|
+
out.push(
|
|
62
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}" font-family="sans-serif" font-size="11">`
|
|
63
|
+
);
|
|
64
|
+
out.push(
|
|
65
|
+
'<defs><marker id="arrow" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto">' +
|
|
66
|
+
'<path d="M0,0 L8,3 L0,6 z" fill="#444"/></marker></defs>'
|
|
67
|
+
);
|
|
68
|
+
out.push('<rect width="100%" height="100%" fill="#fff"/>');
|
|
69
|
+
|
|
70
|
+
const label = (cx, cy, text) =>
|
|
71
|
+
text
|
|
72
|
+
? `<text x="${cx}" y="${cy}" text-anchor="middle" dominant-baseline="middle">${esc(text)}</text>`
|
|
73
|
+
: '';
|
|
74
|
+
|
|
75
|
+
// containers behind everything
|
|
76
|
+
for (const n of containers) {
|
|
77
|
+
const l = n.layout;
|
|
78
|
+
const x = l.x + ox;
|
|
79
|
+
const y = l.y + oy;
|
|
80
|
+
out.push(
|
|
81
|
+
`<rect x="${x}" y="${y}" width="${l.width}" height="${l.height}" rx="8" fill="#f5f7fa" stroke="#9aa7b4"/>`
|
|
82
|
+
);
|
|
83
|
+
out.push(
|
|
84
|
+
`<text x="${x + 8}" y="${y + 16}" font-weight="bold">${esc(n.name || n.type)}</text>`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
// edges
|
|
88
|
+
for (const e of edges) {
|
|
89
|
+
const pts = e.waypoints.map(([x, y]) => `${x + ox},${y + oy}`).join(' ');
|
|
90
|
+
out.push(
|
|
91
|
+
`<polyline points="${pts}" fill="none" stroke="#444" stroke-width="1.5" marker-end="url(#arrow)"/>`
|
|
92
|
+
);
|
|
93
|
+
if (e.name) {
|
|
94
|
+
const m = e.waypoints[Math.floor(e.waypoints.length / 2)];
|
|
95
|
+
out.push(
|
|
96
|
+
`<text x="${m[0] + ox}" y="${m[1] + oy - 4}" text-anchor="middle" fill="#666" font-size="9">${esc(e.name)}</text>`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
for (const a of assocs) {
|
|
101
|
+
const pts = a.waypoints.map(([x, y]) => `${x + ox},${y + oy}`).join(' ');
|
|
102
|
+
out.push(
|
|
103
|
+
`<polyline points="${pts}" fill="none" stroke="#888" stroke-width="1" stroke-dasharray="3,3"/>`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
// annotations
|
|
107
|
+
for (const a of anns) {
|
|
108
|
+
const l = a.layout;
|
|
109
|
+
const x = l.x + ox;
|
|
110
|
+
const y = l.y + oy;
|
|
111
|
+
out.push(
|
|
112
|
+
`<rect x="${x}" y="${y}" width="${l.width}" height="${l.height}" fill="#fffbe6" stroke="#d4b106"/>`
|
|
113
|
+
);
|
|
114
|
+
out.push(
|
|
115
|
+
`<text x="${x + 4}" y="${y + 14}" font-size="9">${esc((a.text || '').slice(0, 40))}</text>`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
// leaf shapes on top
|
|
119
|
+
for (const n of leaves) {
|
|
120
|
+
const l = n.layout;
|
|
121
|
+
const x = l.x + ox;
|
|
122
|
+
const y = l.y + oy;
|
|
123
|
+
const cx = x + l.width / 2;
|
|
124
|
+
const cy = y + l.height / 2;
|
|
125
|
+
if (EVENT.has(n.type)) {
|
|
126
|
+
const sw = n.type === 'endEvent' ? 3 : 1.5;
|
|
127
|
+
out.push(
|
|
128
|
+
`<circle cx="${cx}" cy="${cy}" r="${l.width / 2}" fill="#fff" stroke="#333" stroke-width="${sw}"/>`
|
|
129
|
+
);
|
|
130
|
+
out.push(label(cx, y + l.height + 10, n.name));
|
|
131
|
+
} else if (n.type === 'exclusiveGateway') {
|
|
132
|
+
const p = `${cx},${y} ${x + l.width},${cy} ${cx},${y + l.height} ${x},${cy}`;
|
|
133
|
+
out.push(`<polygon points="${p}" fill="#fff" stroke="#333"/>`);
|
|
134
|
+
out.push(
|
|
135
|
+
`<text x="${cx}" y="${cy}" text-anchor="middle" dominant-baseline="middle" font-size="13">×</text>`
|
|
136
|
+
);
|
|
137
|
+
out.push(label(cx, y + l.height + 10, n.name));
|
|
138
|
+
} else {
|
|
139
|
+
const fill =
|
|
140
|
+
n.type === 'userTask' ? '#e6f4ff' : n.type === 'serviceTask' ? '#f0ffe6' : '#fff';
|
|
141
|
+
out.push(
|
|
142
|
+
`<rect x="${x}" y="${y}" width="${l.width}" height="${l.height}" rx="8" fill="${fill}" stroke="#333"/>`
|
|
143
|
+
);
|
|
144
|
+
out.push(label(cx, cy, n.name));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
out.push('</svg>');
|
|
148
|
+
return out.join('\n');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export { toSvg };
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
// Decompose a process-builder payload into editable files, and recompose it.
|
|
2
|
+
//
|
|
3
|
+
// Layout (see ai_tasks planning.md):
|
|
4
|
+
// process.yaml editable scalar overlay (identity + flags)
|
|
5
|
+
// structure.yaml the authoritative graph: nodes hold their own outgoing
|
|
6
|
+
// edges and (for subProcess) nested child nodes; each node
|
|
7
|
+
// carries its diagram layout {x,y,width,height}; each edge
|
|
8
|
+
// its waypoints. scriptTask bodies are referenced by file.
|
|
9
|
+
// scripts/NNNN_*.js one scriptTask body each, byte-exact
|
|
10
|
+
// pages/<formKey>.yml one userTask page object each
|
|
11
|
+
// .agrippa-pb.json manifest: all scalars verbatim, ns, full diagram, audit,
|
|
12
|
+
// id<->file maps
|
|
13
|
+
//
|
|
14
|
+
// The manifest carries every top-level scalar and the full diagram verbatim, so
|
|
15
|
+
// nothing is ever lost; process.yaml and the per-node layout/waypoints in
|
|
16
|
+
// structure.yaml *override* the manifest on recompose (so edits flow through).
|
|
17
|
+
|
|
18
|
+
import slugify from 'slugify';
|
|
19
|
+
import { Document, visit, parse as yamlParse, stringify as yamlStringify } from 'yaml';
|
|
20
|
+
import { computeChecksum } from './checksum.js';
|
|
21
|
+
import { buildProcess, compareDiagram, compareProcess, diff, parseProcess } from './pbModel.js';
|
|
22
|
+
|
|
23
|
+
const MANIFEST_FILE = '.agrippa-pb.json';
|
|
24
|
+
const STRUCTURE_FILE = 'structure.yaml';
|
|
25
|
+
const PROCESS_FILE = 'process.yaml';
|
|
26
|
+
|
|
27
|
+
// Scalar fields surfaced in process.yaml (editable). Everything else lives in
|
|
28
|
+
// the manifest. These override manifest scalars on recompose.
|
|
29
|
+
const EDITABLE_SCALARS = [
|
|
30
|
+
'document_id',
|
|
31
|
+
'process_name',
|
|
32
|
+
'version',
|
|
33
|
+
'icon',
|
|
34
|
+
'short_description',
|
|
35
|
+
'status',
|
|
36
|
+
'active',
|
|
37
|
+
'is_linear',
|
|
38
|
+
'execute_save',
|
|
39
|
+
'progressbar_enabled',
|
|
40
|
+
'start_date',
|
|
41
|
+
'end_date',
|
|
42
|
+
'favorite',
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
// Container node types whose children nest under them in structure.yaml.
|
|
46
|
+
const CONTAINER_TAGS = new Set(['subProcess', 'transaction']);
|
|
47
|
+
|
|
48
|
+
// Type-specific node fields carried verbatim between the flat model and structure.yaml.
|
|
49
|
+
const NODE_FIELDS = [
|
|
50
|
+
'attrs',
|
|
51
|
+
'class',
|
|
52
|
+
'fields',
|
|
53
|
+
'formKey',
|
|
54
|
+
'attachedToRef',
|
|
55
|
+
'errorEventDefinition',
|
|
56
|
+
'multiInstance',
|
|
57
|
+
'documentation',
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
function toSlug(name) {
|
|
61
|
+
return slugify(String(name ?? ''), { lower: true, strict: true }) || 'node';
|
|
62
|
+
}
|
|
63
|
+
function pad(n) {
|
|
64
|
+
return String(n).padStart(4, '0');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Render structure.yaml with layout/waypoints as compact inline (flow) collections.
|
|
68
|
+
function setFlowDeep(node) {
|
|
69
|
+
if (!node || !node.items) return;
|
|
70
|
+
node.flow = true;
|
|
71
|
+
for (const it of node.items) {
|
|
72
|
+
if (it?.items) setFlowDeep(it); // inner seq (e.g. a waypoint pair)
|
|
73
|
+
if (it?.value?.items) setFlowDeep(it.value);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function stringifyStructure(obj) {
|
|
77
|
+
const doc = new Document(obj);
|
|
78
|
+
visit(doc, {
|
|
79
|
+
Pair(_, pair) {
|
|
80
|
+
const key = pair.key?.value;
|
|
81
|
+
if (key === 'layout' || key === 'waypoints') setFlowDeep(pair.value);
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
return doc.toString({ lineWidth: 0 });
|
|
85
|
+
}
|
|
86
|
+
function pick(obj, keys) {
|
|
87
|
+
const o = {};
|
|
88
|
+
for (const k of keys) if (k in obj) o[k] = obj[k];
|
|
89
|
+
return o;
|
|
90
|
+
}
|
|
91
|
+
function omit(obj, keys) {
|
|
92
|
+
const set = new Set(keys);
|
|
93
|
+
const o = {};
|
|
94
|
+
for (const [k, v] of Object.entries(obj)) if (!set.has(k)) o[k] = v;
|
|
95
|
+
return o;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---------- geometry maps from a parsed diagram ----------
|
|
99
|
+
|
|
100
|
+
function geometryMaps(diagram) {
|
|
101
|
+
const bounds = {};
|
|
102
|
+
const waypoints = {};
|
|
103
|
+
const expanded = {};
|
|
104
|
+
if (diagram) {
|
|
105
|
+
for (const s of diagram.shapes) {
|
|
106
|
+
if (s.bounds) bounds[s.attrs.bpmnElement] = { ...s.bounds };
|
|
107
|
+
if (s.attrs.isExpanded !== undefined)
|
|
108
|
+
expanded[s.attrs.bpmnElement] = s.attrs.isExpanded;
|
|
109
|
+
}
|
|
110
|
+
for (const e of diagram.edges)
|
|
111
|
+
if (e.waypoints?.length)
|
|
112
|
+
waypoints[e.attrs.bpmnElement] = e.waypoints.map((w) => [w.x, w.y]);
|
|
113
|
+
}
|
|
114
|
+
return { bounds, waypoints, expanded };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ---------- nested structure.yaml (decompose side) ----------
|
|
118
|
+
|
|
119
|
+
function structEdge(e, geo) {
|
|
120
|
+
const o = { id: e.id, target: e.target };
|
|
121
|
+
if (e.name !== undefined) o.name = e.name;
|
|
122
|
+
if (e.condition !== undefined) o.condition = e.condition;
|
|
123
|
+
if (e.conditionType !== undefined) o.conditionType = e.conditionType;
|
|
124
|
+
if (e.documentation !== undefined) o.documentation = e.documentation;
|
|
125
|
+
if (geo.waypoints[e.id]) o.waypoints = geo.waypoints[e.id];
|
|
126
|
+
return o;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function structNode(n, model, scriptsMap, geo) {
|
|
130
|
+
const out = { id: n.id, type: n.type };
|
|
131
|
+
if (n.name !== undefined) out.name = n.name;
|
|
132
|
+
for (const k of NODE_FIELDS) if (n[k] !== undefined) out[k] = n[k];
|
|
133
|
+
if (n.type === 'scriptTask') out.script = scriptsMap[n.id];
|
|
134
|
+
if (geo.bounds[n.id]) out.layout = geo.bounds[n.id];
|
|
135
|
+
if (geo.expanded[n.id] !== undefined) out.expanded = geo.expanded[n.id];
|
|
136
|
+
const outEdges = model.edges.filter((e) => e.source === n.id).map((e) => structEdge(e, geo));
|
|
137
|
+
if (outEdges.length) out.edges = outEdges;
|
|
138
|
+
if (CONTAINER_TAGS.has(n.type)) {
|
|
139
|
+
const kids = nestNodes(model, n.id, scriptsMap, geo);
|
|
140
|
+
if (kids.length) out.nodes = kids;
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function nestNodes(model, parentId, scriptsMap, geo) {
|
|
146
|
+
return model.nodes
|
|
147
|
+
.filter((n) => (n.parent ?? null) === parentId)
|
|
148
|
+
.map((n) => structNode(n, model, scriptsMap, geo));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ---------- flatten structure.yaml back to model (recompose side) ----------
|
|
152
|
+
|
|
153
|
+
function flattenNodes(structNodes, parentId, model, read, geo) {
|
|
154
|
+
for (const sn of structNodes || []) {
|
|
155
|
+
const n = { id: sn.id, type: sn.type };
|
|
156
|
+
if (sn.name !== undefined) n.name = sn.name;
|
|
157
|
+
if (parentId) n.parent = parentId;
|
|
158
|
+
for (const k of NODE_FIELDS) if (sn[k] !== undefined) n[k] = sn[k];
|
|
159
|
+
if (sn.type === 'scriptTask') n.script = read(sn.script); // load body from file
|
|
160
|
+
if (sn.layout) geo.bounds[sn.id] = sn.layout;
|
|
161
|
+
if (sn.expanded !== undefined) geo.expanded[sn.id] = sn.expanded;
|
|
162
|
+
model.nodes.push(n);
|
|
163
|
+
|
|
164
|
+
for (const se of sn.edges || []) {
|
|
165
|
+
const e = { id: se.id, source: sn.id, target: se.target };
|
|
166
|
+
if (se.name !== undefined) e.name = se.name;
|
|
167
|
+
if (se.condition !== undefined) e.condition = se.condition;
|
|
168
|
+
if (se.conditionType !== undefined) e.conditionType = se.conditionType;
|
|
169
|
+
if (se.documentation !== undefined) e.documentation = se.documentation;
|
|
170
|
+
if (parentId) e.parent = parentId;
|
|
171
|
+
if (se.waypoints) geo.waypoints[se.id] = se.waypoints;
|
|
172
|
+
model.edges.push(e);
|
|
173
|
+
}
|
|
174
|
+
if (sn.nodes) flattenNodes(sn.nodes, sn.id, model, read, geo);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ---------- decompose: payload -> { files, manifest } ----------
|
|
179
|
+
|
|
180
|
+
function decompose(payload) {
|
|
181
|
+
const { ns, model, diagram } = parseProcess(payload.built_page);
|
|
182
|
+
const geo = geometryMaps(diagram);
|
|
183
|
+
const files = {};
|
|
184
|
+
|
|
185
|
+
// scripts — one file per scriptTask, document order, byte-exact body.
|
|
186
|
+
const scriptsMap = {};
|
|
187
|
+
let seq = 0;
|
|
188
|
+
const usedNames = new Set();
|
|
189
|
+
for (const n of model.nodes) {
|
|
190
|
+
if (n.type !== 'scriptTask') continue;
|
|
191
|
+
seq += 10;
|
|
192
|
+
let base = `${pad(seq)}_${toSlug(n.name || n.id)}`;
|
|
193
|
+
while (usedNames.has(base)) base += '_';
|
|
194
|
+
usedNames.add(base);
|
|
195
|
+
const path = `scripts/${base}.js`;
|
|
196
|
+
files[path] = n.script ?? '';
|
|
197
|
+
scriptsMap[n.id] = path;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// pages — one file per userTask page object; wrapper audit kept in manifest.
|
|
201
|
+
const pagesMap = {};
|
|
202
|
+
const usedPageNames = new Set();
|
|
203
|
+
for (const wrapper of payload.pages || []) {
|
|
204
|
+
const stepkey = wrapper.page?._id?.stepkey ?? wrapper.name;
|
|
205
|
+
const ut = model.nodes.find((x) => x.type === 'userTask' && x.id === stepkey);
|
|
206
|
+
let fname = toSlug(ut?.formKey || wrapper.name || stepkey);
|
|
207
|
+
while (usedPageNames.has(fname)) fname += '_';
|
|
208
|
+
usedPageNames.add(fname);
|
|
209
|
+
const path = `pages/${fname}.yml`;
|
|
210
|
+
files[path] = yamlStringify(wrapper.page, { lineWidth: 0 });
|
|
211
|
+
pagesMap[stepkey] = { file: path, wrapper: omit(wrapper, ['page']) };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// structure.yaml — nested graph: edges under their source node, subProcess
|
|
215
|
+
// children nested, layout/waypoints inline, scriptTask bodies by ref.
|
|
216
|
+
// Annotations/associations carry their own geometry too, so the diagram is
|
|
217
|
+
// fully regenerable from structure.yaml alone (the manifest is never read
|
|
218
|
+
// back for the diagram).
|
|
219
|
+
const structure = {
|
|
220
|
+
process: model.process,
|
|
221
|
+
errors: model.errors,
|
|
222
|
+
nodes: nestNodes(model, null, scriptsMap, geo),
|
|
223
|
+
annotations: model.annotations.map((a) =>
|
|
224
|
+
geo.bounds[a.id] ? { ...a, layout: geo.bounds[a.id] } : a
|
|
225
|
+
),
|
|
226
|
+
associations: model.associations.map((a) =>
|
|
227
|
+
geo.waypoints[a.id] ? { ...a, waypoints: geo.waypoints[a.id] } : a
|
|
228
|
+
),
|
|
229
|
+
};
|
|
230
|
+
files[STRUCTURE_FILE] = stringifyStructure(structure);
|
|
231
|
+
|
|
232
|
+
// process.yaml — editable scalar overlay.
|
|
233
|
+
files[PROCESS_FILE] = yamlStringify(pick(payload, EDITABLE_SCALARS), { lineWidth: 0 });
|
|
234
|
+
|
|
235
|
+
// manifest — scalars/audit/ns + id<->file maps. The diagram is NOT stored:
|
|
236
|
+
// it is regenerated from structure.yaml geometry on recompose.
|
|
237
|
+
const manifest = {
|
|
238
|
+
guid: payload.guid,
|
|
239
|
+
document_id: payload.document_id,
|
|
240
|
+
process_name: payload.process_name,
|
|
241
|
+
scalars: omit(payload, ['built_page', 'pages', 'process_structure']),
|
|
242
|
+
process_structure: payload.process_structure,
|
|
243
|
+
ns,
|
|
244
|
+
scripts: scriptsMap,
|
|
245
|
+
pages: pagesMap,
|
|
246
|
+
};
|
|
247
|
+
files[MANIFEST_FILE] = JSON.stringify(manifest, null, 2);
|
|
248
|
+
|
|
249
|
+
return { files, manifest };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ---------- recompose: read(path)->content -> payload ----------
|
|
253
|
+
|
|
254
|
+
function recompose(read) {
|
|
255
|
+
const manifest = JSON.parse(read(MANIFEST_FILE));
|
|
256
|
+
const structure = yamlParse(read(STRUCTURE_FILE));
|
|
257
|
+
const processOverlay = yamlParse(read(PROCESS_FILE)) || {};
|
|
258
|
+
|
|
259
|
+
const model = {
|
|
260
|
+
process: structure.process,
|
|
261
|
+
errors: structure.errors || [],
|
|
262
|
+
nodes: [],
|
|
263
|
+
edges: [],
|
|
264
|
+
annotations: structure.annotations || [],
|
|
265
|
+
associations: structure.associations || [],
|
|
266
|
+
};
|
|
267
|
+
const geo = { bounds: {}, waypoints: {}, expanded: {} };
|
|
268
|
+
flattenNodes(structure.nodes, null, model, read, geo);
|
|
269
|
+
// annotation/association geometry (top-level lists carry it inline)
|
|
270
|
+
for (const a of model.annotations) if (a.layout) geo.bounds[a.id] = a.layout;
|
|
271
|
+
for (const a of model.associations) if (a.waypoints) geo.waypoints[a.id] = a.waypoints;
|
|
272
|
+
|
|
273
|
+
const built_page = buildProcess({ ns: manifest.ns, model, geo });
|
|
274
|
+
|
|
275
|
+
const pages = [];
|
|
276
|
+
for (const info of Object.values(manifest.pages)) {
|
|
277
|
+
const pageObj = yamlParse(read(info.file));
|
|
278
|
+
pages.push({ ...info.wrapper, page: pageObj });
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
...manifest.scalars,
|
|
283
|
+
...processOverlay,
|
|
284
|
+
built_page,
|
|
285
|
+
pages,
|
|
286
|
+
process_structure: manifest.process_structure,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ---------- verify: decompose -> recompose -> semantic compare ----------
|
|
291
|
+
//
|
|
292
|
+
// 0-loss bar A: behavioral equivalence. Returns a list of human-readable
|
|
293
|
+
// differences (empty array == lossless round-trip).
|
|
294
|
+
|
|
295
|
+
function comparePayload(payload, rebuilt) {
|
|
296
|
+
const diffs = [];
|
|
297
|
+
|
|
298
|
+
// 1. logic graph (<process> subtree), formatting-insensitive
|
|
299
|
+
const procDiff = compareProcess(payload.built_page, rebuilt.built_page);
|
|
300
|
+
if (procDiff) diffs.push(`built_page <process>: ${procDiff}`);
|
|
301
|
+
|
|
302
|
+
// 2. diagram geometry — structural (drawing-order-insensitive, waypoints ordered)
|
|
303
|
+
const diaDiff = compareDiagram(payload.built_page, rebuilt.built_page);
|
|
304
|
+
if (diaDiff) diffs.push(`built_page <bpmndi>: ${diaDiff}`);
|
|
305
|
+
|
|
306
|
+
// 3. pages — deep-equal of page objects (jsonb semantics)
|
|
307
|
+
const pd = diff(payload.pages, rebuilt.pages, '$.pages');
|
|
308
|
+
if (pd) diffs.push(`pages: ${pd}`);
|
|
309
|
+
|
|
310
|
+
// 4. process_structure — verbatim
|
|
311
|
+
const psd = diff(payload.process_structure, rebuilt.process_structure, '$.process_structure');
|
|
312
|
+
if (psd) diffs.push(`process_structure: ${psd}`);
|
|
313
|
+
|
|
314
|
+
// 5. every other top-level scalar
|
|
315
|
+
const sd = diff(
|
|
316
|
+
omit(payload, ['built_page', 'pages', 'process_structure']),
|
|
317
|
+
omit(rebuilt, ['built_page', 'pages', 'process_structure']),
|
|
318
|
+
'$'
|
|
319
|
+
);
|
|
320
|
+
if (sd) diffs.push(`scalars: ${sd}`);
|
|
321
|
+
|
|
322
|
+
return diffs;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ---------- push helpers ----------
|
|
326
|
+
|
|
327
|
+
// Deterministic JSON (recursively sorted keys) for stable checksums.
|
|
328
|
+
function stableStringify(value) {
|
|
329
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`;
|
|
330
|
+
if (value && typeof value === 'object') {
|
|
331
|
+
return `{${Object.keys(value)
|
|
332
|
+
.sort()
|
|
333
|
+
.map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`)
|
|
334
|
+
.join(',')}}`;
|
|
335
|
+
}
|
|
336
|
+
return JSON.stringify(value ?? null);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Checksum of the recomposed payload — stable across runs, changes only when the
|
|
340
|
+
// local files change. clonePb stores this as checksum_at_pull; push recomputes it.
|
|
341
|
+
function localChecksum(read) {
|
|
342
|
+
return computeChecksum(stableStringify(recompose(read)));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Enumerate the wizard's pages from the pages/*.yml files (the authoritative
|
|
346
|
+
// local set). Each page links to its userTask via `page._id.stepkey`; the
|
|
347
|
+
// manifest supplies the upstream page guid (absent => a locally-added page that
|
|
348
|
+
// must be created). `pageFiles` is the list of relative page file paths.
|
|
349
|
+
function enumeratePages(read, pageFiles) {
|
|
350
|
+
const manifest = JSON.parse(read(MANIFEST_FILE));
|
|
351
|
+
return pageFiles.map((file) => {
|
|
352
|
+
const page = yamlParse(read(file));
|
|
353
|
+
const stepkey = page?._id?.stepkey;
|
|
354
|
+
const info = manifest.pages?.[stepkey];
|
|
355
|
+
return {
|
|
356
|
+
stepkey,
|
|
357
|
+
file,
|
|
358
|
+
page,
|
|
359
|
+
guid: info?.wrapper?.guid ?? null,
|
|
360
|
+
wrapper: info?.wrapper ?? null,
|
|
361
|
+
};
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// In-memory round-trip: decompose -> recompose from the produced file map -> compare.
|
|
366
|
+
function verifyRoundTrip(payload) {
|
|
367
|
+
const { files } = decompose(payload);
|
|
368
|
+
const read = (p) => {
|
|
369
|
+
if (!(p in files)) throw new Error(`recompose read missing file: ${p}`);
|
|
370
|
+
return files[p];
|
|
371
|
+
};
|
|
372
|
+
return comparePayload(payload, recompose(read));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export {
|
|
376
|
+
decompose,
|
|
377
|
+
recompose,
|
|
378
|
+
comparePayload,
|
|
379
|
+
verifyRoundTrip,
|
|
380
|
+
stableStringify,
|
|
381
|
+
localChecksum,
|
|
382
|
+
enumeratePages,
|
|
383
|
+
stringifyStructure,
|
|
384
|
+
pad,
|
|
385
|
+
MANIFEST_FILE,
|
|
386
|
+
STRUCTURE_FILE,
|
|
387
|
+
PROCESS_FILE,
|
|
388
|
+
EDITABLE_SCALARS,
|
|
389
|
+
toSlug,
|
|
390
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Disk IO for a decomposed process-builder project.
|
|
2
|
+
|
|
3
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'fs';
|
|
4
|
+
import { dirname, join } from 'path';
|
|
5
|
+
|
|
6
|
+
// Write a { path -> content } file map under baseDir. Content is written
|
|
7
|
+
// byte-exact (scripts must not be reformatted).
|
|
8
|
+
function writeProject(baseDir, files) {
|
|
9
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
10
|
+
const full = join(baseDir, rel);
|
|
11
|
+
mkdirSync(dirname(full), { recursive: true });
|
|
12
|
+
writeFileSync(full, content, 'utf-8');
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// A reader bound to baseDir, suitable for recompose().
|
|
17
|
+
function projectReader(baseDir) {
|
|
18
|
+
return (rel) => readFileSync(join(baseDir, rel), 'utf-8');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Relative paths of the page files under <baseDir>/pages.
|
|
22
|
+
function listPageFiles(baseDir) {
|
|
23
|
+
const dir = join(baseDir, 'pages');
|
|
24
|
+
if (!existsSync(dir)) return [];
|
|
25
|
+
return readdirSync(dir)
|
|
26
|
+
.filter((f) => f.endsWith('.yml') || f.endsWith('.yaml'))
|
|
27
|
+
.map((f) => `pages/${f}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export { writeProject, projectReader, listPageFiles };
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { mkdirSync, writeFileSync, readFileSync, existsSync } from 'fs';
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
|
|
3
2
|
import { dirname } from 'path';
|
|
3
|
+
import slugify from 'slugify';
|
|
4
|
+
import { stringify as yamlStringify } from 'yaml';
|
|
4
5
|
|
|
5
6
|
function toSlug(name) {
|
|
6
7
|
return slugify(name, { lower: true, strict: true });
|
|
@@ -39,4 +40,23 @@ function fileExists(filePath) {
|
|
|
39
40
|
return existsSync(filePath);
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
|
|
43
|
+
// Auto-generated, read-only context: dumps the workflow graph returned by the
|
|
44
|
+
// `agrippa_describe_workflow` MFA to `<dirPath>/workflow.yml`. Not tracked in the
|
|
45
|
+
// workspace -- it is not pushable, it is regenerated on every clone/pull.
|
|
46
|
+
function writeWorkflowDoc(dirPath, structure) {
|
|
47
|
+
const filePath = `${dirPath}/workflow.yml`;
|
|
48
|
+
ensureDir(filePath);
|
|
49
|
+
writeFileSync(filePath, yamlStringify(structure, { lineWidth: 0 }), 'utf-8');
|
|
50
|
+
return filePath;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export {
|
|
54
|
+
toSlug,
|
|
55
|
+
defaultPhasePath,
|
|
56
|
+
defaultMfaPath,
|
|
57
|
+
ensureDir,
|
|
58
|
+
writeCodeFile,
|
|
59
|
+
readCodeFile,
|
|
60
|
+
fileExists,
|
|
61
|
+
writeWorkflowDoc,
|
|
62
|
+
};
|
package/src/commands/autopr.js
CHANGED
|
@@ -78,7 +78,8 @@ async function fetchActivePr(branch) {
|
|
|
78
78
|
const res = await fetch(apiUrl, { headers: devopsHeaders() });
|
|
79
79
|
const data = await res.json();
|
|
80
80
|
if (!res.ok) throw new Error(`DevOps PR fetch failed: ${JSON.stringify(data)}`);
|
|
81
|
-
if (!data.value || data.value.length === 0)
|
|
81
|
+
if (!data.value || data.value.length === 0)
|
|
82
|
+
throw new Error(`No active PR found for branch: ${branch}`);
|
|
82
83
|
return data.value[0];
|
|
83
84
|
}
|
|
84
85
|
|
|
@@ -251,7 +252,9 @@ async function autoprAmend(options) {
|
|
|
251
252
|
const content = readFileSync(changelogPath, 'utf-8');
|
|
252
253
|
const existing = findLineByPrNumber(content, prNumber);
|
|
253
254
|
if (!existing) {
|
|
254
|
-
console.log(
|
|
255
|
+
console.log(
|
|
256
|
+
`Warning: no changelog line found for PR #${prNumber} — skipping changelog update`
|
|
257
|
+
);
|
|
255
258
|
return;
|
|
256
259
|
}
|
|
257
260
|
const lines = content.split('\n');
|
|
@@ -199,7 +199,10 @@ function appendRefsToLine(line, tridentIds, jiras) {
|
|
|
199
199
|
const rawCapture = tridentMatch[1];
|
|
200
200
|
const trailing = rawCapture.match(/([,\s]+)$/)?.[1] ?? '';
|
|
201
201
|
const existingTridents = rawCapture.replace(/[,\s]+$/, '');
|
|
202
|
-
result = result.replace(
|
|
202
|
+
result = result.replace(
|
|
203
|
+
/Trident (#[\d, #]+)/,
|
|
204
|
+
`Trident ${existingTridents}, ${suffix}${trailing}`
|
|
205
|
+
);
|
|
203
206
|
} else {
|
|
204
207
|
const parenMatch = result.match(/\(([^)]*)\)\s*$/);
|
|
205
208
|
if (parenMatch) {
|
package/src/commands/export.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { exportPb } from './exportPb.js';
|
|
2
|
-
import { exportImperex } from './exportImperex.js';
|
|
3
1
|
import { exportEmailTemplates } from './exportEmailTemplates.js';
|
|
4
|
-
import {
|
|
2
|
+
import { exportImperex } from './exportImperex.js';
|
|
5
3
|
import { exportLrp } from './exportLrp.js';
|
|
4
|
+
import { exportPb } from './exportPb.js';
|
|
5
|
+
import { exportWorkflow } from './exportWorkflow.js';
|
|
6
6
|
|
|
7
7
|
function exportRip() {
|
|
8
8
|
console.log('Not implemented yet.');
|