@nanobpm/urban 0.52.0 → 0.53.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 +47 -20
- package/dist/runtime/core/events.d.ts +135 -0
- package/dist/runtime/core/events.js +329 -0
- package/dist/runtime/core/extensions.d.ts +131 -0
- package/dist/runtime/core/extensions.js +137 -0
- package/dist/runtime/core/glob.d.ts +7 -6
- package/dist/runtime/core/glob.js +16 -14
- package/dist/runtime/core/host.d.ts +4 -1
- package/dist/runtime/core/modules/agent.d.ts +17 -0
- package/dist/runtime/core/modules/agent.js +63 -0
- package/dist/runtime/core/modules/deploy.d.ts +8 -5
- package/dist/runtime/core/modules/deploy.js +62 -16
- package/dist/runtime/core/modules/pages.js +397 -20
- package/dist/runtime/core/modules/surfaces.js +8 -0
- package/dist/runtime/core/phase.d.ts +168 -0
- package/dist/runtime/core/phase.js +367 -0
- package/dist/runtime/core/runtime.d.ts +17 -0
- package/dist/runtime/core/runtime.js +30 -0
- package/dist/runtime/index.d.ts +6 -0
- package/dist/runtime/index.js +7 -0
- package/dist/toolkit/derivers/system-brief.d.ts +62 -0
- package/dist/toolkit/derivers/system-brief.js +156 -0
- package/dist/toolkit/gen.d.ts +5 -5
- package/dist/toolkit/gen.js +20 -14
- package/dist/toolkit/index.d.ts +5 -0
- package/dist/toolkit/index.js +3 -0
- package/dist/toolkit/models.d.ts +4 -3
- package/dist/toolkit/models.js +4 -3
- package/package.json +1 -1
- package/src/runtime/core/events.test.ts +382 -0
- package/src/runtime/core/events.ts +370 -0
- package/src/runtime/core/extensions.test.ts +378 -0
- package/src/runtime/core/extensions.ts +276 -0
- package/src/runtime/core/glob.ts +15 -12
- package/src/runtime/core/host.ts +4 -1
- package/src/runtime/core/modules/agent.test.ts +104 -0
- package/src/runtime/core/modules/agent.ts +76 -0
- package/src/runtime/core/modules/deploy.test.ts +84 -16
- package/src/runtime/core/modules/deploy.ts +59 -16
- package/src/runtime/core/modules/pages.test.ts +309 -5
- package/src/runtime/core/modules/pages.ts +398 -20
- package/src/runtime/core/modules/surfaces.ts +9 -0
- package/src/runtime/core/phase.test.ts +350 -0
- package/src/runtime/core/phase.ts +526 -0
- package/src/runtime/core/runtime.extensions.test.ts +151 -0
- package/src/runtime/core/runtime.ts +49 -0
- package/src/runtime/devserver.test.ts +3 -0
- package/src/runtime/index.ts +57 -0
- package/src/toolkit/derivers/system-brief.test.ts +150 -0
- package/src/toolkit/derivers/system-brief.ts +210 -0
- package/src/toolkit/gen.test.ts +28 -4
- package/src/toolkit/gen.ts +20 -13
- package/src/toolkit/index.ts +14 -0
- package/src/toolkit/models.ts +4 -3
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
import { test } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import {
|
|
4
|
+
buildScopeIndex,
|
|
5
|
+
derivePhase,
|
|
6
|
+
deriveInstancePhases,
|
|
7
|
+
furthestReached,
|
|
8
|
+
rollupLineagePhase,
|
|
9
|
+
SCOPE_CONTAINERS,
|
|
10
|
+
type InstancePhase,
|
|
11
|
+
type ProvenanceProgressRow,
|
|
12
|
+
} from "./phase.ts";
|
|
13
|
+
import { buildLineageTree, type LineageEdge } from "./lineage.ts";
|
|
14
|
+
|
|
15
|
+
// A representative model in the spirit of the issue's motivating `plan-fanout.bpmn`: a process
|
|
16
|
+
// with named subProcesses ("Plan", "Implement task") wrapping tasks, a flat task at process level,
|
|
17
|
+
// a nano:phase override (attribute form), and a zeebe:property override.
|
|
18
|
+
const MODEL = `<?xml version="1.0" encoding="UTF-8"?>
|
|
19
|
+
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:zeebe="http://camunda.org/schema/zeebe/1.0" xmlns:nano="http://nanobpm.io/schema/1.0">
|
|
20
|
+
<bpmn:process id="plan-fanout" name="Plan Fanout">
|
|
21
|
+
<bpmn:startEvent id="start" name="Start" />
|
|
22
|
+
<bpmn:subProcess id="plan" name="Plan">
|
|
23
|
+
<bpmn:serviceTask id="draft-plan" name="Draft plan" />
|
|
24
|
+
<bpmn:userTask id="review-plan" name="Review plan" />
|
|
25
|
+
</bpmn:subProcess>
|
|
26
|
+
<bpmn:subProcess id="implement" name="Implement task">
|
|
27
|
+
<bpmn:serviceTask id="write-code" name="Write code">
|
|
28
|
+
<bpmn:extensionElements>
|
|
29
|
+
<zeebe:properties>
|
|
30
|
+
<zeebe:property name="nano:phase" value="Coding (agent)" />
|
|
31
|
+
</zeebe:properties>
|
|
32
|
+
</bpmn:extensionElements>
|
|
33
|
+
</bpmn:serviceTask>
|
|
34
|
+
<bpmn:subProcess id="trial-merge" name="Trial merge">
|
|
35
|
+
<bpmn:serviceTask id="merge-attempt" name="Merge attempt" />
|
|
36
|
+
</bpmn:subProcess>
|
|
37
|
+
</bpmn:subProcess>
|
|
38
|
+
<bpmn:serviceTask id="finalize" name="Finalize plan" nano:phase="Wrap up" />
|
|
39
|
+
<bpmn:endEvent id="dispatched" name="Dispatched" />
|
|
40
|
+
</bpmn:process>
|
|
41
|
+
</bpmn:definitions>`;
|
|
42
|
+
|
|
43
|
+
test("SCOPE_CONTAINERS names the process + nestable scope families", () => {
|
|
44
|
+
assert.deepEqual([...SCOPE_CONTAINERS], ["process", "subProcess", "adHocSubProcess", "transaction"]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("buildScopeIndex records the process and every id-bearing element with its scope chain", () => {
|
|
48
|
+
const idx = buildScopeIndex(MODEL);
|
|
49
|
+
assert.equal(idx.process?.id, "plan-fanout");
|
|
50
|
+
assert.equal(idx.process?.name, "Plan Fanout");
|
|
51
|
+
|
|
52
|
+
// A flat task at process level: chain is just the process.
|
|
53
|
+
assert.deepEqual(idx.elements.get("start")?.scopeChain, ["plan-fanout"]);
|
|
54
|
+
// A task inside a top-level subProcess.
|
|
55
|
+
assert.deepEqual(idx.elements.get("draft-plan")?.scopeChain, ["plan-fanout", "plan"]);
|
|
56
|
+
// A task inside a nested subProcess: full outermost→innermost chain.
|
|
57
|
+
assert.deepEqual(idx.elements.get("merge-attempt")?.scopeChain, [
|
|
58
|
+
"plan-fanout",
|
|
59
|
+
"implement",
|
|
60
|
+
"trial-merge",
|
|
61
|
+
]);
|
|
62
|
+
// Scope containers are flagged and carry their own chain (excluding themselves).
|
|
63
|
+
assert.equal(idx.elements.get("trial-merge")?.isScope, true);
|
|
64
|
+
assert.deepEqual(idx.elements.get("trial-merge")?.scopeChain, ["plan-fanout", "implement"]);
|
|
65
|
+
assert.equal(idx.elements.get("write-code")?.isScope, false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("buildScopeIndex captures nano:phase overrides in attribute and zeebe:property forms", () => {
|
|
69
|
+
const idx = buildScopeIndex(MODEL);
|
|
70
|
+
// Attribute form on a flat task.
|
|
71
|
+
assert.equal(idx.elements.get("finalize")?.phaseOverride, "Wrap up");
|
|
72
|
+
// zeebe:property form nested in extensionElements attaches to the owning service task, not to a
|
|
73
|
+
// sibling or ancestor.
|
|
74
|
+
assert.equal(idx.elements.get("write-code")?.phaseOverride, "Coding (agent)");
|
|
75
|
+
assert.equal(idx.elements.get("implement")?.phaseOverride, undefined);
|
|
76
|
+
// No override → undefined.
|
|
77
|
+
assert.equal(idx.elements.get("draft-plan")?.phaseOverride, undefined);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("buildScopeIndex ignores over-broad matches: bare `phase` attr and non-zeebe/loose `property`", () => {
|
|
81
|
+
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
82
|
+
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:zeebe="http://camunda.org/schema/zeebe/1.0">
|
|
83
|
+
<bpmn:process id="p" name="P">
|
|
84
|
+
<!-- A bare, unprefixed phase="…" attribute must NOT be treated as an override. -->
|
|
85
|
+
<bpmn:serviceTask id="bare-attr" name="Bare attr" phase="not an override" />
|
|
86
|
+
<!-- A bpmn:property (non-zeebe) carrying name="nano:phase" must NOT override. -->
|
|
87
|
+
<bpmn:serviceTask id="bpmn-prop" name="Bpmn prop">
|
|
88
|
+
<bpmn:extensionElements>
|
|
89
|
+
<bpmn:property name="nano:phase" value="not an override" />
|
|
90
|
+
</bpmn:extensionElements>
|
|
91
|
+
</bpmn:serviceTask>
|
|
92
|
+
<!-- A zeebe:property outside any extensionElements block must NOT override. -->
|
|
93
|
+
<bpmn:serviceTask id="loose-prop" name="Loose prop">
|
|
94
|
+
<zeebe:property name="nano:phase" value="not an override" />
|
|
95
|
+
</bpmn:serviceTask>
|
|
96
|
+
</bpmn:process>
|
|
97
|
+
</bpmn:definitions>`;
|
|
98
|
+
const idx = buildScopeIndex(xml);
|
|
99
|
+
assert.equal(idx.elements.get("bare-attr")?.phaseOverride, undefined);
|
|
100
|
+
assert.equal(idx.elements.get("bpmn-prop")?.phaseOverride, undefined);
|
|
101
|
+
assert.equal(idx.elements.get("loose-prop")?.phaseOverride, undefined);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("derivePhase: a scope element is its own leaf crumb, appended exactly once (no duplicate)", () => {
|
|
105
|
+
// Querying a subProcess id (an element that IS a scope) must place it as the leaf crumb after its
|
|
106
|
+
// enclosing scopes — never omitted and never duplicated. Guards the "always append the element as
|
|
107
|
+
// its own leaf" contract, since `scopeChain` excludes the element itself.
|
|
108
|
+
const idx = buildScopeIndex(MODEL);
|
|
109
|
+
const phase = derivePhase(idx, "trial-merge");
|
|
110
|
+
assert.ok(phase);
|
|
111
|
+
assert.deepEqual(
|
|
112
|
+
phase.breadcrumb.map((c) => c.label),
|
|
113
|
+
["Plan Fanout", "Implement task", "Trial merge"],
|
|
114
|
+
);
|
|
115
|
+
assert.deepEqual(
|
|
116
|
+
phase.breadcrumb.map((c) => c.kind),
|
|
117
|
+
["process", "subProcess", "subProcess"],
|
|
118
|
+
);
|
|
119
|
+
assert.equal(phase.fine, "Trial merge");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("derivePhase builds a breadcrumb of enclosing named scopes down to the element", () => {
|
|
123
|
+
const idx = buildScopeIndex(MODEL);
|
|
124
|
+
const phase = derivePhase(idx, "review-plan");
|
|
125
|
+
assert.ok(phase);
|
|
126
|
+
assert.deepEqual(
|
|
127
|
+
phase.breadcrumb.map((c) => c.label),
|
|
128
|
+
["Plan Fanout", "Plan", "Review plan"],
|
|
129
|
+
);
|
|
130
|
+
assert.deepEqual(
|
|
131
|
+
phase.breadcrumb.map((c) => c.kind),
|
|
132
|
+
["process", "subProcess", "element"],
|
|
133
|
+
);
|
|
134
|
+
assert.equal(phase.process, "Plan Fanout");
|
|
135
|
+
assert.equal(phase.coarse, "Plan");
|
|
136
|
+
assert.equal(phase.fine, "Review plan");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("derivePhase: coarse is the top-level subProcess even when nested deeper", () => {
|
|
140
|
+
const idx = buildScopeIndex(MODEL);
|
|
141
|
+
const phase = derivePhase(idx, "merge-attempt");
|
|
142
|
+
assert.ok(phase);
|
|
143
|
+
assert.deepEqual(
|
|
144
|
+
phase.breadcrumb.map((c) => c.label),
|
|
145
|
+
["Plan Fanout", "Implement task", "Trial merge", "Merge attempt"],
|
|
146
|
+
);
|
|
147
|
+
// Outermost meaningful scope = the top-level subProcess directly under the process.
|
|
148
|
+
assert.equal(phase.coarse, "Implement task");
|
|
149
|
+
assert.equal(phase.fine, "Merge attempt");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("derivePhase: a flat (process-level) element yields a task-granular phase for free", () => {
|
|
153
|
+
const idx = buildScopeIndex(MODEL);
|
|
154
|
+
const phase = derivePhase(idx, "start");
|
|
155
|
+
assert.ok(phase);
|
|
156
|
+
assert.deepEqual(
|
|
157
|
+
phase.breadcrumb.map((c) => c.label),
|
|
158
|
+
["Plan Fanout", "Start"],
|
|
159
|
+
);
|
|
160
|
+
// No subProcess → coarse falls back to the process.
|
|
161
|
+
assert.equal(phase.coarse, "Plan Fanout");
|
|
162
|
+
assert.equal(phase.fine, "Start");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("derivePhase granularity: auto is subProcess-then-element", () => {
|
|
166
|
+
const idx = buildScopeIndex(MODEL);
|
|
167
|
+
// Inside a subProcess → auto picks the coarse subProcess label.
|
|
168
|
+
assert.equal(derivePhase(idx, "review-plan")?.label, "Plan");
|
|
169
|
+
// Flat → auto falls to the element label.
|
|
170
|
+
assert.equal(derivePhase(idx, "start")?.label, "Start");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("derivePhase granularity: subProcess and element force a level", () => {
|
|
174
|
+
const idx = buildScopeIndex(MODEL);
|
|
175
|
+
assert.equal(derivePhase(idx, "merge-attempt", { granularity: "subProcess" })?.label, "Implement task");
|
|
176
|
+
assert.equal(derivePhase(idx, "merge-attempt", { granularity: "element" })?.label, "Merge attempt");
|
|
177
|
+
// A flat element forced to subProcess granularity falls back to the process (no subProcess).
|
|
178
|
+
assert.equal(derivePhase(idx, "start", { granularity: "subProcess" })?.label, "Plan Fanout");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("derivePhase: Tier-1 override replaces the structural label and flags its source", () => {
|
|
182
|
+
const idx = buildScopeIndex(MODEL);
|
|
183
|
+
const attrForm = derivePhase(idx, "finalize");
|
|
184
|
+
assert.ok(attrForm);
|
|
185
|
+
assert.equal(attrForm.fine, "Wrap up");
|
|
186
|
+
assert.equal(attrForm.breadcrumb.at(-1)?.source, "override");
|
|
187
|
+
|
|
188
|
+
const propForm = derivePhase(idx, "write-code");
|
|
189
|
+
assert.ok(propForm);
|
|
190
|
+
assert.equal(propForm.fine, "Coding (agent)");
|
|
191
|
+
assert.equal(propForm.breadcrumb.at(-1)?.source, "override");
|
|
192
|
+
// Its structural ancestors remain structural.
|
|
193
|
+
assert.equal(propForm.breadcrumb[0].source, "structural");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("derivePhase: unnamed element falls back to its id (never blank)", () => {
|
|
197
|
+
const idx = buildScopeIndex(
|
|
198
|
+
`<bpmn:process id="p"><bpmn:task id="t42" /></bpmn:process>`,
|
|
199
|
+
);
|
|
200
|
+
const phase = derivePhase(idx, "t42");
|
|
201
|
+
assert.ok(phase);
|
|
202
|
+
assert.equal(phase.fine, "t42");
|
|
203
|
+
assert.equal(phase.breadcrumb.at(-1)?.source, "structural");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("derivePhase returns undefined for an unknown element", () => {
|
|
207
|
+
const idx = buildScopeIndex(MODEL);
|
|
208
|
+
assert.equal(derivePhase(idx, "no-such-element"), undefined);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("derivePhase tolerates namespace prefixes and entity-encoded names", () => {
|
|
212
|
+
const idx = buildScopeIndex(
|
|
213
|
+
`<process id="p" name="A & B"><subProcess id="s" name="Wave <1>"><task id="t" name="Go" /></subProcess></process>`,
|
|
214
|
+
);
|
|
215
|
+
const phase = derivePhase(idx, "t");
|
|
216
|
+
assert.ok(phase);
|
|
217
|
+
assert.deepEqual(
|
|
218
|
+
phase.breadcrumb.map((c) => c.label),
|
|
219
|
+
["A & B", "Wave <1>", "Go"],
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("buildScopeIndex tolerates a literal `>` inside a quoted attribute value (no tag truncation)", () => {
|
|
224
|
+
const idx = buildScopeIndex(
|
|
225
|
+
`<process id="p" name="Root"><subProcess id="s" name="Wave > 1"><task id="t" name="Go" nano:phase="P > Q" /></subProcess></process>`,
|
|
226
|
+
);
|
|
227
|
+
// The `>` inside `name="Wave > 1"` must not truncate the subProcess tag: its id/name and the
|
|
228
|
+
// task's namespaced phase override (which follows the inner `>`) all parse intact.
|
|
229
|
+
assert.equal(idx.elements.get("s")?.name, "Wave > 1");
|
|
230
|
+
assert.equal(idx.elements.get("t")?.name, "Go");
|
|
231
|
+
assert.equal(idx.elements.get("t")?.phaseOverride, "P > Q");
|
|
232
|
+
assert.deepEqual(idx.elements.get("t")?.scopeChain, ["p", "s"]);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("buildScopeIndex parses single-quoted attribute values (id/name/nano:phase, valid XML)", () => {
|
|
236
|
+
// XML permits single-quoted attribute values; `attr()`/`phaseAttr()` must recognize them so
|
|
237
|
+
// ids, names, and namespaced phase overrides are not silently dropped (which would break scope
|
|
238
|
+
// chains). A double-quoted value that itself contains an apostrophe must survive intact too.
|
|
239
|
+
const idx = buildScopeIndex(
|
|
240
|
+
`<process id='p' name='Root'><subProcess id='s' name="Ada's Wave"><task id='t' name='Go' nano:phase='Do & Review' /></subProcess></process>`,
|
|
241
|
+
);
|
|
242
|
+
assert.equal(idx.elements.get("s")?.name, "Ada's Wave");
|
|
243
|
+
assert.equal(idx.elements.get("t")?.name, "Go");
|
|
244
|
+
assert.equal(idx.elements.get("t")?.phaseOverride, "Do & Review");
|
|
245
|
+
assert.deepEqual(idx.elements.get("t")?.scopeChain, ["p", "s"]);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("furthestReached picks the greatest seq per instance and ignores null joins", () => {
|
|
249
|
+
const rows: ProvenanceProgressRow[] = [
|
|
250
|
+
{ instance_key: "i1", element_id: "start", seq: 1, at: "2024-01-01T00:00:00Z" },
|
|
251
|
+
{ instance_key: "i1", element_id: "review-plan", seq: 5, at: "2024-01-01T00:05:00Z" },
|
|
252
|
+
{ instance_key: "i1", element_id: "draft-plan", seq: 3, at: "2024-01-01T00:03:00Z" },
|
|
253
|
+
{ instance_key: "i2", element_id: "finalize", seq: 2, at: "2024-01-01T00:02:00Z" },
|
|
254
|
+
{ instance_key: null, element_id: "x", seq: 9 },
|
|
255
|
+
{ instance_key: "i3", element_id: null, seq: 9 },
|
|
256
|
+
];
|
|
257
|
+
const f = furthestReached(rows);
|
|
258
|
+
assert.equal(f.get("i1")?.elementId, "review-plan");
|
|
259
|
+
assert.equal(f.get("i1")?.seq, 5);
|
|
260
|
+
assert.equal(f.get("i2")?.elementId, "finalize");
|
|
261
|
+
assert.equal(f.has("i3"), false);
|
|
262
|
+
assert.equal(f.has(""), false);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("deriveInstancePhases projects furthest element → phase per instance", () => {
|
|
266
|
+
const idx = buildScopeIndex(MODEL);
|
|
267
|
+
const rows: ProvenanceProgressRow[] = [
|
|
268
|
+
{ instance_key: "i1", element_id: "draft-plan", seq: 1, at: "2024-01-01T00:01:00Z" },
|
|
269
|
+
{ instance_key: "i1", element_id: "review-plan", seq: 2, at: "2024-01-01T00:02:00Z" },
|
|
270
|
+
{ instance_key: "i2", element_id: "merge-attempt", seq: 3, at: "2024-01-01T00:03:00Z" },
|
|
271
|
+
// Furthest element belongs to a different deployed model → skipped.
|
|
272
|
+
{ instance_key: "i3", element_id: "foreign-element", seq: 4, at: "2024-01-01T00:04:00Z" },
|
|
273
|
+
];
|
|
274
|
+
const phases = deriveInstancePhases(idx, rows);
|
|
275
|
+
assert.equal(phases.get("i1")?.phase.label, "Plan");
|
|
276
|
+
assert.equal(phases.get("i1")?.seq, 2);
|
|
277
|
+
assert.equal(phases.get("i1")?.at, "2024-01-01T00:02:00Z");
|
|
278
|
+
assert.equal(phases.get("i2")?.phase.coarse, "Implement task");
|
|
279
|
+
assert.equal(phases.has("i3"), false);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("rollupLineagePhase: epic frontier is the most-recently-advanced contributor", () => {
|
|
283
|
+
// Parent epic i-root fans out to two children; the child that advanced latest defines the frontier.
|
|
284
|
+
const edges: LineageEdge[] = [
|
|
285
|
+
{ rootRequestKey: "i-root", instanceKey: "i-child-a", causedByInstanceKey: "i-root", edgeType: "weak" },
|
|
286
|
+
{ rootRequestKey: "i-root", instanceKey: "i-child-b", causedByInstanceKey: "i-root", edgeType: "weak" },
|
|
287
|
+
];
|
|
288
|
+
const tree = buildLineageTree("i-root", edges);
|
|
289
|
+
const idx = buildScopeIndex(MODEL);
|
|
290
|
+
const rows: ProvenanceProgressRow[] = [
|
|
291
|
+
{ instance_key: "i-root", element_id: "draft-plan", seq: 1, at: "2024-01-01T00:01:00Z" },
|
|
292
|
+
{ instance_key: "i-child-a", element_id: "review-plan", seq: 2, at: "2024-01-01T00:02:00Z" },
|
|
293
|
+
{ instance_key: "i-child-b", element_id: "merge-attempt", seq: 3, at: "2024-01-01T00:09:00Z" },
|
|
294
|
+
];
|
|
295
|
+
const phases = deriveInstancePhases(idx, rows);
|
|
296
|
+
const rollup = rollupLineagePhase(tree, phases);
|
|
297
|
+
assert.equal(rollup.rootRequestKey, "i-root");
|
|
298
|
+
assert.equal(rollup.frontier?.instanceKey, "i-child-b");
|
|
299
|
+
assert.equal(rollup.frontier?.phase.coarse, "Implement task");
|
|
300
|
+
assert.equal(rollup.contributions.length, 3);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("rollupLineagePhase: frontier breaks ties on seq and folds in unattached nodes", () => {
|
|
304
|
+
const tree = buildLineageTree("i-root", [
|
|
305
|
+
{ rootRequestKey: "i-root", instanceKey: "i-root", edgeType: "weak" },
|
|
306
|
+
]);
|
|
307
|
+
// Two contributors with the SAME `at` — greater seq wins the tie.
|
|
308
|
+
const phaseByInstance = new Map<string, InstancePhase>([
|
|
309
|
+
[
|
|
310
|
+
"i-root",
|
|
311
|
+
{ instanceKey: "i-root", phase: derivePhase(buildScopeIndex(MODEL), "start")!, seq: 1, at: "t" },
|
|
312
|
+
],
|
|
313
|
+
// An orphan node not reachable from root but present via the phase map is still considered.
|
|
314
|
+
[
|
|
315
|
+
"i-orphan",
|
|
316
|
+
{ instanceKey: "i-orphan", phase: derivePhase(buildScopeIndex(MODEL), "finalize")!, seq: 9, at: "t" },
|
|
317
|
+
],
|
|
318
|
+
]);
|
|
319
|
+
// Attach the orphan to tree.nodes so the rollup can see it.
|
|
320
|
+
const treeWithOrphan = {
|
|
321
|
+
...tree,
|
|
322
|
+
nodes: [...tree.nodes, { instanceKey: "i-orphan", attachments: [], children: [] }],
|
|
323
|
+
};
|
|
324
|
+
const rollup = rollupLineagePhase(treeWithOrphan, phaseByInstance);
|
|
325
|
+
assert.equal(rollup.frontier?.instanceKey, "i-orphan");
|
|
326
|
+
assert.equal(rollup.contributions.length, 2);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test("rollupLineagePhase: no phased nodes → no frontier", () => {
|
|
330
|
+
const tree = buildLineageTree("i-root", [
|
|
331
|
+
{ rootRequestKey: "i-root", instanceKey: "i-root", edgeType: "weak" },
|
|
332
|
+
]);
|
|
333
|
+
const rollup = rollupLineagePhase(tree, new Map());
|
|
334
|
+
assert.equal(rollup.frontier, undefined);
|
|
335
|
+
assert.deepEqual(rollup.contributions, []);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test("buildScopeIndex is resilient to comments, CDATA and self-closing scopes", () => {
|
|
339
|
+
const idx = buildScopeIndex(
|
|
340
|
+
`<bpmn:process id="p" name="P">
|
|
341
|
+
<!-- a comment with <fake id="x" /> inside -->
|
|
342
|
+
<bpmn:subProcess id="s" name="S">
|
|
343
|
+
<bpmn:serviceTask id="t" name="T"><![CDATA[<not id="y"/>]]></bpmn:serviceTask>
|
|
344
|
+
</bpmn:subProcess>
|
|
345
|
+
</bpmn:process>`,
|
|
346
|
+
);
|
|
347
|
+
assert.equal(idx.elements.has("x"), false);
|
|
348
|
+
assert.equal(idx.elements.has("y"), false);
|
|
349
|
+
assert.deepEqual(idx.elements.get("t")?.scopeChain, ["p", "s"]);
|
|
350
|
+
});
|