libpetri 2.1.0 → 2.3.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.
@@ -1360,8 +1360,28 @@ declare class PetriNetBuilder {
1360
1360
  * channel name is unknown on the interface, or when caller- and
1361
1361
  * instance-side transition timings conflict (per [MOD-021]).
1362
1362
  */
1363
+ compose(instance: Instance<unknown>): this;
1363
1364
  compose(instance: Instance<unknown>, portMappings: ReadonlyMap<string, Place<unknown>> | Record<string, Place<unknown>>): this;
1364
1365
  compose(instance: Instance<unknown>, bind: (b: ComposeBindings) => void): this;
1366
+ /**
1367
+ * Identity-default auto-compose per **MOD-024**.
1368
+ *
1369
+ * Each declared interface port auto-binds to its own `port.place` — the
1370
+ * Place the SubnetDef builder declared via `.inputPort(name, hostPlace)`
1371
+ * (or `outputPort` / `inoutPort`). If the host builder already declares
1372
+ * the equal place, the two merge; if not, the place arrives implicitly
1373
+ * via the rewritten transitions' arcs (same flow as explicit `bindPort`).
1374
+ *
1375
+ * If the subnet declares no interface ports at all, body places are
1376
+ * checked against this builder's place set **by name** (matching the
1377
+ * existing TS Place equality semantics; see [CORE-002] note in
1378
+ * `spec/11-modular-composition.md` MOD-024). Body places that don't match
1379
+ * stay private under their prefixed names per [MOD-010].
1380
+ *
1381
+ * Channels are NOT auto-bound — transition identity is too delicate for
1382
+ * inference. If the subnet declares any channel, this overload throws.
1383
+ */
1384
+ private composeAuto;
1365
1385
  /**
1366
1386
  * @internal Shared compose implementation: validates port and channel
1367
1387
  * bindings, builds the place-substitution map, walks every renamed-body
@@ -1396,6 +1416,16 @@ declare class PetriNetBuilder {
1396
1416
  * channel-handle map — but the prefixed names remain stable.
1397
1417
  */
1398
1418
  private composeInternal;
1419
+ /**
1420
+ * Shared post-mergeMap pipeline: rewrites renamed-body transitions
1421
+ * through `mergeMap`, applies channel merges per **MOD-021**, and adds
1422
+ * the surviving transitions to the builder.
1423
+ *
1424
+ * Used by both the explicit-binding path (`composeInternal`) and the
1425
+ * auto-compose path (`composeAuto` per **MOD-024**). The two paths differ
1426
+ * only in how the (renamed Place name → host Place) `mergeMap` is built.
1427
+ */
1428
+ private applyComposition;
1399
1429
  /**
1400
1430
  * Registers one or more {@link FusionSet} declarations on this builder, per
1401
1431
  * **MOD-060** (fusion set declaration) and **MOD-061** (fusion resolution
@@ -1,5 +1,5 @@
1
- import { b as Transition, a as Place, P as PetriNet, E as EnvironmentPlace, aG as MarkingState, aH as PInvariant, aI as MarkingStateBuilder, aJ as SmtProperty, aK as SmtVerificationResult } from '../petri-net-BDrj4XZE.js';
2
- export { aL as DeadlockFree, aM as MutualExclusion, aN as PlaceBound, aO as Proven, aP as SmtStatistics, aQ as TokenSupplier, aR as Unknown, aS as Unreachable, aT as Verdict, X as VerificationHarness, Y as VerificationResult, aU as Violated, aV as deadlockFree, aW as isProven, aX as isViolated, aY as mutualExclusion, aZ as pInvariant, a_ as pInvariantToString, a$ as placeBound, b0 as propertyDescription, b1 as unreachable } from '../petri-net-BDrj4XZE.js';
1
+ import { b as Transition, a as Place, P as PetriNet, E as EnvironmentPlace, aG as MarkingState, aH as PInvariant, aI as MarkingStateBuilder, aJ as SmtProperty, aK as SmtVerificationResult } from '../petri-net-CKZYPOsL.js';
2
+ export { aL as DeadlockFree, aM as MutualExclusion, aN as PlaceBound, aO as Proven, aP as SmtStatistics, aQ as TokenSupplier, aR as Unknown, aS as Unreachable, aT as Verdict, X as VerificationHarness, Y as VerificationResult, aU as Violated, aV as deadlockFree, aW as isProven, aX as isViolated, aY as mutualExclusion, aZ as pInvariant, a_ as pInvariantToString, a$ as placeBound, b0 as propertyDescription, b1 as unreachable } from '../petri-net-CKZYPOsL.js';
3
3
  import { Expr, init, Bool, FuncDecl } from 'z3-solver';
4
4
 
5
5
  /**
@@ -135,6 +135,16 @@ declare const VIEWER_CSS_VARIABLES: readonly ["--lpv-bg", "--lpv-header-bg", "--
135
135
  * @module viewer
136
136
  */
137
137
 
138
+ /**
139
+ * Subnet (cluster) visibility mode.
140
+ *
141
+ * - `'show'` — render the DOT as-is with `subgraph cluster_*` groupings
142
+ * visible (the default; the post-subnets layout).
143
+ * - `'hide'` — render a flattened variant that drops cluster wrappers
144
+ * and the `ltail`/`lhead` cluster references on cross-cluster edges.
145
+ * Same nodes and edges, no visual grouping.
146
+ */
147
+ type SubnetVisibility = 'show' | 'hide';
138
148
  /** Handle returned by {@link mount}. */
139
149
  interface ViewerHandle {
140
150
  /** The rendered root `<svg>` element. */
@@ -147,6 +157,22 @@ interface ViewerHandle {
147
157
  readonly collapsedPrefixes: ReadonlySet<string>;
148
158
  /** Currently active "show only <prefix>" filter, or null. */
149
159
  readonly activeFilter: string | null;
160
+ /** Current subnet visibility mode. */
161
+ readonly subnets: SubnetVisibility;
162
+ /**
163
+ * Switch between the clustered (`'show'`) and flat (`'hide'`) views.
164
+ * Triggers an internal re-mount via the canonical {@link mount} path —
165
+ * the SVG is regenerated because Graphviz lays out cluster boundaries
166
+ * during layout. Returns the new handle; the old one is disposed.
167
+ *
168
+ * After re-mount, the host container dispatches a bubbling
169
+ * `libpetri-viewer:remount` CustomEvent whose `detail.handle` is the
170
+ * fresh handle. Consumers that cache the handle (e.g. the debug-ui)
171
+ * should listen on the container and update their reference.
172
+ */
173
+ setSubnets(mode: SubnetVisibility): Promise<ViewerHandle>;
174
+ /** Flip {@link subnets} to the other mode and re-mount. See {@link setSubnets}. */
175
+ toggleSubnets(): Promise<ViewerHandle>;
150
176
  /** Collapse a single cluster by prefix. No-op if unknown. */
151
177
  collapse(prefix: string): void;
152
178
  /** Expand a single cluster by prefix. No-op if unknown. */
@@ -203,6 +229,15 @@ interface MountOptions {
203
229
  * cached on the DOT hash; re-mounts on identical DOT skip the pipeline.
204
230
  */
205
231
  readonly layout?: 'elk' | 'graphviz';
232
+ /**
233
+ * Initial subnet visibility mode. Defaults to `'show'` (clustered view).
234
+ * Pass `'hide'` to mount with the flat view from the start. The chrome
235
+ * button toggles between the two at runtime; see {@link ViewerHandle.setSubnets}.
236
+ *
237
+ * If `previousHandle` is supplied and `subnets` is omitted, the previous
238
+ * handle's mode is inherited so live re-renders preserve the user's choice.
239
+ */
240
+ readonly subnets?: SubnetVisibility;
206
241
  }
207
242
  /**
208
243
  * Render a DOT source, mount the resulting SVG into `container`, wire pan/zoom,
@@ -224,4 +259,4 @@ interface MountOptions {
224
259
  */
225
260
  declare function mount(dotSource: string | null, container: HTMLElement, opts?: MountOptions): Promise<ViewerHandle>;
226
261
 
227
- export { type ClusterDescriptor, DEFAULT_PANZOOM_OPTS, type MountOptions, type PanzoomInstance, type PanzoomOptions, VIEWER_CSS_VARIABLES, type ViewerHandle, colorForPrefix, discoverClusters, mount };
262
+ export { type ClusterDescriptor, DEFAULT_PANZOOM_OPTS, type MountOptions, type PanzoomInstance, type PanzoomOptions, type SubnetVisibility, VIEWER_CSS_VARIABLES, type ViewerHandle, colorForPrefix, discoverClusters, mount };
@@ -655,11 +655,46 @@ var VIEWER_CSS_VARIABLES = [
655
655
  "--lpv-faded-cluster-opacity"
656
656
  ];
657
657
 
658
+ // src/viewer/dot-flatten.ts
659
+ var CLUSTER_OPEN = /^\s*subgraph\s+cluster_[A-Za-z0-9_]+\s*\{\s*$/;
660
+ var ONLY_CLOSE_BRACE = /^\s*\}\s*$/;
661
+ function flattenClusters(dot) {
662
+ const lines = dot.split("\n");
663
+ const out = [];
664
+ let clusterDepth = 0;
665
+ for (const line of lines) {
666
+ if (CLUSTER_OPEN.test(line)) {
667
+ clusterDepth++;
668
+ continue;
669
+ }
670
+ if (clusterDepth > 0) {
671
+ if (ONLY_CLOSE_BRACE.test(line)) {
672
+ clusterDepth--;
673
+ continue;
674
+ }
675
+ if (line.includes("[") || line.includes("->")) {
676
+ out.push(stripClusterEdgeAttrs(line));
677
+ }
678
+ continue;
679
+ }
680
+ if (line.includes("->") && (line.includes("ltail=") || line.includes("lhead="))) {
681
+ out.push(stripClusterEdgeAttrs(line));
682
+ continue;
683
+ }
684
+ out.push(line);
685
+ }
686
+ return out.join("\n");
687
+ }
688
+ function stripClusterEdgeAttrs(line) {
689
+ return line.replace(/,\s*ltail="cluster_[^"]*"/g, "").replace(/,\s*lhead="cluster_[^"]*"/g, "").replace(/\[\s*ltail="cluster_[^"]*"\s*,\s*/g, "[").replace(/\[\s*lhead="cluster_[^"]*"\s*,\s*/g, "[").replace(/\[\s*ltail="cluster_[^"]*"\s*\]/g, "[]").replace(/\[\s*lhead="cluster_[^"]*"\s*\]/g, "[]");
690
+ }
691
+
658
692
  // src/viewer/index.ts
659
693
  async function mount(dotSource, container, opts = {}) {
660
694
  const previousHandle = opts.previousHandle ?? null;
661
695
  const preservedCollapsed = previousHandle ? new Set(previousHandle.collapsedPrefixes) : /* @__PURE__ */ new Set();
662
696
  const preservedFilter = previousHandle?.activeFilter ?? null;
697
+ const subnetsMode = opts.subnets ?? previousHandle?.subnets ?? "show";
663
698
  if (previousHandle) {
664
699
  previousHandle.dispose();
665
700
  }
@@ -673,7 +708,8 @@ async function mount(dotSource, container, opts = {}) {
673
708
  } else {
674
709
  const renderModule = await import("../render-QK57X4TP.js");
675
710
  const useElk = (opts.layout ?? "elk") === "elk";
676
- svg = useElk ? await renderModule.renderDotToSvgWithElkLayout(dotSource) : await renderModule.renderDotToSvg(dotSource);
711
+ const renderedDot = subnetsMode === "hide" ? flattenClusters(dotSource) : dotSource;
712
+ svg = useElk ? await renderModule.renderDotToSvgWithElkLayout(renderedDot) : await renderModule.renderDotToSvg(renderedDot);
677
713
  container.innerHTML = "";
678
714
  container.appendChild(svg);
679
715
  }
@@ -716,6 +752,19 @@ async function mount(dotSource, container, opts = {}) {
716
752
  fsBtn.title = "Toggle fullscreen";
717
753
  fsBtn.textContent = "Fullscreen";
718
754
  fsBtn.addEventListener("click", () => toggleFullscreen(container, fsBtn));
755
+ const subnetsBtn = document.createElement("button");
756
+ subnetsBtn.type = "button";
757
+ subnetsBtn.className = "diagram-btn btn-subnets";
758
+ subnetsBtn.title = subnetsMode === "show" ? "Hide subnet groupings" : "Show subnet groupings";
759
+ subnetsBtn.textContent = subnetsMode === "show" ? "Flat view" : "Subnets view";
760
+ if (dotSource == null) {
761
+ subnetsBtn.disabled = true;
762
+ subnetsBtn.title = "Subnet toggle unavailable for pre-rendered SVG";
763
+ }
764
+ subnetsBtn.addEventListener("click", () => {
765
+ void handle.toggleSubnets();
766
+ });
767
+ controls.appendChild(subnetsBtn);
719
768
  controls.appendChild(resetBtn);
720
769
  controls.appendChild(fsBtn);
721
770
  chromeRoot.appendChild(controls);
@@ -723,7 +772,7 @@ async function mount(dotSource, container, opts = {}) {
723
772
  container.style.position = "relative";
724
773
  }
725
774
  container.appendChild(chromeRoot);
726
- if (layoutMode === "elk" && clusters.size > 0) {
775
+ if (layoutMode === "elk" && subnetsMode === "show" && clusters.size > 0) {
727
776
  sidebarHandle = mountSidebar(container, clusters, {
728
777
  onVisibilityChange: (state) => handle.setVisibility(state),
729
778
  onHighlightModeChange: (enabled) => {
@@ -748,6 +797,29 @@ async function mount(dotSource, container, opts = {}) {
748
797
  get activeFilter() {
749
798
  return activeFilter;
750
799
  },
800
+ get subnets() {
801
+ return subnetsMode;
802
+ },
803
+ setSubnets(mode) {
804
+ if (dotSource == null) return Promise.resolve(handle);
805
+ if (mode === subnetsMode) return Promise.resolve(handle);
806
+ return mount(dotSource, container, {
807
+ ...opts,
808
+ previousHandle: handle,
809
+ subnets: mode
810
+ }).then((next) => {
811
+ container.dispatchEvent(
812
+ new CustomEvent("libpetri-viewer:remount", {
813
+ bubbles: true,
814
+ detail: { handle: next, subnets: mode }
815
+ })
816
+ );
817
+ return next;
818
+ });
819
+ },
820
+ toggleSubnets() {
821
+ return handle.setSubnets(subnetsMode === "show" ? "hide" : "show");
822
+ },
751
823
  collapse(prefix) {
752
824
  if (disposed) return;
753
825
  const cluster = clusters.get(prefix);