libpetri 5.1.0 → 6.0.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.
@@ -6,9 +6,11 @@ import {
6
6
  MarkingState,
7
7
  MarkingStateBuilder,
8
8
  NOTE_ENUMERATED,
9
+ PetriNet,
9
10
  SmtVerifier,
10
11
  StateClass,
11
12
  StateClassGraph,
13
+ Transition,
12
14
  Z3ProcessError,
13
15
  Z3Unavailable,
14
16
  Z3_ENV,
@@ -18,8 +20,12 @@ import {
18
20
  canonicalInvariantOrder,
19
21
  checkCertificate,
20
22
  checkLinearBoundExact,
23
+ compareCodePoints,
21
24
  computePInvariants,
22
25
  computePSemiflows,
26
+ countAcross,
27
+ countPhrase,
28
+ countViolation,
23
29
  deadlockFree,
24
30
  decideOverClasses,
25
31
  decode,
@@ -31,6 +37,8 @@ import {
31
37
  encodeNet,
32
38
  encodeStepRelationSmt2,
33
39
  enumerateBranches,
40
+ failureReason,
41
+ findFiringBound,
34
42
  findMaximalTrapIn,
35
43
  findMinimalSiphons,
36
44
  flatNetIndexOf,
@@ -38,39 +46,50 @@ import {
38
46
  flatNetTransitionCount,
39
47
  flatTransition,
40
48
  flatten,
49
+ fork,
41
50
  formatLinearBound,
42
51
  formatLinearDemand,
52
+ formatRanking,
43
53
  formatZ3Version,
44
54
  ignore,
45
55
  isCoveredByInvariants,
56
+ isPassthrough,
46
57
  isProven,
47
58
  isUntimed,
48
59
  isViolated,
49
60
  joinedOrDeadLettered,
50
61
  mutualExclusion,
62
+ one,
63
+ outPlace,
51
64
  pInvariant,
52
65
  pInvariantToString,
53
66
  parseZ3Version,
67
+ place,
54
68
  placeBound,
55
69
  placeholderCertificate,
56
70
  propertyDescription,
71
+ quiescentCount,
57
72
  replayCounterexample,
58
73
  resolveZ3,
59
74
  rethrowIfProgrammingError,
60
75
  runZ3Spacer,
61
76
  runZ3Text,
77
+ strandedPlaces,
62
78
  strandingExcuses,
63
79
  strandsToken,
64
80
  strengthenWithSemiflows,
65
81
  structuralCheck,
66
82
  terminatesAtSink,
83
+ timeoutBudget,
84
+ tokensAcross,
85
+ transform,
67
86
  unreachable,
68
87
  vcScript,
69
88
  verifyViaStateClassGraph,
70
89
  violationDemand,
71
90
  z3Available,
72
91
  z3SolverAt
73
- } from "../chunk-EL4E6LVO.js";
92
+ } from "../chunk-MQZ6IM63.js";
74
93
  import "../chunk-ATT7U5H5.js";
75
94
 
76
95
  // src/verification/analysis/scc-analyzer.ts
@@ -420,6 +439,705 @@ var TimePetriNetAnalyzerBuilder = class {
420
439
  );
421
440
  }
422
441
  };
