libpetri 1.8.5 → 2.1.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.
Files changed (46) hide show
  1. package/README.md +47 -0
  2. package/dist/{chunk-B2D5DMTO.js → chunk-B2CV5M3L.js} +228 -9
  3. package/dist/chunk-B2CV5M3L.js.map +1 -0
  4. package/dist/chunk-H62Z76FY.js +1346 -0
  5. package/dist/chunk-H62Z76FY.js.map +1 -0
  6. package/dist/chunk-RNWTYK5B.js +419 -0
  7. package/dist/chunk-RNWTYK5B.js.map +1 -0
  8. package/dist/chunk-SXK2Z45Z.js +50 -0
  9. package/dist/chunk-SXK2Z45Z.js.map +1 -0
  10. package/dist/debug/index.d.ts +50 -3
  11. package/dist/debug/index.js +64 -6
  12. package/dist/debug/index.js.map +1 -1
  13. package/dist/doclet/index.d.ts +152 -31
  14. package/dist/doclet/index.js +458 -57
  15. package/dist/doclet/index.js.map +1 -1
  16. package/dist/doclet/resources/petrinet-diagrams.css +640 -7
  17. package/dist/doclet/resources/petrinet-diagrams.js +7238 -114
  18. package/dist/dot-exporter-WJMCJEFK.js +8 -0
  19. package/dist/{event-store-BnyHh3TF.d.ts → event-store-2zkXeQkd.d.ts} +1 -1
  20. package/dist/export/index.d.ts +18 -4
  21. package/dist/export/index.js +1 -1
  22. package/dist/index.d.ts +41 -5
  23. package/dist/index.js +1221 -35
  24. package/dist/index.js.map +1 -1
  25. package/dist/petri-net-BDrj4XZE.d.ts +1461 -0
  26. package/dist/render-QK57X4TP.js +73 -0
  27. package/dist/render-QK57X4TP.js.map +1 -0
  28. package/dist/verification/index.d.ts +3 -144
  29. package/dist/verification/index.js +30 -1214
  30. package/dist/verification/index.js.map +1 -1
  31. package/dist/viewer/index.d.ts +227 -0
  32. package/dist/viewer/index.js +877 -0
  33. package/dist/viewer/index.js.map +1 -0
  34. package/dist/viewer/layout/index.d.ts +200 -0
  35. package/dist/viewer/layout/index.js +15 -0
  36. package/dist/viewer/layout/index.js.map +1 -0
  37. package/dist/viewer/viewer-static.iife.js +3 -0
  38. package/dist/viewer/viewer.css +759 -0
  39. package/dist/viewer/viewer.iife.js +7243 -0
  40. package/package.json +28 -8
  41. package/dist/chunk-B2D5DMTO.js.map +0 -1
  42. package/dist/chunk-VQ4XMJTD.js +0 -107
  43. package/dist/chunk-VQ4XMJTD.js.map +0 -1
  44. package/dist/dot-exporter-3CVCH6J4.js +0 -8
  45. package/dist/petri-net-D-GN9g_D.d.ts +0 -570
  46. /package/dist/{dot-exporter-3CVCH6J4.js.map → dot-exporter-WJMCJEFK.js.map} +0 -0
