libpetri 5.0.0 → 5.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.
@@ -220,6 +220,11 @@ function withTimeout(action, timeoutMs, timeoutPlace2, timeoutValue) {
220
220
  };
221
221
  }
222
222
 
223
+ // src/verification/programming-error.ts
224
+ function rethrowIfProgrammingError(e) {
225
+ if (e instanceof TypeError || e instanceof ReferenceError) throw e;
226
+ }
227
+
223
228
  // src/verification/marking-state.ts
224
229
  var MARKING_STATE_KEY = /* @__PURE__ */ Symbol("MarkingState.internal");
225
230
  var MarkingState = class _MarkingState {
@@ -252,9 +257,9 @@ var MarkingState = class _MarkingState {
252
257
  }
253
258
  /** Returns the total number of tokens. */
254
259
  totalTokens() {
255
- let sum = 0;
256
- for (const count of this.tokenCounts.values()) sum += count;
257
- return sum;
260
+ let sum2 = 0;
261
+ for (const count of this.tokenCounts.values()) sum2 += count;
262
+ return sum2;
258
263
  }
259
264
  /** Checks if no tokens exist anywhere. */
260
265
  isEmpty() {
@@ -368,6 +373,53 @@ function propertyDescription(prop) {
368
373
  }
369
374
  }
370
375
 
376
+ // src/verification/rest-set.ts
377
+ function strandingExcuses(flatNet, sinkPlaces, conditional) {
378
+ const P = flatNet.places.length;
379
+ const excuses = new Array(P);
380
+ for (let pid = 0; pid < P; pid++) excuses[pid] = [];
381
+ for (const sink of sinkPlaces) {
382
+ const pid = flatNet.placeIndex.get(sink.name);
383
+ if (pid != null) excuses[pid] = null;
384
+ }
385
+ for (const { marker, places } of conditional) {
386
+ const mid = flatNet.placeIndex.get(marker.name);
387
+ if (mid == null) continue;
388
+ excuses[mid] = null;
389
+ for (const place of places) {
390
+ const pid = flatNet.placeIndex.get(place.name);
391
+ if (pid == null) continue;
392
+ const list = excuses[pid];
393
+ if (list != null && !list.includes(mid)) list.push(mid);
394
+ }
395
+ }
396
+ for (const list of excuses) if (list != null) list.sort((a, b) => a - b);
397
+ return excuses;
398
+ }
399
+ function strandsToken(m, sinkPlaces, conditional) {
400
+ const resting = /* @__PURE__ */ new Set();
401
+ for (const s of sinkPlaces) resting.add(s.name);
402
+ for (const { marker, places } of conditional) {
403
+ resting.add(marker.name);
404
+ if (m.hasTokens(marker)) {
405
+ for (const p of places) resting.add(p.name);
406
+ }
407
+ }
408
+ for (const p of m.placesWithTokens()) {
409
+ if (!resting.has(p.name)) return true;
410
+ }
411
+ return false;
412
+ }
413
+ function describeSinks(sinkPlaces, conditional) {
414
+ const parts = [];
415
+ if (sinkPlaces.size > 0) parts.push(`sinks: ${[...sinkPlaces].map((p) => p.name).join(", ")}`);
416
+ for (const { marker, places } of conditional) {
417
+ const names = [...places].map((p) => p.name);
418
+ parts.push(names.length === 0 ? `when ${marker.name}` : `when ${marker.name}: ${names.join(", ")}`);
419
+ }
420
+ return parts.length === 0 ? null : parts.join("; ");
421
+ }
422
+
371
423
  // src/verification/encoding/flat-transition.ts
372
424
  function flatTransition(name, source, branchIndex, preVector, postVector, inhibitorPlaces, readPlaces, resetPlaces, consumeAll) {
373
425
  return {
@@ -826,6 +878,8 @@ function strengthenWithSemiflows(invariants, semiflows) {
826
878
  }
827
879
  return { invariants: strengthened, added };
828
880
  }
881
+ var MAX_SEMIFLOW_ROWS = 8192;
882
+ var MAX_SEMIFLOW_CANDIDATES = 65536;
829
883
  function computePSemiflows(matrix, flatNet, initialMarking) {
830
884
  const np = matrix.numPlaces();
831
885
  const nt = matrix.numTransitions();
@@ -843,19 +897,21 @@ function computePSemiflows(matrix, flatNet, initialMarking) {
843
897
  const next = rows.filter((r) => r.sig[t] === 0);
844
898
  const pos = rows.filter((r) => r.sig[t] > 0);
845
899
  const neg = rows.filter((r) => r.sig[t] < 0);
846
- for (const rp of pos) {
847
- for (const rn of neg) {
848
- const cp = -rn.sig[t];
849
- const cn = rp.sig[t];
850
- const sig = combineRow(cp, rp.sig, cn, rn.sig);
851
- const weight = combineRow(cp, rp.weight, cn, rn.weight);
852
- if (sig === null || weight === null) continue;
853
- reduceGcd(sig, weight);
854
- next.push({ sig, weight });
900
+ outer:
901
+ for (const rp of pos) {
902
+ for (const rn of neg) {
903
+ if (next.length >= MAX_SEMIFLOW_CANDIDATES) break outer;
904
+ const cp = -rn.sig[t];
905
+ const cn = rp.sig[t];
906
+ const sig = combineRow(cp, rp.sig, cn, rn.sig);
907
+ const weight = combineRow(cp, rp.weight, cn, rn.weight);
908
+ if (sig === null || weight === null) continue;
909
+ reduceGcd(sig, weight);
910
+ next.push({ sig, weight });
911
+ }
855
912
  }
856
- }
857
913
  rows = keepSupportMinimal(next);
858
- if (rows.length > 8192) rows.length = 8192;
914
+ if (rows.length > MAX_SEMIFLOW_ROWS) rows.length = MAX_SEMIFLOW_ROWS;
859
915
  }
860
916
  const semiflows = [];
861
917
  for (const { weight } of rows) {
@@ -892,17 +948,43 @@ function reduceGcd(sig, weight) {
892
948
  }
893
949
  }
894
950
  function keepSupportMinimal(rows) {
895
- const supports = rows.map((r) => {
896
- const s = [];
897
- for (let i = 0; i < r.weight.length; i++) if (r.weight[i] !== 0) s.push(i);
898
- return s;
899
- });
900
- const keep = new Array(rows.length).fill(true);
901
- for (let i = 0; i < rows.length; i++) {
902
- if (!keep[i]) continue;
903
- for (let j = 0; j < rows.length; j++) {
904
- if (i === j || !keep[j]) continue;
905
- if (supports[j].length < supports[i].length && supports[j].every((p) => supports[i].includes(p))) {
951
+ const n = rows.length;
952
+ if (n < 2) return rows;
953
+ const words = rows[0].weight.length + 31 >>> 5 || 1;
954
+ const bits = new Uint32Array(n * words);
955
+ const sizes = new Int32Array(n);
956
+ for (let i = 0; i < n; i++) {
957
+ const w = rows[i].weight;
958
+ let size = 0;
959
+ for (let p = 0; p < w.length; p++) {
960
+ if (w[p] !== 0) {
961
+ const idx = i * words + (p >>> 5);
962
+ bits[idx] = bits[idx] | 1 << (p & 31);
963
+ size++;
964
+ }
965
+ }
966
+ sizes[i] = size;
967
+ }
968
+ const order = new Int32Array(n);
969
+ for (let i = 0; i < n; i++) order[i] = i;
970
+ order.sort((a, b) => sizes[a] - sizes[b]);
971
+ const keep = new Array(n).fill(true);
972
+ for (let oi = 0; oi < n; oi++) {
973
+ const i = order[oi];
974
+ const base = i * words;
975
+ for (let oj = 0; oj < oi; oj++) {
976
+ const j = order[oj];
977
+ if (sizes[j] >= sizes[i]) break;
978
+ const jbase = j * words;
979
+ let subset = true;
980
+ for (let w = 0; w < words; w++) {
981
+ const jb = bits[jbase + w];
982
+ if ((jb & ~bits[base + w]) !== 0) {
983
+ subset = false;
984
+ break;
985
+ }
986
+ }
987
+ if (subset) {
906
988
  keep[i] = false;
907
989
  break;
908
990
  }
@@ -1241,7 +1323,8 @@ function locateZ3(program, env = process.env) {
1241
1323
  const isFile = (p) => {
1242
1324
  try {
1243
1325
  return existsSync(p) && statSync(p).isFile();
1244
- } catch {
1326
+ } catch (e) {
1327
+ rethrowIfProgrammingError(e);
1245
1328
  return false;
1246
1329
  }
1247
1330
  };
@@ -1252,9 +1335,9 @@ function locateZ3(program, env = process.env) {
1252
1335
  const windows = process.platform === "win32";
1253
1336
  for (const dir of searchPath.split(path.delimiter)) {
1254
1337
  if (dir === "") continue;
1255
- const candidate2 = path.join(dir, program);
1256
- if (isFile(candidate2)) return candidate2;
1257
- if (windows && isFile(candidate2 + ".exe")) return candidate2 + ".exe";
1338
+ const candidate = path.join(dir, program);
1339
+ if (isFile(candidate)) return candidate;
1340
+ if (windows && isFile(candidate + ".exe")) return candidate + ".exe";
1258
1341
  }
1259
1342
  return null;
1260
1343
  }
@@ -1300,7 +1383,8 @@ function z3Available(env = process.env) {
1300
1383
  try {
1301
1384
  resolveZ3(env);
1302
1385
  return true;
1303
- } catch {
1386
+ } catch (e) {
1387
+ rethrowIfProgrammingError(e);
1304
1388
  return false;
1305
1389
  }
1306
1390
  }
@@ -1373,6 +1457,7 @@ async function runZ3Spacer(solver, timeoutMs, smt2, phase) {
1373
1457
  try {
1374
1458
  reply = await runZ3Text(solver, smt2, phase, timeoutMs, ["fp.engine=spacer"]);
1375
1459
  } catch (e) {
1460
+ rethrowIfProgrammingError(e);
1376
1461
  return { type: "unknown", reason: String(e?.message ?? e) };
1377
1462
  }
1378
1463
  const stdout = reply.stdout.trim();
@@ -1391,36 +1476,50 @@ async function runZ3Spacer(solver, timeoutMs, smt2, phase) {
1391
1476
  }
1392
1477
 
1393
1478
  // src/verification/z3/smt-encoder.ts
1394
- function encode(flatNet, initialMarking, property, invariants, sinkPlaces = /* @__PURE__ */ new Set(), produceProofs = false) {
1479
+ function encode(flatNet, initialMarking, property, invariants, sinkPlaces = /* @__PURE__ */ new Set(), produceProofs = false, conditionalSinks = []) {
1480
+ return encodeNet(flatNet, initialMarking, property, invariants, { sinkPlaces, produceProofs, conditionalSinks });
1481
+ }
1482
+ function encodeNet(flatNet, initialMarking, property, invariants, options = {}) {
1483
+ const sinkPlaces = options.sinkPlaces ?? /* @__PURE__ */ new Set();
1484
+ const produceProofs = options.produceProofs ?? false;
1485
+ const conditionalSinks = options.conditionalSinks ?? [];
1395
1486
  const P = flatNet.places.length;
1487
+ const T = options.stateEquation ? flatNet.transitions.length : 0;
1396
1488
  const lines = [];
1397
1489
  const envInject = resolveEnvInjection(flatNet);
1398
1490
  if (produceProofs) lines.push("(set-option :produce-proofs true)");
1399
1491
  lines.push("(set-logic HORN)");
1400
1492
  lines.push("");
1401
- lines.push(`(declare-fun Reachable (${ints(P).join(" ")}) Bool)`);
1493
+ lines.push(`(declare-fun Reachable (${ints(P + T).join(" ")}) Bool)`);
1402
1494
  lines.push("(declare-fun Error () Bool)");
1403
1495
  lines.push("");
1404
1496
  const mVars = vars(P, "");
1405
1497
  const mpVars = vars(P, "p");
1498
+ const nVars = counterVars(T, "");
1499
+ const npVars = counterVars(T, "p");
1406
1500
  const m0 = [];
1407
1501
  for (let i = 0; i < P; i++) m0.push(String(initialMarking.tokens(flatNet.places[i])));
1502
+ for (let k = 0; k < T; k++) m0.push("0");
1408
1503
  lines.push(`(assert (Reachable ${m0.join(" ")}))`);
1409
1504
  lines.push("");
1410
- for (const ft of flatNet.transitions) {
1411
- lines.push(encodeTransitionRule(flatNet, ft, mVars, mpVars, invariants));
1505
+ const equation = T > 0 ? stateEquationConditions(flatNet, initialMarking, npVars) : [];
1506
+ for (let k = 0; k < flatNet.transitions.length; k++) {
1507
+ const ft = flatNet.transitions[k];
1508
+ const strengthening = [...invariantConditions(invariants, mpVars)];
1509
+ if (T > 0) strengthening.push(...counterConditions(k, nVars, npVars), ...equation);
1510
+ lines.push(encodeTransitionRule(flatNet, ft, mVars, mpVars, nVars, npVars, strengthening));
1412
1511
  }
1413
1512
  for (const inj of envInject) {
1414
- lines.push(encodeInjectionRule(P, inj.pid, inj.bound, mVars, mpVars));
1513
+ lines.push(encodeInjectionRule(P, inj.pid, inj.bound, mVars, mpVars, nVars, npVars));
1415
1514
  }
1416
1515
  lines.push("");
1417
- lines.push(encodeErrorRule(flatNet, property, mVars, sinkPlaces, envInject));
1516
+ lines.push(encodeErrorRule(flatNet, property, mVars, nVars, sinkPlaces, envInject, conditionalSinks));
1418
1517
  lines.push("");
1419
1518
  lines.push("(assert (not Error))");
1420
1519
  lines.push("(check-sat)");
1421
1520
  if (produceProofs) lines.push("(get-proof)");
1422
1521
  lines.push("(get-model)");
1423
- return { smt2: lines.join("\n"), placeCount: P };
1522
+ return { smt2: lines.join("\n"), placeCount: P, counterCount: T };
1424
1523
  }
1425
1524
  function resolveEnvInjection(flatNet) {
1426
1525
  const out = [];
@@ -1448,9 +1547,44 @@ function vars(P, suffix) {
1448
1547
  for (let i = 0; i < P; i++) out.push(`m${i}${suffix}`);
1449
1548
  return out;
1450
1549
  }
1550
+ function counterVars(T, suffix) {
1551
+ const out = [];
1552
+ for (let k = 0; k < T; k++) out.push(`n${k}${suffix}`);
1553
+ return out;
1554
+ }
1451
1555
  function quantified(names) {
1452
1556
  return names.map((v) => `(${v} Int)`).join(" ");
1453
1557
  }
1558
+ function equationPlaces(flatNet) {
1559
+ const excluded = new Set(nonlinearPlaces(flatNet));
1560
+ for (const inj of resolveEnvInjection(flatNet)) excluded.add(inj.pid);
1561
+ const out = [];
1562
+ for (let p = 0; p < flatNet.places.length; p++) if (!excluded.has(p)) out.push(p);
1563
+ return out;
1564
+ }
1565
+ function counterConditions(fired, nVars, npVars) {
1566
+ const conditions = [];
1567
+ for (let k = 0; k < nVars.length; k++) {
1568
+ conditions.push(k === fired ? `(= ${npVars[k]} (+ ${nVars[k]} 1))` : `(= ${npVars[k]} ${nVars[k]})`);
1569
+ }
1570
+ for (let k = 0; k < npVars.length; k++) conditions.push(`(>= ${npVars[k]} 0)`);
1571
+ return conditions;
1572
+ }
1573
+ function stateEquationConditions(flatNet, initialMarking, nVars, mVars = vars(flatNet.places.length, "p")) {
1574
+ const conditions = [];
1575
+ for (const p of equationPlaces(flatNet)) {
1576
+ const terms = [];
1577
+ for (let t = 0; t < flatNet.transitions.length; t++) {
1578
+ const ft = flatNet.transitions[t];
1579
+ const c = ft.postVector[p] - ft.preVector[p];
1580
+ if (c === 0) continue;
1581
+ terms.push(c === 1 ? nVars[t] : c === -1 ? `(- ${nVars[t]})` : c > 0 ? `(* ${c} ${nVars[t]})` : `(* (- ${-c}) ${nVars[t]})`);
1582
+ }
1583
+ const m0 = initialMarking.tokens(flatNet.places[p]);
1584
+ conditions.push(terms.length === 0 ? `(= ${mVars[p]} ${m0})` : `(= ${mVars[p]} (+ ${m0} ${terms.join(" ")}))`);
1585
+ }
1586
+ return conditions;
1587
+ }
1454
1588
  function firingConditions(flatNet, ft, mVars, mpVars) {
1455
1589
  const P = flatNet.places.length;
1456
1590
  const conditions = [];
@@ -1477,8 +1611,8 @@ function invariantConditions(invariants, names) {
1477
1611
  for (const inv of invariants) {
1478
1612
  const terms = [...inv.support].sort((a, b) => a - b).map((i) => `(* ${inv.weights[i]} ${names[i]})`);
1479
1613
  if (terms.length === 0) continue;
1480
- const sum = terms.length === 1 ? terms[0] : `(+ ${terms.join(" ")})`;
1481
- conditions.push(`(= ${sum} ${inv.constant})`);
1614
+ const sum2 = terms.length === 1 ? terms[0] : `(+ ${terms.join(" ")})`;
1615
+ conditions.push(`(= ${sum2} ${inv.constant})`);
1482
1616
  }
1483
1617
  return conditions;
1484
1618
  }
@@ -1494,50 +1628,61 @@ function injectionConditions(P, pid, bound, mVars, mpVars) {
1494
1628
  }
1495
1629
  return conditions;
1496
1630
  }
1497
- function encodeTransitionRule(flatNet, ft, mVars, mpVars, invariants) {
1498
- const conditions = [`(Reachable ${mVars.join(" ")})`];
1631
+ function encodeTransitionRule(flatNet, ft, mVars, mpVars, nVars, npVars, strengthening) {
1632
+ const conditions = [`(Reachable ${[...mVars, ...nVars].join(" ")})`];
1499
1633
  conditions.push(...firingConditions(flatNet, ft, mVars, mpVars));
1500
- conditions.push(...invariantConditions(invariants, mpVars));
1634
+ conditions.push(...strengthening);
1501
1635
  conditions.push(...envBoundConditions(flatNet, mpVars));
1502
1636
  const body = `(and ${conditions.join("\n ")})`;
1503
- return `(assert (forall (${quantified([...mVars, ...mpVars])})
1637
+ const quantifiedVars = quantified([...mVars, ...mpVars, ...nVars, ...npVars]);
1638
+ return `(assert (forall (${quantifiedVars})
1504
1639
  (=> ${body}
1505
- (Reachable ${mpVars.join(" ")}))))`;
1640
+ (Reachable ${[...mpVars, ...npVars].join(" ")}))))`;
1506
1641
  }
1507
- function encodeInjectionRule(P, pid, bound, mVars, mpVars) {
1508
- const conditions = [`(Reachable ${mVars.join(" ")})`];
1642
+ function encodeInjectionRule(P, pid, bound, mVars, mpVars, nVars, npVars) {
1643
+ const conditions = [`(Reachable ${[...mVars, ...nVars].join(" ")})`];
1509
1644
  conditions.push(...injectionConditions(P, pid, bound, mVars, mpVars));
1645
+ if (nVars.length > 0) conditions.push(...counterConditions(-1, nVars, npVars));
1510
1646
  const body = `(and ${conditions.join("\n ")})`;
1511
- return `(assert (forall (${quantified([...mVars, ...mpVars])})
1647
+ const quantifiedVars = quantified([...mVars, ...mpVars, ...nVars, ...npVars]);
1648
+ return `(assert (forall (${quantifiedVars})
1512
1649
  (=> ${body}
1513
- (Reachable ${mpVars.join(" ")}))))`;
1650
+ (Reachable ${[...mpVars, ...npVars].join(" ")}))))`;
1514
1651
  }
1515
1652
  function conjoin(conditions) {
1516
1653
  if (conditions.length === 0) return "true";
1517
1654
  if (conditions.length === 1) return conditions[0];
1518
1655
  return `(and ${conditions.join(" ")})`;
1519
1656
  }
1520
- function encodeStepRelationSmt2(flatNet) {
1657
+ function encodeStepRelationSmt2(flatNet, stateEquation = false) {
1521
1658
  const P = flatNet.places.length;
1659
+ const T = stateEquation ? flatNet.transitions.length : 0;
1522
1660
  const mVars = vars(P, "");
1523
1661
  const mpVars = vars(P, "p");
1662
+ const nVars = counterVars(T, "");
1663
+ const npVars = counterVars(T, "p");
1524
1664
  const disjuncts = [];
1525
- for (const ft of flatNet.transitions) {
1665
+ for (let k = 0; k < flatNet.transitions.length; k++) {
1666
+ const ft = flatNet.transitions[k];
1526
1667
  const conditions = firingConditions(flatNet, ft, mVars, mpVars);
1668
+ if (T > 0) conditions.push(...counterConditions(k, nVars, npVars));
1527
1669
  conditions.push(...envBoundConditions(flatNet, mpVars));
1528
1670
  disjuncts.push(conjoin(conditions));
1529
1671
  }
1530
1672
  for (const inj of resolveEnvInjection(flatNet)) {
1531
- disjuncts.push(conjoin(injectionConditions(P, inj.pid, inj.bound, mVars, mpVars)));
1673
+ const conditions = injectionConditions(P, inj.pid, inj.bound, mVars, mpVars);
1674
+ if (T > 0) conditions.push(...counterConditions(-1, nVars, npVars));
1675
+ disjuncts.push(conjoin(conditions));
1532
1676
  }
1533
1677
  if (disjuncts.length === 0) return "false";
1534
1678
  if (disjuncts.length === 1) return disjuncts[0];
1535
1679
  return `(or ${disjuncts.join("\n ")})`;
1536
1680
  }
1537
- function encodeErrorRule(flatNet, property, mVars, sinkPlaces, envInject) {
1538
- const violation = encodePropertyViolation(flatNet, property, mVars, sinkPlaces, envInject);
1539
- return `(assert (forall (${quantified(mVars)})
1540
- (=> (and (Reachable ${mVars.join(" ")}) ${violation})
1681
+ function encodeErrorRule(flatNet, property, mVars, nVars, sinkPlaces, envInject, conditionalSinks) {
1682
+ const violation = encodePropertyViolation(flatNet, property, mVars, sinkPlaces, envInject, conditionalSinks);
1683
+ const state = [...mVars, ...nVars];
1684
+ return `(assert (forall (${quantified(state)})
1685
+ (=> (and (Reachable ${state.join(" ")}) ${violation})
1541
1686
  Error)))`;
1542
1687
  }
1543
1688
  function indexOrdered(flatNet, places) {
@@ -1548,19 +1693,16 @@ function indexOrdered(flatNet, places) {
1548
1693
  }
1549
1694
  return [...idx].sort((a, b) => a - b);
1550
1695
  }
1551
- function encodePropertyViolation(flatNet, property, mVars, sinkPlaces, envInject) {
1696
+ function encodePropertyViolation(flatNet, property, mVars, sinkPlaces, envInject, conditionalSinks = []) {
1552
1697
  switch (property.type) {
1553
1698
  // DeadlockFree (VER-002): a quiescent marking that STRANDS a token — holds one
1554
- // in a place that is not a declared sink. The empty marking strands nothing and
1555
- // is therefore not a violation (AC4).
1699
+ // in a place where resting is not permitted. The empty marking strands nothing
1700
+ // and is therefore not a violation (AC4). A conditional sink (VER-014) is
1701
+ // stranded only while every marker that would excuse it is unmarked.
1556
1702
  case "deadlock-free": {
1557
1703
  const conditions = encodeQuiescent(flatNet, mVars, envInject);
1558
1704
  if (conditions == null) return "false";
1559
- const sinks = new Set(indexOrdered(flatNet, sinkPlaces));
1560
- const stranded = [];
1561
- for (let pid = 0; pid < flatNet.places.length; pid++) {
1562
- if (!sinks.has(pid)) stranded.push(`(>= ${mVars[pid]} 1)`);
1563
- }
1705
+ const stranded = strandedConditions(strandingExcuses(flatNet, sinkPlaces, conditionalSinks), mVars);
1564
1706
  if (stranded.length === 0) return "false";
1565
1707
  conditions.push(`(or ${stranded.join(" ")})`);
1566
1708
  return joinConditions(conditions);
@@ -1601,6 +1743,19 @@ function encodePropertyViolation(flatNet, property, mVars, sinkPlaces, envInject
1601
1743
  }
1602
1744
  }
1603
1745
  }
1746
+ function strandedConditions(excuses, counts) {
1747
+ const stranded = [];
1748
+ for (let pid = 0; pid < excuses.length; pid++) {
1749
+ const markers = excuses[pid];
1750
+ if (markers == null) continue;
1751
+ if (markers.length === 0) {
1752
+ stranded.push(`(>= ${counts[pid]} 1)`);
1753
+ } else {
1754
+ stranded.push(`(and (>= ${counts[pid]} 1) ${markers.map((k) => `(= ${counts[k]} 0)`).join(" ")})`);
1755
+ }
1756
+ }
1757
+ return stranded;
1758
+ }
1604
1759
  function joinConditions(conditions) {
1605
1760
  return conditions.length === 0 ? "true" : `(and ${conditions.join("\n ")})`;
1606
1761
  }
@@ -1639,6 +1794,9 @@ function encodeQuiescent(flatNet, mVars, envInject) {
1639
1794
  }
1640
1795
  return disabledConditions;
1641
1796
  }
1797
+ function quiescenceUnreachable(flatNet, envInject) {
1798
+ return encodeQuiescent(flatNet, vars(flatNet.places.length, ""), envInject) === null;
1799
+ }
1642
1800
  function injectionMap(flatNet) {
1643
1801
  const out = /* @__PURE__ */ new Map();
1644
1802
  for (const inj of resolveEnvInjection(flatNet)) out.set(inj.pid, inj.bound);
@@ -1647,7 +1805,7 @@ function injectionMap(flatNet) {
1647
1805
 
1648
1806
  // src/verification/z3/certificate-checker.ts
1649
1807
  var VC_LABELS = ["initiation (VC1)", "consecution (VC2)", "safety (VC3)"];
1650
- async function checkCertificate(certificate, flatNet, initialMarking, property, invariants, sinkPlaces, solver, timeoutMs) {
1808
+ async function checkCertificate(certificate, flatNet, initialMarking, property, invariants, sinkPlaces, solver, timeoutMs, conditionalSinks = [], stateEquation = false) {
1651
1809
  if (certificate == null) {
1652
1810
  return {
1653
1811
  type: "unavailable",
@@ -1660,11 +1818,12 @@ async function checkCertificate(certificate, flatNet, initialMarking, property,
1660
1818
  if (!certificate.includes("(define-fun Reachable ") && !certificate.includes("(define-fun |Reachable| ")) {
1661
1819
  return { type: "unavailable", reason: "certificate does not define Reachable", invariant: certificate };
1662
1820
  }
1663
- const vcs = buildVerificationConditions(certificate, flatNet, initialMarking, property, sinkPlaces, invariants);
1821
+ const vcs = buildVerificationConditions(certificate, flatNet, initialMarking, property, sinkPlaces, invariants, conditionalSinks, stateEquation);
1664
1822
  let results;
1665
1823
  try {
1666
1824
  results = await runVcScript(script(vcs), timeoutMs, solver);
1667
1825
  } catch (e) {
1826
+ rethrowIfProgrammingError(e);
1668
1827
  return { type: "unavailable", reason: String(e?.message ?? e), invariant: certificate };
1669
1828
  }
1670
1829
  for (let i = 0; i < results.length; i++) {
@@ -1675,8 +1834,8 @@ async function checkCertificate(certificate, flatNet, initialMarking, property,
1675
1834
  }
1676
1835
  return { type: "passed", invariant: certificate };
1677
1836
  }
1678
- function vcScript(certificate, flatNet, initialMarking, property, sinkPlaces, invariants) {
1679
- return script(buildVerificationConditions(certificate, flatNet, initialMarking, property, sinkPlaces, invariants));
1837
+ function vcScript(certificate, flatNet, initialMarking, property, sinkPlaces, invariants, conditionalSinks = [], stateEquation = false) {
1838
+ return script(buildVerificationConditions(certificate, flatNet, initialMarking, property, sinkPlaces, invariants, conditionalSinks, stateEquation));
1680
1839
  }
1681
1840
  function shapeFailure(flatNet, invariants) {
1682
1841
  const P = flatNet.places.length;
@@ -1720,14 +1879,29 @@ function parseVcResults(stdout) {
1720
1879
  }
1721
1880
  return results;
1722
1881
  }
1723
- function buildVerificationConditions(certificate, flatNet, initialMarking, property, sinkPlaces, invariants) {
1882
+ function buildVerificationConditions(certificate, flatNet, initialMarking, property, sinkPlaces, invariants, conditionalSinks, stateEquation) {
1724
1883
  const P = flatNet.places.length;
1884
+ const T = stateEquation ? flatNet.transitions.length : 0;
1725
1885
  const mVars = [];
1726
1886
  const mpVars = [];
1727
1887
  for (let i = 0; i < P; i++) {
1728
1888
  mVars.push(`m${i}`);
1729
1889
  mpVars.push(`m${i}p`);
1730
1890
  }
1891
+ const nVars = [];
1892
+ const npVars = [];
1893
+ for (let k = 0; k < T; k++) {
1894
+ nVars.push(`n${k}`);
1895
+ npVars.push(`n${k}p`);
1896
+ }
1897
+ const candidateOf = (m, n) => {
1898
+ const parts = [`(Reachable ${[...m, ...n].join(" ")})`, ...invariantConditions(invariants, m)];
1899
+ if (T > 0) {
1900
+ for (const v of n) parts.push(`(>= ${v} 0)`);
1901
+ parts.push(...stateEquationConditions(flatNet, initialMarking, n, m));
1902
+ }
1903
+ return conjoin(parts);
1904
+ };
1731
1905
  const prelude = [
1732
1906
  "; IC3/PDR certificate check (plain SMT-LIB2, not HORN):",
1733
1907
  "; each VC below must be unsat for the certificate to stand.",
@@ -1736,19 +1910,22 @@ function buildVerificationConditions(certificate, flatNet, initialMarking, prope
1736
1910
  ];
1737
1911
  for (const v of mVars) prelude.push(`(declare-const ${v} Int)`);
1738
1912
  for (const v of mpVars) prelude.push(`(declare-const ${v} Int)`);
1913
+ for (const v of nVars) prelude.push(`(declare-const ${v} Int)`);
1914
+ for (const v of npVars) prelude.push(`(declare-const ${v} Int)`);
1739
1915
  const m0 = [];
1740
1916
  for (let i = 0; i < P; i++) m0.push(String(initialMarking.tokens(flatNet.places[i])));
1741
- const vc1 = [`(assert (not ${candidate(m0, invariants)}))`];
1742
- const nonNegative = mVars.map((v) => `(assert (>= ${v} 0))`);
1743
- const step = encodeStepRelationSmt2(flatNet);
1917
+ const n0 = new Array(T).fill("0");
1918
+ const vc1 = [`(assert (not ${candidateOf(m0, n0)}))`];
1919
+ const nonNegative = [...mVars, ...nVars].map((v) => `(assert (>= ${v} 0))`);
1920
+ const step = encodeStepRelationSmt2(flatNet, stateEquation);
1744
1921
  const vc2 = [
1745
1922
  ...nonNegative,
1746
- `(assert ${candidate(mVars, invariants)})`,
1923
+ `(assert ${candidateOf(mVars, nVars)})`,
1747
1924
  `(assert ${step})`,
1748
- `(assert (not ${candidate(mpVars, invariants)}))`
1925
+ `(assert (not ${candidateOf(mpVars, npVars)}))`
1749
1926
  ];
1750
- const bad = encodePropertyViolation(flatNet, property, mVars, sinkPlaces, resolveEnvInjection(flatNet));
1751
- const vc3 = [...nonNegative, `(assert ${candidate(mVars, invariants)})`, `(assert ${bad})`];
1927
+ const bad = encodePropertyViolation(flatNet, property, mVars, sinkPlaces, resolveEnvInjection(flatNet), conditionalSinks);
1928
+ const vc3 = [...nonNegative, `(assert ${candidateOf(mVars, nVars)})`, `(assert ${bad})`];
1752
1929
  return { prelude, asserts: [vc1, vc2, vc3] };
1753
1930
  }
1754
1931
  function script(vcs) {
@@ -1812,8 +1989,190 @@ function reasonUnknown(reply) {
1812
1989
  reason = reason.trim();
1813
1990
  return reason === "" ? null : reason;
1814
1991
  }
1815
- function candidate(names, invariants) {
1816
- return conjoin([`(Reachable ${names.join(" ")})`, ...invariantConditions(invariants, names)]);
1992
+
1993
+ // src/verification/z3/linear-bound.ts
1994
+ function violationDemand(flatNet, property) {
1995
+ const demand = /* @__PURE__ */ new Map();
1996
+ switch (property.type) {
1997
+ case "unreachable":
1998
+ for (const p of property.places) {
1999
+ const pid = flatNet.placeIndex.get(p.name);
2000
+ if (pid != null) demand.set(pid, 1);
2001
+ }
2002
+ break;
2003
+ case "mutual-exclusion": {
2004
+ for (const p of [property.p1, property.p2]) {
2005
+ const pid = flatNet.placeIndex.get(p.name);
2006
+ if (pid != null) demand.set(pid, 1);
2007
+ }
2008
+ break;
2009
+ }
2010
+ case "place-bound":
2011
+ case "branch-place-bound": {
2012
+ const pid = flatNet.placeIndex.get(property.place.name);
2013
+ if (pid != null) demand.set(pid, property.bound + 1);
2014
+ break;
2015
+ }
2016
+ case "deadlock-free":
2017
+ case "terminates-at-sink":
2018
+ case "joined-or-dead-lettered":
2019
+ return null;
2020
+ }
2021
+ return demand.size === 0 ? null : demand;
2022
+ }
2023
+ function zeroWeightPlaces(flatNet) {
2024
+ const zero = new Set(nonlinearPlaces(flatNet));
2025
+ for (const inj of resolveEnvInjection(flatNet)) zero.add(inj.pid);
2026
+ return zero;
2027
+ }
2028
+ function encodeLinearBound(flatNet, initialMarking, property) {
2029
+ const demand = violationDemand(flatNet, property);
2030
+ if (demand == null) return null;
2031
+ const P = flatNet.places.length;
2032
+ const zero = zeroWeightPlaces(flatNet);
2033
+ const lines = [];
2034
+ lines.push("; Linear state-equation bound (VER-015): y >= 0 with y.C <= 0 on every");
2035
+ lines.push("; transition gives y.M <= y.M0 for every reachable M; sat = the violating");
2036
+ lines.push("; markings' demand exceeds that bound, so none is reachable.");
2037
+ lines.push("(set-logic QF_LIA)");
2038
+ for (let p = 0; p < P; p++) lines.push(`(declare-const y${p} Int)`);
2039
+ for (let p = 0; p < P; p++) lines.push(`(assert (>= y${p} 0))`);
2040
+ for (const p of [...zero].sort((a, b) => a - b)) lines.push(`(assert (= y${p} 0))`);
2041
+ for (const ft of flatNet.transitions) {
2042
+ const terms = [];
2043
+ for (let p = 0; p < P; p++) {
2044
+ const c = ft.postVector[p] - ft.preVector[p];
2045
+ if (c !== 0) terms.push(term(c, `y${p}`));
2046
+ }
2047
+ if (terms.length > 0) lines.push(`(assert (<= ${sum(terms)} 0))`);
2048
+ }
2049
+ const demandTerms = [];
2050
+ for (const p of [...demand.keys()].sort((a, b) => a - b)) demandTerms.push(term(demand.get(p), `y${p}`));
2051
+ const initTerms = ["1"];
2052
+ for (let p = 0; p < P; p++) {
2053
+ const m0 = initialMarking.tokens(flatNet.places[p]);
2054
+ if (m0 > 0) initTerms.push(term(m0, `y${p}`));
2055
+ }
2056
+ lines.push(`(assert (>= ${sum(demandTerms)} ${sum(initTerms)}))`);
2057
+ lines.push("(check-sat)");
2058
+ lines.push("(get-model)");
2059
+ return lines.join("\n");
2060
+ }
2061
+ function term(c, v) {
2062
+ if (c === 1) return v;
2063
+ if (c === -1) return `(- ${v})`;
2064
+ return c > 0 ? `(* ${c} ${v})` : `(* (- ${-c}) ${v})`;
2065
+ }
2066
+ function sum(terms) {
2067
+ return terms.length === 1 ? terms[0] : `(+ ${terms.join(" ")})`;
2068
+ }
2069
+ function decodeLinearBound(stdout, placeCount) {
2070
+ const y = new Array(placeCount).fill(0n);
2071
+ let seen = false;
2072
+ for (const def of extractDefineFuns(stdout)) {
2073
+ const m = /^\(define-fun\s+y(\d+)\s+\(\)\s+Int\s+(\(-\s*(\d+)\s*\)|(\d+))\s*\)$/s.exec(def.trim());
2074
+ if (m == null) continue;
2075
+ const pid = Number(m[1]);
2076
+ if (pid >= placeCount) continue;
2077
+ y[pid] = m[3] != null ? -BigInt(m[3]) : BigInt(m[4]);
2078
+ seen = true;
2079
+ }
2080
+ return seen ? y : null;
2081
+ }
2082
+ function checkLinearBoundExact(flatNet, initialMarking, property, y) {
2083
+ const demand = violationDemand(flatNet, property);
2084
+ if (demand == null) return null;
2085
+ const P = flatNet.places.length;
2086
+ if (y.length !== P) return null;
2087
+ const zero = zeroWeightPlaces(flatNet);
2088
+ for (let p = 0; p < P; p++) {
2089
+ if (y[p] < 0n) return null;
2090
+ if (zero.has(p) && y[p] !== 0n) return null;
2091
+ }
2092
+ for (const ft of flatNet.transitions) {
2093
+ let delta = 0n;
2094
+ for (let p = 0; p < P; p++) {
2095
+ if (y[p] === 0n) continue;
2096
+ delta += y[p] * BigInt(ft.postVector[p] - ft.preVector[p]);
2097
+ }
2098
+ if (delta > 0n) return null;
2099
+ }
2100
+ let constant = 0n;
2101
+ for (let p = 0; p < P; p++) {
2102
+ if (y[p] !== 0n) constant += y[p] * BigInt(initialMarking.tokens(flatNet.places[p]));
2103
+ }
2104
+ let demandValue = 0n;
2105
+ for (const [p, d] of demand) demandValue += y[p] * BigInt(d);
2106
+ if (demandValue < constant + 1n) return null;
2107
+ return { weights: y, constant, demandValue };
2108
+ }
2109
+ function formatLinearBound(flatNet, bound) {
2110
+ const parts = [];
2111
+ for (let p = 0; p < bound.weights.length; p++) {
2112
+ const w = bound.weights[p];
2113
+ if (w === 0n) continue;
2114
+ parts.push(w === 1n ? flatNet.places[p].name : `${w}*${flatNet.places[p].name}`);
2115
+ }
2116
+ return `${parts.length === 0 ? "0" : parts.join(" + ")} <= ${bound.constant}`;
2117
+ }
2118
+ function formatLinearDemand(flatNet, property, bound) {
2119
+ const demand = violationDemand(flatNet, property) ?? /* @__PURE__ */ new Map();
2120
+ const parts = [];
2121
+ for (const p of [...demand.keys()].sort((a, b) => a - b)) {
2122
+ const w = bound.weights[p] * BigInt(demand.get(p));
2123
+ if (w === 0n) continue;
2124
+ parts.push(w === 1n ? flatNet.places[p].name : `${w}*${flatNet.places[p].name}`);
2125
+ }
2126
+ return `${parts.length === 0 ? "0" : parts.join(" + ")} >= ${bound.demandValue}`;
2127
+ }
2128
+
2129
+ // src/verification/graph-decision.ts
2130
+ function decideOverClasses(view, property, sinkPlaces, conditionalSinks = []) {
2131
+ const firstWhere = (pred) => {
2132
+ for (let i = 0; i < view.count; i++) {
2133
+ if (pred(i)) return i;
2134
+ }
2135
+ return -1;
2136
+ };
2137
+ switch (property.type) {
2138
+ case "place-bound":
2139
+ case "branch-place-bound":
2140
+ return firstWhere((i) => view.markingOf(i).tokens(property.place) > property.bound);
2141
+ case "unreachable":
2142
+ return firstWhere((i) => {
2143
+ const m = view.markingOf(i);
2144
+ for (const p of property.places) {
2145
+ if (!m.hasTokens(p)) return false;
2146
+ }
2147
+ return true;
2148
+ });
2149
+ case "mutual-exclusion":
2150
+ return firstWhere((i) => {
2151
+ const m = view.markingOf(i);
2152
+ return m.hasTokens(property.p1) && m.hasTokens(property.p2);
2153
+ });
2154
+ // DeadlockFree (VER-002): a quiescent class that strands a token — some marked
2155
+ // place is not where resting is permitted, the conditional sinks of VER-014
2156
+ // included. The empty marking strands nothing (AC4).
2157
+ case "deadlock-free":
2158
+ return firstWhere((i) => view.isQuiescent(i) && strandsToken(view.markingOf(i), sinkPlaces, conditionalSinks));
2159
+ // TerminatesAtSink (VER-002): a quiescent class that marks NO declared sink.
2160
+ // Inverts with DeadlockFree on the empty marking, by design.
2161
+ case "terminates-at-sink":
2162
+ return firstWhere((i) => view.isQuiescent(i) && !anySinkMarked(view.markingOf(i), sinkPlaces));
2163
+ // JoinedOrDeadLettered (NU-040 AC4): a quiescent class still holding a pending
2164
+ // token. No sink clause.
2165
+ case "joined-or-dead-lettered":
2166
+ return firstWhere((i) => view.isQuiescent(i) && view.markingOf(i).hasTokens(property.pending));
2167
+ }
2168
+ }
2169
+ function anySinkMarked(m, sinks) {
2170
+ const sinkNames = /* @__PURE__ */ new Set();
2171
+ for (const s of sinks) sinkNames.add(s.name);
2172
+ for (const p of m.placesWithTokens()) {
2173
+ if (sinkNames.has(p.name)) return true;
2174
+ }
2175
+ return false;
1817
2176
  }
1818
2177
 
1819
2178
  // src/verification/analysis/dbm.ts
@@ -1922,6 +2281,35 @@ var DBM = class _DBM {
1922
2281
  allNames.push(...newClockNames);
1923
2282
  return new _DBM(newBounds, newDim, allNames, false).canonicalize();
1924
2283
  }
2284
+ /**
2285
+ * The same zone with its clocks reordered: clock `k` of the result is clock
2286
+ * `order[k]` of this DBM. `order` must be a permutation of `0..clockCount()-1`.
2287
+ *
2288
+ * The state-class graph applies this to put every class's clocks in the one
2289
+ * canonical order (VER-010), so two arrivals at the same marking and zone whose
2290
+ * transitions became enabled in a different sequence share a key instead of
2291
+ * being counted as two classes. The reference row and column stay put; the
2292
+ * matrix is copied once, O(dim²) against the O(dim³) canonicalisation every
2293
+ * successor already pays.
2294
+ */
2295
+ permuted(order) {
2296
+ if (this._empty) return this;
2297
+ const n = this.clockNames.length;
2298
+ const dim = this.dim;
2299
+ const out = new Float64Array(dim * dim);
2300
+ out[0] = 0;
2301
+ const names = new Array(n);
2302
+ for (let i = 0; i < n; i++) {
2303
+ const oi = order[i] + 1;
2304
+ names[i] = this.clockNames[order[i]];
2305
+ out[(i + 1) * dim] = this.bounds[oi * dim];
2306
+ out[i + 1] = this.bounds[oi];
2307
+ for (let j = 0; j < n; j++) {
2308
+ out[(i + 1) * dim + (j + 1)] = this.bounds[oi * dim + (order[j] + 1)];
2309
+ }
2310
+ }
2311
+ return new _DBM(out, dim, names, false);
2312
+ }
1925
2313
  /** Lets time pass: set all lower bounds to 0. */
1926
2314
  letTimePass() {
1927
2315
  if (this._empty) return this;
@@ -1953,6 +2341,24 @@ var DBM = class _DBM {
1953
2341
  }
1954
2342
  return true;
1955
2343
  }
2344
+ /**
2345
+ * The zone's identity for state-class dedup: the clock names and the FULL
2346
+ * canonical matrix, every difference bound included.
2347
+ *
2348
+ * {@link toString} prints only the per-clock projections `[lo, hi]`, and two
2349
+ * zones can agree on every projection while disagreeing on a difference
2350
+ * constraint `θi - θj <= c` — the class where one transition must fire no later
2351
+ * than another versus the class where either may go first. Keying on the
2352
+ * projections merges those, and since the graph explores only the first
2353
+ * arrival's successors, a marking reachable only from the second is lost: a
2354
+ * false `proven`. This key is what {@link equals} compares, rendered.
2355
+ */
2356
+ zoneKey() {
2357
+ if (this._empty) return "DBM[empty]";
2358
+ const parts = [this.clockNames.join(",")];
2359
+ for (let i = 0; i < this.bounds.length; i++) parts.push(formatBound(this.bounds[i]));
2360
+ return parts.join("|");
2361
+ }
1956
2362
  toString() {
1957
2363
  if (this._empty) return "DBM[empty]";
1958
2364
  const parts = [];
@@ -2201,10 +2607,35 @@ var StateClassGraph = class _StateClassGraph {
2201
2607
  }
2202
2608
  };
2203
2609
  function classKey(sc) {
2204
- return `${sc.marking.toString()}|${sc.firingDomain.toString()}`;
2610
+ return `${sc.marking.toString()}|${sc.firingDomain.zoneKey()}`;
2611
+ }
2612
+ function canonicalOrder(transitions) {
2613
+ let sorted = true;
2614
+ for (let i = 1; i < transitions.length; i++) {
2615
+ if (transitions[i].name < transitions[i - 1].name) {
2616
+ sorted = false;
2617
+ break;
2618
+ }
2619
+ }
2620
+ if (sorted) return null;
2621
+ const order = new Array(transitions.length);
2622
+ for (let i = 0; i < order.length; i++) order[i] = i;
2623
+ order.sort((a, b) => {
2624
+ const na = transitions[a].name;
2625
+ const nb = transitions[b].name;
2626
+ return na < nb ? -1 : na > nb ? 1 : a - b;
2627
+ });
2628
+ return order;
2629
+ }
2630
+ function permute(items, order) {
2631
+ const out = new Array(items.length);
2632
+ for (let i = 0; i < order.length; i++) out[i] = items[order[i]];
2633
+ return out;
2205
2634
  }
2206
2635
  function initialStateClass(net, initialMarking, envPlaces, envMode) {
2207
- const enabledTransitions = findEnabledTransitions(net, initialMarking, envPlaces, envMode);
2636
+ const found = findEnabledTransitions(net, initialMarking, envPlaces, envMode);
2637
+ const order = canonicalOrder(found);
2638
+ const enabledTransitions = order === null ? found : permute(found, order);
2208
2639
  const clockNames = enabledTransitions.map((t) => t.name);
2209
2640
  const lowerBounds = enabledTransitions.map((t) => earliest(t.timing) / 1e3);
2210
2641
  const upperBounds = enabledTransitions.map((t) => latest(t.timing) / 1e3);
@@ -2249,14 +2680,19 @@ function computeSuccessor(net, current, fired, environmentPlaces, environmentMod
2249
2680
  const newClockNames = newlyEnabled.map((t) => t.name);
2250
2681
  const newLowerBounds = newlyEnabled.map((t) => earliest(t.timing) / 1e3);
2251
2682
  const newUpperBounds = newlyEnabled.map((t) => latest(t.timing) / 1e3);
2252
- const firedDBM = current.firingDomain.fireTransition(
2683
+ let firedDBM = current.firingDomain.fireTransition(
2253
2684
  firedIdx,
2254
2685
  newClockNames,
2255
2686
  newLowerBounds,
2256
2687
  newUpperBounds,
2257
2688
  persistentIndices
2258
2689
  );
2259
- const allEnabled = [...persistent, ...newlyEnabled];
2690
+ let allEnabled = [...persistent, ...newlyEnabled];
2691
+ const order = canonicalOrder(allEnabled);
2692
+ if (order !== null) {
2693
+ allEnabled = permute(allEnabled, order);
2694
+ firedDBM = firedDBM.permuted(order);
2695
+ }
2260
2696
  const readyEarliest = allEnabled.map((_, k) => firedDBM.getLowerBound(k));
2261
2697
  const newDBM = firedDBM.letTimePass();
2262
2698
  return new StateClass(newMarking, newDBM, allEnabled, readyEarliest);
@@ -2328,10 +2764,7 @@ function fireTransition(marking, transition, outputPlaces, environmentPlaces, en
2328
2764
  consumeFromPlace(builder, spec.place, toConsume, environmentPlaces, environmentMode);
2329
2765
  }
2330
2766
  for (const arc of transition.resets) {
2331
- const current = marking.tokens(arc.place);
2332
- if (current > 0) {
2333
- builder.removeTokens(arc.place, current);
2334
- }
2767
+ builder.tokens(arc.place, 0);
2335
2768
  }
2336
2769
  for (const place of outputPlaces) {
2337
2770
  builder.addTokens(place, 1);
@@ -2348,12 +2781,72 @@ function consumeFromPlace(builder, place, count, environmentPlaces, environmentM
2348
2781
  }
2349
2782
  }
2350
2783
 
2784
+ // src/verification/scg-verifier.ts
2785
+ function isUntimed(net) {
2786
+ for (const t of net.transitions) {
2787
+ if (t.timing.type !== "immediate") return false;
2788
+ }
2789
+ return true;
2790
+ }
2791
+ var NOTE_ENUMERATED = "\nNote: decided by bounded state-space enumeration \u2014 the state-class graph closed, so the verdict is sound AND complete: a `violated` is a real firing sequence, not a possibly-spurious over-approximation. The net is untimed, so this is the same claim the encoders make (VER-017).\n";
2792
+ function verifyViaStateClassGraph(net, initial, property, sinkPlaces, maxClasses, conditionalSinks = []) {
2793
+ const graph = StateClassGraph.build(net, initial, maxClasses);
2794
+ const classes = graph.stateClasses();
2795
+ if (!graph.isComplete()) return { kind: "truncated", classCount: classes.length };
2796
+ const violating = decideOverClasses(
2797
+ {
2798
+ count: classes.length,
2799
+ markingOf: (i) => classes[i].marking,
2800
+ isQuiescent: (i) => graph.successors(classes[i]).size === 0
2801
+ },
2802
+ property,
2803
+ sinkPlaces,
2804
+ conditionalSinks
2805
+ );
2806
+ if (violating >= 0) {
2807
+ const [trace, transitions] = counterexamplePath(graph, classes[violating]);
2808
+ return { kind: "decided", verdict: { type: "violated" }, trace, transitions, classCount: classes.length };
2809
+ }
2810
+ return {
2811
+ kind: "decided",
2812
+ verdict: { type: "proven", method: "state-space enumeration (VER-017)", inductiveInvariant: null },
2813
+ trace: [],
2814
+ transitions: [],
2815
+ classCount: classes.length
2816
+ };
2817
+ }
2818
+ function counterexamplePath(graph, target) {
2819
+ const parent = /* @__PURE__ */ new Map();
2820
+ const via = /* @__PURE__ */ new Map();
2821
+ const seen = /* @__PURE__ */ new Set([graph.initialClass]);
2822
+ const queue = [graph.initialClass];
2823
+ while (queue.length > 0) {
2824
+ const current = queue.shift();
2825
+ if (current === target) break;
2826
+ for (const [transition, edges] of graph.outgoingBranchEdges(current)) {
2827
+ for (const edge of edges) {
2828
+ if (seen.has(edge.target)) continue;
2829
+ seen.add(edge.target);
2830
+ parent.set(edge.target, current);
2831
+ via.set(edge.target, transition.name);
2832
+ queue.push(edge.target);
2833
+ }
2834
+ }
2835
+ }
2836
+ const chain = [];
2837
+ for (let cur = target; cur != null; cur = parent.get(cur)) {
2838
+ chain.push(cur);
2839
+ }
2840
+ chain.reverse();
2841
+ return [chain.map((sc) => sc.marking), chain.slice(1).map((sc) => via.get(sc))];
2842
+ }
2843
+
2351
2844
  // src/verification/z3/counterexample-decoder.ts
2352
- function decode(answer, flatNet) {
2353
- const states = decodeStateSet(answer, flatNet);
2845
+ function decode(answer, flatNet, counterCount = 0) {
2846
+ const states = decodeStateSet(answer, flatNet, counterCount);
2354
2847
  return { states, note: states.size === 0 ? "no ground Reachable states in the z3 proof" : null };
2355
2848
  }
2356
- function decodeStateSet(answer, flatNet) {
2849
+ function decodeStateSet(answer, flatNet, counterCount = 0) {
2357
2850
  const byKey = /* @__PURE__ */ new Map();
2358
2851
  const P = flatNet.places.length;
2359
2852
  for (const head of ["(Reachable", "(|Reachable|"]) {
@@ -2370,8 +2863,8 @@ function decodeStateSet(answer, flatNet) {
2370
2863
  if (end < 0) break;
2371
2864
  const inner = answer.slice(start + head.length, end - 1);
2372
2865
  const args = parseGroundIntArgs(inner);
2373
- if (args != null && args.length === P) {
2374
- const marking = toMarking(args, flatNet);
2866
+ if (args != null && args.length === P + counterCount) {
2867
+ const marking = toMarking(counterCount === 0 ? args : args.slice(0, P), flatNet);
2375
2868
  const key = marking.toString();
2376
2869
  if (!byKey.has(key)) byKey.set(key, marking);
2377
2870
  }
@@ -2559,16 +3052,19 @@ function sinkIndices(flatNet, sinkPlaces) {
2559
3052
  }
2560
3053
  return idx;
2561
3054
  }
2562
- function satisfiesBadIndexed(index, state, property, sinkPlaces) {
3055
+ function satisfiesBadIndexed(index, state, property, sinkPlaces, conditionalSinks) {
2563
3056
  const flatNet = index.flatNet;
2564
3057
  switch (property.type) {
2565
- // DeadlockFree (VER-002): quiescent AND some marked place is not a declared
2566
- // sink. Mirrors the encoder's `stranded` disjunction.
3058
+ // DeadlockFree (VER-002): quiescent AND some marked place is not where resting
3059
+ // is permitted — a conditional sink (VER-014) counts only while every marker
3060
+ // that would excuse it is unmarked. Mirrors the encoder's `stranded` disjunction.
2567
3061
  case "deadlock-free": {
2568
3062
  if (!isQuiescent(index, state)) return false;
2569
- const sinks = sinkIndices(flatNet, sinkPlaces);
3063
+ const excuses = strandingExcuses(flatNet, sinkPlaces, conditionalSinks);
2570
3064
  for (let pid = 0; pid < flatNet.places.length; pid++) {
2571
- if (!sinks.has(pid) && state[pid] >= 1) return true;
3065
+ const markers = excuses[pid];
3066
+ if (markers == null || state[pid] < 1) continue;
3067
+ if (markers.every((k) => state[k] === 0)) return true;
2572
3068
  }
2573
3069
  return false;
2574
3070
  }
@@ -2611,7 +3107,7 @@ function satisfiesBadIndexed(index, state, property, sinkPlaces) {
2611
3107
  }
2612
3108
  }
2613
3109
  }
2614
- function replayCounterexample(flatNet, initial, decodedStates, property, sinkPlaces, options = {}) {
3110
+ function replayCounterexample(flatNet, initial, decodedStates, property, sinkPlaces, options = {}, conditionalSinks = []) {
2615
3111
  const segmentBudget = options.segmentBudget ?? 3;
2616
3112
  const nodeBudget = options.nodeBudget ?? 1e4;
2617
3113
  const anchors = /* @__PURE__ */ new Set();
@@ -2628,7 +3124,7 @@ function replayCounterexample(flatNet, initial, decodedStates, property, sinkPla
2628
3124
  };
2629
3125
  }
2630
3126
  const index = buildIndex(flatNet);
2631
- if (satisfiesBadIndexed(index, initial, property, sinkPlaces)) {
3127
+ if (satisfiesBadIndexed(index, initial, property, sinkPlaces, conditionalSinks)) {
2632
3128
  return { kind: "confirmed", states: [initial], steps: [], nodesExplored: 1 };
2633
3129
  }
2634
3130
  const nodes = [{ state: initial, step: null, parent: -1, segment: 0 }];
@@ -2657,7 +3153,7 @@ function replayCounterexample(flatNet, initial, decodedStates, property, sinkPla
2657
3153
  }
2658
3154
  nodes.push({ state: succ.state, step: succ.step, parent: idx, segment });
2659
3155
  const childIdx = nodes.length - 1;
2660
- if (satisfiesBadIndexed(index, succ.state, property, sinkPlaces)) {
3156
+ if (satisfiesBadIndexed(index, succ.state, property, sinkPlaces, conditionalSinks)) {
2661
3157
  const chain = reconstruct(nodes, childIdx);
2662
3158
  return { kind: "confirmed", ...chain, nodesExplored: nodes.length };
2663
3159
  }
@@ -2805,7 +3301,7 @@ function buildLayout(plan, P) {
2805
3301
  function quantified2(names) {
2806
3302
  return names.map((v) => `(${v} Int)`).join(" ");
2807
3303
  }
2808
- function encodeColoured(plan, flat, initial, property, invariants, sinkPlaces) {
3304
+ function encodeColoured(plan, flat, initial, property, invariants, sinkPlaces, conditionalSinks = []) {
2809
3305
  const P = flat.places.length;
2810
3306
  const k = plan.k;
2811
3307
  const lay = buildLayout(plan, P);
@@ -2874,13 +3370,13 @@ function encodeColoured(plan, flat, initial, property, invariants, sinkPlaces) {
2874
3370
  }
2875
3371
  }
2876
3372
  lines.push("");
2877
- const error = encodeError(plan, lay, flat, property, sinkPlaces, injectionMap(flat));
3373
+ const error = encodeError(plan, lay, flat, property, sinkPlaces, injectionMap(flat), conditionalSinks);
2878
3374
  if (error == null) return null;
2879
3375
  lines.push(error);
2880
3376
  lines.push("");
2881
3377
  lines.push("(assert (not Error))");
2882
3378
  lines.push("(check-sat)");
2883
- return { smt2: lines.join("\n"), placeCount: P };
3379
+ return { smt2: lines.join("\n"), placeCount: P, counterCount: 0 };
2884
3380
  }
2885
3381
  function encodeRule(plan, lay, invariants, fill) {
2886
3382
  const enab = [];
@@ -2942,17 +3438,17 @@ function liftedInvariant(inv, plan, lay, names) {
2942
3438
  terms.push(w === 1 ? agg : `(* ${w} ${agg})`);
2943
3439
  }
2944
3440
  if (terms.length === 0) return null;
2945
- const sum = terms.length === 1 ? terms[0] : `(+ ${terms.join(" ")})`;
2946
- return `(= ${sum} ${inv.constant})`;
3441
+ const sum2 = terms.length === 1 ? terms[0] : `(+ ${terms.join(" ")})`;
3442
+ return `(= ${sum2} ${inv.constant})`;
2947
3443
  }
2948
- function encodeError(plan, lay, flat, property, sinkPlaces, envInj) {
2949
- const violation = encodeViolation(plan, lay, flat, property, sinkPlaces, envInj);
3444
+ function encodeError(plan, lay, flat, property, sinkPlaces, envInj, conditionalSinks) {
3445
+ const violation = encodeViolation(plan, lay, flat, property, sinkPlaces, envInj, conditionalSinks);
2950
3446
  if (violation == null) return null;
2951
3447
  return `(assert (forall (${quantified2(lay.cur)})
2952
3448
  (=> (and (Reachable ${lay.cur.join(" ")}) ${violation})
2953
3449
  Error)))`;
2954
3450
  }
2955
- function encodeViolation(plan, lay, flat, property, sinkPlaces, envInj) {
3451
+ function encodeViolation(plan, lay, flat, property, sinkPlaces, envInj, conditionalSinks) {
2956
3452
  const anyPlacePresent = (places) => {
2957
3453
  const conds = indexOrdered(flat, places).map((pid) => `(>= ${aggregate(plan, lay, pid, lay.cur)} 1)`);
2958
3454
  return conds.length === 0 ? "false" : `(and ${conds.join(" ")})`;
@@ -2968,16 +3464,15 @@ function encodeViolation(plan, lay, flat, property, sinkPlaces, envInj) {
2968
3464
  return anyPlacePresent([property.p1, property.p2]);
2969
3465
  case "unreachable":
2970
3466
  return anyPlacePresent(property.places);
2971
- // DeadlockFree (VER-002): quiescent AND some marked place is not a declared
2972
- // sink. Mirrors the flat encoder's `stranded` disjunction.
3467
+ // DeadlockFree (VER-002): quiescent AND some marked place is not where resting
3468
+ // is permitted (VER-014). Mirrors the flat encoder's `stranded` disjunction over
3469
+ // the aggregate (all-colour) count of each place.
2973
3470
  case "deadlock-free": {
2974
3471
  const conds = encodeColouredQuiescent(plan, lay, flat, envInj);
2975
3472
  if (conds == null) return "false";
2976
- const sinks = new Set(indexOrdered(flat, sinkPlaces));
2977
- const stranded = [];
2978
- for (let pid = 0; pid < flat.places.length; pid++) {
2979
- if (!sinks.has(pid)) stranded.push(`(>= ${aggregate(plan, lay, pid, lay.cur)} 1)`);
2980
- }
3473
+ const counts = [];
3474
+ for (let pid = 0; pid < flat.places.length; pid++) counts.push(aggregate(plan, lay, pid, lay.cur));
3475
+ const stranded = strandedConditions(strandingExcuses(flat, sinkPlaces, conditionalSinks), counts);
2981
3476
  if (stranded.length === 0) return "false";
2982
3477
  conds.push(`(or ${stranded.join(" ")})`);
2983
3478
  return joinColoured(conds);
@@ -3071,8 +3566,8 @@ function encodeColouredQuiescent(plan, lay, flat, envInj) {
3071
3566
  disabledConditions.push("true");
3072
3567
  continue;
3073
3568
  }
3074
- const term = colouredDisabledTerm(cls, plan, lay);
3075
- if (term != null) reasons.push(term);
3569
+ const term2 = colouredDisabledTerm(cls, plan, lay);
3570
+ if (term2 != null) reasons.push(term2);
3076
3571
  if (reasons.length === 0) return null;
3077
3572
  disabledConditions.push(reasons.length === 1 ? reasons[0] : `(or ${reasons.join(" ")})`);
3078
3573
  }
@@ -3268,7 +3763,7 @@ var NameStateClass = class {
3268
3763
  }
3269
3764
  };
3270
3765
  function baseKeyOf(base) {
3271
- return `${base.marking.toString()}|${base.firingDomain.toString()}`;
3766
+ return `${base.marking.toString()}|${base.firingDomain.zoneKey()}`;
3272
3767
  }
3273
3768
 
3274
3769
  // src/verification/analysis/name-state-class-graph.ts
@@ -3499,7 +3994,7 @@ function enablingSymbols(names, colouredIn) {
3499
3994
 
3500
3995
  // src/verification/nu-scg-verifier.ts
3501
3996
  var NOTE_EXACT = "\nNote: \u03BD-join correlation decided exactly via the state-class-graph name-partition quotient \u2014 the symbolic graph closed, so the verdict is sound AND complete (no spurious different-name counterexample; quiescence is name-aware), beyond the bounded-budget fragment (NU-050, Route B).\n";
3502
- function verifyViaNameScg(net, initial, property, sinkPlaces, environmentPlaces, environmentMode, maxClasses, fragmentMode, carrierPlaces, prioritySemantics) {
3997
+ function verifyViaNameScg(net, initial, property, sinkPlaces, environmentPlaces, environmentMode, maxClasses, fragmentMode, carrierPlaces, prioritySemantics, conditionalSinks = []) {
3503
3998
  const fragment = classify(net, fragmentMode, carrierPlaces);
3504
3999
  if (fragment === null) return null;
3505
4000
  for (const p of initial.placesWithTokens()) {
@@ -3526,9 +4021,9 @@ function verifyViaNameScg(net, initial, property, sinkPlaces, environmentPlaces,
3526
4021
  classCount: scg.classCount()
3527
4022
  };
3528
4023
  }
3529
- const violating = decide(scg, property, sinkPlaces);
4024
+ const violating = decide(scg, property, sinkPlaces, conditionalSinks);
3530
4025
  if (violating >= 0) {
3531
- const [trace, transitions] = counterexamplePath(scg, violating);
4026
+ const [trace, transitions] = counterexamplePath2(scg, violating);
3532
4027
  return { verdict: { type: "violated" }, trace, transitions, note: NOTE_EXACT, classCount: scg.classCount() };
3533
4028
  }
3534
4029
  return {
@@ -3539,61 +4034,19 @@ function verifyViaNameScg(net, initial, property, sinkPlaces, environmentPlaces,
3539
4034
  classCount: scg.classCount()
3540
4035
  };
3541
4036
  }
3542
- function decide(scg, property, sinkPlaces) {
3543
- const firstWhere = (pred) => {
3544
- for (let i = 0; i < scg.classCount(); i++) {
3545
- if (pred(i)) return i;
3546
- }
3547
- return -1;
3548
- };
3549
- switch (property.type) {
3550
- case "place-bound":
3551
- case "branch-place-bound":
3552
- return firstWhere((i) => scg.markingOf(i).tokens(property.place) > property.bound);
3553
- case "unreachable":
3554
- return firstWhere((i) => {
3555
- const m = scg.markingOf(i);
3556
- for (const p of property.places) {
3557
- if (!m.hasTokens(p)) return false;
3558
- }
3559
- return true;
3560
- });
3561
- case "mutual-exclusion":
3562
- return firstWhere((i) => {
3563
- const m = scg.markingOf(i);
3564
- return m.hasTokens(property.p1) && m.hasTokens(property.p2);
3565
- });
3566
- // DeadlockFree (VER-002): a quiescent class that strands a token — some marked
3567
- // place is not a declared sink. The empty marking strands nothing (AC4).
3568
- case "deadlock-free":
3569
- return firstWhere((i) => scg.successorsOf(i).length === 0 && !allTokensInSinks(scg.markingOf(i), sinkPlaces));
3570
- // TerminatesAtSink (VER-002): a quiescent class that marks NO declared sink.
3571
- // Inverts with DeadlockFree on the empty marking, by design.
3572
- case "terminates-at-sink":
3573
- return firstWhere((i) => scg.successorsOf(i).length === 0 && !anySinkMarked(scg.markingOf(i), sinkPlaces));
3574
- // JoinedOrDeadLettered (NU-040 AC4): a quiescent class still holding a pending
3575
- // token. No sink clause.
3576
- case "joined-or-dead-lettered":
3577
- return firstWhere((i) => scg.successorsOf(i).length === 0 && scg.markingOf(i).hasTokens(property.pending));
3578
- }
3579
- }
3580
- function allTokensInSinks(m, sinks) {
3581
- const sinkNames = /* @__PURE__ */ new Set();
3582
- for (const s of sinks) sinkNames.add(s.name);
3583
- for (const p of m.placesWithTokens()) {
3584
- if (!sinkNames.has(p.name)) return false;
3585
- }
3586
- return true;
3587
- }
3588
- function anySinkMarked(m, sinks) {
3589
- const sinkNames = /* @__PURE__ */ new Set();
3590
- for (const s of sinks) sinkNames.add(s.name);
3591
- for (const p of m.placesWithTokens()) {
3592
- if (sinkNames.has(p.name)) return true;
3593
- }
3594
- return false;
4037
+ function decide(scg, property, sinkPlaces, conditionalSinks) {
4038
+ return decideOverClasses(
4039
+ {
4040
+ count: scg.classCount(),
4041
+ markingOf: (i) => scg.markingOf(i),
4042
+ isQuiescent: (i) => scg.successorsOf(i).length === 0
4043
+ },
4044
+ property,
4045
+ sinkPlaces,
4046
+ conditionalSinks
4047
+ );
3595
4048
  }
3596
- function counterexamplePath(scg, target) {
4049
+ function counterexamplePath2(scg, target) {
3597
4050
  const n = scg.classCount();
3598
4051
  const parent = new Array(n).fill(-1);
3599
4052
  const via = new Array(n).fill("");
@@ -3633,13 +4086,17 @@ var SmtVerifier = class _SmtVerifier {
3633
4086
  _property = deadlockFree();
3634
4087
  _environmentPlaces = /* @__PURE__ */ new Set();
3635
4088
  _sinkPlaces = /* @__PURE__ */ new Set();
4089
+ _conditionalSinks = [];
3636
4090
  _budgetPlaces = /* @__PURE__ */ new Set();
3637
4091
  _environmentMode = alwaysAvailable();
3638
4092
  _timeoutMs = 6e4;
3639
4093
  _certificateCheck = true;
3640
4094
  _counterexampleReplay = true;
3641
4095
  _semiflowInvariants = false;
4096
+ _stateEquation = false;
4097
+ _linearBound = true;
3642
4098
  _nuMaxClasses = 1e5;
4099
+ _enumerationMaxClasses = 5e4;
3643
4100
  _fragmentMode = "base";
3644
4101
  _carrierPlaces = /* @__PURE__ */ new Set();
3645
4102
  _prioritySemantics = "none";
@@ -3669,13 +4126,47 @@ var SmtVerifier = class _SmtVerifier {
3669
4126
  return this;
3670
4127
  }
3671
4128
  /**
3672
- * Declares expected sink (terminal) places for deadlock-freedom analysis.
3673
- * Markings where any sink place has a token are not considered deadlocks.
4129
+ * Declares expected sink (terminal) places for deadlock-freedom analysis
4130
+ * (VER-002): a token resting in one is never stranded, and `TerminatesAtSink`
4131
+ * asks whether one of them was reached.
3674
4132
  */
3675
4133
  sinkPlaces(...places) {
3676
4134
  for (const p of places) this._sinkPlaces.add(p);
3677
4135
  return this;
3678
4136
  }
4137
+ /**
4138
+ * Declares places where a token may rest **while `marker` holds a token**
4139
+ * (VER-014) — a designed terminal such as a halt or pause marker, under which
4140
+ * the work it interrupted legitimately stays where it was delivered.
4141
+ *
4142
+ * `DeadlockFree` then reads a quiescent marking against the union of the
4143
+ * declared sinks, the markers, and every conditional set whose marker is marked:
4144
+ * a token in `p` is stranded only when none of those excuse it. The marker
4145
+ * itself is at rest whenever it is marked, so `sinkPlacesWhen(halt)` with no
4146
+ * further places excuses exactly the halt token. Repeated calls for one marker
4147
+ * accumulate; declarations for several markers union. `TerminatesAtSink` is
4148
+ * unaffected and reads only {@link sinkPlaces}.
4149
+ *
4150
+ * ```ts
4151
+ * SmtVerifier.forNet(net)
4152
+ * .property(deadlockFree())
4153
+ * .sinkPlaces(done) // may always rest
4154
+ * .sinkPlacesWhen(halt, inbox, pending) // may rest once the run halted
4155
+ * .sinkPlacesWhen(pause, inbox) // may rest while paused
4156
+ * ```
4157
+ *
4158
+ * An unresolved marker or place contributes nothing, as an unresolved sink
4159
+ * does: a mistyped marker makes the property stricter, never laxer.
4160
+ */
4161
+ sinkPlacesWhen(marker, ...places) {
4162
+ let entry = this._conditionalSinks.find((c) => c.marker.name === marker.name);
4163
+ if (entry == null) {
4164
+ entry = { marker, places: /* @__PURE__ */ new Set() };
4165
+ this._conditionalSinks.push(entry);
4166
+ }
4167
+ for (const p of places) entry.places.add(p);
4168
+ return this;
4169
+ }
3679
4170
  /**
3680
4171
  * Declares ν-net budget places (NU-040): places whose token count bounds the
3681
4172
  * live correlation pool (they gate fresh-name minting). Declaring at least one
@@ -3753,10 +4244,67 @@ var SmtVerifier = class _SmtVerifier {
3753
4244
  * `Certificate check: not applicable (name-coloured encoding)`. Off by default so
3754
4245
  * reports stay byte-equal.
3755
4246
  */
4247
+ /**
4248
+ * `'auto'` decides whether the semiflows would add **information to the
4249
+ * encoding**, which is not the same question as whether they would appear in
4250
+ * {@link SmtVerificationResult.invariants} for a caller who reads them.
4251
+ *
4252
+ * A complete basis spans every conservation law of the net, so a semiflow it
4253
+ * spans constrains nothing further and IC3 gains nothing from it — that is why
4254
+ * `'auto'` skips the enumeration there. But the basis is the *signed*
4255
+ * null-space, and a law it spans need not appear in it in **non-negative**
4256
+ * form; only the Farkas enumeration produces that. A caller inspecting the
4257
+ * invariant list for a law of a given shape — "a non-negative law weighting the
4258
+ * budget place and every running place positively" — can therefore find nothing
4259
+ * on a net that plainly has one. Such a caller should ask for the union
4260
+ * explicitly: `'auto'` is the setting to prefer for verification, not for
4261
+ * harvesting.
4262
+ */
3756
4263
  semiflowInvariants(enabled) {
3757
4264
  this._semiflowInvariants = enabled;
3758
4265
  return this;
3759
4266
  }
4267
+ /**
4268
+ * Enables/disables the linear state-equation bound phase (VER-015; default:
4269
+ * enabled). A reachability-safety property whose violating markings exceed some
4270
+ * `y·M <= y·M0` with `y >= 0`, `y·C <= 0` is then proven structurally, from one
4271
+ * linear query re-checked in exact integer arithmetic, before any fixpoint search.
4272
+ * Disable it to force the IC3/PDR path — for its certificate, or to exercise the
4273
+ * fixpoint engine itself.
4274
+ */
4275
+ linearBound(enabled) {
4276
+ this._linearBound = enabled;
4277
+ return this;
4278
+ }
4279
+ /**
4280
+ * Encodes the **state equation** with firing counters (VER-016; default:
4281
+ * disabled — the encoding then carries places only).
4282
+ *
4283
+ * The flat encoding gains one counter `n_t` per flat transition and every
4284
+ * transition rule conjoins the marking equation `M' = M0 + C·n'` for each place
4285
+ * whose column is exact (no consume-all / reset arc, not injected). Every linear
4286
+ * consequence of the marking equation — the equality laws of VER-005/VER-007
4287
+ * **and** the inequality laws `y·M ≤ y·M0` (`y ≥ 0, y·C ≤ 0`) and their mixed-sign
4288
+ * kin, which are what an *ordering* argument ("both join slots armed means every
4289
+ * upstream stage has run, so nothing can still halt") looks like in linear
4290
+ * arithmetic — is then available to Spacer as a fact rather than a lemma it has to
4291
+ * invent. On a 50-place agent-dispatch workflow, proper completion under conditional
4292
+ * sinks went from `unknown` after 120 s to `proven` in 1.5 s with this as the only
4293
+ * change; a 53-place pipeline stage before a join, `unknown` at 300 s, proves in
4294
+ * under a second.
4295
+ *
4296
+ * The cost is a larger state (places + transitions) and a slower witness search
4297
+ * on genuinely violated properties (about 1.5× on the nets above), so it is opt-in.
4298
+ * Soundness is unchanged: the counters are exact bookkeeping, the equation holds
4299
+ * on every reachable state by construction (`Strengthening.lean`, the same shape
4300
+ * as the equality laws), and the certificate check re-proves it against the raw
4301
+ * step relation, whose only counter knowledge is the increment. Not applied to the
4302
+ * name-coloured encoding or Route B, which the report says when it applies.
4303
+ */
4304
+ stateEquation(enabled) {
4305
+ this._stateEquation = enabled;
4306
+ return this;
4307
+ }
3760
4308
  /**
3761
4309
  * Sets the class-count cap for the ν-aware state-class-graph analysis (NU-050,
3762
4310
  * Route B). When the symbolic name-aware graph would exceed this, the analysis
@@ -3767,6 +4315,29 @@ var SmtVerifier = class _SmtVerifier {
3767
4315
  this._nuMaxClasses = max;
3768
4316
  return this;
3769
4317
  }
4318
+ /**
4319
+ * Sets the class budget for the bounded state-space enumeration route
4320
+ * (VER-017; default 50 000). `0` disables the route, so every query goes to
4321
+ * the SMT pipeline.
4322
+ *
4323
+ * When the state-class graph closes within the budget the property is decided
4324
+ * exactly — sound and complete over the timed semantics — and no solver runs.
4325
+ * This is what makes a long pipeline tractable: IC3 needs a frame per stage and
4326
+ * its cost climbs with the cube of the length, while enumeration is linear in
4327
+ * the reachable state space. A forty-node chain (370 places, 1 967 classes)
4328
+ * takes 410 s on the fixpoint path and 0.11 s here.
4329
+ *
4330
+ * The route declines when the graph exceeds the budget, and the SMT pipeline
4331
+ * then runs unchanged — it can only add verdicts, never remove them. It is
4332
+ * skipped for ν-nets, which have their own exact route (NU-050, Route B), for
4333
+ * nets with environment places, whose injection the graph does not model, and
4334
+ * for **timed** nets, where its verdict would be the weaker timed claim rather
4335
+ * than the untimed one the encoders make (VER-004).
4336
+ */
4337
+ enumerationMaxClasses(max) {
4338
+ this._enumerationMaxClasses = max;
4339
+ return this;
4340
+ }
3770
4341
  /**
3771
4342
  * Selects the ν-net coloured-place fragment for Route B (NU-051). `base`
3772
4343
  * (default) admits the shipped mint → matched-join fragment only; `extended`
@@ -3849,7 +4420,8 @@ var SmtVerifier = class _SmtVerifier {
3849
4420
  this._initialMarking,
3850
4421
  this._property,
3851
4422
  invariants,
3852
- this._sinkPlaces
4423
+ this._sinkPlaces,
4424
+ this._conditionalSinks
3853
4425
  )
3854
4426
  };
3855
4427
  }
@@ -3866,35 +4438,45 @@ var SmtVerifier = class _SmtVerifier {
3866
4438
  requireOutputProducingActions(this.net);
3867
4439
  const flatNet = flatten(this.net, this._environmentPlaces, this._environmentMode);
3868
4440
  const matrix = IncidenceMatrix.from(flatNet);
3869
- const { valid: basis } = validateInvariantsExact(
4441
+ const { valid: basis, dropped: basisDropped } = validateInvariantsExact(
3870
4442
  matrix,
3871
4443
  computePInvariants(matrix, flatNet, this._initialMarking),
3872
4444
  flatNet,
3873
4445
  this._initialMarking
3874
4446
  );
3875
- const { valid: semiflows } = validateInvariantsExact(
4447
+ const autoUnion = this._semiflowInvariants === "auto" && basisDropped.some((d) => d.reason.includes("Strengthening.lean H1"));
4448
+ const scriptsHasMatch = [...this.net.transitions].some((t) => t.matchSpec !== null);
4449
+ const { valid: semiflows } = this._semiflowInvariants === true || autoUnion || scriptsHasMatch && this._budgetPlaces.size > 0 ? validateInvariantsExact(
3876
4450
  matrix,
3877
4451
  computePSemiflows(matrix, flatNet, this._initialMarking),
3878
4452
  flatNet,
3879
4453
  this._initialMarking
3880
- );
4454
+ ) : { valid: [] };
3881
4455
  let invariants = basis;
3882
- if (this._semiflowInvariants) invariants = strengthenWithSemiflows(basis, semiflows).invariants;
4456
+ if (this._semiflowInvariants === true || autoUnion) invariants = strengthenWithSemiflows(basis, semiflows).invariants;
3883
4457
  invariants = canonicalInvariantOrder(invariants);
3884
4458
  const attempt = this.colouredAttempt(flatNet, invariants, semiflows);
4459
+ const bound = attempt.plan == null && this._linearBound && !(this._environmentPlaces.size > 0 && this._environmentMode.type === "ignore") ? encodeLinearBound(flatNet, this._initialMarking, this._property) : null;
3885
4460
  if (attempt.encoding != null) {
3886
- return { horn: attempt.encoding.smt2, certificate: null, coloured: true };
4461
+ return { horn: attempt.encoding.smt2, certificate: null, coloured: true, bound };
3887
4462
  }
3888
- const horn = encode(flatNet, this._initialMarking, this._property, invariants, this._sinkPlaces, this._counterexampleReplay).smt2;
4463
+ const flat = encodeNet(flatNet, this._initialMarking, this._property, invariants, {
4464
+ sinkPlaces: this._sinkPlaces,
4465
+ produceProofs: this._counterexampleReplay,
4466
+ conditionalSinks: this._conditionalSinks,
4467
+ stateEquation: this._stateEquation
4468
+ });
3889
4469
  const certificate = vcScript(
3890
- placeholderCertificate(flatNet.places.length),
4470
+ placeholderCertificate(flatNet.places.length + flat.counterCount),
3891
4471
  flatNet,
3892
4472
  this._initialMarking,
3893
4473
  this._property,
3894
4474
  this._sinkPlaces,
3895
- invariants
4475
+ invariants,
4476
+ this._conditionalSinks,
4477
+ this._stateEquation
3896
4478
  );
3897
- return { horn, certificate, coloured: false };
4479
+ return { horn: flat.smt2, certificate, coloured: false, bound };
3898
4480
  }
3899
4481
  /**
3900
4482
  * Runs the verification pipeline.
@@ -3907,10 +4489,34 @@ var SmtVerifier = class _SmtVerifier {
3907
4489
  const report = [];
3908
4490
  report.push("=== IC3/PDR SAFETY VERIFICATION ===\n");
3909
4491
  report.push(`Net: ${this.net.name}`);
3910
- const propDesc = this._sinkPlaces.size === 0 ? propertyDescription(this._property) : `${propertyDescription(this._property)} (sinks: ${[...this._sinkPlaces].map((p) => p.name).join(", ")})`;
4492
+ const sinkDesc = describeSinks(this._sinkPlaces, this._conditionalSinks);
4493
+ const propDesc = sinkDesc === null ? propertyDescription(this._property) : `${propertyDescription(this._property)} (${sinkDesc})`;
3911
4494
  report.push(`Property: ${propDesc}`);
3912
4495
  report.push(`Timeout: ${(this._timeoutMs / 1e3).toFixed(0)}s
3913
4496
  `);
4497
+ const absent = unresolvedPropertyPlaceInNet(this.net, this._property);
4498
+ if (absent != null) {
4499
+ const reason = `property names a place that does not resolve in the net ('${absent}'); refusing to certify (the encoding would be vacuously proven)`;
4500
+ report.push("=== RESULT ===\n");
4501
+ report.push(`UNKNOWN: ${reason}`);
4502
+ return buildResult(
4503
+ { type: "unknown", reason },
4504
+ report.join("\n"),
4505
+ [],
4506
+ [],
4507
+ [],
4508
+ [],
4509
+ performance.now() - start,
4510
+ {
4511
+ places: [...this.net.places].length,
4512
+ transitions: [...this.net.transitions].length,
4513
+ invariantsFound: 0,
4514
+ structuralResult: "n/a (unresolved property place)"
4515
+ },
4516
+ null,
4517
+ "unavailable"
4518
+ );
4519
+ }
3914
4520
  const hasMatch = [...this.net.transitions].some((t) => t.matchSpec !== null);
3915
4521
  const nuBounded = this._budgetPlaces.size > 0;
3916
4522
  if (hasMatch && (!isReachabilitySafety(this._property) || !nuBounded)) {
@@ -3924,7 +4530,8 @@ var SmtVerifier = class _SmtVerifier {
3924
4530
  this._nuMaxClasses,
3925
4531
  this._fragmentMode,
3926
4532
  this._carrierPlaces,
3927
- this._prioritySemantics
4533
+ this._prioritySemantics,
4534
+ this._conditionalSinks
3928
4535
  );
3929
4536
  const deferToRouteA = outcome !== null && outcome.verdict.type === "unknown" && !isReachabilitySafety(this._property) && nuBounded;
3930
4537
  if (outcome !== null && !deferToRouteA) {
@@ -3952,7 +4559,9 @@ var SmtVerifier = class _SmtVerifier {
3952
4559
  transitions: [...this.net.transitions].length,
3953
4560
  invariantsFound: 0,
3954
4561
  structuralResult: "n/a (\u03BD name-partition SCG)"
3955
- }
4562
+ },
4563
+ null,
4564
+ "nu-scg"
3956
4565
  );
3957
4566
  } else if (deferToRouteA) {
3958
4567
  report.push(
@@ -3965,6 +4574,47 @@ var SmtVerifier = class _SmtVerifier {
3965
4574
  );
3966
4575
  }
3967
4576
  }
4577
+ if (!hasMatch && this._environmentPlaces.size === 0 && this._enumerationMaxClasses > 0 && isUntimed(this.net)) {
4578
+ const enumerated = verifyViaStateClassGraph(
4579
+ this.net,
4580
+ this._initialMarking,
4581
+ this._property,
4582
+ this._sinkPlaces,
4583
+ this._enumerationMaxClasses,
4584
+ this._conditionalSinks
4585
+ );
4586
+ if (enumerated.kind === "decided") {
4587
+ report.push("=== Bounded state-space enumeration (VER-017) ===");
4588
+ report.push(` State classes: ${enumerated.classCount}`);
4589
+ report.push(" P-invariants: not computed (no encoding is built on this route)");
4590
+ report.push(NOTE_ENUMERATED);
4591
+ if (enumerated.transitions.length > 0) {
4592
+ report.push(` Counterexample trace: ${enumerated.trace.length} states, ${enumerated.transitions.length} transitions`);
4593
+ }
4594
+ return buildResult(
4595
+ enumerated.verdict,
4596
+ report.join("\n"),
4597
+ [],
4598
+ [],
4599
+ enumerated.trace,
4600
+ enumerated.transitions,
4601
+ performance.now() - start,
4602
+ {
4603
+ places: [...this.net.places].length,
4604
+ transitions: [...this.net.transitions].length,
4605
+ invariantsFound: 0,
4606
+ structuralResult: "n/a (state-space enumeration)"
4607
+ },
4608
+ // The graph path IS a firing sequence, so a violation is ordered and
4609
+ // confirmed by construction; there is nothing left to replay.
4610
+ enumerated.verdict.type === "violated" ? true : null,
4611
+ "enumeration"
4612
+ );
4613
+ }
4614
+ report.push(
4615
+ `Bounded state-space enumeration truncated at ${this._enumerationMaxClasses} classes (VER-017); verifying via the SMT pipeline.`
4616
+ );
4617
+ }
3968
4618
  report.push("Phase 1: Flattening net...");
3969
4619
  const flatNet = flatten(this.net, this._environmentPlaces, this._environmentMode);
3970
4620
  report.push(` Places: ${flatNet.places.length}`);
@@ -3989,7 +4639,7 @@ var SmtVerifier = class _SmtVerifier {
3989
4639
  }
3990
4640
  report.push(` Result: ${structResultStr}
3991
4641
  `);
3992
- if (this._property.type === "deadlock-free" && !hasMatch && this._sinkPlaces.size === 0 && structResult.type === "no-potential-deadlock" && this._environmentPlaces.size === 0) {
4642
+ if (this._property.type === "deadlock-free" && !hasMatch && commonerApplies(flatNet) && this._sinkPlaces.size === 0 && this._conditionalSinks.length === 0 && structResult.type === "no-potential-deadlock" && this._environmentPlaces.size === 0) {
3993
4643
  report.push("=== RESULT ===\n");
3994
4644
  report.push("PROVEN (structural): Deadlock-freedom verified by Commoner's theorem.");
3995
4645
  report.push(" All siphons contain initially marked traps.");
@@ -4002,7 +4652,9 @@ var SmtVerifier = class _SmtVerifier {
4002
4652
  [],
4003
4653
  [],
4004
4654
  performance.now() - start,
4005
- { places: flatNet.places.length, transitions: flatNet.transitions.length, invariantsFound: 0, structuralResult: structResultStr }
4655
+ { places: flatNet.places.length, transitions: flatNet.transitions.length, invariantsFound: 0, structuralResult: structResultStr },
4656
+ null,
4657
+ "structural"
4006
4658
  );
4007
4659
  }
4008
4660
  report.push("Phase 3: Computing P-invariants...");
@@ -4013,15 +4665,21 @@ var SmtVerifier = class _SmtVerifier {
4013
4665
  flatNet,
4014
4666
  this._initialMarking
4015
4667
  );
4016
- const { valid: semiflows, dropped: droppedSemiflows } = validateInvariantsExact(
4668
+ const basisLostALaw = droppedInvariants.some((d) => d.reason.includes("Strengthening.lean H1"));
4669
+ const semiflowsWanted = this._semiflowInvariants === true || this._semiflowInvariants === "auto" && basisLostALaw || hasMatch && nuBounded;
4670
+ const { valid: semiflows, dropped: droppedSemiflows } = semiflowsWanted ? validateInvariantsExact(
4017
4671
  matrix,
4018
4672
  computePSemiflows(matrix, flatNet, this._initialMarking),
4019
4673
  flatNet,
4020
4674
  this._initialMarking
4021
- );
4675
+ ) : { valid: [], dropped: [] };
4022
4676
  report.push(` Found: ${basisInvariants.length} P-invariant(s)`);
4677
+ if (this._semiflowInvariants === "auto") {
4678
+ report.push(basisLostALaw ? " Semiflow union: ON (auto \u2014 the basis lost a law to the H1 guard)" : " Semiflow union: off (auto \u2014 the basis is complete, so the semiflows would add no constraint the encoding does not already have; they may still differ in FORM)");
4679
+ }
4680
+ const unionWanted = this._semiflowInvariants === true || this._semiflowInvariants === "auto" && basisLostALaw;
4023
4681
  let invariants = basisInvariants;
4024
- if (this._semiflowInvariants) {
4682
+ if (unionWanted) {
4025
4683
  const { invariants: strengthened, added } = strengthenWithSemiflows(basisInvariants, semiflows);
4026
4684
  invariants = strengthened;
4027
4685
  report.push(` Semiflows encoded as invariants: ${added}`);
@@ -4045,6 +4703,11 @@ var SmtVerifier = class _SmtVerifier {
4045
4703
  report.push(` Dropped: ${droppedSemiflows.length} semiflow(s) failed the exact re-check`);
4046
4704
  }
4047
4705
  report.push("");
4706
+ if (!isReachabilitySafety(this._property) && quiescenceUnreachable(flatNet, resolveEnvInjection(flatNet))) {
4707
+ report.push(
4708
+ " NOTE: no marking of this net can be quiescent \u2014 a transition is enabled in every marking (an environment-gated one under modelled injection, VER-006). Every quiescence property is therefore vacuously true here, and a `proven` says nothing about the net."
4709
+ );
4710
+ }
4048
4711
  report.push("Phase 4: IC3/PDR verification via Z3 Spacer...");
4049
4712
  const stats = {
4050
4713
  places: flatNet.places.length,
@@ -4056,6 +4719,7 @@ var SmtVerifier = class _SmtVerifier {
4056
4719
  try {
4057
4720
  solver = resolveZ3();
4058
4721
  } catch (e) {
4722
+ rethrowIfProgrammingError(e);
4059
4723
  const reason = e instanceof Z3Unavailable ? e.message : String(e?.message ?? e);
4060
4724
  report.push(` Solver: z3 unavailable (${reason})`);
4061
4725
  report.push(` Status: UNKNOWN (${reason})
@@ -4063,11 +4727,35 @@ var SmtVerifier = class _SmtVerifier {
4063
4727
  report.push("=== RESULT ===\n");
4064
4728
  report.push(`UNKNOWN: Could not determine ${propDesc}`);
4065
4729
  report.push(` Reason: ${reason}`);
4066
- return buildResult({ type: "unknown", reason }, report.join("\n"), invariants, [], [], [], performance.now() - start, stats);
4730
+ return buildResult({ type: "unknown", reason }, report.join("\n"), invariants, [], [], [], performance.now() - start, stats, null, "unavailable");
4067
4731
  }
4068
4732
  report.push(` Solver: z3 ${formatZ3Version(solver.version)}`);
4069
4733
  const colouredAttempt = this.colouredAttempt(flatNet, invariants, semiflows);
4070
4734
  const colouredPlan = colouredAttempt.plan;
4735
+ if (this._linearBound && colouredPlan == null && isReachabilitySafety(this._property) && !(this._environmentPlaces.size > 0 && this._environmentMode.type === "ignore")) {
4736
+ const proof = await this.linearBoundProof(flatNet, solver, report);
4737
+ if (proof != null) {
4738
+ report.push(" Certificate check: not applicable (structural proof)");
4739
+ report.push("");
4740
+ report.push("=== RESULT ===\n");
4741
+ report.push(`PROVEN (structural): ${propDesc}`);
4742
+ report.push(" Linear state-equation bound: y >= 0 with y.C <= 0 gives y.M <= y.M0 on every");
4743
+ report.push(" reachable marking, and the violating markings exceed it (VER-015).");
4744
+ report.push(` ${proof}`);
4745
+ return this.applyNuGuard(buildResult(
4746
+ { type: "proven", method: "structural", inductiveInvariant: null },
4747
+ report.join("\n"),
4748
+ invariants,
4749
+ [],
4750
+ [],
4751
+ [],
4752
+ performance.now() - start,
4753
+ stats,
4754
+ null,
4755
+ "structural"
4756
+ ), hasMatch, nuBounded, false);
4757
+ }
4758
+ }
4071
4759
  let encoding;
4072
4760
  if (colouredPlan != null) {
4073
4761
  report.push(
@@ -4079,7 +4767,7 @@ var SmtVerifier = class _SmtVerifier {
4079
4767
  report.push(" Status: UNKNOWN (unresolved property place)\n");
4080
4768
  report.push("=== RESULT ===\n");
4081
4769
  report.push(`UNKNOWN: ${reason}`);
4082
- return buildResult({ type: "unknown", reason }, report.join("\n"), invariants, [], [], [], performance.now() - start, stats);
4770
+ return buildResult({ type: "unknown", reason }, report.join("\n"), invariants, [], [], [], performance.now() - start, stats, null, "unavailable");
4083
4771
  }
4084
4772
  encoding = coloured;
4085
4773
  } else {
@@ -4089,9 +4777,20 @@ var SmtVerifier = class _SmtVerifier {
4089
4777
  report.push(" Status: UNKNOWN (unresolved property place)\n");
4090
4778
  report.push("=== RESULT ===\n");
4091
4779
  report.push(`UNKNOWN: ${reason}`);
4092
- return buildResult({ type: "unknown", reason }, report.join("\n"), invariants, [], [], [], performance.now() - start, stats);
4780
+ return buildResult({ type: "unknown", reason }, report.join("\n"), invariants, [], [], [], performance.now() - start, stats, null, "unavailable");
4093
4781
  }
4094
- encoding = encode(flatNet, this._initialMarking, this._property, invariants, this._sinkPlaces, this._counterexampleReplay);
4782
+ encoding = encodeNet(flatNet, this._initialMarking, this._property, invariants, {
4783
+ sinkPlaces: this._sinkPlaces,
4784
+ produceProofs: this._counterexampleReplay,
4785
+ conditionalSinks: this._conditionalSinks,
4786
+ stateEquation: this._stateEquation
4787
+ });
4788
+ if (this._stateEquation) {
4789
+ report.push(` State equation: encoded over ${encoding.counterCount} firing counters (VER-016)`);
4790
+ }
4791
+ }
4792
+ if (this._stateEquation && colouredPlan != null) {
4793
+ report.push(" State equation: not applied (name-coloured encoding)");
4095
4794
  }
4096
4795
  const queryResult = await runZ3Spacer(
4097
4796
  solver,
@@ -4123,7 +4822,9 @@ var SmtVerifier = class _SmtVerifier {
4123
4822
  invariants,
4124
4823
  this._sinkPlaces,
4125
4824
  solver,
4126
- this._timeoutMs
4825
+ this._timeoutMs,
4826
+ this._conditionalSinks,
4827
+ this._stateEquation
4127
4828
  );
4128
4829
  const reason = certificateDowngradeReason(certificate);
4129
4830
  if (reason != null) {
@@ -4167,7 +4868,7 @@ var SmtVerifier = class _SmtVerifier {
4167
4868
  }
4168
4869
  case "violated": {
4169
4870
  report.push(" Status: SAT (counterexample found)\n");
4170
- const decoded = decode(queryResult.answer, flatNet);
4871
+ const decoded = decode(queryResult.answer, flatNet, encoding.counterCount);
4171
4872
  if (decoded.note != null) report.push(` Counterexample decoding: ${decoded.note}`);
4172
4873
  let confirmed = null;
4173
4874
  let trace = [...decoded.states];
@@ -4179,7 +4880,8 @@ var SmtVerifier = class _SmtVerifier {
4179
4880
  this._initialMarking,
4180
4881
  decoded.states,
4181
4882
  this._property,
4182
- this._sinkPlaces
4883
+ this._sinkPlaces,
4884
+ this._conditionalSinks
4183
4885
  );
4184
4886
  if (assessment.kind === "confirmed") {
4185
4887
  confirmed = true;
@@ -4252,6 +4954,49 @@ var SmtVerifier = class _SmtVerifier {
4252
4954
  }
4253
4955
  }
4254
4956
  }
4957
+ /**
4958
+ * Runs the linear state-equation bound query (VER-015) and re-checks its answer in
4959
+ * exact integer arithmetic. Returns the bound as the report prints it when one
4960
+ * separates the violation, `null` otherwise (no bound, solver inconclusive, or a
4961
+ * model that failed the re-check — each named in the report). Never the last word:
4962
+ * `null` hands over to the fixpoint query.
4963
+ */
4964
+ async linearBoundProof(flatNet, solver, report) {
4965
+ const script2 = encodeLinearBound(flatNet, this._initialMarking, this._property);
4966
+ if (script2 == null) return null;
4967
+ let reply;
4968
+ try {
4969
+ reply = await runZ3Text(solver, script2, "bound", this._timeoutMs, []);
4970
+ } catch (e) {
4971
+ rethrowIfProgrammingError(e);
4972
+ report.push(` Linear state-equation bound: inconclusive (${String(e?.message ?? e)})`);
4973
+ return null;
4974
+ }
4975
+ const stdout = reply.stdout.trim();
4976
+ switch (classifyFirstLine(stdout)) {
4977
+ case "sat": {
4978
+ const y = decodeLinearBound(stdout, flatNet.places.length);
4979
+ const bound = y == null ? null : checkLinearBoundExact(flatNet, this._initialMarking, this._property, y);
4980
+ if (bound == null) {
4981
+ report.push(" Linear state-equation bound: inconclusive (solver model failed the exact re-check)");
4982
+ return null;
4983
+ }
4984
+ const rendered = `${formatLinearBound(flatNet, bound)}; violation needs ${formatLinearDemand(flatNet, this._property, bound)}`;
4985
+ report.push(` Linear state-equation bound: ${rendered}`);
4986
+ report.push(" Status: bound excludes every violating marking (re-checked in exact integer arithmetic)");
4987
+ return rendered;
4988
+ }
4989
+ case "unsat":
4990
+ report.push(" Linear state-equation bound: none separates the violation");
4991
+ return null;
4992
+ case "unknown":
4993
+ report.push(" Linear state-equation bound: inconclusive (Z3 answered unknown)");
4994
+ return null;
4995
+ default:
4996
+ report.push(` Linear state-equation bound: inconclusive (${failureReason(reply, timeoutBudget(this._timeoutMs))})`);
4997
+ return null;
4998
+ }
4999
+ }
4255
5000
  /**
4256
5001
  * ν-net soundness guard (NU-040, NU-050). Applied only when the net contains
4257
5002
  * match (ν-join) transitions, and only to a proven/violated verdict (an
@@ -4306,7 +5051,7 @@ function isReachabilitySafety(property) {
4306
5051
  return false;
4307
5052
  }
4308
5053
  }
4309
- function assessCounterexample(flatNet, initialMarking, decodedStates, property, sinkPlaces) {
5054
+ function assessCounterexample(flatNet, initialMarking, decodedStates, property, sinkPlaces, conditionalSinks = []) {
4310
5055
  if (decodedStates.size === 0) {
4311
5056
  return {
4312
5057
  kind: "unconfirmed",
@@ -4320,9 +5065,12 @@ function assessCounterexample(flatNet, initialMarking, decodedStates, property,
4320
5065
  vectorize(initialMarking, flatNet),
4321
5066
  [...decodedStates].map((m) => vectorize(m, flatNet)),
4322
5067
  property,
4323
- sinkPlaces
5068
+ sinkPlaces,
5069
+ {},
5070
+ conditionalSinks
4324
5071
  );
4325
5072
  } catch (e) {
5073
+ rethrowIfProgrammingError(e);
4326
5074
  outcome = { kind: "exhausted", reason: `replay threw: ${e?.message ?? e}`, nodesExplored: 0 };
4327
5075
  }
4328
5076
  switch (outcome.kind) {
@@ -4373,6 +5121,40 @@ Downgraded to UNKNOWN: ${reason}
4373
5121
  function truncate(s, max) {
4374
5122
  return s.length <= max ? s : `${s.slice(0, max)}\u2026 (${s.length - max} chars truncated)`;
4375
5123
  }
5124
+ function unresolvedPropertyPlaceInNet(net, property) {
5125
+ const declared = /* @__PURE__ */ new Set();
5126
+ for (const p of net.places) declared.add(p.name);
5127
+ for (const place of propertyPlaces(property)) {
5128
+ if (!declared.has(place.name)) return place.name;
5129
+ }
5130
+ return null;
5131
+ }
5132
+ function commonerApplies(flatNet) {
5133
+ for (const ft of flatNet.transitions) {
5134
+ if (ft.readPlaces.length > 0 || ft.inhibitorPlaces.length > 0 || ft.resetPlaces.length > 0) return false;
5135
+ if (ft.consumeAll.some(Boolean)) return false;
5136
+ if (ft.preVector.some((w) => w > 1)) return false;
5137
+ }
5138
+ return true;
5139
+ }
5140
+ function propertyPlaces(property) {
5141
+ switch (property.type) {
5142
+ case "deadlock-free":
5143
+ return [];
5144
+ case "terminates-at-sink":
5145
+ return [];
5146
+ case "mutual-exclusion":
5147
+ return [property.p1, property.p2];
5148
+ case "place-bound":
5149
+ return [property.place];
5150
+ case "branch-place-bound":
5151
+ return [property.place];
5152
+ case "unreachable":
5153
+ return [...property.places];
5154
+ case "joined-or-dead-lettered":
5155
+ return [property.pending];
5156
+ }
5157
+ }
4376
5158
  function unresolvedPropertyPlace(flatNet, property) {
4377
5159
  const named = (() => {
4378
5160
  switch (property.type) {
@@ -4408,8 +5190,8 @@ function formatInvariant(inv, flatNet) {
4408
5190
  }
4409
5191
  return `${parts.length === 0 ? "0" : parts.join(" + ")} = ${inv.constant}`;
4410
5192
  }
4411
- function buildResult(verdict, report, invariants, discoveredInvariants, trace, transitions, elapsedMs, statistics, counterexampleConfirmed = null) {
4412
- return { verdict, report, invariants, discoveredInvariants, counterexampleTrace: trace, counterexampleTransitions: transitions, counterexampleConfirmed, elapsedMs, statistics };
5193
+ function buildResult(verdict, report, invariants, discoveredInvariants, trace, transitions, elapsedMs, statistics, counterexampleConfirmed = null, route = "smt") {
5194
+ return { verdict, route, report, invariants, discoveredInvariants, counterexampleTrace: trace, counterexampleTransitions: transitions, counterexampleConfirmed, elapsedMs, statistics };
4413
5195
  }
4414
5196
 
4415
5197
  // src/verification/smt-verification-result.ts
@@ -4445,6 +5227,7 @@ export {
4445
5227
  transformAsync,
4446
5228
  produce,
4447
5229
  withTimeout,
5230
+ rethrowIfProgrammingError,
4448
5231
  MarkingState,
4449
5232
  MarkingStateBuilder,
4450
5233
  deadlockFree,
@@ -4455,6 +5238,9 @@ export {
4455
5238
  branchPlaceBound,
4456
5239
  joinedOrDeadLettered,
4457
5240
  propertyDescription,
5241
+ strandingExcuses,
5242
+ strandsToken,
5243
+ describeSinks,
4458
5244
  flatTransition,
4459
5245
  alwaysAvailable,
4460
5246
  bounded,
@@ -4484,13 +5270,24 @@ export {
4484
5270
  runZ3Text,
4485
5271
  runZ3Spacer,
4486
5272
  encode,
5273
+ encodeNet,
4487
5274
  encodeStepRelationSmt2,
4488
5275
  checkCertificate,
4489
5276
  vcScript,
5277
+ violationDemand,
5278
+ encodeLinearBound,
5279
+ decodeLinearBound,
5280
+ checkLinearBoundExact,
5281
+ formatLinearBound,
5282
+ formatLinearDemand,
5283
+ decideOverClasses,
4490
5284
  DBM,
4491
5285
  StateClass,
4492
5286
  requireOutputProducingActions,
4493
5287
  StateClassGraph,
5288
+ isUntimed,
5289
+ NOTE_ENUMERATED,
5290
+ verifyViaStateClassGraph,
4494
5291
  decode,
4495
5292
  decodeStateSet,
4496
5293
  flatNetPlaceCount,
@@ -4502,4 +5299,4 @@ export {
4502
5299
  isProven,
4503
5300
  isViolated
4504
5301
  };
4505
- //# sourceMappingURL=chunk-75KEJQGC.js.map
5302
+ //# sourceMappingURL=chunk-EL4E6LVO.js.map