libpetri 4.1.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() {
@@ -331,6 +336,9 @@ var MarkingStateBuilder = class {
331
336
  function deadlockFree() {
332
337
  return { type: "deadlock-free" };
333
338
  }
339
+ function terminatesAtSink() {
340
+ return { type: "terminates-at-sink" };
341
+ }
334
342
  function mutualExclusion(p1, p2) {
335
343
  return { type: "mutual-exclusion", p1, p2 };
336
344
  }
@@ -350,6 +358,8 @@ function propertyDescription(prop) {
350
358
  switch (prop.type) {
351
359
  case "deadlock-free":
352
360
  return "Deadlock-freedom";
361
+ case "terminates-at-sink":
362
+ return "Terminates at a declared sink";
353
363
  case "mutual-exclusion":
354
364
  return `Mutual exclusion of ${prop.p1.name} and ${prop.p2.name}`;
355
365
  case "place-bound":
@@ -363,6 +373,53 @@ function propertyDescription(prop) {
363
373
  }
364
374
  }
365
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
+
366
423
  // src/verification/encoding/flat-transition.ts
367
424
  function flatTransition(name, source, branchIndex, preVector, postVector, inhibitorPlaces, readPlaces, resetPlaces, consumeAll) {
368
425
  return {
@@ -821,6 +878,8 @@ function strengthenWithSemiflows(invariants, semiflows) {
821
878
  }
822
879
  return { invariants: strengthened, added };
823
880
  }
881
+ var MAX_SEMIFLOW_ROWS = 8192;
882
+ var MAX_SEMIFLOW_CANDIDATES = 65536;
824
883
  function computePSemiflows(matrix, flatNet, initialMarking) {
825
884
  const np = matrix.numPlaces();
826
885
  const nt = matrix.numTransitions();
@@ -838,19 +897,21 @@ function computePSemiflows(matrix, flatNet, initialMarking) {
838
897
  const next = rows.filter((r) => r.sig[t] === 0);
839
898
  const pos = rows.filter((r) => r.sig[t] > 0);
840
899
  const neg = rows.filter((r) => r.sig[t] < 0);
841
- for (const rp of pos) {
842
- for (const rn of neg) {
843
- const cp = -rn.sig[t];
844
- const cn = rp.sig[t];
845
- const sig = combineRow(cp, rp.sig, cn, rn.sig);
846
- const weight = combineRow(cp, rp.weight, cn, rn.weight);
847
- if (sig === null || weight === null) continue;
848
- reduceGcd(sig, weight);
849
- 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
+ }
850
912
  }
851
- }
852
913
  rows = keepSupportMinimal(next);
853
- if (rows.length > 8192) rows.length = 8192;
914
+ if (rows.length > MAX_SEMIFLOW_ROWS) rows.length = MAX_SEMIFLOW_ROWS;
854
915
  }
855
916
  const semiflows = [];
856
917
  for (const { weight } of rows) {
@@ -887,17 +948,43 @@ function reduceGcd(sig, weight) {
887
948
  }
888
949
  }
889
950
  function keepSupportMinimal(rows) {
890
- const supports = rows.map((r) => {
891
- const s = [];
892
- for (let i = 0; i < r.weight.length; i++) if (r.weight[i] !== 0) s.push(i);
893
- return s;
894
- });
895
- const keep = new Array(rows.length).fill(true);
896
- for (let i = 0; i < rows.length; i++) {
897
- if (!keep[i]) continue;
898
- for (let j = 0; j < rows.length; j++) {
899
- if (i === j || !keep[j]) continue;
900
- 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) {
901
988
  keep[i] = false;
902
989
  break;
903
990
  }
@@ -1236,7 +1323,8 @@ function locateZ3(program, env = process.env) {
1236
1323
  const isFile = (p) => {
1237
1324
  try {
1238
1325
  return existsSync(p) && statSync(p).isFile();
1239
- } catch {
1326
+ } catch (e) {
1327
+ rethrowIfProgrammingError(e);
1240
1328
  return false;
1241
1329
  }
1242
1330
  };
@@ -1247,9 +1335,9 @@ function locateZ3(program, env = process.env) {
1247
1335
  const windows = process.platform === "win32";
1248
1336
  for (const dir of searchPath.split(path.delimiter)) {
1249
1337
  if (dir === "") continue;
1250
- const candidate2 = path.join(dir, program);
1251
- if (isFile(candidate2)) return candidate2;
1252
- 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";
1253
1341
  }
1254
1342
  return null;
1255
1343
  }
@@ -1295,7 +1383,8 @@ function z3Available(env = process.env) {
1295
1383
  try {
1296
1384
  resolveZ3(env);
1297
1385
  return true;
1298
- } catch {
1386
+ } catch (e) {
1387
+ rethrowIfProgrammingError(e);
1299
1388
  return false;
1300
1389
  }
1301
1390
  }
@@ -1368,6 +1457,7 @@ async function runZ3Spacer(solver, timeoutMs, smt2, phase) {
1368
1457
  try {
1369
1458
  reply = await runZ3Text(solver, smt2, phase, timeoutMs, ["fp.engine=spacer"]);
1370
1459
  } catch (e) {
1460
+ rethrowIfProgrammingError(e);
1371
1461
  return { type: "unknown", reason: String(e?.message ?? e) };
1372
1462
  }
1373
1463
  const stdout = reply.stdout.trim();
@@ -1386,36 +1476,50 @@ async function runZ3Spacer(solver, timeoutMs, smt2, phase) {
1386
1476
  }
1387
1477
 
1388
1478
  // src/verification/z3/smt-encoder.ts
1389
- 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 ?? [];
1390
1486
  const P = flatNet.places.length;
1487
+ const T = options.stateEquation ? flatNet.transitions.length : 0;
1391
1488
  const lines = [];
1392
1489
  const envInject = resolveEnvInjection(flatNet);
1393
1490
  if (produceProofs) lines.push("(set-option :produce-proofs true)");
1394
1491
  lines.push("(set-logic HORN)");
1395
1492
  lines.push("");
1396
- lines.push(`(declare-fun Reachable (${ints(P).join(" ")}) Bool)`);
1493
+ lines.push(`(declare-fun Reachable (${ints(P + T).join(" ")}) Bool)`);
1397
1494
  lines.push("(declare-fun Error () Bool)");
1398
1495
  lines.push("");
1399
1496
  const mVars = vars(P, "");
1400
1497
  const mpVars = vars(P, "p");
1498
+ const nVars = counterVars(T, "");
1499
+ const npVars = counterVars(T, "p");
1401
1500
  const m0 = [];
1402
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");
1403
1503
  lines.push(`(assert (Reachable ${m0.join(" ")}))`);
1404
1504
  lines.push("");
1405
- for (const ft of flatNet.transitions) {
1406
- 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));
1407
1511
  }
1408
1512
  for (const inj of envInject) {
1409
- lines.push(encodeInjectionRule(P, inj.pid, inj.bound, mVars, mpVars));
1513
+ lines.push(encodeInjectionRule(P, inj.pid, inj.bound, mVars, mpVars, nVars, npVars));
1410
1514
  }
1411
1515
  lines.push("");
1412
- lines.push(encodeErrorRule(flatNet, property, mVars, sinkPlaces, envInject));
1516
+ lines.push(encodeErrorRule(flatNet, property, mVars, nVars, sinkPlaces, envInject, conditionalSinks));
1413
1517
  lines.push("");
1414
1518
  lines.push("(assert (not Error))");
1415
1519
  lines.push("(check-sat)");
1416
1520
  if (produceProofs) lines.push("(get-proof)");
1417
1521
  lines.push("(get-model)");
1418
- return { smt2: lines.join("\n"), placeCount: P };
1522
+ return { smt2: lines.join("\n"), placeCount: P, counterCount: T };
1419
1523
  }
1420
1524
  function resolveEnvInjection(flatNet) {
1421
1525
  const out = [];
@@ -1443,9 +1547,44 @@ function vars(P, suffix) {
1443
1547
  for (let i = 0; i < P; i++) out.push(`m${i}${suffix}`);
1444
1548
  return out;
1445
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
+ }
1446
1555
  function quantified(names) {
1447
1556
  return names.map((v) => `(${v} Int)`).join(" ");
1448
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
+ }
1449
1588
  function firingConditions(flatNet, ft, mVars, mpVars) {
1450
1589
  const P = flatNet.places.length;
1451
1590
  const conditions = [];
@@ -1472,8 +1611,8 @@ function invariantConditions(invariants, names) {
1472
1611
  for (const inv of invariants) {
1473
1612
  const terms = [...inv.support].sort((a, b) => a - b).map((i) => `(* ${inv.weights[i]} ${names[i]})`);
1474
1613
  if (terms.length === 0) continue;
1475
- const sum = terms.length === 1 ? terms[0] : `(+ ${terms.join(" ")})`;
1476
- conditions.push(`(= ${sum} ${inv.constant})`);
1614
+ const sum2 = terms.length === 1 ? terms[0] : `(+ ${terms.join(" ")})`;
1615
+ conditions.push(`(= ${sum2} ${inv.constant})`);
1477
1616
  }
1478
1617
  return conditions;
1479
1618
  }
@@ -1489,50 +1628,61 @@ function injectionConditions(P, pid, bound, mVars, mpVars) {
1489
1628
  }
1490
1629
  return conditions;
1491
1630
  }
1492
- function encodeTransitionRule(flatNet, ft, mVars, mpVars, invariants) {
1493
- const conditions = [`(Reachable ${mVars.join(" ")})`];
1631
+ function encodeTransitionRule(flatNet, ft, mVars, mpVars, nVars, npVars, strengthening) {
1632
+ const conditions = [`(Reachable ${[...mVars, ...nVars].join(" ")})`];
1494
1633
  conditions.push(...firingConditions(flatNet, ft, mVars, mpVars));
1495
- conditions.push(...invariantConditions(invariants, mpVars));
1634
+ conditions.push(...strengthening);
1496
1635
  conditions.push(...envBoundConditions(flatNet, mpVars));
1497
1636
  const body = `(and ${conditions.join("\n ")})`;
1498
- return `(assert (forall (${quantified([...mVars, ...mpVars])})
1637
+ const quantifiedVars = quantified([...mVars, ...mpVars, ...nVars, ...npVars]);
1638
+ return `(assert (forall (${quantifiedVars})
1499
1639
  (=> ${body}
1500
- (Reachable ${mpVars.join(" ")}))))`;
1640
+ (Reachable ${[...mpVars, ...npVars].join(" ")}))))`;
1501
1641
  }
1502
- function encodeInjectionRule(P, pid, bound, mVars, mpVars) {
1503
- const conditions = [`(Reachable ${mVars.join(" ")})`];
1642
+ function encodeInjectionRule(P, pid, bound, mVars, mpVars, nVars, npVars) {
1643
+ const conditions = [`(Reachable ${[...mVars, ...nVars].join(" ")})`];
1504
1644
  conditions.push(...injectionConditions(P, pid, bound, mVars, mpVars));
1645
+ if (nVars.length > 0) conditions.push(...counterConditions(-1, nVars, npVars));
1505
1646
  const body = `(and ${conditions.join("\n ")})`;
1506
- return `(assert (forall (${quantified([...mVars, ...mpVars])})
1647
+ const quantifiedVars = quantified([...mVars, ...mpVars, ...nVars, ...npVars]);
1648
+ return `(assert (forall (${quantifiedVars})
1507
1649
  (=> ${body}
1508
- (Reachable ${mpVars.join(" ")}))))`;
1650
+ (Reachable ${[...mpVars, ...npVars].join(" ")}))))`;
1509
1651
  }
1510
1652
  function conjoin(conditions) {
1511
1653
  if (conditions.length === 0) return "true";
1512
1654
  if (conditions.length === 1) return conditions[0];
1513
1655
  return `(and ${conditions.join(" ")})`;
1514
1656
  }
1515
- function encodeStepRelationSmt2(flatNet) {
1657
+ function encodeStepRelationSmt2(flatNet, stateEquation = false) {
1516
1658
  const P = flatNet.places.length;
1659
+ const T = stateEquation ? flatNet.transitions.length : 0;
1517
1660
  const mVars = vars(P, "");
1518
1661
  const mpVars = vars(P, "p");
1662
+ const nVars = counterVars(T, "");
1663
+ const npVars = counterVars(T, "p");
1519
1664
  const disjuncts = [];
1520
- for (const ft of flatNet.transitions) {
1665
+ for (let k = 0; k < flatNet.transitions.length; k++) {
1666
+ const ft = flatNet.transitions[k];
1521
1667
  const conditions = firingConditions(flatNet, ft, mVars, mpVars);
1668
+ if (T > 0) conditions.push(...counterConditions(k, nVars, npVars));
1522
1669
  conditions.push(...envBoundConditions(flatNet, mpVars));
1523
1670
  disjuncts.push(conjoin(conditions));
1524
1671
  }
1525
1672
  for (const inj of resolveEnvInjection(flatNet)) {
1526
- 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));
1527
1676
  }
1528
1677
  if (disjuncts.length === 0) return "false";
1529
1678
  if (disjuncts.length === 1) return disjuncts[0];
1530
1679
  return `(or ${disjuncts.join("\n ")})`;
1531
1680
  }
1532
- function encodeErrorRule(flatNet, property, mVars, sinkPlaces, envInject) {
1533
- const violation = encodePropertyViolation(flatNet, property, mVars, sinkPlaces, envInject);
1534
- return `(assert (forall (${quantified(mVars)})
1535
- (=> (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})
1536
1686
  Error)))`;
1537
1687
  }
1538
1688
  function indexOrdered(flatNet, places) {
@@ -1543,10 +1693,30 @@ function indexOrdered(flatNet, places) {
1543
1693
  }
1544
1694
  return [...idx].sort((a, b) => a - b);
1545
1695
  }
1546
- function encodePropertyViolation(flatNet, property, mVars, sinkPlaces, envInject) {
1696
+ function encodePropertyViolation(flatNet, property, mVars, sinkPlaces, envInject, conditionalSinks = []) {
1547
1697
  switch (property.type) {
1548
- case "deadlock-free":
1549
- return encodeDeadlock(flatNet, mVars, sinkPlaces, envInject);
1698
+ // DeadlockFree (VER-002): a quiescent marking that STRANDS a token — holds one
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.
1702
+ case "deadlock-free": {
1703
+ const conditions = encodeQuiescent(flatNet, mVars, envInject);
1704
+ if (conditions == null) return "false";
1705
+ const stranded = strandedConditions(strandingExcuses(flatNet, sinkPlaces, conditionalSinks), mVars);
1706
+ if (stranded.length === 0) return "false";
1707
+ conditions.push(`(or ${stranded.join(" ")})`);
1708
+ return joinConditions(conditions);
1709
+ }
1710
+ // TerminatesAtSink (VER-002): a quiescent marking that reached NO declared sink.
1711
+ // This is the predicate DeadlockFree carried before the VER-002 split, unchanged.
1712
+ case "terminates-at-sink": {
1713
+ const conditions = encodeQuiescent(flatNet, mVars, envInject);
1714
+ if (conditions == null) return "false";
1715
+ for (const pid of indexOrdered(flatNet, sinkPlaces)) {
1716
+ conditions.push(`(= ${mVars[pid]} 0)`);
1717
+ }
1718
+ return joinConditions(conditions);
1719
+ }
1550
1720
  case "mutual-exclusion": {
1551
1721
  const conditions = indexOrdered(flatNet, [property.p1, property.p2]).map((i) => `(>= ${mVars[i]} 1)`);
1552
1722
  return conditions.length === 0 ? "false" : `(and ${conditions.join(" ")})`;
@@ -1560,14 +1730,36 @@ function encodePropertyViolation(flatNet, property, mVars, sinkPlaces, envInject
1560
1730
  const conditions = indexOrdered(flatNet, property.places).map((i) => `(>= ${mVars[i]} 1)`);
1561
1731
  return conditions.length === 0 ? "false" : `(and ${conditions.join(" ")})`;
1562
1732
  }
1733
+ // JoinedOrDeadLettered (NU-040 AC4): a quiescent state that still holds a
1734
+ // `pending` token is a stranded correlation group. Carries NO sink clause — a
1735
+ // declared sink must not excuse a stranded group.
1563
1736
  case "joined-or-dead-lettered": {
1564
- const deadlock = encodeDeadlock(flatNet, mVars, sinkPlaces, envInject);
1565
1737
  const pid = flatNet.placeIndex.get(property.pending.name);
1566
- return pid == null ? "false" : `(and ${deadlock} (>= ${mVars[pid]} 1))`;
1738
+ if (pid == null) return "false";
1739
+ const conditions = encodeQuiescent(flatNet, mVars, envInject);
1740
+ if (conditions == null) return "false";
1741
+ conditions.push(`(>= ${mVars[pid]} 1)`);
1742
+ return joinConditions(conditions);
1743
+ }
1744
+ }
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(" ")})`);
1567
1755
  }
1568
1756
  }
1757
+ return stranded;
1569
1758
  }
1570
- function encodeDeadlock(flatNet, mVars, sinkPlaces, envInject) {
1759
+ function joinConditions(conditions) {
1760
+ return conditions.length === 0 ? "true" : `(and ${conditions.join("\n ")})`;
1761
+ }
1762
+ function encodeQuiescent(flatNet, mVars, envInject) {
1571
1763
  const envBound = /* @__PURE__ */ new Map();
1572
1764
  for (const inj of envInject) envBound.set(inj.pid, inj.bound);
1573
1765
  const disabledConditions = [];
@@ -1597,13 +1789,13 @@ function encodeDeadlock(flatNet, mVars, sinkPlaces, envInject) {
1597
1789
  disabledConditions.push("true");
1598
1790
  continue;
1599
1791
  }
1600
- if (disableReasons.length === 0) return "false";
1792
+ if (disableReasons.length === 0) return null;
1601
1793
  disabledConditions.push(`(or ${disableReasons.join(" ")})`);
1602
1794
  }
1603
- for (const pid of indexOrdered(flatNet, sinkPlaces)) {
1604
- disabledConditions.push(`(= ${mVars[pid]} 0)`);
1605
- }
1606
- return disabledConditions.length === 0 ? "true" : `(and ${disabledConditions.join("\n ")})`;
1795
+ return disabledConditions;
1796
+ }
1797
+ function quiescenceUnreachable(flatNet, envInject) {
1798
+ return encodeQuiescent(flatNet, vars(flatNet.places.length, ""), envInject) === null;
1607
1799
  }
1608
1800
  function injectionMap(flatNet) {
1609
1801
  const out = /* @__PURE__ */ new Map();
@@ -1613,7 +1805,7 @@ function injectionMap(flatNet) {
1613
1805
 
1614
1806
  // src/verification/z3/certificate-checker.ts
1615
1807
  var VC_LABELS = ["initiation (VC1)", "consecution (VC2)", "safety (VC3)"];
1616
- 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) {
1617
1809
  if (certificate == null) {
1618
1810
  return {
1619
1811
  type: "unavailable",
@@ -1626,11 +1818,12 @@ async function checkCertificate(certificate, flatNet, initialMarking, property,
1626
1818
  if (!certificate.includes("(define-fun Reachable ") && !certificate.includes("(define-fun |Reachable| ")) {
1627
1819
  return { type: "unavailable", reason: "certificate does not define Reachable", invariant: certificate };
1628
1820
  }
1629
- const vcs = buildVerificationConditions(certificate, flatNet, initialMarking, property, sinkPlaces, invariants);
1821
+ const vcs = buildVerificationConditions(certificate, flatNet, initialMarking, property, sinkPlaces, invariants, conditionalSinks, stateEquation);
1630
1822
  let results;
1631
1823
  try {
1632
1824
  results = await runVcScript(script(vcs), timeoutMs, solver);
1633
1825
  } catch (e) {
1826
+ rethrowIfProgrammingError(e);
1634
1827
  return { type: "unavailable", reason: String(e?.message ?? e), invariant: certificate };
1635
1828
  }
1636
1829
  for (let i = 0; i < results.length; i++) {
@@ -1641,8 +1834,8 @@ async function checkCertificate(certificate, flatNet, initialMarking, property,
1641
1834
  }
1642
1835
  return { type: "passed", invariant: certificate };
1643
1836
  }
1644
- function vcScript(certificate, flatNet, initialMarking, property, sinkPlaces, invariants) {
1645
- 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));
1646
1839
  }
1647
1840
  function shapeFailure(flatNet, invariants) {
1648
1841
  const P = flatNet.places.length;
@@ -1686,14 +1879,29 @@ function parseVcResults(stdout) {
1686
1879
  }
1687
1880
  return results;
1688
1881
  }
1689
- function buildVerificationConditions(certificate, flatNet, initialMarking, property, sinkPlaces, invariants) {
1882
+ function buildVerificationConditions(certificate, flatNet, initialMarking, property, sinkPlaces, invariants, conditionalSinks, stateEquation) {
1690
1883
  const P = flatNet.places.length;
1884
+ const T = stateEquation ? flatNet.transitions.length : 0;
1691
1885
  const mVars = [];
1692
1886
  const mpVars = [];
1693
1887
  for (let i = 0; i < P; i++) {
1694
1888
  mVars.push(`m${i}`);
1695
1889
  mpVars.push(`m${i}p`);
1696
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
+ };
1697
1905
  const prelude = [
1698
1906
  "; IC3/PDR certificate check (plain SMT-LIB2, not HORN):",
1699
1907
  "; each VC below must be unsat for the certificate to stand.",
@@ -1702,19 +1910,22 @@ function buildVerificationConditions(certificate, flatNet, initialMarking, prope
1702
1910
  ];
1703
1911
  for (const v of mVars) prelude.push(`(declare-const ${v} Int)`);
1704
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)`);
1705
1915
  const m0 = [];
1706
1916
  for (let i = 0; i < P; i++) m0.push(String(initialMarking.tokens(flatNet.places[i])));
1707
- const vc1 = [`(assert (not ${candidate(m0, invariants)}))`];
1708
- const nonNegative = mVars.map((v) => `(assert (>= ${v} 0))`);
1709
- 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);
1710
1921
  const vc2 = [
1711
1922
  ...nonNegative,
1712
- `(assert ${candidate(mVars, invariants)})`,
1923
+ `(assert ${candidateOf(mVars, nVars)})`,
1713
1924
  `(assert ${step})`,
1714
- `(assert (not ${candidate(mpVars, invariants)}))`
1925
+ `(assert (not ${candidateOf(mpVars, npVars)}))`
1715
1926
  ];
1716
- const bad = encodePropertyViolation(flatNet, property, mVars, sinkPlaces, resolveEnvInjection(flatNet));
1717
- 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})`];
1718
1929
  return { prelude, asserts: [vc1, vc2, vc3] };
1719
1930
  }
1720
1931
  function script(vcs) {
@@ -1778,8 +1989,190 @@ function reasonUnknown(reply) {
1778
1989
  reason = reason.trim();
1779
1990
  return reason === "" ? null : reason;
1780
1991
  }
1781
- function candidate(names, invariants) {
1782
- 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;
1783
2176
  }
1784
2177
 
1785
2178
  // src/verification/analysis/dbm.ts
@@ -1888,6 +2281,35 @@ var DBM = class _DBM {
1888
2281
  allNames.push(...newClockNames);
1889
2282
  return new _DBM(newBounds, newDim, allNames, false).canonicalize();
1890
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
+ }
1891
2313
  /** Lets time pass: set all lower bounds to 0. */
1892
2314
  letTimePass() {
1893
2315
  if (this._empty) return this;
@@ -1919,6 +2341,24 @@ var DBM = class _DBM {
1919
2341
  }
1920
2342
  return true;
1921
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
+ }
1922
2362
  toString() {
1923
2363
  if (this._empty) return "DBM[empty]";
1924
2364
  const parts = [];
@@ -2167,10 +2607,35 @@ var StateClassGraph = class _StateClassGraph {
2167
2607
  }
2168
2608
  };
2169
2609
  function classKey(sc) {
2170
- 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;
2171
2634
  }
2172
2635
  function initialStateClass(net, initialMarking, envPlaces, envMode) {
2173
- 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);
2174
2639
  const clockNames = enabledTransitions.map((t) => t.name);
2175
2640
  const lowerBounds = enabledTransitions.map((t) => earliest(t.timing) / 1e3);
2176
2641
  const upperBounds = enabledTransitions.map((t) => latest(t.timing) / 1e3);
@@ -2215,14 +2680,19 @@ function computeSuccessor(net, current, fired, environmentPlaces, environmentMod
2215
2680
  const newClockNames = newlyEnabled.map((t) => t.name);
2216
2681
  const newLowerBounds = newlyEnabled.map((t) => earliest(t.timing) / 1e3);
2217
2682
  const newUpperBounds = newlyEnabled.map((t) => latest(t.timing) / 1e3);
2218
- const firedDBM = current.firingDomain.fireTransition(
2683
+ let firedDBM = current.firingDomain.fireTransition(
2219
2684
  firedIdx,
2220
2685
  newClockNames,
2221
2686
  newLowerBounds,
2222
2687
  newUpperBounds,
2223
2688
  persistentIndices
2224
2689
  );
2225
- 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
+ }
2226
2696
  const readyEarliest = allEnabled.map((_, k) => firedDBM.getLowerBound(k));
2227
2697
  const newDBM = firedDBM.letTimePass();
2228
2698
  return new StateClass(newMarking, newDBM, allEnabled, readyEarliest);
@@ -2294,10 +2764,7 @@ function fireTransition(marking, transition, outputPlaces, environmentPlaces, en
2294
2764
  consumeFromPlace(builder, spec.place, toConsume, environmentPlaces, environmentMode);
2295
2765
  }
2296
2766
  for (const arc of transition.resets) {
2297
- const current = marking.tokens(arc.place);
2298
- if (current > 0) {
2299
- builder.removeTokens(arc.place, current);
2300
- }
2767
+ builder.tokens(arc.place, 0);
2301
2768
  }
2302
2769
  for (const place of outputPlaces) {
2303
2770
  builder.addTokens(place, 1);
@@ -2314,12 +2781,72 @@ function consumeFromPlace(builder, place, count, environmentPlaces, environmentM
2314
2781
  }
2315
2782
  }
2316
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
+
2317
2844
  // src/verification/z3/counterexample-decoder.ts
2318
- function decode(answer, flatNet) {
2319
- const states = decodeStateSet(answer, flatNet);
2845
+ function decode(answer, flatNet, counterCount = 0) {
2846
+ const states = decodeStateSet(answer, flatNet, counterCount);
2320
2847
  return { states, note: states.size === 0 ? "no ground Reachable states in the z3 proof" : null };
2321
2848
  }
2322
- function decodeStateSet(answer, flatNet) {
2849
+ function decodeStateSet(answer, flatNet, counterCount = 0) {
2323
2850
  const byKey = /* @__PURE__ */ new Map();
2324
2851
  const P = flatNet.places.length;
2325
2852
  for (const head of ["(Reachable", "(|Reachable|"]) {
@@ -2336,8 +2863,8 @@ function decodeStateSet(answer, flatNet) {
2336
2863
  if (end < 0) break;
2337
2864
  const inner = answer.slice(start + head.length, end - 1);
2338
2865
  const args = parseGroundIntArgs(inner);
2339
- if (args != null && args.length === P) {
2340
- 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);
2341
2868
  const key = marking.toString();
2342
2869
  if (!byKey.has(key)) byKey.set(key, marking);
2343
2870
  }
@@ -2511,20 +3038,41 @@ function enabledRelaxEnv(state, ft, envInj) {
2511
3038
  }
2512
3039
  return true;
2513
3040
  }
2514
- function isDeadlockA(index, state) {
3041
+ function isQuiescent(index, state) {
2515
3042
  for (const ft of index.flatNet.transitions) {
2516
3043
  if (enabledRelaxEnv(state, ft, index.envInj)) return false;
2517
3044
  }
2518
3045
  return true;
2519
3046
  }
2520
- function satisfiesBadIndexed(index, state, property, sinkPlaces) {
3047
+ function sinkIndices(flatNet, sinkPlaces) {
3048
+ const idx = /* @__PURE__ */ new Set();
3049
+ for (const sink of sinkPlaces) {
3050
+ const i = flatNetIndexOf(flatNet, sink);
3051
+ if (i >= 0) idx.add(i);
3052
+ }
3053
+ return idx;
3054
+ }
3055
+ function satisfiesBadIndexed(index, state, property, sinkPlaces, conditionalSinks) {
2521
3056
  const flatNet = index.flatNet;
2522
3057
  switch (property.type) {
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.
2523
3061
  case "deadlock-free": {
2524
- if (!isDeadlockA(index, state)) return false;
2525
- for (const sink of sinkPlaces) {
2526
- const idx = flatNetIndexOf(flatNet, sink);
2527
- if (idx >= 0 && state[idx] > 0) return false;
3062
+ if (!isQuiescent(index, state)) return false;
3063
+ const excuses = strandingExcuses(flatNet, sinkPlaces, conditionalSinks);
3064
+ for (let pid = 0; pid < flatNet.places.length; pid++) {
3065
+ const markers = excuses[pid];
3066
+ if (markers == null || state[pid] < 1) continue;
3067
+ if (markers.every((k) => state[k] === 0)) return true;
3068
+ }
3069
+ return false;
3070
+ }
3071
+ // TerminatesAtSink (VER-002): quiescent AND no declared sink marked.
3072
+ case "terminates-at-sink": {
3073
+ if (!isQuiescent(index, state)) return false;
3074
+ for (const pid of sinkIndices(flatNet, sinkPlaces)) {
3075
+ if (state[pid] !== 0) return false;
2528
3076
  }
2529
3077
  return true;
2530
3078
  }
@@ -2540,10 +3088,12 @@ function satisfiesBadIndexed(index, state, property, sinkPlaces) {
2540
3088
  if (idx < 0) return false;
2541
3089
  return state[idx] > property.bound;
2542
3090
  }
3091
+ // JoinedOrDeadLettered (NU-040 AC4): quiescent AND `pending` marked. No sink
3092
+ // clause — a marked sink must not excuse a stranded group.
2543
3093
  case "joined-or-dead-lettered": {
2544
3094
  const idx = flatNetIndexOf(flatNet, property.pending);
2545
3095
  if (idx < 0) return false;
2546
- return isDeadlockA(index, state) && state[idx] >= 1;
3096
+ return isQuiescent(index, state) && state[idx] >= 1;
2547
3097
  }
2548
3098
  case "unreachable": {
2549
3099
  let resolved = 0;
@@ -2557,7 +3107,7 @@ function satisfiesBadIndexed(index, state, property, sinkPlaces) {
2557
3107
  }
2558
3108
  }
2559
3109
  }
2560
- function replayCounterexample(flatNet, initial, decodedStates, property, sinkPlaces, options = {}) {
3110
+ function replayCounterexample(flatNet, initial, decodedStates, property, sinkPlaces, options = {}, conditionalSinks = []) {
2561
3111
  const segmentBudget = options.segmentBudget ?? 3;
2562
3112
  const nodeBudget = options.nodeBudget ?? 1e4;
2563
3113
  const anchors = /* @__PURE__ */ new Set();
@@ -2574,7 +3124,7 @@ function replayCounterexample(flatNet, initial, decodedStates, property, sinkPla
2574
3124
  };
2575
3125
  }
2576
3126
  const index = buildIndex(flatNet);
2577
- if (satisfiesBadIndexed(index, initial, property, sinkPlaces)) {
3127
+ if (satisfiesBadIndexed(index, initial, property, sinkPlaces, conditionalSinks)) {
2578
3128
  return { kind: "confirmed", states: [initial], steps: [], nodesExplored: 1 };
2579
3129
  }
2580
3130
  const nodes = [{ state: initial, step: null, parent: -1, segment: 0 }];
@@ -2603,7 +3153,7 @@ function replayCounterexample(flatNet, initial, decodedStates, property, sinkPla
2603
3153
  }
2604
3154
  nodes.push({ state: succ.state, step: succ.step, parent: idx, segment });
2605
3155
  const childIdx = nodes.length - 1;
2606
- if (satisfiesBadIndexed(index, succ.state, property, sinkPlaces)) {
3156
+ if (satisfiesBadIndexed(index, succ.state, property, sinkPlaces, conditionalSinks)) {
2607
3157
  const chain = reconstruct(nodes, childIdx);
2608
3158
  return { kind: "confirmed", ...chain, nodesExplored: nodes.length };
2609
3159
  }
@@ -2751,7 +3301,7 @@ function buildLayout(plan, P) {
2751
3301
  function quantified2(names) {
2752
3302
  return names.map((v) => `(${v} Int)`).join(" ");
2753
3303
  }
2754
- function encodeColoured(plan, flat, initial, property, invariants, sinkPlaces) {
3304
+ function encodeColoured(plan, flat, initial, property, invariants, sinkPlaces, conditionalSinks = []) {
2755
3305
  const P = flat.places.length;
2756
3306
  const k = plan.k;
2757
3307
  const lay = buildLayout(plan, P);
@@ -2820,13 +3370,13 @@ function encodeColoured(plan, flat, initial, property, invariants, sinkPlaces) {
2820
3370
  }
2821
3371
  }
2822
3372
  lines.push("");
2823
- const error = encodeError(plan, lay, flat, property, sinkPlaces, injectionMap(flat));
3373
+ const error = encodeError(plan, lay, flat, property, sinkPlaces, injectionMap(flat), conditionalSinks);
2824
3374
  if (error == null) return null;
2825
3375
  lines.push(error);
2826
3376
  lines.push("");
2827
3377
  lines.push("(assert (not Error))");
2828
3378
  lines.push("(check-sat)");
2829
- return { smt2: lines.join("\n"), placeCount: P };
3379
+ return { smt2: lines.join("\n"), placeCount: P, counterCount: 0 };
2830
3380
  }
2831
3381
  function encodeRule(plan, lay, invariants, fill) {
2832
3382
  const enab = [];
@@ -2888,17 +3438,17 @@ function liftedInvariant(inv, plan, lay, names) {
2888
3438
  terms.push(w === 1 ? agg : `(* ${w} ${agg})`);
2889
3439
  }
2890
3440
  if (terms.length === 0) return null;
2891
- const sum = terms.length === 1 ? terms[0] : `(+ ${terms.join(" ")})`;
2892
- return `(= ${sum} ${inv.constant})`;
3441
+ const sum2 = terms.length === 1 ? terms[0] : `(+ ${terms.join(" ")})`;
3442
+ return `(= ${sum2} ${inv.constant})`;
2893
3443
  }
2894
- function encodeError(plan, lay, flat, property, sinkPlaces, envInj) {
2895
- 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);
2896
3446
  if (violation == null) return null;
2897
3447
  return `(assert (forall (${quantified2(lay.cur)})
2898
3448
  (=> (and (Reachable ${lay.cur.join(" ")}) ${violation})
2899
3449
  Error)))`;
2900
3450
  }
2901
- function encodeViolation(plan, lay, flat, property, sinkPlaces, envInj) {
3451
+ function encodeViolation(plan, lay, flat, property, sinkPlaces, envInj, conditionalSinks) {
2902
3452
  const anyPlacePresent = (places) => {
2903
3453
  const conds = indexOrdered(flat, places).map((pid) => `(>= ${aggregate(plan, lay, pid, lay.cur)} 1)`);
2904
3454
  return conds.length === 0 ? "false" : `(and ${conds.join(" ")})`;
@@ -2914,13 +3464,37 @@ function encodeViolation(plan, lay, flat, property, sinkPlaces, envInj) {
2914
3464
  return anyPlacePresent([property.p1, property.p2]);
2915
3465
  case "unreachable":
2916
3466
  return anyPlacePresent(property.places);
2917
- case "deadlock-free":
2918
- return encodeColouredDeadlock(plan, lay, flat, sinkPlaces, envInj);
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.
3470
+ case "deadlock-free": {
3471
+ const conds = encodeColouredQuiescent(plan, lay, flat, envInj);
3472
+ if (conds == null) return "false";
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);
3476
+ if (stranded.length === 0) return "false";
3477
+ conds.push(`(or ${stranded.join(" ")})`);
3478
+ return joinColoured(conds);
3479
+ }
3480
+ // TerminatesAtSink (VER-002): quiescent AND no declared sink marked.
3481
+ case "terminates-at-sink": {
3482
+ const conds = encodeColouredQuiescent(plan, lay, flat, envInj);
3483
+ if (conds == null) return "false";
3484
+ for (const pid of indexOrdered(flat, sinkPlaces)) {
3485
+ conds.push(`(= ${aggregate(plan, lay, pid, lay.cur)} 0)`);
3486
+ }
3487
+ return joinColoured(conds);
3488
+ }
3489
+ // JoinedOrDeadLettered (NU-040 AC4): quiescent AND `pending` marked, with NO
3490
+ // sink clause.
2919
3491
  case "joined-or-dead-lettered": {
2920
3492
  const pid = flat.placeIndex.get(property.pending.name);
2921
3493
  if (pid == null) return null;
2922
- const deadlock = encodeColouredDeadlock(plan, lay, flat, sinkPlaces, envInj);
2923
- return `(and ${deadlock} (>= ${aggregate(plan, lay, pid, lay.cur)} 1))`;
3494
+ const conds = encodeColouredQuiescent(plan, lay, flat, envInj);
3495
+ if (conds == null) return "false";
3496
+ conds.push(`(>= ${aggregate(plan, lay, pid, lay.cur)} 1)`);
3497
+ return joinColoured(conds);
2924
3498
  }
2925
3499
  }
2926
3500
  }
@@ -2978,7 +3552,10 @@ function colouredDisabledTerm(cls, plan, lay) {
2978
3552
  }
2979
3553
  }
2980
3554
  }
2981
- function encodeColouredDeadlock(plan, lay, flat, sinkPlaces, envInj) {
3555
+ function joinColoured(conds) {
3556
+ return conds.length === 0 ? "true" : `(and ${conds.join(" ")})`;
3557
+ }
3558
+ function encodeColouredQuiescent(plan, lay, flat, envInj) {
2982
3559
  const disabledConditions = [];
2983
3560
  for (let ti = 0; ti < plan.classes.length; ti++) {
2984
3561
  const cls = plan.classes[ti];
@@ -2989,15 +3566,12 @@ function encodeColouredDeadlock(plan, lay, flat, sinkPlaces, envInj) {
2989
3566
  disabledConditions.push("true");
2990
3567
  continue;
2991
3568
  }
2992
- const term = colouredDisabledTerm(cls, plan, lay);
2993
- if (term != null) reasons.push(term);
2994
- if (reasons.length === 0) return "false";
3569
+ const term2 = colouredDisabledTerm(cls, plan, lay);
3570
+ if (term2 != null) reasons.push(term2);
3571
+ if (reasons.length === 0) return null;
2995
3572
  disabledConditions.push(reasons.length === 1 ? reasons[0] : `(or ${reasons.join(" ")})`);
2996
3573
  }
2997
- for (const pid of indexOrdered(flat, sinkPlaces)) {
2998
- disabledConditions.push(`(= ${aggregate(plan, lay, pid, lay.cur)} 0)`);
2999
- }
3000
- return disabledConditions.length === 0 ? "true" : `(and ${disabledConditions.join(" ")})`;
3574
+ return disabledConditions;
3001
3575
  }
3002
3576
 
3003
3577
  // src/verification/analysis/name-fragment.ts
@@ -3189,7 +3763,7 @@ var NameStateClass = class {
3189
3763
  }
3190
3764
  };
3191
3765
  function baseKeyOf(base) {
3192
- return `${base.marking.toString()}|${base.firingDomain.toString()}`;
3766
+ return `${base.marking.toString()}|${base.firingDomain.zoneKey()}`;
3193
3767
  }
3194
3768
 
3195
3769
  // src/verification/analysis/name-state-class-graph.ts
@@ -3420,7 +3994,7 @@ function enablingSymbols(names, colouredIn) {
3420
3994
 
3421
3995
  // src/verification/nu-scg-verifier.ts
3422
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";
3423
- 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 = []) {
3424
3998
  const fragment = classify(net, fragmentMode, carrierPlaces);
3425
3999
  if (fragment === null) return null;
3426
4000
  for (const p of initial.placesWithTokens()) {
@@ -3447,9 +4021,9 @@ function verifyViaNameScg(net, initial, property, sinkPlaces, environmentPlaces,
3447
4021
  classCount: scg.classCount()
3448
4022
  };
3449
4023
  }
3450
- const violating = decide(scg, property, sinkPlaces);
4024
+ const violating = decide(scg, property, sinkPlaces, conditionalSinks);
3451
4025
  if (violating >= 0) {
3452
- const [trace, transitions] = counterexamplePath(scg, violating);
4026
+ const [trace, transitions] = counterexamplePath2(scg, violating);
3453
4027
  return { verdict: { type: "violated" }, trace, transitions, note: NOTE_EXACT, classCount: scg.classCount() };
3454
4028
  }
3455
4029
  return {
@@ -3460,45 +4034,19 @@ function verifyViaNameScg(net, initial, property, sinkPlaces, environmentPlaces,
3460
4034
  classCount: scg.classCount()
3461
4035
  };
3462
4036
  }
3463
- function decide(scg, property, sinkPlaces) {
3464
- const firstWhere = (pred) => {
3465
- for (let i = 0; i < scg.classCount(); i++) {
3466
- if (pred(i)) return i;
3467
- }
3468
- return -1;
3469
- };
3470
- switch (property.type) {
3471
- case "place-bound":
3472
- case "branch-place-bound":
3473
- return firstWhere((i) => scg.markingOf(i).tokens(property.place) > property.bound);
3474
- case "unreachable":
3475
- return firstWhere((i) => {
3476
- const m = scg.markingOf(i);
3477
- for (const p of property.places) {
3478
- if (!m.hasTokens(p)) return false;
3479
- }
3480
- return true;
3481
- });
3482
- case "mutual-exclusion":
3483
- return firstWhere((i) => {
3484
- const m = scg.markingOf(i);
3485
- return m.hasTokens(property.p1) && m.hasTokens(property.p2);
3486
- });
3487
- case "deadlock-free":
3488
- return firstWhere((i) => scg.successorsOf(i).length === 0 && !allTokensInSinks(scg.markingOf(i), sinkPlaces));
3489
- case "joined-or-dead-lettered":
3490
- return firstWhere((i) => scg.successorsOf(i).length === 0 && scg.markingOf(i).hasTokens(property.pending));
3491
- }
3492
- }
3493
- function allTokensInSinks(m, sinks) {
3494
- const sinkNames = /* @__PURE__ */ new Set();
3495
- for (const s of sinks) sinkNames.add(s.name);
3496
- for (const p of m.placesWithTokens()) {
3497
- if (!sinkNames.has(p.name)) return false;
3498
- }
3499
- return true;
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
+ );
3500
4048
  }
3501
- function counterexamplePath(scg, target) {
4049
+ function counterexamplePath2(scg, target) {
3502
4050
  const n = scg.classCount();
3503
4051
  const parent = new Array(n).fill(-1);
3504
4052
  const via = new Array(n).fill("");
@@ -3538,13 +4086,17 @@ var SmtVerifier = class _SmtVerifier {
3538
4086
  _property = deadlockFree();
3539
4087
  _environmentPlaces = /* @__PURE__ */ new Set();
3540
4088
  _sinkPlaces = /* @__PURE__ */ new Set();
4089
+ _conditionalSinks = [];
3541
4090
  _budgetPlaces = /* @__PURE__ */ new Set();
3542
4091
  _environmentMode = alwaysAvailable();
3543
4092
  _timeoutMs = 6e4;
3544
4093
  _certificateCheck = true;
3545
4094
  _counterexampleReplay = true;
3546
4095
  _semiflowInvariants = false;
4096
+ _stateEquation = false;
4097
+ _linearBound = true;
3547
4098
  _nuMaxClasses = 1e5;
4099
+ _enumerationMaxClasses = 5e4;
3548
4100
  _fragmentMode = "base";
3549
4101
  _carrierPlaces = /* @__PURE__ */ new Set();
3550
4102
  _prioritySemantics = "none";
@@ -3574,13 +4126,47 @@ var SmtVerifier = class _SmtVerifier {
3574
4126
  return this;
3575
4127
  }
3576
4128
  /**
3577
- * Declares expected sink (terminal) places for deadlock-freedom analysis.
3578
- * 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.
3579
4132
  */
3580
4133
  sinkPlaces(...places) {
3581
4134
  for (const p of places) this._sinkPlaces.add(p);
3582
4135
  return this;
3583
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
+ }
3584
4170
  /**
3585
4171
  * Declares ν-net budget places (NU-040): places whose token count bounds the
3586
4172
  * live correlation pool (they gate fresh-name minting). Declaring at least one
@@ -3658,10 +4244,67 @@ var SmtVerifier = class _SmtVerifier {
3658
4244
  * `Certificate check: not applicable (name-coloured encoding)`. Off by default so
3659
4245
  * reports stay byte-equal.
3660
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
+ */
3661
4263
  semiflowInvariants(enabled) {
3662
4264
  this._semiflowInvariants = enabled;
3663
4265
  return this;
3664
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
+ }
3665
4308
  /**
3666
4309
  * Sets the class-count cap for the ν-aware state-class-graph analysis (NU-050,
3667
4310
  * Route B). When the symbolic name-aware graph would exceed this, the analysis
@@ -3672,6 +4315,29 @@ var SmtVerifier = class _SmtVerifier {
3672
4315
  this._nuMaxClasses = max;
3673
4316
  return this;
3674
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
+ }
3675
4341
  /**
3676
4342
  * Selects the ν-net coloured-place fragment for Route B (NU-051). `base`
3677
4343
  * (default) admits the shipped mint → matched-join fragment only; `extended`
@@ -3754,7 +4420,8 @@ var SmtVerifier = class _SmtVerifier {
3754
4420
  this._initialMarking,
3755
4421
  this._property,
3756
4422
  invariants,
3757
- this._sinkPlaces
4423
+ this._sinkPlaces,
4424
+ this._conditionalSinks
3758
4425
  )
3759
4426
  };
3760
4427
  }
@@ -3771,35 +4438,45 @@ var SmtVerifier = class _SmtVerifier {
3771
4438
  requireOutputProducingActions(this.net);
3772
4439
  const flatNet = flatten(this.net, this._environmentPlaces, this._environmentMode);
3773
4440
  const matrix = IncidenceMatrix.from(flatNet);
3774
- const { valid: basis } = validateInvariantsExact(
4441
+ const { valid: basis, dropped: basisDropped } = validateInvariantsExact(
3775
4442
  matrix,
3776
4443
  computePInvariants(matrix, flatNet, this._initialMarking),
3777
4444
  flatNet,
3778
4445
  this._initialMarking
3779
4446
  );
3780
- 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(
3781
4450
  matrix,
3782
4451
  computePSemiflows(matrix, flatNet, this._initialMarking),
3783
4452
  flatNet,
3784
4453
  this._initialMarking
3785
- );
4454
+ ) : { valid: [] };
3786
4455
  let invariants = basis;
3787
- if (this._semiflowInvariants) invariants = strengthenWithSemiflows(basis, semiflows).invariants;
4456
+ if (this._semiflowInvariants === true || autoUnion) invariants = strengthenWithSemiflows(basis, semiflows).invariants;
3788
4457
  invariants = canonicalInvariantOrder(invariants);
3789
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;
3790
4460
  if (attempt.encoding != null) {
3791
- return { horn: attempt.encoding.smt2, certificate: null, coloured: true };
4461
+ return { horn: attempt.encoding.smt2, certificate: null, coloured: true, bound };
3792
4462
  }
3793
- 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
+ });
3794
4469
  const certificate = vcScript(
3795
- placeholderCertificate(flatNet.places.length),
4470
+ placeholderCertificate(flatNet.places.length + flat.counterCount),
3796
4471
  flatNet,
3797
4472
  this._initialMarking,
3798
4473
  this._property,
3799
4474
  this._sinkPlaces,
3800
- invariants
4475
+ invariants,
4476
+ this._conditionalSinks,
4477
+ this._stateEquation
3801
4478
  );
3802
- return { horn, certificate, coloured: false };
4479
+ return { horn: flat.smt2, certificate, coloured: false, bound };
3803
4480
  }
3804
4481
  /**
3805
4482
  * Runs the verification pipeline.
@@ -3812,10 +4489,34 @@ var SmtVerifier = class _SmtVerifier {
3812
4489
  const report = [];
3813
4490
  report.push("=== IC3/PDR SAFETY VERIFICATION ===\n");
3814
4491
  report.push(`Net: ${this.net.name}`);
3815
- 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})`;
3816
4494
  report.push(`Property: ${propDesc}`);
3817
4495
  report.push(`Timeout: ${(this._timeoutMs / 1e3).toFixed(0)}s
3818
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
+ }
3819
4520
  const hasMatch = [...this.net.transitions].some((t) => t.matchSpec !== null);
3820
4521
  const nuBounded = this._budgetPlaces.size > 0;
3821
4522
  if (hasMatch && (!isReachabilitySafety(this._property) || !nuBounded)) {
@@ -3829,7 +4530,8 @@ var SmtVerifier = class _SmtVerifier {
3829
4530
  this._nuMaxClasses,
3830
4531
  this._fragmentMode,
3831
4532
  this._carrierPlaces,
3832
- this._prioritySemantics
4533
+ this._prioritySemantics,
4534
+ this._conditionalSinks
3833
4535
  );
3834
4536
  const deferToRouteA = outcome !== null && outcome.verdict.type === "unknown" && !isReachabilitySafety(this._property) && nuBounded;
3835
4537
  if (outcome !== null && !deferToRouteA) {
@@ -3857,7 +4559,9 @@ var SmtVerifier = class _SmtVerifier {
3857
4559
  transitions: [...this.net.transitions].length,
3858
4560
  invariantsFound: 0,
3859
4561
  structuralResult: "n/a (\u03BD name-partition SCG)"
3860
- }
4562
+ },
4563
+ null,
4564
+ "nu-scg"
3861
4565
  );
3862
4566
  } else if (deferToRouteA) {
3863
4567
  report.push(
@@ -3870,6 +4574,47 @@ var SmtVerifier = class _SmtVerifier {
3870
4574
  );
3871
4575
  }
3872
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
+ }
3873
4618
  report.push("Phase 1: Flattening net...");
3874
4619
  const flatNet = flatten(this.net, this._environmentPlaces, this._environmentMode);
3875
4620
  report.push(` Places: ${flatNet.places.length}`);
@@ -3894,7 +4639,7 @@ var SmtVerifier = class _SmtVerifier {
3894
4639
  }
3895
4640
  report.push(` Result: ${structResultStr}
3896
4641
  `);
3897
- 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) {
3898
4643
  report.push("=== RESULT ===\n");
3899
4644
  report.push("PROVEN (structural): Deadlock-freedom verified by Commoner's theorem.");
3900
4645
  report.push(" All siphons contain initially marked traps.");
@@ -3907,7 +4652,9 @@ var SmtVerifier = class _SmtVerifier {
3907
4652
  [],
3908
4653
  [],
3909
4654
  performance.now() - start,
3910
- { 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"
3911
4658
  );
3912
4659
  }
3913
4660
  report.push("Phase 3: Computing P-invariants...");
@@ -3918,15 +4665,21 @@ var SmtVerifier = class _SmtVerifier {
3918
4665
  flatNet,
3919
4666
  this._initialMarking
3920
4667
  );
3921
- 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(
3922
4671
  matrix,
3923
4672
  computePSemiflows(matrix, flatNet, this._initialMarking),
3924
4673
  flatNet,
3925
4674
  this._initialMarking
3926
- );
4675
+ ) : { valid: [], dropped: [] };
3927
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;
3928
4681
  let invariants = basisInvariants;
3929
- if (this._semiflowInvariants) {
4682
+ if (unionWanted) {
3930
4683
  const { invariants: strengthened, added } = strengthenWithSemiflows(basisInvariants, semiflows);
3931
4684
  invariants = strengthened;
3932
4685
  report.push(` Semiflows encoded as invariants: ${added}`);
@@ -3950,6 +4703,11 @@ var SmtVerifier = class _SmtVerifier {
3950
4703
  report.push(` Dropped: ${droppedSemiflows.length} semiflow(s) failed the exact re-check`);
3951
4704
  }
3952
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
+ }
3953
4711
  report.push("Phase 4: IC3/PDR verification via Z3 Spacer...");
3954
4712
  const stats = {
3955
4713
  places: flatNet.places.length,
@@ -3961,6 +4719,7 @@ var SmtVerifier = class _SmtVerifier {
3961
4719
  try {
3962
4720
  solver = resolveZ3();
3963
4721
  } catch (e) {
4722
+ rethrowIfProgrammingError(e);
3964
4723
  const reason = e instanceof Z3Unavailable ? e.message : String(e?.message ?? e);
3965
4724
  report.push(` Solver: z3 unavailable (${reason})`);
3966
4725
  report.push(` Status: UNKNOWN (${reason})
@@ -3968,11 +4727,35 @@ var SmtVerifier = class _SmtVerifier {
3968
4727
  report.push("=== RESULT ===\n");
3969
4728
  report.push(`UNKNOWN: Could not determine ${propDesc}`);
3970
4729
  report.push(` Reason: ${reason}`);
3971
- 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");
3972
4731
  }
3973
4732
  report.push(` Solver: z3 ${formatZ3Version(solver.version)}`);
3974
4733
  const colouredAttempt = this.colouredAttempt(flatNet, invariants, semiflows);
3975
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
+ }
3976
4759
  let encoding;
3977
4760
  if (colouredPlan != null) {
3978
4761
  report.push(
@@ -3984,7 +4767,7 @@ var SmtVerifier = class _SmtVerifier {
3984
4767
  report.push(" Status: UNKNOWN (unresolved property place)\n");
3985
4768
  report.push("=== RESULT ===\n");
3986
4769
  report.push(`UNKNOWN: ${reason}`);
3987
- 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");
3988
4771
  }
3989
4772
  encoding = coloured;
3990
4773
  } else {
@@ -3994,9 +4777,20 @@ var SmtVerifier = class _SmtVerifier {
3994
4777
  report.push(" Status: UNKNOWN (unresolved property place)\n");
3995
4778
  report.push("=== RESULT ===\n");
3996
4779
  report.push(`UNKNOWN: ${reason}`);
3997
- 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");
3998
4781
  }
3999
- 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)");
4000
4794
  }
4001
4795
  const queryResult = await runZ3Spacer(
4002
4796
  solver,
@@ -4028,7 +4822,9 @@ var SmtVerifier = class _SmtVerifier {
4028
4822
  invariants,
4029
4823
  this._sinkPlaces,
4030
4824
  solver,
4031
- this._timeoutMs
4825
+ this._timeoutMs,
4826
+ this._conditionalSinks,
4827
+ this._stateEquation
4032
4828
  );
4033
4829
  const reason = certificateDowngradeReason(certificate);
4034
4830
  if (reason != null) {
@@ -4072,7 +4868,7 @@ var SmtVerifier = class _SmtVerifier {
4072
4868
  }
4073
4869
  case "violated": {
4074
4870
  report.push(" Status: SAT (counterexample found)\n");
4075
- const decoded = decode(queryResult.answer, flatNet);
4871
+ const decoded = decode(queryResult.answer, flatNet, encoding.counterCount);
4076
4872
  if (decoded.note != null) report.push(` Counterexample decoding: ${decoded.note}`);
4077
4873
  let confirmed = null;
4078
4874
  let trace = [...decoded.states];
@@ -4084,7 +4880,8 @@ var SmtVerifier = class _SmtVerifier {
4084
4880
  this._initialMarking,
4085
4881
  decoded.states,
4086
4882
  this._property,
4087
- this._sinkPlaces
4883
+ this._sinkPlaces,
4884
+ this._conditionalSinks
4088
4885
  );
4089
4886
  if (assessment.kind === "confirmed") {
4090
4887
  confirmed = true;
@@ -4157,6 +4954,49 @@ var SmtVerifier = class _SmtVerifier {
4157
4954
  }
4158
4955
  }
4159
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
+ }
4160
5000
  /**
4161
5001
  * ν-net soundness guard (NU-040, NU-050). Applied only when the net contains
4162
5002
  * match (ν-join) transitions, and only to a proven/violated verdict (an
@@ -4206,11 +5046,12 @@ function isReachabilitySafety(property) {
4206
5046
  case "unreachable":
4207
5047
  return true;
4208
5048
  case "deadlock-free":
5049
+ case "terminates-at-sink":
4209
5050
  case "joined-or-dead-lettered":
4210
5051
  return false;
4211
5052
  }
4212
5053
  }
4213
- function assessCounterexample(flatNet, initialMarking, decodedStates, property, sinkPlaces) {
5054
+ function assessCounterexample(flatNet, initialMarking, decodedStates, property, sinkPlaces, conditionalSinks = []) {
4214
5055
  if (decodedStates.size === 0) {
4215
5056
  return {
4216
5057
  kind: "unconfirmed",
@@ -4224,9 +5065,12 @@ function assessCounterexample(flatNet, initialMarking, decodedStates, property,
4224
5065
  vectorize(initialMarking, flatNet),
4225
5066
  [...decodedStates].map((m) => vectorize(m, flatNet)),
4226
5067
  property,
4227
- sinkPlaces
5068
+ sinkPlaces,
5069
+ {},
5070
+ conditionalSinks
4228
5071
  );
4229
5072
  } catch (e) {
5073
+ rethrowIfProgrammingError(e);
4230
5074
  outcome = { kind: "exhausted", reason: `replay threw: ${e?.message ?? e}`, nodesExplored: 0 };
4231
5075
  }
4232
5076
  switch (outcome.kind) {
@@ -4277,11 +5121,47 @@ Downgraded to UNKNOWN: ${reason}
4277
5121
  function truncate(s, max) {
4278
5122
  return s.length <= max ? s : `${s.slice(0, max)}\u2026 (${s.length - max} chars truncated)`;
4279
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
+ }
4280
5158
  function unresolvedPropertyPlace(flatNet, property) {
4281
5159
  const named = (() => {
4282
5160
  switch (property.type) {
4283
5161
  case "deadlock-free":
4284
5162
  return [];
5163
+ case "terminates-at-sink":
5164
+ return [];
4285
5165
  case "mutual-exclusion":
4286
5166
  return [property.p1, property.p2];
4287
5167
  case "place-bound":
@@ -4310,8 +5190,8 @@ function formatInvariant(inv, flatNet) {
4310
5190
  }
4311
5191
  return `${parts.length === 0 ? "0" : parts.join(" + ")} = ${inv.constant}`;
4312
5192
  }
4313
- function buildResult(verdict, report, invariants, discoveredInvariants, trace, transitions, elapsedMs, statistics, counterexampleConfirmed = null) {
4314
- 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 };
4315
5195
  }
4316
5196
 
4317
5197
  // src/verification/smt-verification-result.ts
@@ -4347,15 +5227,20 @@ export {
4347
5227
  transformAsync,
4348
5228
  produce,
4349
5229
  withTimeout,
5230
+ rethrowIfProgrammingError,
4350
5231
  MarkingState,
4351
5232
  MarkingStateBuilder,
4352
5233
  deadlockFree,
5234
+ terminatesAtSink,
4353
5235
  mutualExclusion,
4354
5236
  placeBound,
4355
5237
  unreachable,
4356
5238
  branchPlaceBound,
4357
5239
  joinedOrDeadLettered,
4358
5240
  propertyDescription,
5241
+ strandingExcuses,
5242
+ strandsToken,
5243
+ describeSinks,
4359
5244
  flatTransition,
4360
5245
  alwaysAvailable,
4361
5246
  bounded,
@@ -4385,13 +5270,24 @@ export {
4385
5270
  runZ3Text,
4386
5271
  runZ3Spacer,
4387
5272
  encode,
5273
+ encodeNet,
4388
5274
  encodeStepRelationSmt2,
4389
5275
  checkCertificate,
4390
5276
  vcScript,
5277
+ violationDemand,
5278
+ encodeLinearBound,
5279
+ decodeLinearBound,
5280
+ checkLinearBoundExact,
5281
+ formatLinearBound,
5282
+ formatLinearDemand,
5283
+ decideOverClasses,
4391
5284
  DBM,
4392
5285
  StateClass,
4393
5286
  requireOutputProducingActions,
4394
5287
  StateClassGraph,
5288
+ isUntimed,
5289
+ NOTE_ENUMERATED,
5290
+ verifyViaStateClassGraph,
4395
5291
  decode,
4396
5292
  decodeStateSet,
4397
5293
  flatNetPlaceCount,
@@ -4403,4 +5299,4 @@ export {
4403
5299
  isProven,
4404
5300
  isViolated
4405
5301
  };
4406
- //# sourceMappingURL=chunk-FD3S4TZM.js.map
5302
+ //# sourceMappingURL=chunk-EL4E6LVO.js.map