442
+
443
+ // src/verification/open-net/contract.ts
444
+ var CONTRACT_KEY = /* @__PURE__ */ Symbol("OpenNetContract.internal");
445
+ var OpenNetContract = class {
446
+ /** Tokens the subnet holds before anything arrives: its own resources and any shared pool it borrows from. */
447
+ initialMarking;
448
+ arrivals;
449
+ clauses;
450
+ /** Places that may hold any number of tokens at quiescence. */
451
+ rest;
452
+ terminals;
453
+ /** Transitions the environment fires: neighbours that react to what the subnet sends. */
454
+ environment;
455
+ /** Whether every run must come to rest. */
456
+ requiresTermination;
457
+ /** @internal Use {@link OpenNetContract.builder}. */
458
+ constructor(key, initialMarking, arrivals, clauses, rest, terminals, environment, requiresTermination) {
459
+ if (key !== CONTRACT_KEY) throw new Error("Use OpenNetContract.builder() to create instances");
460
+ this.initialMarking = initialMarking;
461
+ this.arrivals = arrivals;
462
+ this.clauses = clauses;
463
+ this.rest = rest;
464
+ this.terminals = terminals;
465
+ this.environment = environment;
466
+ this.requiresTermination = requiresTermination;
467
+ }
468
+ static builder() {
469
+ return new OpenNetContractBuilder();
470
+ }
471
+ /**
472
+ * Every place the initial marking, an arrival group, a clause, the rest set or a terminal
473
+ * marker names, then every place an environment transition touches, in first-mention
474
+ * order: the places a port trace reports. A terminal's excused places are not included;
475
+ * `closeOpenNet` adds them to the closed net itself.
476
+ */
477
+ places() {
478
+ const seen = /* @__PURE__ */ new Map();
479
+ const add = (p) => {
480
+ if (!seen.has(p.name)) seen.set(p.name, p);
481
+ };
482
+ for (const p of this.initialMarking.placesWithTokens()) add(p);
483
+ for (const g of this.arrivals) g.places.forEach(add);
484
+ for (const c of this.clauses) c.places.forEach(add);
485
+ this.rest.forEach(add);
486
+ for (const t of this.terminals) add(t.marker);
487
+ for (const t of this.environment) transitionPlaces(t).forEach(add);
488
+ return [...seen.values()];
489
+ }
490
+ /** The contract as the report prints it, one line per part. */
491
+ describe() {
492
+ const names = (ps) => ps.map((p) => p.name).join(", ");
493
+ const lines = [` Initial marking: ${this.initialMarking.toString()}`];
494
+ lines.push(this.arrivals.length === 0 ? " Arrivals: none" : ` Arrivals: ${this.arrivals.map((g) => `${countPhrase(g.min, g.max)} onto {${names(g.places)}}`).join("; ")}`);
495
+ lines.push(this.clauses.length === 0 ? " At quiescence: no count clauses" : ` At quiescence: ${this.clauses.map((c) => `${c.name} = ${countPhrase(c.min, c.max)} across {${names(c.places)}}`).join("; ")}`);
496
+ if (this.rest.length > 0) lines.push(` Rest: ${names(this.rest)}`);
497
+ for (const t of this.terminals) {
498
+ lines.push(t.excused.length === 0 ? ` Terminal: when ${t.marker.name}` : ` Terminal: when ${t.marker.name}: ${names(t.excused)}`);
499
+ }
500
+ if (this.environment.length > 0) {
501
+ lines.push(` Environment transitions: ${this.environment.map((t) => t.name).join(", ")}`);
502
+ }
503
+ lines.push(` Termination: ${this.requiresTermination ? "every run comes to rest" : "not required"}`);
504
+ return lines;
505
+ }
506
+ };
507
+ function transitionPlaces(t) {
508
+ return [
509
+ ...t.inputSpecs.map((s) => s.place),
510
+ ...t.reads.map((a) => a.place),
511
+ ...t.inhibitors.map((a) => a.place),
512
+ ...t.resets.map((a) => a.place),
513
+ ...t.outputPlaces()
514
+ ];
515
+ }
516
+ var OpenNetContractBuilder = class {
517
+ _initialMarking = MarkingState.empty();
518
+ _arrivals = [];
519
+ _clauses = [];
520
+ _rest = /* @__PURE__ */ new Map();
521
+ _terminals = [];
522
+ _environment = [];
523
+ _requiresTermination = true;
524
+ initialMarking(arg) {
525
+ if (arg instanceof MarkingState) {
526
+ this._initialMarking = arg;
527
+ } else {
528
+ const builder = MarkingState.builder();
529
+ arg(builder);
530
+ this._initialMarking = builder.build();
531
+ }
532
+ return this;
533
+ }
534
+ /** The environment delivers exactly `count` tokens, each onto one of `places`, at any point of the run. */
535
+ arrive(count, ...places) {
536
+ return this.arriveBetween(count, count, ...places);
537
+ }
538
+ /** The environment delivers at most `max` tokens, possibly none. `arriveAtMost(1, halt)` is "never or once". */
539
+ arriveAtMost(max, ...places) {
540
+ return this.arriveBetween(0, max, ...places);
541
+ }
542
+ /** The environment delivers between `min` and `max` tokens in total, each onto one of `places`, at any point of the run. */
543
+ arriveBetween(min, max, ...places) {
544
+ if (!Number.isInteger(min) || !Number.isInteger(max) || min < 0 || max < min || max < 1) {
545
+ throw new Error(
546
+ `OpenNetContract: an arrival group needs whole bounds with 0 <= min <= max and max >= 1, got ${min}..${max}. A bound is both the runtime cap and the width of the claim, so it is finite.`
547
+ );
548
+ }
549
+ this._arrivals.push({ places: distinct(places, "an arrival group"), min, max });
550
+ return this;
551
+ }
552
+ /** At every quiescent marking, exactly `count` tokens across `places`. */
553
+ expect(name, count, ...places) {
554
+ return this.expectBetween(name, count, count, ...places);
555
+ }
556
+ /** At every quiescent marking, between `min` and `max` tokens across `places`; `max` may be `Infinity`. */
557
+ expectBetween(name, min, max, ...places) {
558
+ if (name.length === 0) throw new Error("OpenNetContract: a clause needs a name");
559
+ if (this._clauses.some((c) => c.name === name)) {
560
+ throw new Error(`OpenNetContract: duplicate clause name '${name}'`);
561
+ }
562
+ if (!Number.isInteger(min) || min < 0 || !(max === Infinity || Number.isInteger(max)) || max < min) {
563
+ throw new Error(`OpenNetContract: clause '${name}' needs whole bounds with 0 <= min <= max, got ${min}..${max}`);
564
+ }
565
+ this._clauses.push({ name, places: distinct(places, `clause '${name}'`), min, max });
566
+ return this;
567
+ }
568
+ /** Places that may hold any number of tokens at quiescence. */
569
+ rest(...places) {
570
+ for (const p of places) if (!this._rest.has(p.name)) this._rest.set(p.name, p);
571
+ return this;
572
+ }
573
+ /**
574
+ * A designed terminal: while `marker` holds a token, lower bounds are waived and tokens may
575
+ * rest on `excused`. Repeated calls for one marker accumulate.
576
+ */
577
+ terminal(marker, ...excused) {
578
+ let entry = this._terminals.find((t) => t.marker.name === marker.name);
579
+ if (entry == null) {
580
+ entry = { marker, excused: /* @__PURE__ */ new Map() };
581
+ this._terminals.push(entry);
582
+ }
583
+ for (const p of excused) if (!entry.excused.has(p.name)) entry.excused.set(p.name, p);
584
+ return this;
585
+ }
586
+ /**
587
+ * Transitions the environment fires: a neighbour that reacts to what the subnet sends, such
588
+ * as a tool answering a request. An arrival group cannot say that: its tokens do not wait
589
+ * for a request.
590
+ *
591
+ * They join the closed net unchanged and are marked as environment steps in the port trace.
592
+ * A place only they touch belongs to the environment and may hold tokens at quiescence; a
593
+ * place they share with the subnet is a port, judged like any other. Their actions never
594
+ * run, so one that declares outputs may keep `passthrough()`.
595
+ */
596
+ environment(...transitions) {
597
+ for (const t of transitions) {
598
+ if (this._environment.some((e) => e.name === t.name)) {
599
+ throw new Error(`OpenNetContract: duplicate environment transition '${t.name}'`);
600
+ }
601
+ this._environment.push(t);
602
+ }
603
+ return this;
604
+ }
605
+ /** Whether every run must come to rest (default `true`). */
606
+ requireTermination(required) {
607
+ this._requiresTermination = required;
608
+ return this;
609
+ }
610
+ build() {
611
+ return new OpenNetContract(
612
+ CONTRACT_KEY,
613
+ this._initialMarking,
614
+ [...this._arrivals],
615
+ [...this._clauses],
616
+ [...this._rest.values()],
617
+ this._terminals.map((t) => ({ marker: t.marker, excused: [...t.excused.values()] })),
618
+ [...this._environment],
619
+ this._requiresTermination
620
+ );
621
+ }
622
+ };
623
+ function distinct(places, what) {
624
+ if (places.length === 0) throw new Error(`OpenNetContract: ${what} names no place`);
625
+ const byName = /* @__PURE__ */ new Map();
626
+ for (const p of places) if (!byName.has(p.name)) byName.set(p.name, p);
627
+ return [...byName.values()];
628
+ }
629
+
630
+ // src/verification/open-net/closure.ts
631
+ var ENVIRONMENT_ACTION = transform(() => null);
632
+ function closeOpenNet(net, contract) {
633
+ const byName = /* @__PURE__ */ new Map();
634
+ for (const p of net.places) if (!byName.has(p.name)) byName.set(p.name, p);
635
+ const taken = new Set(byName.keys());
636
+ for (const t of net.transitions) taken.add(t.name);
637
+ const fresh = (name) => {
638
+ if (taken.has(name)) {
639
+ throw new Error(`VER-022: closing the net would add '${name}', which the net already declares`);
640
+ }
641
+ taken.add(name);
642
+ return name;
643
+ };
644
+ const environment = /* @__PURE__ */ new Map();
645
+ const environmentPlaces = /* @__PURE__ */ new Map();
646
+ for (const t of contract.environment) {
647
+ environment.set(fresh(t.name), { kind: "transition" });
648
+ for (const p of transitionPlaces(t)) {
649
+ if (!byName.has(p.name) && !environmentPlaces.has(p.name)) environmentPlaces.set(p.name, p);
650
+ }
651
+ }
652
+ for (const [name, p] of environmentPlaces) {
653
+ taken.add(name);
654
+ byName.set(name, p);
655
+ }
656
+ const undeclared = [];
657
+ const extra = [];
658
+ const named = [...contract.places(), ...contract.terminals.flatMap((t) => t.excused)];
659
+ for (const p of named) {
660
+ if (byName.has(p.name)) continue;
661
+ undeclared.push(p.name);
662
+ byName.set(p.name, p);
663
+ extra.push(p);
664
+ }
665
+ const canonical = (p) => byName.get(p.name) ?? p;
666
+ const envTransitions = [];
667
+ const marking = MarkingState.builder().copyFrom(contract.initialMarking);
668
+ const inject = (group, source, target, optional) => {
669
+ const name = fresh(`env:arrive${optional ? "?" : ""}[${group}]:${target.name}`);
670
+ envTransitions.push(
671
+ Transition.builder(name).inputs(one(source)).outputs(outPlace(target)).action(fork()).build()
672
+ );
673
+ environment.set(name, { kind: "arrival", group, place: target.name });
674
+ };
675
+ contract.arrivals.forEach((group, i) => {
676
+ if (group.min > 0) {
677
+ const source = place(fresh(`env:arrivals[${i}]`));
678
+ marking.tokens(source, group.min);
679
+ for (const p of group.places) inject(i, source, canonical(p), false);
680
+ }
681
+ if (group.max > group.min) {
682
+ const source = place(fresh(`env:optional[${i}]`));
683
+ marking.tokens(source, group.max - group.min);
684
+ for (const p of group.places) inject(i, source, canonical(p), true);
685
+ const name = fresh(`env:decline[${i}]`);
686
+ envTransitions.push(Transition.builder(name).inputs(one(source)).build());
687
+ environment.set(name, { kind: "decline", group: i });
688
+ }
689
+ });
690
+ const placeholder = new Set(contract.environment.filter((t) => t.outputSpec !== null && isPassthrough(t.action)).map((t) => t.name));
691
+ const closed = PetriNet.builder(`${net.name}+environment`).places(...net.places, ...extra).transitions(...net.transitions, ...contract.environment, ...envTransitions).build().bindActionsWithResolver((name) => placeholder.has(name) ? ENVIRONMENT_ACTION : null);
692
+ return {
693
+ net: closed,
694
+ initialMarking: marking.build(),
695
+ environment,
696
+ environmentPlaces: [...environmentPlaces.values()],
697
+ undeclared
698
+ };
699
+ }
700
+
701
+ // src/verification/open-net/predicate.ts
702
+ function restDeclarationOf(contract, closed) {
703
+ const sinks = /* @__PURE__ */ new Map();
704
+ for (const c of contract.clauses) for (const p of c.places) if (!sinks.has(p.name)) sinks.set(p.name, p);
705
+ for (const p of contract.rest) if (!sinks.has(p.name)) sinks.set(p.name, p);
706
+ for (const p of closed.environmentPlaces) if (!sinks.has(p.name)) sinks.set(p.name, p);
707
+ return {
708
+ sinks: new Set(sinks.values()),
709
+ conditional: contract.terminals.map((t) => ({ marker: t.marker, places: new Set(t.excused) }))
710
+ };
711
+ }
712
+ function waiverMarkers(contract) {
713
+ return contract.terminals.map((t) => t.marker);
714
+ }
715
+ function strandedNames(m, rest) {
716
+ return strandedPlaces(m, rest.sinks, rest.conditional).sort((a, b) => compareCodePoints(a.name, b.name));
717
+ }
718
+ function quiescenceFindings(m, contract, rest) {
719
+ const findings = [];
720
+ const markers = waiverMarkers(contract);
721
+ for (const clause of contract.clauses) {
722
+ const bound = countViolation(m, clause.places, clause.min, clause.max, markers);
723
+ if (bound !== null) findings.push({ kind: "clause", clause, count: tokensAcross(m, clause.places), bound });
724
+ }
725
+ for (const place2 of strandedNames(m, rest)) {
726
+ findings.push({ kind: "stranded", place: place2.name, count: m.tokens(place2) });
727
+ }
728
+ return findings;
729
+ }
730
+ function subjectOf(f) {
731
+ return f.kind === "clause" ? f.clause.name : f.place;
732
+ }
733
+ function describeFinding(f) {
734
+ if (f.kind === "stranded") {
735
+ return `${f.place} holds ${f.count} at quiescence, and nothing in the contract lets a token rest there`;
736
+ }
737
+ const { clause } = f;
738
+ const expected = countAcross(clause.min, clause.max, clause.places);
739
+ return f.bound === "upper" ? `${expected} at quiescence, found ${f.count} (an upper bound holds under a terminal too)` : `${expected} at quiescence, found ${f.count}`;
740
+ }
741
+
742
+ // src/verification/open-net/result.ts
743
+ function contractViolation(closed, tracedPlaces, violation) {
744
+ const { transitions, markings } = violation;
745
+ const portTrace = [];
746
+ if (markings.length === transitions.length + 1) {
747
+ for (let i = 0; i < transitions.length; i++) {
748
+ const before = markings[i];
749
+ const after = markings[i + 1];
750
+ const changes = [];
751
+ for (const p of tracedPlaces) {
752
+ const delta = after.tokens(p) - before.tokens(p);
753
+ if (delta !== 0) changes.push({ place: p.name, delta });
754
+ }
755
+ const env = closed.environment.get(transitions[i]);
756
+ if (changes.length > 0 || env !== void 0) {
757
+ portTrace.push({ step: i + 1, transition: transitions[i], environment: env?.kind ?? null, changes });
758
+ }
759
+ }
760
+ }
761
+ return { ...violation, portTrace };
762
+ }
763
+
764
+ // src/verification/open-net/graph-route.ts
765
+ function decideOnGraph(closed, contract, maxClasses, tracedPlaces) {
766
+ const graph = StateClassGraph.build(
767
+ closed.net,
768
+ closed.initialMarking,
769
+ maxClasses,
770
+ void 0,
771
+ void 0,
772
+ { untimed: true }
773
+ );
774
+ const classes = graph.stateClasses();
775
+ const rest = restDeclarationOf(contract, closed);
776
+ const tree = bfsTree(graph);
777
+ const first = /* @__PURE__ */ new Map();
778
+ for (const sc of classes) {
779
+ if (sc.enabledTransitions.length > 0) continue;
780
+ for (const finding of quiescenceFindings(sc.marking, contract, rest)) {
781
+ const key = `${finding.kind}:${subjectOf(finding)}`;
782
+ if (!first.has(key)) first.set(key, { finding, target: sc });
783
+ }
784
+ }
785
+ const clauseOrder = new Map(contract.clauses.map((c, i) => [c.name, i]));
786
+ const rank = (f) => f.kind === "clause" ? clauseOrder.get(f.clause.name) : contract.clauses.length;
787
+ const ordered = [...first.values()].sort((a, b) => rank(a.finding) - rank(b.finding) || compareCodePoints(subjectOf(a.finding), subjectOf(b.finding)));
788
+ const violations = ordered.map(({ finding, target }) => {
789
+ const path = pathTo(tree, target);
790
+ return contractViolation(closed, tracedPlaces, {
791
+ kind: finding.kind,
792
+ subject: subjectOf(finding),
793
+ detail: describeFinding(finding),
794
+ transitions: path.transitions,
795
+ markings: path.classes.map((c) => c.marking),
796
+ cycleStart: null,
797
+ confirmed: true
798
+ });
799
+ });
800
+ if (contract.requiresTermination) {
801
+ const cycle = findCycle(graph, tree);
802
+ if (cycle !== null) violations.push(contractViolation(closed, tracedPlaces, cycle));
803
+ }
804
+ return { complete: graph.isComplete(), classCount: classes.length, violations };
805
+ }
806
+ function bfsTree(graph) {
807
+ const tree = /* @__PURE__ */ new Map();
808
+ const seen = /* @__PURE__ */ new Set([graph.initialClass]);
809
+ const queue = [graph.initialClass];
810
+ for (let head = 0; head < queue.length; head++) {
811
+ const current = queue[head];
812
+ for (const [transition, edges] of graph.outgoingBranchEdges(current)) {
813
+ for (const edge of edges) {
814
+ if (seen.has(edge.target)) continue;
815
+ seen.add(edge.target);
816
+ tree.set(edge.target, { parent: current, via: transition.name });
817
+ queue.push(edge.target);
818
+ }
819
+ }
820
+ }
821
+ return tree;
822
+ }
823
+ function pathTo(tree, target) {
824
+ const classes = [];
825
+ const transitions = [];
826
+ for (let cur = target; cur !== void 0; ) {
827
+ classes.push(cur);
828
+ const edge = tree.get(cur);
829
+ if (edge === void 0) break;
830
+ transitions.push(edge.via);
831
+ cur = edge.parent;
832
+ }
833
+ return { classes: classes.reverse(), transitions: transitions.reverse() };
834
+ }
835
+ function findCycle(graph, tree) {
836
+ const edgesOf = (sc) => {
837
+ const out = [];
838
+ for (const [t, edges] of graph.outgoingBranchEdges(sc)) for (const e of edges) out.push([t.name, e.target]);
839
+ return out;
840
+ };
841
+ const position = /* @__PURE__ */ new Map([[graph.initialClass, 0]]);
842
+ const stack = [{ node: graph.initialClass, edges: edgesOf(graph.initialClass), next: 0, via: null }];
843
+ while (stack.length > 0) {
844
+ const top = stack[stack.length - 1];
845
+ if (top.next >= top.edges.length) {
846
+ position.set(top.node, -1);
847
+ stack.pop();
848
+ continue;
849
+ }
850
+ const [via, target] = top.edges[top.next++];
851
+ const at = position.get(target);
852
+ if (at === void 0) {
853
+ position.set(target, stack.length);
854
+ stack.push({ node: target, edges: edgesOf(target), next: 0, via });
855
+ } else if (at >= 0) {
856
+ const loop = stack.slice(at);
857
+ const stem = pathTo(tree, target);
858
+ const cycle = [...loop.slice(1).map((f) => f.via), via];
859
+ return {
860
+ kind: "termination",
861
+ subject: "termination",
862
+ detail: `a run can repeat ${cycle.join(" \u2192 ")} forever without coming to rest`,
863
+ transitions: [...stem.transitions, ...cycle],
864
+ markings: [...stem.classes.map((c) => c.marking), ...loop.slice(1).map((f) => f.node.marking), target.marking],
865
+ cycleStart: stem.transitions.length,
866
+ confirmed: true
867
+ };
868
+ }
869
+ }
870
+ return null;
871
+ }
872
+
873
+ // src/verification/open-net/report.ts
874
+ function renderReport(input) {
875
+ const { net, closed, contract, graph, smtLines, verdict, violations } = input;
876
+ const lines = ["=== OPEN-NET CONTRACT VERIFICATION (VER-022) ===", ""];
877
+ lines.push(`Net: ${net.name}, closed by ${closed.environment.size} environment transitions over ${contract.arrivals.length} arrival groups`);
878
+ lines.push("Contract:", ...contract.describe());
879
+ if (closed.undeclared.length > 0) {
880
+ lines.push(`Not declared by the net: ${closed.undeclared.join(", ")} (no arc touches them; a clause there counts zero)`);
881
+ }
882
+ lines.push("");
883
+ if (graph === null) {
884
+ lines.push(`State-class graph: skipped (${input.graphSkipped ?? "class budget 0"})`);
885
+ } else {
886
+ lines.push("=== State-class graph (untimed, priority-blind) ===");
887
+ lines.push(graph.complete ? ` Classes: ${graph.classCount}, closed` : ` Classes: ${graph.classCount}, truncated at the class budget of ${input.maxClasses}`);
888
+ }
889
+ if (smtLines !== null) {
890
+ lines.push("", "=== SMT route ===", ...smtLines);
891
+ }
892
+ lines.push("", "=== RESULT ===");
893
+ switch (verdict.type) {
894
+ case "proven":
895
+ lines.push(`PROVEN: every quiescent marking meets the contract${contract.requiresTermination ? " and every run comes to rest" : ""} (${verdict.method})`);
896
+ break;
897
+ case "unknown":
898
+ lines.push(`UNKNOWN: ${verdict.reason}`);
899
+ break;
900
+ case "violated":
901
+ lines.push(`VIOLATED: ${violations.length} ${violations.length === 1 ? "part" : "parts"} of the contract broken`);
902
+ for (const v of violations) lines.push(...renderViolation(v));
903
+ break;
904
+ }
905
+ return lines.join("\n");
906
+ }
907
+ function renderViolation(v) {
908
+ const lines = [` [${v.subject}] ${v.kind}: ${v.detail}`];
909
+ if (!v.confirmed) lines.push(" (the solver's counterexample did not replay as an ordered firing sequence)");
910
+ if (v.portTrace.length > 0) {
911
+ lines.push(" Port trace:");
912
+ for (const s of v.portTrace) {
913
+ if (v.cycleStart !== null && s.step === v.cycleStart + 1) lines.push(" -- the cycle starts here --");
914
+ const env = s.environment === null ? "" : s.environment === "transition" ? " [environment]" : ` [environment ${s.environment}]`;
915
+ const changes = s.changes.map((c) => `${c.place} ${c.delta > 0 ? "+" : ""}${c.delta}`).join(", ");
916
+ lines.push(` ${s.step}. ${s.transition}${env}${changes.length > 0 ? ` ${changes}` : ""}`);
917
+ }
918
+ }
919
+ if (v.cycleStart !== null) {
920
+ const stem = v.transitions.slice(0, v.cycleStart);
921
+ const cycle = `then repeating ${v.transitions.slice(v.cycleStart).join(", ")}`;
922
+ lines.push(` Firing sequence: ${stem.length === 0 ? cycle : `${stem.join(", ")}, ${cycle}`}`);
923
+ } else if (v.transitions.length > 0) {
924
+ lines.push(` Firing sequence: ${v.transitions.join(", ")}`);
925
+ }
926
+ const last = v.markings.at(-1);
927
+ if (last !== void 0) lines.push(` ${v.kind === "termination" ? "Marking on the cycle" : "Quiescent marking"}: ${last.toString()}`);
928
+ return lines;
929
+ }
930
+
931
+ // src/verification/open-net/smt-route.ts
932
+ async function decideViaSmt(closed, contract, tracedPlaces, configure, terminationTimeoutMs) {
933
+ const violations = [];
934
+ const undecided = [];
935
+ const lines = [];
936
+ const certificates = [];
937
+ const net = untimed(closed.net);
938
+ for (const part of partsFor(closed, contract)) {
939
+ if (part.kind === "vacuous") {
940
+ lines.push(` [${part.subject}] ${part.detail}: proven (no query needed)`);
941
+ continue;
942
+ }
943
+ const q = part.query;
944
+ let verifier = SmtVerifier.forNet(net).initialMarking(closed.initialMarking).property(q.property).sinkPlaces(...q.sinks).enumerationMaxClasses(0);
945
+ for (const c of q.conditional) verifier = verifier.sinkPlacesWhen(c.marker, ...c.places);
946
+ const result = await configure(verifier).verify();
947
+ const verdict = result.verdict;
948
+ lines.push(` [${q.subject}] ${propertyDescription(q.property)}: ${verdict.type}` + (verdict.type === "unknown" ? ` (${verdict.reason})` : ""));
949
+ if (verdict.type === "unknown") undecided.push(`${q.subject}: ${verdict.reason}`);
950
+ else if (verdict.type === "violated") violations.push(contractViolation(closed, tracedPlaces, q.onViolated(result)));
951
+ else if (verdict.inductiveInvariant !== null) {
952
+ certificates.push({ subject: q.subject, invariant: verdict.inductiveInvariant });
953
+ }
954
+ }
955
+ if (contract.requiresTermination) {
956
+ const termination = await terminationByRanking(closed, terminationTimeoutMs);
957
+ if (termination.proven) {
958
+ lines.push(` [termination] Firing bound (VER-019): ${termination.detail}`);
959
+ } else {
960
+ lines.push(` [termination] Firing bound (VER-019): undecided (${termination.reason})`);
961
+ undecided.push(`termination: ${termination.reason}`);
962
+ }
963
+ }
964
+ return { violations, undecided, lines, certificates };
965
+ }
966
+ function untimed(net) {
967
+ if (isUntimed(net)) return net;
968
+ const transitions = [...net.transitions].map((t) => {
969
+ if (t.timing.type === "immediate") return t;
970
+ const b = Transition.builder(t.name).inputs(...t.inputSpecs).priority(t.priority).action(t.action);
971
+ if (t.outputSpec !== null) b.outputs(t.outputSpec);
972
+ for (const arc of t.inhibitors) b.inhibitor(arc.place);
973
+ for (const arc of t.reads) b.read(arc.place);
974
+ for (const arc of t.resets) b.reset(arc.place);
975
+ if (t.matchSpec !== null) b.match(t.matchSpec);
976
+ return b.build();
977
+ });
978
+ return PetriNet.builder(net.name).places(...net.places).transitions(...transitions).build();
979
+ }
980
+ function partsFor(closed, contract) {
981
+ const rest = restDeclarationOf(contract, closed);
982
+ const markers = waiverMarkers(contract);
983
+ const quiescentMarking = (result) => result.counterexampleConfirmed === true ? result.counterexampleTrace.at(-1) : void 0;
984
+ const witness = (result) => ({
985
+ transitions: [...result.counterexampleTransitions],
986
+ markings: [...result.counterexampleTrace],
987
+ cycleStart: null,
988
+ confirmed: result.counterexampleConfirmed === true
989
+ });
990
+ const stranding = {
991
+ subject: "stranding",
992
+ property: deadlockFree(),
993
+ sinks: [...rest.sinks],
994
+ conditional: rest.conditional,
995
+ onViolated: (result) => {
996
+ const last = quiescentMarking(result);
997
+ const stranded = last === void 0 ? [] : strandedNames(last, rest).map((p) => p.name);
998
+ return {
999
+ kind: "stranded",
1000
+ subject: stranded.length === 0 ? "stranding" : stranded.join(", "),
1001
+ detail: stranded.length === 0 ? "the solver found a reachable quiescent marking that leaves a token where the contract lets none rest" : `${stranded.join(", ")} ${stranded.length === 1 ? "holds" : "hold"} a token at quiescence, and nothing in the contract lets one rest there`,
1002
+ ...witness(result)
1003
+ };
1004
+ }
1005
+ };
1006
+ const parts = [{ kind: "query", query: stranding }];
1007
+ for (const clause of contract.clauses) {
1008
+ const across = countAcross(clause.min, clause.max, clause.places);
1009
+ if (clause.min === 0 && clause.max === Infinity) {
1010
+ parts.push({ kind: "vacuous", subject: clause.name, detail: `${across} at quiescence` });
1011
+ continue;
1012
+ }
1013
+ const query = {
1014
+ subject: clause.name,
1015
+ property: quiescentCount(clause.places, clause.min, clause.max, markers),
1016
+ sinks: [],
1017
+ conditional: [],
1018
+ onViolated: (result) => {
1019
+ const last = quiescentMarking(result);
1020
+ return {
1021
+ kind: "clause",
1022
+ subject: clause.name,
1023
+ detail: last === void 0 ? `${across} at quiescence: the solver found a quiescent marking outside it` : `${across} at quiescence, found ${tokensAcross(last, clause.places)}`,
1024
+ ...witness(result)
1025
+ };
1026
+ }
1027
+ };
1028
+ parts.push({ kind: "query", query });
1029
+ }
1030
+ return parts;
1031
+ }
1032
+ async function terminationByRanking(closed, timeoutMs) {
1033
+ const flat = flatten(closed.net);
1034
+ const initial = flat.places.map((p) => closed.initialMarking.tokens(p));
1035
+ let solver;
1036
+ try {
1037
+ solver = resolveZ3();
1038
+ } catch (e) {
1039
+ if (e instanceof Z3Unavailable) return { proven: false, reason: e.message };
1040
+ throw e;
1041
+ }
1042
+ const ask = async (script) => {
1043
+ try {
1044
+ const reply = await runZ3Text(solver, script, "ranking", timeoutMs, []);
1045
+ const answer = firstLine(reply.stdout);
1046
+ if (answer === "sat" || answer === "unsat" || answer === "unknown") return reply.stdout;
1047
+ return new Error(failureReason(reply, timeoutBudget(timeoutMs)));
1048
+ } catch (e) {
1049
+ rethrowIfProgrammingError(e);
1050
+ return new Error(String(e?.message ?? e));
1051
+ }
1052
+ };
1053
+ const ranking = await findFiringBound(flat, initial, ask);
1054
+ switch (ranking.kind) {
1055
+ case "bound":
1056
+ return {
1057
+ proven: true,
1058
+ detail: `every run has at most ${ranking.bound.bound} firings (${formatRanking(flat, ranking.bound)} drops on every firing)`
1059
+ };
1060
+ case "unbounded":
1061
+ return {
1062
+ proven: false,
1063
+ reason: ranking.repeatable === null ? "no firing bound: no weights drop on every firing" : `no firing bound: the marking equation lets ${ranking.repeatable.map((t) => flat.transitions[t].name).join(", ")} repeat`
1064
+ };
1065
+ case "rejected":
1066
+ return { proven: false, reason: "the firing-bound ranking failed the exact re-check" };
1067
+ case "unknown":
1068
+ return { proven: false, reason: "the firing-bound query answered unknown" };
1069
+ case "failed":
1070
+ return { proven: false, reason: ranking.reason };
1071
+ }
1072
+ }
1073
+ function firstLine(stdout) {
1074
+ return stdout.split("\n").find((line) => line.trim() !== "")?.trim() ?? "";
1075
+ }
1076
+
1077
+ // src/verification/open-net/verify-open-net.ts
1078
+ var DEFAULT_MAX_CLASSES = 5e4;
1079
+ var METHOD_ENUMERATION = "open-net contract by state-space enumeration (VER-022)";
1080
+ var METHOD_SMT = "open-net contract by the SMT pipeline (VER-022)";
1081
+ var SKIPPED_BY_BUDGET = "class budget 0";
1082
+ var SKIPPED_BY_MATCH = "the closed net declares match (\u03BD-join) transitions, which the graph does not model";
1083
+ async function verifyOpenNet(net, contract, options = {}) {
1084
+ const start = performance.now();
1085
+ const closed = closeOpenNet(net, contract);
1086
+ const maxClasses = options.maxClasses ?? DEFAULT_MAX_CLASSES;
1087
+ const useSmt = options.smt ?? true;
1088
+ const tracedPlaces = contract.places();
1089
+ const graphSkipped = [...closed.net.transitions].some((t) => t.matchSpec !== null) ? SKIPPED_BY_MATCH : maxClasses > 0 ? null : SKIPPED_BY_BUDGET;
1090
+ const graph = graphSkipped === null ? decideOnGraph(closed, contract, maxClasses, tracedPlaces) : null;
1091
+ const result = (verdict, route, violations, smtLines) => ({
1092
+ verdict,
1093
+ violations,
1094
+ route,
1095
+ classCount: graph?.classCount ?? 0,
1096
+ graphComplete: graph?.complete ?? false,
1097
+ report: renderReport({ net, closed, contract, maxClasses, graph, graphSkipped, smtLines, verdict, violations }),
1098
+ closedNet: closed.net,
1099
+ closedMarking: closed.initialMarking,
1100
+ elapsedMs: performance.now() - start
1101
+ });
1102
+ if (graph !== null) {
1103
+ if (graph.violations.length > 0) {
1104
+ return result({ type: "violated" }, "enumeration", graph.violations, null);
1105
+ }
1106
+ if (graph.complete) {
1107
+ return result({ type: "proven", method: METHOD_ENUMERATION, inductiveInvariant: null }, "enumeration", [], null);
1108
+ }
1109
+ }
1110
+ const why = graph !== null ? `the state-class graph did not close within ${maxClasses} classes` : graphSkipped === SKIPPED_BY_BUDGET ? "the state-class graph was skipped" : `the state-class graph was skipped: ${graphSkipped}`;
1111
+ if (!useSmt) {
1112
+ return result({ type: "unknown", reason: `${why}, and the SMT route is disabled` }, "enumeration", [], null);
1113
+ }
1114
+ const smt = await decideViaSmt(
1115
+ closed,
1116
+ contract,
1117
+ tracedPlaces,
1118
+ options.configureSmt ?? ((v) => v),
1119
+ options.terminationTimeoutMs ?? 6e4
1120
+ );
1121
+ if (smt.violations.length > 0) return result({ type: "violated" }, "smt", smt.violations, smt.lines);
1122
+ if (smt.undecided.length === 0) {
1123
+ return result(
1124
+ { type: "proven", method: METHOD_SMT, inductiveInvariant: combineCertificates(smt.certificates) },
1125
+ "smt",
1126
+ [],
1127
+ smt.lines
1128
+ );
1129
+ }
1130
+ return result(
1131
+ { type: "unknown", reason: `${why}; left undecided by the SMT route: ${smt.undecided.join("; ")}` },
1132
+ "smt",
1133
+ [],
1134
+ smt.lines
1135
+ );
1136
+ }
1137
+ function combineCertificates(certificates) {
1138
+ if (certificates.length === 0) return null;
1139
+ return certificates.map((c) => `[${c.subject}] ${c.invariant}`).join("\n");
1140
+ }
423
1141
  export {
424
1142
  DBM,
425
1143
  DUMP_ENV,
@@ -428,6 +1146,8 @@ export {
428
1146
  MarkingState,
429
1147
  MarkingStateBuilder,
430
1148
  NOTE_ENUMERATED,
1149
+ OpenNetContract,
1150
+ OpenNetContractBuilder,
431
1151
  SmtVerifier,
432
1152
  StateClass,
433
1153
  StateClassGraph,
@@ -445,6 +1165,7 @@ export {
445
1165
  canonicalInvariantOrder,
446
1166
  checkCertificate,
447
1167
  checkLinearBoundExact,
1168
+ closeOpenNet,
448
1169
  computePInvariants,
449
1170
  computePSemiflows,
450
1171
  computeSCCs,
@@ -482,11 +1203,13 @@ export {
482
1203
  placeBound,
483
1204
  placeholderCertificate,
484
1205
  propertyDescription,
1206
+ quiescentCount,
485
1207
  replayCounterexample,
486
1208
  resolveZ3,
487
1209
  rethrowIfProgrammingError,
488
1210
  runZ3Spacer,
489
1211
  runZ3Text,
1212
+ strandedPlaces,
490
1213
  strandingExcuses,
491
1214
  strandsToken,
492
1215
  strengthenWithSemiflows,
@@ -494,6 +1217,7 @@ export {
494
1217
  terminatesAtSink,
495
1218
  unreachable,
496
1219
  vcScript,
1220
+ verifyOpenNet,
497
1221
  verifyViaStateClassGraph,
498
1222
  violationDemand,
499
1223
  z3Available,