@@ -0,0 +1,419 @@
1
+ // src/viewer/layout/preprocess.ts
2
+ function classifyKind(id) {
3
+ if (id.startsWith("p_")) return "place";
4
+ if (id.startsWith("t_")) return "transition";
5
+ return "junction";
6
+ }
7
+ function parseAttrs(body) {
8
+ const attrs = {};
9
+ for (const m of body.matchAll(/(\w+)=("(?:[^"]|\\.)*"|\S+?)(?=[\s,]|$)/g)) {
10
+ const [, key, raw] = m;
11
+ const v = raw.startsWith('"') && raw.endsWith('"') ? raw.slice(1, -1) : raw;
12
+ attrs[key] = v;
13
+ }
14
+ return attrs;
15
+ }
16
+ function parseLibpetriDot(dot) {
17
+ const nodes = /* @__PURE__ */ new Map();
18
+ for (const m of dot.matchAll(/^\s*([pjt]_[A-Za-z0-9_]+)\s*\[(.*)\];\s*$/gm)) {
19
+ const [, id, body] = m;
20
+ nodes.set(id, { id, kind: classifyKind(id), attrs: parseAttrs(body) });
21
+ }
22
+ const clusters = /* @__PURE__ */ new Map();
23
+ const nodeToCluster = /* @__PURE__ */ new Map();
24
+ for (const cm of dot.matchAll(/subgraph (cluster_\w+)\s*\{([^]*?)^\s*\}/gm)) {
25
+ const [, clusterId, body] = cm;
26
+ const memberIds = [...body.matchAll(/^\s*([pjt]_[A-Za-z0-9_]+)\s*\[/gm)].map((m) => m[1]);
27
+ clusters.set(clusterId, {
28
+ id: clusterId,
29
+ shortName: clusterId.replace(/^cluster_/, ""),
30
+ nodes: memberIds
31
+ });
32
+ for (const n of memberIds) nodeToCluster.set(n, clusterId);
33
+ }
34
+ const stripped = dot.replace(/subgraph cluster_\w+\s*\{[^]*?^\s*\}/gm, "");
35
+ for (const m of stripped.matchAll(/^\s*([pjt]_[A-Za-z0-9_]+)\s*\[/gm)) {
36
+ const id = m[1];
37
+ if (!nodes.has(id)) {
38
+ nodes.set(id, { id, kind: classifyKind(id), attrs: {} });
39
+ }
40
+ }
41
+ const orphans = [...nodes.keys()].filter((n) => !nodeToCluster.has(n));
42
+ const edges = [];
43
+ for (const m of dot.matchAll(/([pjt]_[A-Za-z0-9_]+)\s*->\s*([pjt]_[A-Za-z0-9_]+)\s*(\[[^\]]*\])?/g)) {
44
+ const [, src, dst, rawAttrs] = m;
45
+ const attrs = rawAttrs ?? "";
46
+ if (attrs.includes("lhead=") || attrs.includes("ltail=")) continue;
47
+ let arc = "normal";
48
+ if (attrs.includes('label="reset"') || attrs.includes("#fd7e14")) arc = "reset";
49
+ else if (attrs.includes('arrowhead="odot"') || attrs.includes("#dc3545")) arc = "inhibitor";
50
+ else if (attrs.includes('label="read"')) arc = "read";
51
+ const stripped2 = attrs.startsWith("[") && attrs.endsWith("]") ? attrs.slice(1, -1) : "";
52
+ edges.push({ src, dst, arc, rawAttrs: stripped2 });
53
+ }
54
+ return { nodes, clusters, nodeToCluster, orphans, edges };
55
+ }
56
+ function foldOrphans(graph, threshold = 0.7) {
57
+ if (threshold <= 0) return graph;
58
+ const nodeToCluster = new Map(graph.nodeToCluster);
59
+ const clusterMembers = /* @__PURE__ */ new Map();
60
+ for (const [id, c] of graph.clusters) clusterMembers.set(id, [...c.nodes]);
61
+ const tally = /* @__PURE__ */ new Map();
62
+ const bump = (orphan, other) => {
63
+ const oc = nodeToCluster.get(other);
64
+ if (!oc) return;
65
+ let t = tally.get(orphan);
66
+ if (!t) {
67
+ t = /* @__PURE__ */ new Map();
68
+ tally.set(orphan, t);
69
+ }
70
+ t.set(oc, (t.get(oc) ?? 0) + 1);
71
+ };
72
+ for (const e of graph.edges) {
73
+ const sIsOrphan = !nodeToCluster.has(e.src);
74
+ const dIsOrphan = !nodeToCluster.has(e.dst);
75
+ if (sIsOrphan && !dIsOrphan) bump(e.src, e.dst);
76
+ if (dIsOrphan && !sIsOrphan) bump(e.dst, e.src);
77
+ }
78
+ for (const id of graph.orphans) {
79
+ const t = tally.get(id);
80
+ if (!t) continue;
81
+ let total = 0;
82
+ let bestCluster = null;
83
+ let bestCount = 0;
84
+ for (const [c, n] of t) {
85
+ total += n;
86
+ if (n > bestCount) {
87
+ bestCount = n;
88
+ bestCluster = c;
89
+ }
90
+ }
91
+ if (bestCluster && total > 0 && bestCount / total >= threshold) {
92
+ clusterMembers.get(bestCluster).push(id);
93
+ nodeToCluster.set(id, bestCluster);
94
+ }
95
+ }
96
+ const clusters = /* @__PURE__ */ new Map();
97
+ for (const [id, c] of graph.clusters) {
98
+ clusters.set(id, { ...c, nodes: clusterMembers.get(id) });
99
+ }
100
+ const orphans = [...graph.nodes.keys()].filter((n) => !nodeToCluster.has(n));
101
+ return { ...graph, clusters, nodeToCluster, orphans };
102
+ }
103
+ function replicateShared(graph, opts = {}) {
104
+ const max = opts.max ?? Infinity;
105
+ const replicateTransitions = opts.replicateTransitions ?? false;
106
+ const nodes = new Map(graph.nodes);
107
+ const nodeToCluster = new Map(graph.nodeToCluster);
108
+ const clusterMembers = /* @__PURE__ */ new Map();
109
+ for (const [id, c] of graph.clusters) clusterMembers.set(id, [...c.nodes]);
110
+ const edges = graph.edges.map((e) => ({ ...e }));
111
+ const foreignByNode = /* @__PURE__ */ new Map();
112
+ const noteForeign = (nodeId, otherId) => {
113
+ if (!nodeId.startsWith("p_") && !(replicateTransitions && nodeId.startsWith("t_"))) return;
114
+ const home = nodeToCluster.get(nodeId);
115
+ const otherCluster = nodeToCluster.get(otherId);
116
+ if (!otherCluster) return;
117
+ if (otherCluster === home) return;
118
+ let s = foreignByNode.get(nodeId);
119
+ if (!s) {
120
+ s = /* @__PURE__ */ new Set();
121
+ foreignByNode.set(nodeId, s);
122
+ }
123
+ s.add(otherCluster);
124
+ };
125
+ for (const e of edges) {
126
+ noteForeign(e.src, e.dst);
127
+ noteForeign(e.dst, e.src);
128
+ }
129
+ const replicaMap = /* @__PURE__ */ new Map();
130
+ let replicatedPlaces = 0;
131
+ let totalCopies = 0;
132
+ for (const [placeId, foreigns] of foreignByNode) {
133
+ if (foreigns.size === 0) continue;
134
+ if (foreigns.size > max) continue;
135
+ const perCluster = /* @__PURE__ */ new Map();
136
+ const orig = nodes.get(placeId);
137
+ if (!orig) continue;
138
+ for (const clusterId of foreigns) {
139
+ const shortName = clusterId.replace(/^cluster_/, "");
140
+ const repId = `${placeId}__rep__${shortName}`;
141
+ perCluster.set(clusterId, repId);
142
+ clusterMembers.get(clusterId).push(repId);
143
+ nodeToCluster.set(repId, clusterId);
144
+ nodes.set(repId, {
145
+ id: repId,
146
+ kind: orig.kind,
147
+ attrs: { ...orig.attrs },
148
+ replica: true,
149
+ replicaOf: placeId
150
+ });
151
+ totalCopies++;
152
+ }
153
+ replicaMap.set(placeId, perCluster);
154
+ replicatedPlaces++;
155
+ nodes.set(placeId, { ...orig, replica: true, replicaOf: placeId });
156
+ }
157
+ for (const e of edges) {
158
+ const sMap = replicaMap.get(e.src);
159
+ const dMap = replicaMap.get(e.dst);
160
+ if (sMap) {
161
+ const dCl = nodeToCluster.get(e.dst);
162
+ if (dCl && sMap.has(dCl)) e.src = sMap.get(dCl);
163
+ }
164
+ if (dMap) {
165
+ const sCl = nodeToCluster.get(e.src);
166
+ if (sCl && dMap.has(sCl)) e.dst = dMap.get(sCl);
167
+ }
168
+ }
169
+ for (const [origId] of replicaMap) {
170
+ if (nodeToCluster.has(origId)) continue;
171
+ const stillHasEdge = edges.some((e) => e.src === origId || e.dst === origId);
172
+ if (!stillHasEdge) nodes.delete(origId);
173
+ }
174
+ const clusters = /* @__PURE__ */ new Map();
175
+ for (const [id, c] of graph.clusters) {
176
+ clusters.set(id, { ...c, nodes: clusterMembers.get(id) });
177
+ }
178
+ const orphans = [...nodes.keys()].filter((n) => !nodeToCluster.has(n));
179
+ return {
180
+ nodes,
181
+ clusters,
182
+ nodeToCluster,
183
+ orphans,
184
+ edges,
185
+ replicateStats: { replicatedPlaces, totalCopies }
186
+ };
187
+ }
188
+
189
+ // src/viewer/layout/elk-place.ts
190
+ import ELK from "elkjs/lib/elk.bundled.js";
191
+ var ORCHESTRATOR_CLUSTER_ID = "cluster_orchestrator";
192
+ var PLACE_RADIUS = 22;
193
+ var PLACE_CHARW = 7.4;
194
+ var PLACE_PAD_BELOW = 16;
195
+ var FONT_PLACE = 13;
196
+ var TRANSITION_MIN_W = 180;
197
+ var TRANSITION_H = 40;
198
+ var TRANSITION_CHARW = 8.5;
199
+ var JUNCTION_SIZE = 28;
200
+ var FONT_CLUSTER = 17;
201
+ var ROOT_SPACING = 90;
202
+ var CLUSTER_SPACING = 16;
203
+ function nodeDims(node) {
204
+ if (node.kind === "place") {
205
+ const label2 = (node.attrs.xlabel ?? node.attrs.label ?? node.id).replace(/\\n/g, " ");
206
+ const labelWidth = label2.length * PLACE_CHARW;
207
+ const circleD = PLACE_RADIUS * 2 + 4;
208
+ return {
209
+ width: Math.max(circleD, labelWidth),
210
+ height: circleD + PLACE_PAD_BELOW + FONT_PLACE
211
+ };
212
+ }
213
+ if (node.kind === "junction") {
214
+ return { width: JUNCTION_SIZE, height: JUNCTION_SIZE };
215
+ }
216
+ const label = (node.attrs.label ?? node.id).replace(/\\n/g, " ");
217
+ return {
218
+ width: Math.max(TRANSITION_MIN_W, label.length * TRANSITION_CHARW),
219
+ height: TRANSITION_H
220
+ };
221
+ }
222
+ var CLUSTER_OPTIONS = {
223
+ "elk.padding": `[top=${FONT_CLUSTER + 16},left=16,bottom=16,right=16]`,
224
+ "elk.algorithm": "layered",
225
+ "elk.direction": "DOWN",
226
+ "elk.spacing.nodeNode": String(CLUSTER_SPACING),
227
+ "elk.layered.spacing.nodeNodeBetweenLayers": String(CLUSTER_SPACING + 6),
228
+ "elk.edgeRouting": "ORTHOGONAL"
229
+ };
230
+ var ROOT_OPTIONS = {
231
+ "elk.algorithm": "org.eclipse.elk.rectpacking",
232
+ "elk.aspectRatio": "1.4",
233
+ "elk.padding": "[top=24,left=24,bottom=24,right=24]",
234
+ "elk.spacing.nodeNode": String(ROOT_SPACING)
235
+ };
236
+ var ORCHESTRATOR_OPTIONS = {
237
+ ...CLUSTER_OPTIONS,
238
+ "elk.aspectRatio": "2.4",
239
+ "elk.spacing.nodeNode": "10"
240
+ };
241
+ function partitionEdges(graph) {
242
+ const byCluster = /* @__PURE__ */ new Map();
243
+ const cross = [];
244
+ const edgeIdToKey = /* @__PURE__ */ new Map();
245
+ const ownerOf = (id) => graph.nodeToCluster.get(id) ?? ORCHESTRATOR_CLUSTER_ID;
246
+ graph.edges.forEach((e, i) => {
247
+ const so = ownerOf(e.src);
248
+ const dt = ownerOf(e.dst);
249
+ const id = `e${i}`;
250
+ edgeIdToKey.set(id, `${e.src}->${e.dst}`);
251
+ const elkEdge = {
252
+ id,
253
+ sources: [e.src],
254
+ targets: [e.dst]
255
+ };
256
+ if (so === dt) {
257
+ let list = byCluster.get(so);
258
+ if (!list) {
259
+ list = [];
260
+ byCluster.set(so, list);
261
+ }
262
+ list.push(elkEdge);
263
+ } else {
264
+ cross.push(elkEdge);
265
+ }
266
+ });
267
+ return { byCluster, cross, edgeIdToKey };
268
+ }
269
+ async function elkLayout(graph) {
270
+ const { byCluster, cross, edgeIdToKey } = partitionEdges(graph);
271
+ const elkChildren = [];
272
+ for (const [clusterId, cluster] of graph.clusters) {
273
+ elkChildren.push({
274
+ id: clusterId,
275
+ layoutOptions: CLUSTER_OPTIONS,
276
+ children: cluster.nodes.map((id) => graph.nodes.get(id)).filter((n) => n !== void 0).map((n) => ({ id: n.id, ...nodeDims(n) })),
277
+ edges: byCluster.get(clusterId) ?? []
278
+ });
279
+ }
280
+ elkChildren.push({
281
+ id: ORCHESTRATOR_CLUSTER_ID,
282
+ layoutOptions: ORCHESTRATOR_OPTIONS,
283
+ children: graph.orphans.map((id) => graph.nodes.get(id)).filter((n) => n !== void 0).map((n) => ({ id: n.id, ...nodeDims(n) })),
284
+ edges: byCluster.get(ORCHESTRATOR_CLUSTER_ID) ?? []
285
+ });
286
+ const elk = new ELK();
287
+ const rootGraph = {
288
+ id: "root",
289
+ layoutOptions: ROOT_OPTIONS,
290
+ children: elkChildren,
291
+ edges: cross
292
+ };
293
+ const layout = await elk.layout(rootGraph);
294
+ const nodePositions = /* @__PURE__ */ new Map();
295
+ const clusterBoxes = /* @__PURE__ */ new Map();
296
+ const edgePaths = /* @__PURE__ */ new Map();
297
+ const walk = (node, parentX, parentY) => {
298
+ const ax = parentX + (node.x ?? 0);
299
+ const ay = parentY + (node.y ?? 0);
300
+ const isCluster = node.id !== "root" && (node.children?.length ?? 0) > 0;
301
+ if (isCluster) {
302
+ clusterBoxes.set(node.id, {
303
+ x: ax,
304
+ y: ay,
305
+ width: node.width ?? 0,
306
+ height: node.height ?? 0
307
+ });
308
+ }
309
+ if (node.id !== "root" && !isCluster) {
310
+ nodePositions.set(node.id, {
311
+ x: ax,
312
+ y: ay,
313
+ width: node.width ?? 0,
314
+ height: node.height ?? 0
315
+ });
316
+ }
317
+ const edgeOffsetX = node.id === "root" ? parentX : ax;
318
+ const edgeOffsetY = node.id === "root" ? parentY : ay;
319
+ for (const edge of node.edges ?? []) {
320
+ const key = edge.id ? edgeIdToKey.get(edge.id) : void 0;
321
+ const section = edge.sections?.[0];
322
+ if (!key || !section) continue;
323
+ edgePaths.set(key, {
324
+ start: {
325
+ x: edgeOffsetX + section.startPoint.x,
326
+ y: edgeOffsetY + section.startPoint.y
327
+ },
328
+ end: {
329
+ x: edgeOffsetX + section.endPoint.x,
330
+ y: edgeOffsetY + section.endPoint.y
331
+ },
332
+ bends: (section.bendPoints ?? []).map((p) => ({
333
+ x: edgeOffsetX + p.x,
334
+ y: edgeOffsetY + p.y
335
+ }))
336
+ });
337
+ }
338
+ for (const child of node.children ?? []) walk(child, ax, ay);
339
+ };
340
+ walk(layout, 0, 0);
341
+ return {
342
+ nodePositions,
343
+ clusterBoxes,
344
+ edgePaths,
345
+ totalWidth: layout.width ?? 0,
346
+ totalHeight: layout.height ?? 0
347
+ };
348
+ }
349
+ function quote(value) {
350
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
351
+ }
352
+ function nodeAttrLine(node, pos) {
353
+ const attrs = [];
354
+ const cx = pos.x + pos.width / 2;
355
+ const cy = pos.y + pos.height / 2;
356
+ attrs.push(`pos=${quote(`${cx.toFixed(2)},${cy.toFixed(2)}!`)}`);
357
+ for (const [k, v] of Object.entries(node.attrs)) {
358
+ if (k === "pos") continue;
359
+ attrs.push(`${k}=${quote(v)}`);
360
+ }
361
+ return `${node.id} [${attrs.join(", ")}];`;
362
+ }
363
+ function edgeAttrLine(src, dst, rawAttrs) {
364
+ const body = rawAttrs.trim();
365
+ return `${src} -> ${dst}` + (body ? ` [${body}];` : ";");
366
+ }
367
+ function writeBack(graph, layout) {
368
+ const lines = [];
369
+ lines.push("digraph LibpetriPinned {");
370
+ lines.push(" rankdir=TB;");
371
+ lines.push(' overlap="false";');
372
+ lines.push(' compound="true";');
373
+ lines.push(' fontname="Helvetica,Arial,sans-serif";');
374
+ lines.push(' outputorder="edgesfirst";');
375
+ lines.push(' node [fontname="Helvetica,Arial,sans-serif", fontsize=10];');
376
+ lines.push(' edge [fontname="Helvetica,Arial,sans-serif", fontsize=9];');
377
+ for (const [clusterId, cluster] of graph.clusters) {
378
+ const box = layout.clusterBoxes.get(clusterId);
379
+ lines.push(` subgraph ${clusterId} {`);
380
+ lines.push(` label=${quote(cluster.shortName)};`);
381
+ lines.push(` style="rounded,dashed";`);
382
+ lines.push(` bgcolor="#FAFAFA";`);
383
+ lines.push(` penwidth=1.5;`);
384
+ if (box) {
385
+ const x1 = box.x.toFixed(2);
386
+ const y1 = box.y.toFixed(2);
387
+ const x2 = (box.x + box.width).toFixed(2);
388
+ const y2 = (box.y + box.height).toFixed(2);
389
+ lines.push(` bb=${quote(`${x1},${y1},${x2},${y2}`)};`);
390
+ }
391
+ for (const nodeId of cluster.nodes) {
392
+ const node = graph.nodes.get(nodeId);
393
+ const pos = layout.nodePositions.get(nodeId);
394
+ if (!node || !pos) continue;
395
+ lines.push(" " + nodeAttrLine(node, pos));
396
+ }
397
+ lines.push(" }");
398
+ }
399
+ for (const orphanId of graph.orphans) {
400
+ const node = graph.nodes.get(orphanId);
401
+ const pos = layout.nodePositions.get(orphanId);
402
+ if (!node || !pos) continue;
403
+ lines.push(" " + nodeAttrLine(node, pos));
404
+ }
405
+ for (const edge of graph.edges) {
406
+ lines.push(" " + edgeAttrLine(edge.src, edge.dst, edge.rawAttrs));
407
+ }
408
+ lines.push("}");
409
+ return lines.join("\n");
410
+ }
411
+
412
+ export {
413
+ parseLibpetriDot,
414
+ foldOrphans,
415
+ replicateShared,
416
+ elkLayout,
417
+ writeBack
418
+ };
419
+ //# sourceMappingURL=chunk-RNWTYK5B.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/viewer/layout/preprocess.ts","../src/viewer/layout/elk-place.ts"],"sourcesContent":["/**\n * Graph pre-processing for the C0-replicate-default layout pipeline.\n *\n * Three pure-ish transforms operate on a {@link GraphModel} parsed from a\n * libpetri-exported DOT string:\n *\n * parseLibpetriDot(dot) → GraphModel\n * foldOrphans(graph, threshold) → GraphModel (REASSIGN_ORPHANS)\n * replicateShared(graph, opts) → GraphModel (REPLICATE_SHARED)\n *\n * The pipeline turns a flat cluster soup into a cluster-internal one by\n * (a) folding orphan nodes whose edges concentrate in one cluster into that\n * cluster, and (b) cloning shared places into every foreign cluster they\n * touch so cross-cluster spaghetti disappears. The resulting GraphModel is\n * what ELK lays out (Stage 2) and Graphviz then renders in pin-mode.\n *\n * Algorithm lineage: ported from\n * `/home/db/otto-repos/nucleus_marvin/.scratch/javadoc-layout-exp/CHECKPOINTS/run16_GOOD_v3_skip-junctions.mjs`\n * lines 14–220. The arc-type detection, replica naming\n * (`${placeId}__rep__${clusterShortName}`), and the `__replica` /\n * `replicaOf` markers are load-bearing — downstream stages key off them.\n *\n * @module viewer/layout/preprocess\n */\n\nexport type ArcType = 'normal' | 'reset' | 'inhibitor' | 'read';\nexport type NodeKind = 'place' | 'transition' | 'junction';\n\nexport interface ParsedNode {\n readonly id: string;\n readonly kind: NodeKind;\n readonly attrs: Record<string, string>;\n /** True iff this node was emitted by replicateShared. */\n readonly replica?: boolean;\n /** For a replica, the id of the original logical place. */\n readonly replicaOf?: string;\n}\n\nexport interface ParsedEdge {\n readonly src: string;\n readonly dst: string;\n readonly arc: ArcType;\n /**\n * Raw attribute body from the libpetri DOT (between the `[…]` brackets,\n * brackets stripped). Carries style/penwidth/label that we want to\n * round-trip verbatim so the rendered output matches `DotExporter`'s\n * native look.\n */\n readonly rawAttrs: string;\n}\n\nexport interface ParsedCluster {\n readonly id: string; // \"cluster_productSearch\"\n readonly shortName: string; // \"productSearch\"\n readonly nodes: readonly string[];\n}\n\nexport interface GraphModel {\n readonly nodes: ReadonlyMap<string, ParsedNode>;\n readonly clusters: ReadonlyMap<string, ParsedCluster>;\n readonly nodeToCluster: ReadonlyMap<string, string>;\n /** Node ids not in any cluster (rendered at the orchestrator root). */\n readonly orphans: readonly string[];\n readonly edges: readonly ParsedEdge[];\n}\n\nfunction classifyKind(id: string): NodeKind {\n if (id.startsWith('p_')) return 'place';\n if (id.startsWith('t_')) return 'transition';\n return 'junction';\n}\n\n/**\n * Recover key=value attributes from inside a DOT `[...]` block.\n *\n * Mirrors v3 line 29. The regex is intentionally permissive about quoted\n * values containing escaped quotes; it does not validate DOT syntax.\n */\nfunction parseAttrs(body: string): Record<string, string> {\n const attrs: Record<string, string> = {};\n for (const m of body.matchAll(/(\\w+)=(\"(?:[^\"]|\\\\.)*\"|\\S+?)(?=[\\s,]|$)/g)) {\n const [, key, raw] = m as unknown as [string, string, string];\n const v = raw.startsWith('\"') && raw.endsWith('\"') ? raw.slice(1, -1) : raw;\n attrs[key] = v;\n }\n return attrs;\n}\n\n/**\n * Parse a libpetri-exported DOT string into a {@link GraphModel}.\n *\n * Tolerates the current exporter shape (see\n * `java/src/main/java/org/libpetri/export/StyleConstants.java`):\n * - inhibitor arcs use `arrowhead=\"odot\"` + color `#dc3545`\n * - reset arcs use `label=\"reset\"` + color `#fd7e14`\n * - read arcs use `label=\"read\"` + color `#6c757d`\n *\n * Mirrors v3 lines 24–103.\n */\nexport function parseLibpetriDot(dot: string): GraphModel {\n const nodes = new Map<string, ParsedNode>();\n // Greedy `.*` (no /s flag) consumes the full attrs body up to the line's\n // trailing `];`. The v3 reference used `[^\\]]*` which silently dropped\n // transitions whose label contained `[…]` text (e.g., `\"[0, ∞]ms\"`); we\n // can't tolerate that here because the typed model wants every cluster\n // member to have a Node entry.\n for (const m of dot.matchAll(/^\\s*([pjt]_[A-Za-z0-9_]+)\\s*\\[(.*)\\];\\s*$/gm)) {\n const [, id, body] = m as unknown as [string, string, string];\n nodes.set(id, { id, kind: classifyKind(id), attrs: parseAttrs(body) });\n }\n\n const clusters = new Map<string, ParsedCluster>();\n const nodeToCluster = new Map<string, string>();\n for (const cm of dot.matchAll(/subgraph (cluster_\\w+)\\s*\\{([^]*?)^\\s*\\}/gm)) {\n const [, clusterId, body] = cm as unknown as [string, string, string];\n const memberIds = [...body.matchAll(/^\\s*([pjt]_[A-Za-z0-9_]+)\\s*\\[/gm)]\n .map(m => (m as unknown as [string, string])[1]);\n clusters.set(clusterId, {\n id: clusterId,\n shortName: clusterId.replace(/^cluster_/, ''),\n nodes: memberIds,\n });\n for (const n of memberIds) nodeToCluster.set(n, clusterId);\n }\n\n // Catch nodes declared at root (orphans) — strip cluster bodies, then scan\n const stripped = dot.replace(/subgraph cluster_\\w+\\s*\\{[^]*?^\\s*\\}/gm, '');\n for (const m of stripped.matchAll(/^\\s*([pjt]_[A-Za-z0-9_]+)\\s*\\[/gm)) {\n const id = (m as unknown as [string, string])[1];\n if (!nodes.has(id)) {\n // Root-declared node without full attrs (rare); seed with empty attrs.\n nodes.set(id, { id, kind: classifyKind(id), attrs: {} });\n }\n }\n\n const orphans = [...nodes.keys()].filter(n => !nodeToCluster.has(n));\n\n const edges: ParsedEdge[] = [];\n for (const m of dot.matchAll(/([pjt]_[A-Za-z0-9_]+)\\s*->\\s*([pjt]_[A-Za-z0-9_]+)\\s*(\\[[^\\]]*\\])?/g)) {\n const [, src, dst, rawAttrs] = m as unknown as [string, string, string, string | undefined];\n const attrs = rawAttrs ?? '';\n // Cluster-anchor edges (lhead/ltail) are exporter-internal routing hints,\n // not real arcs in the Petri net — skip per v3 line 97.\n if (attrs.includes('lhead=') || attrs.includes('ltail=')) continue;\n let arc: ArcType = 'normal';\n if (attrs.includes('label=\"reset\"') || attrs.includes('#fd7e14')) arc = 'reset';\n else if (attrs.includes('arrowhead=\"odot\"') || attrs.includes('#dc3545')) arc = 'inhibitor';\n else if (attrs.includes('label=\"read\"')) arc = 'read';\n // Strip the `[…]` brackets so writeBack can splice the body into a new\n // attr list. Empty if libpetri emitted a bare `src -> dst;`.\n const stripped = attrs.startsWith('[') && attrs.endsWith(']')\n ? attrs.slice(1, -1)\n : '';\n edges.push({ src, dst, arc, rawAttrs: stripped });\n }\n\n return { nodes, clusters, nodeToCluster, orphans, edges };\n}\n\n/**\n * Adopt orphan nodes into the cluster their edges most prefer.\n *\n * For each orphan, tally how many of its edges touch each cluster. If the\n * dominant cluster gets at least `threshold` of the orphan's cluster-touching\n * edges, the orphan is reassigned to that cluster.\n *\n * Mirrors v3 lines 105–136. Default threshold 0.7 matches REASSIGN_ORPHANS.\n */\nexport function foldOrphans(graph: GraphModel, threshold = 0.7): GraphModel {\n if (threshold <= 0) return graph;\n\n const nodeToCluster = new Map(graph.nodeToCluster);\n const clusterMembers = new Map<string, string[]>();\n for (const [id, c] of graph.clusters) clusterMembers.set(id, [...c.nodes]);\n\n const tally = new Map<string, Map<string, number>>();\n const bump = (orphan: string, other: string): void => {\n const oc = nodeToCluster.get(other);\n if (!oc) return;\n let t = tally.get(orphan);\n if (!t) { t = new Map(); tally.set(orphan, t); }\n t.set(oc, (t.get(oc) ?? 0) + 1);\n };\n\n for (const e of graph.edges) {\n const sIsOrphan = !nodeToCluster.has(e.src);\n const dIsOrphan = !nodeToCluster.has(e.dst);\n if (sIsOrphan && !dIsOrphan) bump(e.src, e.dst);\n if (dIsOrphan && !sIsOrphan) bump(e.dst, e.src);\n }\n\n for (const id of graph.orphans) {\n const t = tally.get(id);\n if (!t) continue;\n let total = 0;\n let bestCluster: string | null = null;\n let bestCount = 0;\n for (const [c, n] of t) {\n total += n;\n if (n > bestCount) { bestCount = n; bestCluster = c; }\n }\n if (bestCluster && total > 0 && bestCount / total >= threshold) {\n clusterMembers.get(bestCluster)!.push(id);\n nodeToCluster.set(id, bestCluster);\n }\n }\n\n const clusters = new Map<string, ParsedCluster>();\n for (const [id, c] of graph.clusters) {\n clusters.set(id, { ...c, nodes: clusterMembers.get(id)! });\n }\n const orphans = [...graph.nodes.keys()].filter(n => !nodeToCluster.has(n));\n return { ...graph, clusters, nodeToCluster, orphans };\n}\n\nexport interface ReplicateOptions {\n /**\n * Skip places that touch more than this many foreign clusters. Per the\n * plan's \"no cap\" decision, default is `Infinity`. Set to a finite value\n * to suppress replication of hub places that would otherwise produce many\n * copies.\n */\n readonly max?: number;\n /** If true, replicate transitions as well as places. Default false. */\n readonly replicateTransitions?: boolean;\n}\n\nexport interface ReplicateStats {\n readonly replicatedPlaces: number;\n readonly totalCopies: number;\n}\n\n/**\n * For each place P, replicate it into every foreign cluster touched by its\n * edges, and redirect those cross-cluster edges to the local copy.\n *\n * Replica id convention is load-bearing: `${placeId}__rep__${shortClusterName}`.\n * Downstream stages (mount-time tagging, overlay highlight, click-all-copies)\n * key off this naming and the `replica: true` / `replicaOf` markers.\n *\n * If P has a home cluster (folded earlier), the original stays as the primary\n * copy and is also marked as a replica so the ⇄ glyph renders consistently.\n * If P is an orphan with all its edges redirected, the original is dropped.\n *\n * Mirrors v3 lines 138–221.\n */\nexport function replicateShared(\n graph: GraphModel,\n opts: ReplicateOptions = {},\n): GraphModel & { readonly replicateStats: ReplicateStats } {\n const max = opts.max ?? Infinity;\n const replicateTransitions = opts.replicateTransitions ?? false;\n\n const nodes = new Map(graph.nodes);\n const nodeToCluster = new Map(graph.nodeToCluster);\n const clusterMembers = new Map<string, string[]>();\n for (const [id, c] of graph.clusters) clusterMembers.set(id, [...c.nodes]);\n const edges = graph.edges.map(e => ({ ...e }));\n\n // 1. Compute foreign-cluster set per replicable node.\n const foreignByNode = new Map<string, Set<string>>();\n const noteForeign = (nodeId: string, otherId: string): void => {\n if (!nodeId.startsWith('p_') && !(replicateTransitions && nodeId.startsWith('t_'))) return;\n const home = nodeToCluster.get(nodeId);\n const otherCluster = nodeToCluster.get(otherId);\n if (!otherCluster) return;\n if (otherCluster === home) return;\n let s = foreignByNode.get(nodeId);\n if (!s) { s = new Set(); foreignByNode.set(nodeId, s); }\n s.add(otherCluster);\n };\n for (const e of edges) {\n noteForeign(e.src, e.dst);\n noteForeign(e.dst, e.src);\n }\n\n // 2. Emit replicas for nodes within the cap.\n const replicaMap = new Map<string, Map<string, string>>(); // orig → cluster → replicaId\n let replicatedPlaces = 0;\n let totalCopies = 0;\n for (const [placeId, foreigns] of foreignByNode) {\n if (foreigns.size === 0) continue;\n if (foreigns.size > max) continue;\n const perCluster = new Map<string, string>();\n const orig = nodes.get(placeId);\n if (!orig) continue;\n for (const clusterId of foreigns) {\n const shortName = clusterId.replace(/^cluster_/, '');\n const repId = `${placeId}__rep__${shortName}`;\n perCluster.set(clusterId, repId);\n clusterMembers.get(clusterId)!.push(repId);\n nodeToCluster.set(repId, clusterId);\n nodes.set(repId, {\n id: repId,\n kind: orig.kind,\n attrs: { ...orig.attrs },\n replica: true,\n replicaOf: placeId,\n });\n totalCopies++;\n }\n replicaMap.set(placeId, perCluster);\n replicatedPlaces++;\n // Mark the original as a shared place so the ⇄ glyph renders uniformly.\n nodes.set(placeId, { ...orig, replica: true, replicaOf: placeId });\n }\n\n // 3. Redirect cross-cluster edges to the local replica on each end.\n for (const e of edges) {\n const sMap = replicaMap.get(e.src);\n const dMap = replicaMap.get(e.dst);\n if (sMap) {\n const dCl = nodeToCluster.get(e.dst);\n if (dCl && sMap.has(dCl)) e.src = sMap.get(dCl)!;\n }\n if (dMap) {\n const sCl = nodeToCluster.get(e.src);\n if (sCl && dMap.has(sCl)) e.dst = dMap.get(sCl)!;\n }\n }\n\n // 4. Drop orphan originals whose edges all got redirected away.\n for (const [origId] of replicaMap) {\n if (nodeToCluster.has(origId)) continue; // folded — keep as primary\n const stillHasEdge = edges.some(e => e.src === origId || e.dst === origId);\n if (!stillHasEdge) nodes.delete(origId);\n }\n\n const clusters = new Map<string, ParsedCluster>();\n for (const [id, c] of graph.clusters) {\n clusters.set(id, { ...c, nodes: clusterMembers.get(id)! });\n }\n const orphans = [...nodes.keys()].filter(n => !nodeToCluster.has(n));\n\n return {\n nodes,\n clusters,\n nodeToCluster,\n orphans,\n edges,\n replicateStats: { replicatedPlaces, totalCopies },\n };\n}\n","/**\n * ELK placement + DOT pin-mode rewrite.\n *\n * Stage 2 of the C0 pipeline. Takes the {@link GraphModel} produced by\n * {@link parseLibpetriDot} → {@link foldOrphans} → {@link replicateShared},\n * runs ELKjs to compute absolute `(x, y)` per node and bounding boxes per\n * cluster, then emits a fresh DOT string with positions pinned via\n * `pos=\"x,y!\"` and `bb=\"…\"`. Downstream callers render the result through\n * `@viz-js/viz` with `engine: 'neato'` + `graphAttributes: { nop: '1' }` —\n * Graphviz uses the pinned positions verbatim and routes edges around the\n * pinned cluster boundaries.\n *\n * The two functions are pure: `elkLayout` takes only the graph; `writeBack`\n * takes (graph, layout). Neither touches the original DOT text — the model\n * after replication has nodes the original DOT doesn't, so patching the\n * original would be incorrect.\n *\n * @module viewer/layout/elk-place\n */\n\nimport ELK from 'elkjs/lib/elk.bundled.js';\nimport type {\n ElkExtendedEdge,\n ElkNode,\n} from 'elkjs/lib/elk-api.js';\nimport type { GraphModel, ParsedNode } from './preprocess.js';\n\nconst ORCHESTRATOR_CLUSTER_ID = 'cluster_orchestrator';\n\n// Visual sizing — kept proportional to the v3 reference renderer so the\n// resulting bounding box is comparable to C0-GOOD_v3.svg.\nconst PLACE_RADIUS = 22;\nconst PLACE_CHARW = 7.4;\nconst PLACE_PAD_BELOW = 16;\nconst FONT_PLACE = 13;\nconst TRANSITION_MIN_W = 180;\nconst TRANSITION_H = 40;\nconst TRANSITION_CHARW = 8.5;\nconst JUNCTION_SIZE = 28;\nconst FONT_CLUSTER = 17;\nconst ROOT_SPACING = 90;\nconst CLUSTER_SPACING = 16;\n\n/**\n * Width/height in points that ELK should reserve for a node. Mirrors\n * `nodeDims` in v3 lines 66–78 — places get extra height to leave room for\n * the xlabel underneath the circle.\n */\nfunction nodeDims(node: ParsedNode): { width: number; height: number } {\n if (node.kind === 'place') {\n const label = (node.attrs.xlabel ?? node.attrs.label ?? node.id).replace(/\\\\n/g, ' ');\n const labelWidth = label.length * PLACE_CHARW;\n const circleD = PLACE_RADIUS * 2 + 4;\n return {\n width: Math.max(circleD, labelWidth),\n height: circleD + PLACE_PAD_BELOW + FONT_PLACE,\n };\n }\n if (node.kind === 'junction') {\n return { width: JUNCTION_SIZE, height: JUNCTION_SIZE };\n }\n const label = (node.attrs.label ?? node.id).replace(/\\\\n/g, ' ');\n return {\n width: Math.max(TRANSITION_MIN_W, label.length * TRANSITION_CHARW),\n height: TRANSITION_H,\n };\n}\n\nexport interface NodePosition {\n readonly x: number;\n readonly y: number;\n readonly width: number;\n readonly height: number;\n}\n\nexport interface ClusterBox {\n readonly x: number;\n readonly y: number;\n readonly width: number;\n readonly height: number;\n}\n\nexport interface EdgePath {\n readonly start: { x: number; y: number };\n readonly end: { x: number; y: number };\n readonly bends: ReadonlyArray<{ x: number; y: number }>;\n}\n\nexport interface LayoutResult {\n readonly nodePositions: ReadonlyMap<string, NodePosition>;\n readonly clusterBoxes: ReadonlyMap<string, ClusterBox>;\n /** Keyed on `${src}->${dst}` — ELK-computed edge routes for `nop=2`. */\n readonly edgePaths: ReadonlyMap<string, EdgePath>;\n readonly totalWidth: number;\n readonly totalHeight: number;\n}\n\nconst CLUSTER_OPTIONS: Record<string, string> = {\n 'elk.padding': `[top=${FONT_CLUSTER + 16},left=16,bottom=16,right=16]`,\n 'elk.algorithm': 'layered',\n 'elk.direction': 'DOWN',\n 'elk.spacing.nodeNode': String(CLUSTER_SPACING),\n 'elk.layered.spacing.nodeNodeBetweenLayers': String(CLUSTER_SPACING + 6),\n 'elk.edgeRouting': 'ORTHOGONAL',\n};\n\nconst ROOT_OPTIONS: Record<string, string> = {\n 'elk.algorithm': 'org.eclipse.elk.rectpacking',\n 'elk.aspectRatio': '1.4',\n 'elk.padding': '[top=24,left=24,bottom=24,right=24]',\n 'elk.spacing.nodeNode': String(ROOT_SPACING),\n};\n\nconst ORCHESTRATOR_OPTIONS: Record<string, string> = {\n ...CLUSTER_OPTIONS,\n 'elk.aspectRatio': '2.4',\n 'elk.spacing.nodeNode': '10',\n};\n\n/**\n * Group edges by the cluster that contains both endpoints (or 'root' if\n * the edge crosses cluster boundaries).\n *\n * Orphans count as living in `cluster_orchestrator` for routing purposes,\n * matching v3's `ownerOf` (line 223).\n */\nfunction partitionEdges(graph: GraphModel): {\n byCluster: Map<string, ElkExtendedEdge[]>;\n cross: ElkExtendedEdge[];\n /** edgeId → \"src->dst\" so the layout walk can recover the original key. */\n edgeIdToKey: Map<string, string>;\n} {\n const byCluster = new Map<string, ElkExtendedEdge[]>();\n const cross: ElkExtendedEdge[] = [];\n const edgeIdToKey = new Map<string, string>();\n const ownerOf = (id: string): string =>\n graph.nodeToCluster.get(id) ?? ORCHESTRATOR_CLUSTER_ID;\n\n graph.edges.forEach((e, i) => {\n const so = ownerOf(e.src);\n const dt = ownerOf(e.dst);\n const id = `e${i}`;\n edgeIdToKey.set(id, `${e.src}->${e.dst}`);\n const elkEdge: ElkExtendedEdge = {\n id,\n sources: [e.src],\n targets: [e.dst],\n };\n if (so === dt) {\n let list = byCluster.get(so);\n if (!list) { list = []; byCluster.set(so, list); }\n list.push(elkEdge);\n } else {\n cross.push(elkEdge);\n }\n });\n return { byCluster, cross, edgeIdToKey };\n}\n\n/**\n * Run ELK layout against the C0 graph and return absolute positions.\n *\n * Orphan nodes are wrapped in a synthetic `cluster_orchestrator` so ELK\n * lays them out as a single block alongside the real subnets, which the\n * `rectpacking` root algorithm then packs.\n */\nexport async function elkLayout(graph: GraphModel): Promise<LayoutResult> {\n const { byCluster, cross, edgeIdToKey } = partitionEdges(graph);\n\n const elkChildren: ElkNode[] = [];\n for (const [clusterId, cluster] of graph.clusters) {\n elkChildren.push({\n id: clusterId,\n layoutOptions: CLUSTER_OPTIONS,\n children: cluster.nodes\n .map(id => graph.nodes.get(id))\n .filter((n): n is ParsedNode => n !== undefined)\n .map(n => ({ id: n.id, ...nodeDims(n) })),\n edges: byCluster.get(clusterId) ?? [],\n });\n }\n // Synthetic orchestrator cluster holds the remaining orphans\n elkChildren.push({\n id: ORCHESTRATOR_CLUSTER_ID,\n layoutOptions: ORCHESTRATOR_OPTIONS,\n children: graph.orphans\n .map(id => graph.nodes.get(id))\n .filter((n): n is ParsedNode => n !== undefined)\n .map(n => ({ id: n.id, ...nodeDims(n) })),\n edges: byCluster.get(ORCHESTRATOR_CLUSTER_ID) ?? [],\n });\n\n const elk = new ELK();\n const rootGraph: ElkNode = {\n id: 'root',\n layoutOptions: ROOT_OPTIONS,\n children: elkChildren,\n edges: cross,\n };\n const layout: ElkNode = await elk.layout(rootGraph);\n\n const nodePositions = new Map<string, NodePosition>();\n const clusterBoxes = new Map<string, ClusterBox>();\n const edgePaths = new Map<string, EdgePath>();\n\n const walk = (node: ElkNode, parentX: number, parentY: number): void => {\n const ax = parentX + (node.x ?? 0);\n const ay = parentY + (node.y ?? 0);\n const isCluster = node.id !== 'root' && (node.children?.length ?? 0) > 0;\n if (isCluster) {\n clusterBoxes.set(node.id, {\n x: ax, y: ay,\n width: node.width ?? 0,\n height: node.height ?? 0,\n });\n }\n if (node.id !== 'root' && !isCluster) {\n nodePositions.set(node.id, {\n x: ax, y: ay,\n width: node.width ?? 0,\n height: node.height ?? 0,\n });\n }\n // Edges inside this node — their section coords are in the parent's\n // coordinate frame, so the offset to absolutize is (parentX, parentY)\n // for root edges, or (ax, ay) for cluster-internal edges.\n const edgeOffsetX = node.id === 'root' ? parentX : ax;\n const edgeOffsetY = node.id === 'root' ? parentY : ay;\n for (const edge of node.edges ?? []) {\n const key = edge.id ? edgeIdToKey.get(edge.id) : undefined;\n const section = edge.sections?.[0];\n if (!key || !section) continue;\n edgePaths.set(key, {\n start: {\n x: edgeOffsetX + section.startPoint.x,\n y: edgeOffsetY + section.startPoint.y,\n },\n end: {\n x: edgeOffsetX + section.endPoint.x,\n y: edgeOffsetY + section.endPoint.y,\n },\n bends: (section.bendPoints ?? []).map(p => ({\n x: edgeOffsetX + p.x,\n y: edgeOffsetY + p.y,\n })),\n });\n }\n for (const child of node.children ?? []) walk(child, ax, ay);\n };\n walk(layout, 0, 0);\n\n return {\n nodePositions,\n clusterBoxes,\n edgePaths,\n totalWidth: layout.width ?? 0,\n totalHeight: layout.height ?? 0,\n };\n}\n\n/**\n * Escape an attribute value for inclusion in a DOT string literal.\n *\n * Quotes are doubled (per the DOT escape convention `\\\"`).\n */\nfunction quote(value: string): string {\n return `\"${value.replace(/\\\\/g, '\\\\\\\\').replace(/\"/g, '\\\\\"')}\"`;\n}\n\n/**\n * Render a node's attribute list — preserving the parsed attrs (including\n * libpetri's `width`/`height`, which are the *visual* shape dimensions)\n * and adding the ELK-computed `pos` so Graphviz `nop` can pin it.\n *\n * The `pos=\"x,y!\"` form (with trailing `!`) is the neato pin-mode marker.\n * Do NOT emit ELK's layout-reserved width/height to Graphviz — those are\n * the bounding boxes needed for ELK packing (which account for xlabel\n * spillover), not the visible shape sizes. libpetri's standard styles use\n * `width=0.35` for places (25pt circles); overriding with ELK's\n * ~3 inch label-padded box made Graphviz render 114pt-radius circles.\n */\nfunction nodeAttrLine(node: ParsedNode, pos: NodePosition): string {\n const attrs: string[] = [];\n // Center the pinned position on the node's bbox (ELK gives the top-left).\n const cx = pos.x + pos.width / 2;\n const cy = pos.y + pos.height / 2;\n attrs.push(`pos=${quote(`${cx.toFixed(2)},${cy.toFixed(2)}!`)}`);\n for (const [k, v] of Object.entries(node.attrs)) {\n if (k === 'pos') continue;\n attrs.push(`${k}=${quote(v)}`);\n }\n return `${node.id} [${attrs.join(', ')}];`;\n}\n\nfunction edgeAttrLine(src: string, dst: string, rawAttrs: string): string {\n // Splice libpetri's original attrs verbatim so style/penwidth/label/color\n // match `DotExporter` 1:1. We do NOT emit `pos=` for edges — under engine\n // `nop` Graphviz routes edges itself around the pinned node positions,\n // which gives proper boundary clipping (arrowheads land at the visual\n // node edge) and avoids needing a custom polyline-to-spline converter.\n // The only \"custom\" arrowhead is `arrowhead=\"odot\"` for inhibitor edges,\n // which libpetri's exporter already writes into rawAttrs.\n const body = rawAttrs.trim();\n return `${src} -> ${dst}` + (body ? ` [${body}];` : ';');\n}\n\n/**\n * Emit a Graphviz DOT string with positions pinned. Render the result with\n * `@viz-js/viz` using:\n *\n * ```ts\n * viz.renderSVGElement(dot, {\n * engine: 'nop',\n * yInvert: true,\n * });\n * ```\n *\n * `engine: 'nop'` (a.k.a. `nop1`) honours the pinned `pos=` on every node\n * and routes edges around them. We use this rather than `nop2` because\n * `nop2` would force us to provide a spline for every edge ourselves (no\n * routing) — and Graphviz's own routing produces correctly-clipped\n * arrowheads at the visual node boundary, which our ELK polylines did not\n * (ELK uses the layout-reserved bbox, our visual circle is much smaller).\n *\n * `yInvert: true` matches ELK's Y-down convention to Graphviz's Y-up.\n */\nexport function writeBack(graph: GraphModel, layout: LayoutResult): string {\n const lines: string[] = [];\n lines.push('digraph LibpetriPinned {');\n lines.push(' rankdir=TB;');\n lines.push(' overlap=\"false\";');\n lines.push(' compound=\"true\";');\n lines.push(' fontname=\"Helvetica,Arial,sans-serif\";');\n lines.push(' outputorder=\"edgesfirst\";');\n lines.push(' node [fontname=\"Helvetica,Arial,sans-serif\", fontsize=10];');\n lines.push(' edge [fontname=\"Helvetica,Arial,sans-serif\", fontsize=9];');\n\n // Clusters with their pinned bounding boxes\n for (const [clusterId, cluster] of graph.clusters) {\n const box = layout.clusterBoxes.get(clusterId);\n lines.push(` subgraph ${clusterId} {`);\n lines.push(` label=${quote(cluster.shortName)};`);\n lines.push(` style=\"rounded,dashed\";`);\n lines.push(` bgcolor=\"#FAFAFA\";`);\n lines.push(` penwidth=1.5;`);\n if (box) {\n const x1 = box.x.toFixed(2);\n const y1 = box.y.toFixed(2);\n const x2 = (box.x + box.width).toFixed(2);\n const y2 = (box.y + box.height).toFixed(2);\n lines.push(` bb=${quote(`${x1},${y1},${x2},${y2}`)};`);\n }\n for (const nodeId of cluster.nodes) {\n const node = graph.nodes.get(nodeId);\n const pos = layout.nodePositions.get(nodeId);\n if (!node || !pos) continue;\n lines.push(' ' + nodeAttrLine(node, pos));\n }\n lines.push(' }');\n }\n\n // Orphans (root-level nodes) — laid out inside the synthetic\n // orchestrator cluster but emitted at root so they sit alongside the\n // real subnets without their own labelled box.\n for (const orphanId of graph.orphans) {\n const node = graph.nodes.get(orphanId);\n const pos = layout.nodePositions.get(orphanId);\n if (!node || !pos) continue;\n lines.push(' ' + nodeAttrLine(node, pos));\n }\n\n // Edges — emit just the source -> dest pair plus libpetri's original\n // styling attrs. Graphviz `nop` routes the line itself and clips arrow\n // heads to the visual node boundary.\n for (const edge of graph.edges) {\n lines.push(' ' + edgeAttrLine(edge.src, edge.dst, edge.rawAttrs));\n }\n\n lines.push('}');\n return lines.join('\\n');\n}\n"],"mappings":"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","names":["stripped","label"]}
@@ -0,0 +1,50 @@
1
+ // src/event/net-event.ts
2
+ function eventTransitionName(event) {
3
+ switch (event.type) {
4
+ case "transition-enabled":
5
+ case "transition-clock-restarted":
6
+ case "transition-started":
7
+ case "transition-completed":
8
+ case "transition-failed":
9
+ case "transition-timed-out":
10
+ case "action-timed-out":
11
+ case "log-message":
12
+ return event.transitionName;
13
+ default:
14
+ return null;
15
+ }
16
+ }
17
+ function instancePrefixOfName(name) {
18
+ if (name == null) return void 0;
19
+ const idx = name.lastIndexOf("/");
20
+ if (idx <= 0) return void 0;
21
+ return name.substring(0, idx);
22
+ }
23
+ function eventInstancePrefix(event) {
24
+ switch (event.type) {
25
+ case "transition-enabled":
26
+ case "transition-clock-restarted":
27
+ case "transition-started":
28
+ case "transition-completed":
29
+ case "transition-failed":
30
+ case "transition-timed-out":
31
+ case "action-timed-out":
32
+ case "log-message":
33
+ return instancePrefixOfName(event.transitionName);
34
+ case "token-added":
35
+ case "token-removed":
36
+ return instancePrefixOfName(event.placeName);
37
+ default:
38
+ return void 0;
39
+ }
40
+ }
41
+ function isFailureEvent(event) {
42
+ return event.type === "transition-failed" || event.type === "transition-timed-out" || event.type === "action-timed-out";
43
+ }
44
+
45
+ export {
46
+ eventTransitionName,
47
+ eventInstancePrefix,
48
+ isFailureEvent
49
+ };
50
+ //# sourceMappingURL=chunk-SXK2Z45Z.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/event/net-event.ts"],"sourcesContent":["import type { Token } from '../core/token.js';\n\n/**\n * Events emitted during Petri Net execution.\n * Discriminated union capturing all observable state changes.\n */\nexport type NetEvent =\n | ExecutionStarted\n | ExecutionCompleted\n | TransitionEnabled\n | TransitionClockRestarted\n | TransitionStarted\n | TransitionCompleted\n | TransitionFailed\n | TransitionTimedOut\n | ActionTimedOut\n | TokenAdded\n | TokenRemoved\n | LogMessage\n | MarkingSnapshot;\n\n// ======================== Execution Lifecycle ========================\n\nexport interface ExecutionStarted {\n readonly type: 'execution-started';\n readonly timestamp: number;\n readonly netName: string;\n readonly executionId: string;\n}\n\nexport interface ExecutionCompleted {\n readonly type: 'execution-completed';\n readonly timestamp: number;\n readonly netName: string;\n readonly executionId: string;\n readonly totalDurationMs: number;\n}\n\n// ======================== Transition Lifecycle ========================\n\nexport interface TransitionEnabled {\n readonly type: 'transition-enabled';\n readonly timestamp: number;\n readonly transitionName: string;\n}\n\nexport interface TransitionClockRestarted {\n readonly type: 'transition-clock-restarted';\n readonly timestamp: number;\n readonly transitionName: string;\n}\n\nexport interface TransitionStarted {\n readonly type: 'transition-started';\n readonly timestamp: number;\n readonly transitionName: string;\n readonly consumedTokens: readonly Token<unknown>[];\n}\n\nexport interface TransitionCompleted {\n readonly type: 'transition-completed';\n readonly timestamp: number;\n readonly transitionName: string;\n readonly producedTokens: readonly Token<unknown>[];\n readonly durationMs: number;\n}\n\nexport interface TransitionFailed {\n readonly type: 'transition-failed';\n readonly timestamp: number;\n readonly transitionName: string;\n readonly errorMessage: string;\n readonly exceptionType: string;\n /** Original stack trace, if available. */\n readonly stack?: string;\n}\n\n/**\n * Emitted when a transition exceeds its deadline (upper time bound) without firing.\n * Classical TPN semantics: transition is forcibly disabled by the executor in\n * `updateDirtyTransitions()` when elapsed time exceeds `latest(timing)`.\n */\nexport interface TransitionTimedOut {\n readonly type: 'transition-timed-out';\n readonly timestamp: number;\n readonly transitionName: string;\n /** The deadline that was exceeded, in milliseconds from enablement. */\n readonly deadlineMs: number;\n /** Actual time elapsed since enablement, in milliseconds. */\n readonly actualDurationMs: number;\n}\n\nexport interface ActionTimedOut {\n readonly type: 'action-timed-out';\n readonly timestamp: number;\n readonly transitionName: string;\n readonly timeoutMs: number;\n}\n\n// ======================== Token Movement ========================\n\nexport interface TokenAdded {\n readonly type: 'token-added';\n readonly timestamp: number;\n readonly placeName: string;\n readonly token: Token<unknown>;\n}\n\nexport interface TokenRemoved {\n readonly type: 'token-removed';\n readonly timestamp: number;\n readonly placeName: string;\n readonly token: Token<unknown>;\n}\n\n// ======================== Log Capture ========================\n\nexport interface LogMessage {\n readonly type: 'log-message';\n readonly timestamp: number;\n readonly transitionName: string;\n readonly logger: string;\n readonly level: string;\n readonly message: string;\n readonly error: string | null;\n readonly errorMessage: string | null;\n}\n\n// ======================== Checkpointing ========================\n\n/**\n * Snapshot of the full marking (token state) at a point in time.\n * Emitted at two points during execution:\n * 1. After initialization (before the main loop) — captures the initial marking\n * 2. Before the execution-completed event — captures the final marking\n */\nexport interface MarkingSnapshot {\n readonly type: 'marking-snapshot';\n readonly timestamp: number;\n /** Place name -> tokens in that place at snapshot time. Only non-empty places are included. */\n readonly marking: ReadonlyMap<string, readonly Token<unknown>[]>;\n}\n\n// ======================== Helper Functions ========================\n\n/** Extracts transition name from events that have one. Returns null otherwise. */\nexport function eventTransitionName(event: NetEvent): string | null {\n switch (event.type) {\n case 'transition-enabled':\n case 'transition-clock-restarted':\n case 'transition-started':\n case 'transition-completed':\n case 'transition-failed':\n case 'transition-timed-out':\n case 'action-timed-out':\n case 'log-message':\n return event.transitionName;\n default:\n return null;\n }\n}\n\n/**\n * Returns the instance prefix derived from a place or transition name per\n * `spec/11-modular-composition.md` **MOD-041**: the substring before the\n * **last** `/`, or `undefined` when the name is not part of any composed\n * subnet instance (no `/`).\n *\n * This helper is duplicated inside the event package (rather than delegated\n * to {@link import('../export/subnet-prefixes.js')}) to keep the event\n * subsystem dependency-free of the export layer per the package-isolation\n * contract.\n *\n * @param name place or transition name (may be `null` / `undefined`)\n * @returns derived instance prefix, or `undefined`\n */\nexport function instancePrefixOfName(name: string | null | undefined): string | undefined {\n if (name == null) return undefined;\n const idx = name.lastIndexOf('/');\n if (idx <= 0) return undefined;\n return name.substring(0, idx);\n}\n\n/**\n * Derived instance prefix per **MOD-041** for any {@link NetEvent} that\n * names a transition or a place. Returns `undefined` for execution lifecycle\n * events and marking snapshots (which do not carry a single\n * transition/place name) and for events whose name has no `/`.\n *\n * Mirrors the per-record `instancePrefix()` getter on the Java\n * {@code NetEvent} sealed hierarchy.\n */\nexport function eventInstancePrefix(event: NetEvent): string | undefined {\n switch (event.type) {\n case 'transition-enabled':\n case 'transition-clock-restarted':\n case 'transition-started':\n case 'transition-completed':\n case 'transition-failed':\n case 'transition-timed-out':\n case 'action-timed-out':\n case 'log-message':\n return instancePrefixOfName(event.transitionName);\n case 'token-added':\n case 'token-removed':\n return instancePrefixOfName(event.placeName);\n default:\n return undefined;\n }\n}\n\n/** Checks if the event is a failure type. */\nexport function isFailureEvent(event: NetEvent): boolean {\n return event.type === 'transition-failed'\n || event.type === 'transition-timed-out'\n || event.type === 'action-timed-out';\n}\n"],"mappings":";AAkJO,SAAS,oBAAoB,OAAgC;AAClE,UAAQ,MAAM,MAAM;AAAA,IAClB,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AACH,aAAO,MAAM;AAAA,IACf;AACE,aAAO;AAAA,EACX;AACF;AAgBO,SAAS,qBAAqB,MAAqD;AACxF,MAAI,QAAQ,KAAM,QAAO;AACzB,QAAM,MAAM,KAAK,YAAY,GAAG;AAChC,MAAI,OAAO,EAAG,QAAO;AACrB,SAAO,KAAK,UAAU,GAAG,GAAG;AAC9B;AAWO,SAAS,oBAAoB,OAAqC;AACvE,UAAQ,MAAM,MAAM;AAAA,IAClB,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AACH,aAAO,qBAAqB,MAAM,cAAc;AAAA,IAClD,KAAK;AAAA,IACL,KAAK;AACH,aAAO,qBAAqB,MAAM,SAAS;AAAA,IAC7C;AACE,aAAO;AAAA,EACX;AACF;AAGO,SAAS,eAAe,OAA0B;AACvD,SAAO,MAAM,SAAS,uBACjB,MAAM,SAAS,0BACf,MAAM,SAAS;AACtB;","names":[]}
@@ -1,6 +1,6 @@
1
1
  import { Writable } from 'node:stream';
2
- import { a as PetriNet, b as Transition, T as Token } from '../petri-net-D-GN9g_D.js';
3
- import { E as EventStore, N as NetEvent } from '../event-store-BnyHh3TF.js';
2
+ import { P as PetriNet, b as Transition, T as Token } from '../petri-net-BDrj4XZE.js';
3
+ import { E as EventStore, N as NetEvent } from '../event-store-2zkXeQkd.js';
4
4
 
5
5
  /**
6
6
  * Commands sent from debug UI client to server via WebSocket.
@@ -135,15 +135,49 @@ interface PlaceInfo {
135
135
  readonly isStart: boolean;
136
136
  readonly isEnd: boolean;
137
137
  readonly isEnvironment: boolean;
138
+ /**
139
+ * Derived instance prefix per `spec/11-modular-composition.md` **MOD-041**:
140
+ * the substring before the last `/` of {@link name}, omitted entirely from
141
+ * the wire when the place is not part of any composed instance.
142
+ */
143
+ readonly instancePrefix?: string;
138
144
  }
139
145
  interface TransitionInfo {
140
146
  readonly name: string;
141
147
  readonly graphId: string;
148
+ /**
149
+ * Derived instance prefix per `spec/11-modular-composition.md` **MOD-041**:
150
+ * the substring before the last `/` of {@link name}, omitted entirely from
151
+ * the wire when the transition is not part of any composed instance.
152
+ */
153
+ readonly instancePrefix?: string;
142
154
  }
143
155
  interface NetStructure {
144
156
  readonly places: readonly PlaceInfo[];
145
157
  readonly transitions: readonly TransitionInfo[];
146
158
  }
159
+ /**
160
+ * Wire-facing descriptor of one composed subnet instance per
161
+ * `spec/11-modular-composition.md` **MOD-041**.
162
+ *
163
+ * Mirrors {@code SubnetInstanceInfo} in Java: carries **names** instead of
164
+ * object references so the JSON shape stays simple. {@link parentPrefix} is
165
+ * absent (omitted from the wire) for top-level instances. {@link defName} is
166
+ * absent in v1 — the runtime does not track {@code SubnetDef} provenance
167
+ * once composition has flattened the topology.
168
+ */
169
+ interface SubnetInstanceInfo {
170
+ /** Instantiation prefix (e.g. `"buf1"` or `"outer/inner"` for nested). */
171
+ readonly prefix: string;
172
+ /** Originating subnet definition name; absent in v1. */
173
+ readonly defName?: string;
174
+ /** Full prefixed transition names belonging to this instance. */
175
+ readonly transitionNames: readonly string[];
176
+ /** Full prefixed place names belonging to this instance. */
177
+ readonly exposedPlaceNames: readonly string[];
178
+ /** Parent-instance prefix; absent for top-level instances. */
179
+ readonly parentPrefix?: string;
180
+ }
147
181
  interface ArchiveSummary {
148
182
  readonly sessionId: string;
149
183
  readonly key: string;
@@ -164,6 +198,13 @@ type DebugResponse = {
164
198
  readonly inFlightTransitions: readonly string[];
165
199
  readonly eventCount: number;
166
200
  readonly mode: string;
201
+ /**
202
+ * Composed subnet instance descriptors per
203
+ * `spec/11-modular-composition.md` **MOD-041**. Empty array for nets
204
+ * that were not produced via composition (no `/` in any node name) —
205
+ * keeps the wire payload compact for the flat case.
206
+ */
207
+ readonly subnetInstances: readonly SubnetInstanceInfo[];
167
208
  } | {
168
209
  readonly type: 'unsubscribed';
169
210
  readonly sessionId: string;
@@ -350,7 +391,13 @@ interface DebugSession {
350
391
  */
351
392
  readonly tags: Record<string, string>;
352
393
  }
353
- /** Builds the net structure from a session's stored place and transition info. */
394
+ /**
395
+ * Builds the net structure from a session's stored place and transition info.
396
+ *
397
+ * Populates the per-place and per-transition `instancePrefix` field per
398
+ * `spec/11-modular-composition.md` **MOD-041**: derived from the `/` prefix
399
+ * portion of each name, omitted entirely for flat names.
400
+ */
354
401
  declare function buildNetStructure(session: DebugSession): NetStructure;
355
402
  type EventStoreFactory = (sessionId: string) => DebugEventStore;
356
403
  declare class DebugSessionRegistry {