@fairfox/polly 0.16.0 → 0.18.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.
@@ -13,6 +13,48 @@ var __export = (target, all) => {
13
13
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
14
14
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
15
15
 
16
+ // tools/verify/src/analysis/non-interference.ts
17
+ var exports_non_interference = {};
18
+ __export(exports_non_interference, {
19
+ checkNonInterference: () => checkNonInterference
20
+ });
21
+ function checkNonInterference(subsystems, handlers) {
22
+ const violations = [];
23
+ const fieldOwner = new Map;
24
+ for (const [name, sub] of Object.entries(subsystems)) {
25
+ for (const field of sub.state) {
26
+ fieldOwner.set(field, name);
27
+ }
28
+ }
29
+ const handlerSubsystem = new Map;
30
+ for (const [name, sub] of Object.entries(subsystems)) {
31
+ for (const h of sub.handlers) {
32
+ handlerSubsystem.set(h, name);
33
+ }
34
+ }
35
+ for (const handler of handlers) {
36
+ const subsystemName = handlerSubsystem.get(handler.messageType);
37
+ if (!subsystemName)
38
+ continue;
39
+ for (const assignment of handler.assignments) {
40
+ const fieldName = assignment.field;
41
+ const owner = fieldOwner.get(fieldName);
42
+ if (owner && owner !== subsystemName) {
43
+ violations.push({
44
+ handler: handler.messageType,
45
+ subsystem: subsystemName,
46
+ writesTo: fieldName,
47
+ ownedBy: owner
48
+ });
49
+ }
50
+ }
51
+ }
52
+ return {
53
+ valid: violations.length === 0,
54
+ violations
55
+ };
56
+ }
57
+
16
58
  // tools/verify/src/codegen/invariants.ts
17
59
  import { Node as Node3, Project as Project3 } from "ts-morph";
18
60
 
@@ -341,6 +383,7 @@ class TemporalTLAGenerator {
341
383
  var exports_tla = {};
342
384
  __export(exports_tla, {
343
385
  generateTLA: () => generateTLA,
386
+ generateSubsystemTLA: () => generateSubsystemTLA,
344
387
  TLAValidationError: () => TLAValidationError,
345
388
  TLAGenerator: () => TLAGenerator
346
389
  });
@@ -351,6 +394,49 @@ async function generateTLA(config, analysis) {
351
394
  const generator = new TLAGenerator;
352
395
  return await generator.generate(config, analysis);
353
396
  }
397
+ async function generateSubsystemTLA(_subsystemName, subsystem, config, analysis) {
398
+ const stateFields = new Set(subsystem.state);
399
+ const handlerNames = new Set(subsystem.handlers);
400
+ const filteredState = {};
401
+ for (const [field, fieldConfig] of Object.entries(config.state)) {
402
+ if (stateFields.has(field)) {
403
+ filteredState[field] = fieldConfig;
404
+ }
405
+ }
406
+ const filteredMessages = {
407
+ ...config.messages,
408
+ include: subsystem.handlers
409
+ };
410
+ filteredMessages.exclude = undefined;
411
+ if (filteredMessages.perMessageBounds) {
412
+ const filteredBounds = {};
413
+ for (const [msg, bound] of Object.entries(filteredMessages.perMessageBounds)) {
414
+ if (handlerNames.has(msg)) {
415
+ filteredBounds[msg] = bound;
416
+ }
417
+ }
418
+ filteredMessages.perMessageBounds = filteredBounds;
419
+ }
420
+ const filteredConfig = {
421
+ ...config,
422
+ state: filteredState,
423
+ messages: filteredMessages,
424
+ subsystems: undefined
425
+ };
426
+ if (filteredConfig.tier2?.temporalConstraints) {
427
+ filteredConfig.tier2 = {
428
+ ...filteredConfig.tier2,
429
+ temporalConstraints: filteredConfig.tier2.temporalConstraints.filter((tc) => handlerNames.has(tc.before) && handlerNames.has(tc.after))
430
+ };
431
+ }
432
+ const filteredAnalysis = {
433
+ ...analysis,
434
+ messageTypes: analysis.messageTypes.filter((mt) => handlerNames.has(mt)),
435
+ handlers: analysis.handlers.filter((h) => handlerNames.has(h.messageType))
436
+ };
437
+ const generator = new TLAGenerator;
438
+ return await generator.generate(filteredConfig, filteredAnalysis);
439
+ }
354
440
  var TLAValidationError, TLAGenerator;
355
441
  var init_tla = __esm(() => {
356
442
  init_invariants();
@@ -1832,7 +1918,7 @@ var init_tla = __esm(() => {
1832
1918
  this.addTemporalConstraints(config.tier2.temporalConstraints);
1833
1919
  }
1834
1920
  this.line("\\* State constraint to bound state space");
1835
- this.addStateConstraint(config);
1921
+ this.addStateConstraint(config, _analysis);
1836
1922
  this.line("=============================================================================");
1837
1923
  }
1838
1924
  addTemporalConstraints(constraints) {
@@ -1862,10 +1948,11 @@ var init_tla = __esm(() => {
1862
1948
  });
1863
1949
  }
1864
1950
  }
1865
- addStateConstraint(config) {
1951
+ addStateConstraint(config, analysis) {
1866
1952
  const hasPerMessageBounds = config.messages.perMessageBounds && Object.keys(config.messages.perMessageBounds).length > 0;
1867
1953
  const hasBoundedExploration = config.tier2?.boundedExploration?.maxDepth !== undefined;
1868
- const needsConjunction = hasPerMessageBounds || hasBoundedExploration;
1954
+ const hasUserConstraints = (analysis.globalStateConstraints?.length ?? 0) > 0;
1955
+ const needsConjunction = hasPerMessageBounds || hasBoundedExploration || hasUserConstraints;
1869
1956
  this.line("StateConstraint ==");
1870
1957
  this.indent++;
1871
1958
  if (needsConjunction) {
@@ -1879,6 +1966,13 @@ var init_tla = __esm(() => {
1879
1966
  if (hasBoundedExploration && config.tier2?.boundedExploration?.maxDepth) {
1880
1967
  this.line(`/\\ TLCGet("level") <= ${config.tier2.boundedExploration.maxDepth} \\* Tier 2: Bounded exploration`);
1881
1968
  }
1969
+ if (hasUserConstraints && analysis.globalStateConstraints) {
1970
+ for (const constraint of analysis.globalStateConstraints) {
1971
+ const tlaExpr = this.tsExpressionToTLA(constraint.expression, false);
1972
+ this.line(`\\* ${constraint.name}${constraint.message ? `: ${constraint.message}` : ""}`);
1973
+ this.line(`/\\ \\A ctx \\in Contexts : (${tlaExpr})`);
1974
+ }
1975
+ }
1882
1976
  } else {
1883
1977
  this.line("Len(messages) <= MaxMessages");
1884
1978
  }
@@ -1887,6 +1981,9 @@ var init_tla = __esm(() => {
1887
1981
  if (hasBoundedExploration && config.tier2?.boundedExploration?.maxDepth) {
1888
1982
  console.log(`[INFO] [TLAGenerator] Tier 2: Bounded exploration with maxDepth = ${config.tier2.boundedExploration.maxDepth}`);
1889
1983
  }
1984
+ if (hasUserConstraints && analysis.globalStateConstraints) {
1985
+ console.log(`[INFO] [TLAGenerator] ${analysis.globalStateConstraints.length} user-defined state constraint(s) added to CONSTRAINT clause`);
1986
+ }
1890
1987
  }
1891
1988
  addDeliveredTracking() {
1892
1989
  this.line("");
@@ -2366,6 +2463,400 @@ var init_docker = () => {};
2366
2463
  import * as fs4 from "node:fs";
2367
2464
  import * as path4 from "node:path";
2368
2465
 
2466
+ // tools/verify/src/analysis/expression-validator.ts
2467
+ var SIGNAL_VALUE_FIELD = /([a-zA-Z_]\w*)\.value\.([a-zA-Z_][\w.]*)/g;
2468
+ var SIGNAL_VALUE_BARE = /([a-zA-Z_]\w*)\.value\b(?!\.)/g;
2469
+ var STATE_DOT_FIELD = /state\.([a-zA-Z_][\w.]*)/g;
2470
+ var PAYLOAD_REF = /payload\.\w+/;
2471
+ function extractFieldRefs(expression) {
2472
+ const refs = [];
2473
+ for (const m of expression.matchAll(SIGNAL_VALUE_FIELD)) {
2474
+ const prefix = m[1];
2475
+ const suffix = m[2];
2476
+ refs.push(`${prefix}.${suffix}`);
2477
+ }
2478
+ for (const m of expression.matchAll(SIGNAL_VALUE_BARE)) {
2479
+ refs.push(m[1]);
2480
+ }
2481
+ for (const m of expression.matchAll(STATE_DOT_FIELD)) {
2482
+ refs.push(m[1]);
2483
+ }
2484
+ return [...new Set(refs)];
2485
+ }
2486
+ function fieldInConfig(fieldRef, configKeys, stateConfig) {
2487
+ if (configKeys.has(fieldRef))
2488
+ return true;
2489
+ const underscored = fieldRef.replace(/\./g, "_");
2490
+ if (configKeys.has(underscored))
2491
+ return true;
2492
+ const dotted = fieldRef.replace(/_/g, ".");
2493
+ if (configKeys.has(dotted))
2494
+ return true;
2495
+ if (stateConfig && fieldRef.endsWith(".length")) {
2496
+ const parent = fieldRef.slice(0, -".length".length);
2497
+ const parentEntry = resolveConfigEntry(parent, stateConfig);
2498
+ if (parentEntry !== undefined && isArrayLike(parentEntry))
2499
+ return true;
2500
+ }
2501
+ return false;
2502
+ }
2503
+ function resolveConfigEntry(fieldRef, stateConfig) {
2504
+ if (fieldRef in stateConfig)
2505
+ return stateConfig[fieldRef];
2506
+ const underscored = fieldRef.replace(/\./g, "_");
2507
+ if (underscored in stateConfig)
2508
+ return stateConfig[underscored];
2509
+ const dotted = fieldRef.replace(/_/g, ".");
2510
+ if (dotted in stateConfig)
2511
+ return stateConfig[dotted];
2512
+ return;
2513
+ }
2514
+ function isArrayLike(configEntry) {
2515
+ if (configEntry && typeof configEntry === "object") {
2516
+ const obj = configEntry;
2517
+ if (obj.type === "array")
2518
+ return true;
2519
+ if ("maxLength" in obj)
2520
+ return true;
2521
+ if ("maxSize" in obj)
2522
+ return true;
2523
+ }
2524
+ return false;
2525
+ }
2526
+ function isNullable(configEntry) {
2527
+ if (Array.isArray(configEntry)) {
2528
+ return configEntry.includes(null);
2529
+ }
2530
+ if (configEntry && typeof configEntry === "object") {
2531
+ const obj = configEntry;
2532
+ if (obj.abstract === true)
2533
+ return true;
2534
+ if (Array.isArray(obj.values)) {
2535
+ return obj.values.includes(null);
2536
+ }
2537
+ }
2538
+ return false;
2539
+ }
2540
+ var UNSUPPORTED_METHODS = /\.(some|every|find|filter)\s*\(/;
2541
+ var OPTIONAL_CHAIN = /\?\./;
2542
+ var NULL_COMPARISON = /(===?\s*null|!==?\s*null|null\s*===?|null\s*!==?)/;
2543
+ function checkUnmodeledFields(expression, configKeys, stateConfig, messageType, conditionType, location) {
2544
+ const warnings = [];
2545
+ const refs = extractFieldRefs(expression);
2546
+ for (const ref of refs) {
2547
+ if (!fieldInConfig(ref, configKeys, stateConfig)) {
2548
+ warnings.push({
2549
+ kind: "unmodeled_field",
2550
+ message: `${conditionType}() in ${messageType} references unmodeled field '${ref}'`,
2551
+ messageType,
2552
+ conditionType,
2553
+ expression,
2554
+ location,
2555
+ suggestion: `Add '${ref}' to state config or remove from ${conditionType}()`
2556
+ });
2557
+ }
2558
+ }
2559
+ return warnings;
2560
+ }
2561
+ function checkUnsupportedMethods(expression, configKeys, stateConfig, messageType, conditionType, location) {
2562
+ const match = expression.match(UNSUPPORTED_METHODS);
2563
+ if (!match)
2564
+ return [];
2565
+ const refs = extractFieldRefs(expression);
2566
+ const allUnmodeled = refs.length > 0 && refs.every((r) => !fieldInConfig(r, configKeys, stateConfig));
2567
+ if (allUnmodeled)
2568
+ return [];
2569
+ return [
2570
+ {
2571
+ kind: "unsupported_method",
2572
+ message: `${conditionType}() in ${messageType} uses .${match[1]}() which cannot be translated to TLA+`,
2573
+ messageType,
2574
+ conditionType,
2575
+ expression,
2576
+ location,
2577
+ suggestion: `Rewrite without .${match[1]}() or model the check as a boolean state field`
2578
+ }
2579
+ ];
2580
+ }
2581
+ function checkOptionalChaining(expression, messageType, conditionType, location) {
2582
+ if (!OPTIONAL_CHAIN.test(expression))
2583
+ return [];
2584
+ return [
2585
+ {
2586
+ kind: "optional_chaining",
2587
+ message: `${conditionType}() in ${messageType} uses optional chaining (?.)`,
2588
+ messageType,
2589
+ conditionType,
2590
+ expression,
2591
+ location,
2592
+ suggestion: "Optional chaining translation is fragile — consider explicit null checks or restructuring"
2593
+ }
2594
+ ];
2595
+ }
2596
+ function checkNullComparisons(expression, stateConfig, configKeys, messageType, conditionType, location) {
2597
+ if (!NULL_COMPARISON.test(expression))
2598
+ return [];
2599
+ const warnings = [];
2600
+ const refs = extractFieldRefs(expression);
2601
+ for (const ref of refs) {
2602
+ if (!fieldInConfig(ref, configKeys, stateConfig))
2603
+ continue;
2604
+ const entry = resolveConfigEntry(ref, stateConfig);
2605
+ if (entry !== undefined && !isNullable(entry)) {
2606
+ warnings.push({
2607
+ kind: "null_comparison",
2608
+ message: `${conditionType}() in ${messageType} compares '${ref}' to null but field is not nullable`,
2609
+ messageType,
2610
+ conditionType,
2611
+ expression,
2612
+ location,
2613
+ suggestion: `Add null to '${ref}' values in state config, or remove the null check`
2614
+ });
2615
+ }
2616
+ }
2617
+ return warnings;
2618
+ }
2619
+ var WEAK_NEGATION = /([a-zA-Z_][\w.]*(?:\.value\.[\w.]+|\.value\b|))?\s*!==?\s*("[^"]*"|'[^']*'|\d+|true|false|null)/;
2620
+ function checkWeakPostconditions(expression, handler, messageType, conditionType, location) {
2621
+ if (conditionType !== "ensures")
2622
+ return [];
2623
+ const match = expression.match(WEAK_NEGATION);
2624
+ if (!match)
2625
+ return [];
2626
+ const refs = extractFieldRefs(expression);
2627
+ if (refs.length === 0)
2628
+ return [];
2629
+ for (const ref of refs) {
2630
+ const underscoredRef = ref.replace(/\./g, "_");
2631
+ const hasAssignment = handler.assignments.some((a) => a.field === ref || a.field === underscoredRef || a.field.replace(/_/g, ".") === ref);
2632
+ if (hasAssignment) {
2633
+ const assignment = handler.assignments.find((a) => a.field === ref || a.field === underscoredRef || a.field.replace(/_/g, ".") === ref);
2634
+ const assignedValue = assignment ? JSON.stringify(assignment.value) : "<value>";
2635
+ return [
2636
+ {
2637
+ kind: "weak_postcondition",
2638
+ message: `ensures() in ${messageType} uses !== for '${ref}' which has a concrete assignment`,
2639
+ messageType,
2640
+ conditionType,
2641
+ expression,
2642
+ location,
2643
+ suggestion: `Consider ensures(${ref} === ${assignedValue}) for a stronger postcondition`
2644
+ }
2645
+ ];
2646
+ }
2647
+ }
2648
+ return [];
2649
+ }
2650
+ function validateExpressions(handlers, stateConfig) {
2651
+ const warnings = [];
2652
+ let validCount = 0;
2653
+ const configKeys = new Set(Object.keys(stateConfig));
2654
+ for (const handler of handlers) {
2655
+ const conditions = [
2656
+ ...handler.preconditions.map((c) => ({
2657
+ cond: c,
2658
+ type: "requires"
2659
+ })),
2660
+ ...handler.postconditions.map((c) => ({
2661
+ cond: c,
2662
+ type: "ensures"
2663
+ }))
2664
+ ];
2665
+ for (const { cond, type } of conditions) {
2666
+ const refs = extractFieldRefs(cond.expression);
2667
+ const isPayloadOnly = refs.length === 0 && PAYLOAD_REF.test(cond.expression);
2668
+ const loc = {
2669
+ file: handler.location.file,
2670
+ line: cond.location.line,
2671
+ column: cond.location.column
2672
+ };
2673
+ const condWarnings = [];
2674
+ if (!isPayloadOnly) {
2675
+ condWarnings.push(...checkUnmodeledFields(cond.expression, configKeys, stateConfig, handler.messageType, type, loc));
2676
+ }
2677
+ condWarnings.push(...checkUnsupportedMethods(cond.expression, configKeys, stateConfig, handler.messageType, type, loc));
2678
+ condWarnings.push(...checkOptionalChaining(cond.expression, handler.messageType, type, loc));
2679
+ condWarnings.push(...checkNullComparisons(cond.expression, stateConfig, configKeys, handler.messageType, type, loc));
2680
+ condWarnings.push(...checkWeakPostconditions(cond.expression, handler, handler.messageType, type, loc));
2681
+ if (condWarnings.length === 0) {
2682
+ validCount++;
2683
+ } else {
2684
+ warnings.push(...condWarnings);
2685
+ }
2686
+ }
2687
+ }
2688
+ return { warnings, validCount, warnCount: warnings.length };
2689
+ }
2690
+
2691
+ // tools/verify/src/config/types.ts
2692
+ function isAdapterConfig(config) {
2693
+ return "adapter" in config;
2694
+ }
2695
+ function isLegacyConfig(config) {
2696
+ return "messages" in config && !("adapter" in config);
2697
+ }
2698
+
2699
+ // tools/verify/src/analysis/state-space-estimator.ts
2700
+ function typedFieldCardinality(name, obj) {
2701
+ if (!("type" in obj))
2702
+ return null;
2703
+ switch (obj.type) {
2704
+ case "boolean":
2705
+ return { name, cardinality: 2, kind: "boolean" };
2706
+ case "enum":
2707
+ if (Array.isArray(obj.values)) {
2708
+ return { name, cardinality: obj.values.length, kind: "enum" };
2709
+ }
2710
+ return { name, cardinality: "unbounded", kind: "enum" };
2711
+ case "number":
2712
+ if (typeof obj.min === "number" && typeof obj.max === "number") {
2713
+ return { name, cardinality: obj.max - obj.min + 1, kind: "number" };
2714
+ }
2715
+ return { name, cardinality: "unbounded", kind: "number" };
2716
+ case "array":
2717
+ return { name, cardinality: "unbounded", kind: "array" };
2718
+ case "string":
2719
+ return { name, cardinality: "unbounded", kind: "string" };
2720
+ default:
2721
+ return null;
2722
+ }
2723
+ }
2724
+ function legacyFieldCardinality(name, obj) {
2725
+ if ("values" in obj && Array.isArray(obj.values)) {
2726
+ const base = obj.values.length;
2727
+ const extra = obj.abstract === true ? 1 : 0;
2728
+ return {
2729
+ name,
2730
+ cardinality: base + extra,
2731
+ kind: obj.abstract ? "enum (abstract)" : "enum (values)"
2732
+ };
2733
+ }
2734
+ if ("maxLength" in obj && !("type" in obj)) {
2735
+ return { name, cardinality: "unbounded", kind: "array" };
2736
+ }
2737
+ if ("min" in obj && "max" in obj && typeof obj.min === "number" && typeof obj.max === "number" && !("type" in obj)) {
2738
+ return { name, cardinality: obj.max - obj.min + 1, kind: "number" };
2739
+ }
2740
+ return null;
2741
+ }
2742
+ function fieldCardinality(name, value) {
2743
+ if (Array.isArray(value)) {
2744
+ return { name, cardinality: value.length, kind: "enum (literal)" };
2745
+ }
2746
+ if (typeof value !== "object" || value === null) {
2747
+ return { name, cardinality: "unbounded", kind: "unknown" };
2748
+ }
2749
+ const obj = value;
2750
+ return typedFieldCardinality(name, obj) ?? legacyFieldCardinality(name, obj) ?? { name, cardinality: "unbounded", kind: "unknown" };
2751
+ }
2752
+ function permutations(n, k) {
2753
+ if (k > n)
2754
+ return 1;
2755
+ let result = 1;
2756
+ for (let i = 0;i < k; i++) {
2757
+ result *= n - i;
2758
+ }
2759
+ return result;
2760
+ }
2761
+ function getHandlerCount(config, analysis) {
2762
+ if (isLegacyConfig(config)) {
2763
+ const msgs = config.messages;
2764
+ if (msgs.include) {
2765
+ return msgs.include.length;
2766
+ }
2767
+ if (msgs.exclude) {
2768
+ return Math.max(0, analysis.handlers.length - msgs.exclude.length);
2769
+ }
2770
+ }
2771
+ return analysis.handlers.length;
2772
+ }
2773
+ function getMaxInFlight(config) {
2774
+ if (isLegacyConfig(config)) {
2775
+ return config.messages.maxInFlight ?? 1;
2776
+ }
2777
+ if (isAdapterConfig(config)) {
2778
+ return config.bounds?.maxInFlight ?? 1;
2779
+ }
2780
+ return 1;
2781
+ }
2782
+ function getMaxTabs(config) {
2783
+ if (isLegacyConfig(config)) {
2784
+ return config.messages.maxTabs ?? 1;
2785
+ }
2786
+ return 1;
2787
+ }
2788
+ function getFeasibility(states) {
2789
+ if (states < 1e5)
2790
+ return "trivial";
2791
+ if (states <= 1e6)
2792
+ return "feasible";
2793
+ if (states <= 1e7)
2794
+ return "slow";
2795
+ return "infeasible";
2796
+ }
2797
+ function feasibilityLabel(f) {
2798
+ switch (f) {
2799
+ case "trivial":
2800
+ return "trivial (should complete in seconds)";
2801
+ case "feasible":
2802
+ return "feasible (should complete in minutes)";
2803
+ case "slow":
2804
+ return "slow (may take 10–30 minutes)";
2805
+ case "infeasible":
2806
+ return "infeasible (likely won't terminate)";
2807
+ }
2808
+ }
2809
+ function estimateStateSpace(config, analysis) {
2810
+ const state = config.state;
2811
+ const fields = [];
2812
+ const warnings = [];
2813
+ const suggestions = [];
2814
+ for (const [name, value] of Object.entries(state)) {
2815
+ fields.push(fieldCardinality(name, value));
2816
+ }
2817
+ const unboundedFields = fields.filter((f) => f.cardinality === "unbounded");
2818
+ const boundedFields = fields.filter((f) => f.cardinality !== "unbounded");
2819
+ const fieldProduct = boundedFields.length > 0 ? boundedFields.reduce((acc, f) => acc * f.cardinality, 1) : 1;
2820
+ if (unboundedFields.length > 0) {
2821
+ warnings.push(`${unboundedFields.length} field(s) have unbounded domains: ${unboundedFields.map((f) => f.name).join(", ")}`);
2822
+ suggestions.push(`Fields ${unboundedFields.map((f) => f.name).join(", ")} have unbounded domains — their actual impact depends on handler logic`);
2823
+ }
2824
+ const handlerCount = getHandlerCount(config, analysis);
2825
+ const maxInFlight = getMaxInFlight(config);
2826
+ const maxTabs = getMaxTabs(config);
2827
+ const contextCount = maxTabs + 1;
2828
+ const totalStateSpace = fieldProduct ** contextCount;
2829
+ const interleavingFactor = permutations(handlerCount, maxInFlight);
2830
+ const estimatedStates = totalStateSpace * interleavingFactor;
2831
+ const feasibility = getFeasibility(estimatedStates);
2832
+ if (handlerCount > 15) {
2833
+ suggestions.push("Consider splitting into subsystems");
2834
+ }
2835
+ for (const f of boundedFields) {
2836
+ if (f.cardinality > 50) {
2837
+ suggestions.push(`Consider reducing bounds for field "${f.name}" (${f.cardinality} values)`);
2838
+ }
2839
+ }
2840
+ if (maxInFlight > 2) {
2841
+ const reducedInterleaving = permutations(handlerCount, 2);
2842
+ const reduction = reducedInterleaving > 0 ? Math.round(interleavingFactor / reducedInterleaving) : 1;
2843
+ suggestions.push(`Reducing maxInFlight from ${maxInFlight} to 2 would reduce interleaving by ~${reduction}x`);
2844
+ }
2845
+ return {
2846
+ fields,
2847
+ fieldProduct,
2848
+ handlerCount,
2849
+ maxInFlight,
2850
+ contextCount,
2851
+ totalStateSpace,
2852
+ interleavingFactor,
2853
+ estimatedStates,
2854
+ feasibility,
2855
+ warnings,
2856
+ suggestions
2857
+ };
2858
+ }
2859
+
2369
2860
  // tools/verify/src/codegen/config.ts
2370
2861
  class ConfigGenerator {
2371
2862
  lines = [];
@@ -2930,6 +3421,9 @@ class ConfigValidator {
2930
3421
  if (config.tier2) {
2931
3422
  this.validateTier2Optimizations(config.tier2);
2932
3423
  }
3424
+ if (config.subsystems) {
3425
+ this.validateSubsystems(config.subsystems, config.state);
3426
+ }
2933
3427
  }
2934
3428
  findNullPlaceholders(obj, path2) {
2935
3429
  if (obj === null || obj === undefined) {
@@ -3246,6 +3740,68 @@ class ConfigValidator {
3246
3740
  }
3247
3741
  }
3248
3742
  }
3743
+ validateSubsystems(subsystems, stateConfig) {
3744
+ const stateFieldNames = Object.keys(stateConfig);
3745
+ const allAssignedHandlers = new Map;
3746
+ const allAssignedFields = new Map;
3747
+ for (const [subsystemName, subsystem] of Object.entries(subsystems)) {
3748
+ for (const field of subsystem.state) {
3749
+ if (!stateFieldNames.includes(field)) {
3750
+ this.issues.push({
3751
+ type: "invalid_value",
3752
+ severity: "error",
3753
+ field: `subsystems.${subsystemName}.state`,
3754
+ message: `State field "${field}" does not exist in top-level state config`,
3755
+ suggestion: `Available fields: ${stateFieldNames.join(", ")}`
3756
+ });
3757
+ }
3758
+ const existingOwner = allAssignedFields.get(field);
3759
+ if (existingOwner) {
3760
+ this.issues.push({
3761
+ type: "invalid_value",
3762
+ severity: "warning",
3763
+ field: `subsystems.${subsystemName}.state`,
3764
+ message: `State field "${field}" is assigned to both "${existingOwner}" and "${subsystemName}"`,
3765
+ suggestion: "State fields should be partitioned across subsystems for non-interference"
3766
+ });
3767
+ } else {
3768
+ allAssignedFields.set(field, subsystemName);
3769
+ }
3770
+ }
3771
+ if (subsystem.handlers.length === 0) {
3772
+ this.issues.push({
3773
+ type: "invalid_value",
3774
+ severity: "warning",
3775
+ field: `subsystems.${subsystemName}.handlers`,
3776
+ message: `Subsystem "${subsystemName}" has no handlers`,
3777
+ suggestion: "Add at least one handler to the subsystem"
3778
+ });
3779
+ }
3780
+ for (const handler of subsystem.handlers) {
3781
+ const existingOwner = allAssignedHandlers.get(handler);
3782
+ if (existingOwner) {
3783
+ this.issues.push({
3784
+ type: "invalid_value",
3785
+ severity: "error",
3786
+ field: `subsystems.${subsystemName}.handlers`,
3787
+ message: `Handler "${handler}" is assigned to both "${existingOwner}" and "${subsystemName}"`,
3788
+ suggestion: "Each handler must belong to exactly one subsystem"
3789
+ });
3790
+ } else {
3791
+ allAssignedHandlers.set(handler, subsystemName);
3792
+ }
3793
+ }
3794
+ if (subsystem.state.length === 0) {
3795
+ this.issues.push({
3796
+ type: "invalid_value",
3797
+ severity: "warning",
3798
+ field: `subsystems.${subsystemName}.state`,
3799
+ message: `Subsystem "${subsystemName}" has no state fields`,
3800
+ suggestion: "Add at least one state field to the subsystem"
3801
+ });
3802
+ }
3803
+ }
3804
+ }
3249
3805
  }
3250
3806
  function validateConfig(configPath) {
3251
3807
  const validator = new ConfigValidator;
@@ -3664,7 +4220,8 @@ class HandlerExtractor {
3664
4220
  packageRoot;
3665
4221
  warnings;
3666
4222
  currentFunctionParams = [];
3667
- constructor(tsConfigPath) {
4223
+ contextOverrides;
4224
+ constructor(tsConfigPath, contextOverrides) {
3668
4225
  this.project = new Project({
3669
4226
  tsConfigFilePath: tsConfigPath
3670
4227
  });
@@ -3672,6 +4229,7 @@ class HandlerExtractor {
3672
4229
  this.relationshipExtractor = new RelationshipExtractor;
3673
4230
  this.analyzedFiles = new Set;
3674
4231
  this.warnings = [];
4232
+ this.contextOverrides = contextOverrides || new Map;
3675
4233
  this.packageRoot = this.findPackageRoot(tsConfigPath);
3676
4234
  }
3677
4235
  warnUnsupportedPattern(pattern, location, suggestion) {
@@ -3717,13 +4275,14 @@ class HandlerExtractor {
3717
4275
  const messageTypes = new Set;
3718
4276
  const invalidMessageTypes = new Set;
3719
4277
  const stateConstraints = [];
4278
+ const globalStateConstraints = [];
3720
4279
  const verifiedStates = [];
3721
4280
  this.warnings = [];
3722
4281
  const allSourceFiles = this.project.getSourceFiles();
3723
4282
  const entryPoints = allSourceFiles.filter((f) => this.isWithinPackage(f.getFilePath()));
3724
4283
  this.debugLogSourceFiles(allSourceFiles, entryPoints);
3725
4284
  for (const entryPoint of entryPoints) {
3726
- this.analyzeFileAndImports(entryPoint, handlers, messageTypes, invalidMessageTypes, stateConstraints, verifiedStates);
4285
+ this.analyzeFileAndImports(entryPoint, handlers, messageTypes, invalidMessageTypes, stateConstraints, globalStateConstraints, verifiedStates);
3727
4286
  }
3728
4287
  if (verifiedStates.length > 0) {
3729
4288
  if (process.env["POLLY_DEBUG"]) {
@@ -3753,11 +4312,12 @@ class HandlerExtractor {
3753
4312
  handlers,
3754
4313
  messageTypes,
3755
4314
  stateConstraints,
4315
+ globalStateConstraints,
3756
4316
  verifiedStates,
3757
4317
  warnings: this.warnings
3758
4318
  };
3759
4319
  }
3760
- analyzeFileAndImports(sourceFile, handlers, messageTypes, invalidMessageTypes, stateConstraints, verifiedStates) {
4320
+ analyzeFileAndImports(sourceFile, handlers, messageTypes, invalidMessageTypes, stateConstraints, globalStateConstraints, verifiedStates) {
3761
4321
  const filePath = sourceFile.getFilePath();
3762
4322
  if (this.analyzedFiles.has(filePath)) {
3763
4323
  return;
@@ -3771,6 +4331,8 @@ class HandlerExtractor {
3771
4331
  this.categorizeHandlerMessageTypes(fileHandlers, messageTypes, invalidMessageTypes);
3772
4332
  const fileConstraints = this.extractStateConstraintsFromFile(sourceFile);
3773
4333
  stateConstraints.push(...fileConstraints);
4334
+ const fileGlobalConstraints = this.extractGlobalStateConstraintsFromFile(sourceFile);
4335
+ globalStateConstraints.push(...fileGlobalConstraints);
3774
4336
  const fileVerifiedStates = this.extractVerifiedStatesFromFile(sourceFile);
3775
4337
  verifiedStates.push(...fileVerifiedStates);
3776
4338
  const importDeclarations = sourceFile.getImportDeclarations();
@@ -3784,7 +4346,7 @@ class HandlerExtractor {
3784
4346
  }
3785
4347
  continue;
3786
4348
  }
3787
- this.analyzeFileAndImports(importedFile, handlers, messageTypes, invalidMessageTypes, stateConstraints, verifiedStates);
4349
+ this.analyzeFileAndImports(importedFile, handlers, messageTypes, invalidMessageTypes, stateConstraints, globalStateConstraints, verifiedStates);
3788
4350
  } else if (process.env["POLLY_DEBUG"]) {
3789
4351
  const specifier = importDecl.getModuleSpecifierValue();
3790
4352
  if (!specifier.startsWith("node:") && !this.isNodeModuleImport(specifier)) {
@@ -3885,8 +4447,20 @@ class HandlerExtractor {
3885
4447
  handlers.push(handler);
3886
4448
  }
3887
4449
  }
4450
+ if (methodName === "ws") {
4451
+ this.extractElysiaWsHandlers(node, context, filePath, handlers);
4452
+ }
4453
+ if (this.isRestMethod(methodName) && this.isWebFrameworkFile(node.getSourceFile())) {
4454
+ const restHandler = this.extractRestHandler(node, methodName, context, filePath);
4455
+ if (restHandler) {
4456
+ handlers.push(restHandler);
4457
+ }
4458
+ }
3888
4459
  }
3889
4460
  }
4461
+ isRestMethod(name) {
4462
+ return ["get", "post", "put", "delete", "patch"].includes(name);
4463
+ }
3890
4464
  isElseIfStatement(node) {
3891
4465
  const parent = node.getParent();
3892
4466
  return parent !== undefined && Node2.isIfStatement(parent);
@@ -3952,6 +4526,135 @@ class HandlerExtractor {
3952
4526
  parameters
3953
4527
  };
3954
4528
  }
4529
+ extractElysiaWsHandlers(node, context, filePath, handlers) {
4530
+ const args = node.getArguments();
4531
+ if (args.length < 2)
4532
+ return;
4533
+ const routeArg = args[0];
4534
+ if (!routeArg || !Node2.isStringLiteral(routeArg))
4535
+ return;
4536
+ const routePath = routeArg.getLiteralValue();
4537
+ const configArg = args[1];
4538
+ if (!configArg || !Node2.isObjectLiteralExpression(configArg))
4539
+ return;
4540
+ const callbacks = ["message", "open", "close"];
4541
+ for (const cbName of callbacks) {
4542
+ const prop = configArg.getProperty(cbName);
4543
+ if (!prop)
4544
+ continue;
4545
+ let funcBody = null;
4546
+ if (Node2.isMethodDeclaration(prop)) {
4547
+ funcBody = prop;
4548
+ } else if (Node2.isPropertyAssignment(prop)) {
4549
+ const init = prop.getInitializer();
4550
+ if (init && (Node2.isArrowFunction(init) || Node2.isFunctionExpression(init))) {
4551
+ funcBody = init;
4552
+ }
4553
+ }
4554
+ if (!funcBody)
4555
+ continue;
4556
+ if (cbName === "message") {
4557
+ const body = funcBody.getBody();
4558
+ if (!body)
4559
+ continue;
4560
+ const subHandlers = this.extractSubHandlersFromBody(body, context, filePath);
4561
+ if (subHandlers.length > 0) {
4562
+ handlers.push(...subHandlers);
4563
+ } else {
4564
+ handlers.push(this.buildWsHandler(`ws_message`, routePath, context, filePath, funcBody, node.getStartLineNumber()));
4565
+ }
4566
+ } else {
4567
+ handlers.push(this.buildWsHandler(`ws_${cbName}`, routePath, context, filePath, funcBody, node.getStartLineNumber()));
4568
+ }
4569
+ }
4570
+ }
4571
+ extractSubHandlersFromBody(body, context, filePath) {
4572
+ const subHandlers = [];
4573
+ body.forEachDescendant((child) => {
4574
+ if (Node2.isIfStatement(child) && !this.isElseIfStatement(child)) {
4575
+ const typeGuardHandlers = this.extractTypeGuardHandlers(child, context, filePath);
4576
+ subHandlers.push(...typeGuardHandlers);
4577
+ }
4578
+ if (Node2.isSwitchStatement(child)) {
4579
+ const switchHandlers = this.extractSwitchCaseHandlers(child, context, filePath);
4580
+ subHandlers.push(...switchHandlers);
4581
+ }
4582
+ });
4583
+ return subHandlers;
4584
+ }
4585
+ buildWsHandler(messageType, _routePath, context, filePath, funcBody, line) {
4586
+ const assignments = [];
4587
+ const preconditions = [];
4588
+ const postconditions = [];
4589
+ this.currentFunctionParams = this.extractParameterNames(funcBody);
4590
+ this.extractAssignments(funcBody, assignments);
4591
+ this.extractVerificationConditions(funcBody, preconditions, postconditions);
4592
+ this.currentFunctionParams = [];
4593
+ return {
4594
+ messageType,
4595
+ node: context,
4596
+ assignments,
4597
+ preconditions,
4598
+ postconditions,
4599
+ location: { file: filePath, line },
4600
+ origin: "event"
4601
+ };
4602
+ }
4603
+ extractRestHandler(node, methodName, context, filePath) {
4604
+ const args = node.getArguments();
4605
+ if (args.length < 2)
4606
+ return null;
4607
+ const routeArg = args[0];
4608
+ if (!routeArg || !Node2.isStringLiteral(routeArg))
4609
+ return null;
4610
+ const routePath = routeArg.getLiteralValue();
4611
+ const httpMethod = methodName.toUpperCase();
4612
+ const messageType = `${httpMethod} ${routePath}`;
4613
+ const handlerArg = args[1];
4614
+ const assignments = [];
4615
+ const preconditions = [];
4616
+ const postconditions = [];
4617
+ let actualHandler = null;
4618
+ if (Node2.isArrowFunction(handlerArg) || Node2.isFunctionExpression(handlerArg)) {
4619
+ actualHandler = handlerArg;
4620
+ } else if (Node2.isIdentifier(handlerArg)) {
4621
+ actualHandler = this.resolveFunctionReference(handlerArg);
4622
+ }
4623
+ let parameters;
4624
+ if (actualHandler) {
4625
+ this.currentFunctionParams = this.extractParameterNames(actualHandler);
4626
+ parameters = this.currentFunctionParams.length > 0 ? [...this.currentFunctionParams] : undefined;
4627
+ this.extractAssignments(actualHandler, assignments);
4628
+ this.extractVerificationConditions(actualHandler, preconditions, postconditions);
4629
+ this.currentFunctionParams = [];
4630
+ }
4631
+ return {
4632
+ messageType,
4633
+ node: context,
4634
+ assignments,
4635
+ preconditions,
4636
+ postconditions,
4637
+ location: {
4638
+ file: filePath,
4639
+ line: node.getStartLineNumber()
4640
+ },
4641
+ origin: "event",
4642
+ parameters,
4643
+ handlerKind: "rest",
4644
+ httpMethod,
4645
+ routePath
4646
+ };
4647
+ }
4648
+ isWebFrameworkFile(sourceFile) {
4649
+ const frameworks = ["elysia", "express", "hono", "fastify", "koa", "@elysiajs/eden"];
4650
+ for (const importDecl of sourceFile.getImportDeclarations()) {
4651
+ const specifier = importDecl.getModuleSpecifierValue();
4652
+ if (frameworks.some((fw) => specifier === fw || specifier.startsWith(`${fw}/`))) {
4653
+ return true;
4654
+ }
4655
+ }
4656
+ return false;
4657
+ }
3955
4658
  extractAssignments(funcNode, assignments) {
3956
4659
  funcNode.forEachDescendant((node) => {
3957
4660
  if (Node2.isBinaryExpression(node)) {
@@ -4786,32 +5489,64 @@ class HandlerExtractor {
4786
5489
  try {
4787
5490
  const ifStmt = ifNode;
4788
5491
  const condition = ifStmt.getExpression();
4789
- if (!Node2.isCallExpression(condition)) {
4790
- return null;
5492
+ if (Node2.isCallExpression(condition)) {
5493
+ const funcExpr = condition.getExpression();
5494
+ const funcName = Node2.isIdentifier(funcExpr) ? funcExpr.getText() : undefined;
5495
+ this.debugLogProcessingFunction(funcName);
5496
+ const messageType = this.resolveMessageType(funcExpr, funcName, typeGuards);
5497
+ if (!messageType) {
5498
+ this.debugLogUnresolvedMessageType(funcName);
5499
+ return null;
5500
+ }
5501
+ const line = ifStmt.getStartLineNumber();
5502
+ const relationships = this.extractRelationshipsFromIfBlock(ifStmt, messageType);
5503
+ return {
5504
+ messageType,
5505
+ node: context,
5506
+ assignments: [],
5507
+ preconditions: [],
5508
+ postconditions: [],
5509
+ location: { file: filePath, line },
5510
+ relationships
5511
+ };
4791
5512
  }
4792
- const funcExpr = condition.getExpression();
4793
- const funcName = Node2.isIdentifier(funcExpr) ? funcExpr.getText() : undefined;
4794
- this.debugLogProcessingFunction(funcName);
4795
- const messageType = this.resolveMessageType(funcExpr, funcName, typeGuards);
4796
- if (!messageType) {
4797
- this.debugLogUnresolvedMessageType(funcName);
4798
- return null;
5513
+ if (Node2.isBinaryExpression(condition)) {
5514
+ const messageType = this.extractMessageTypeFromEqualityCheck(condition);
5515
+ if (messageType) {
5516
+ const line = ifStmt.getStartLineNumber();
5517
+ const relationships = this.extractRelationshipsFromIfBlock(ifStmt, messageType);
5518
+ return {
5519
+ messageType,
5520
+ node: context,
5521
+ assignments: [],
5522
+ preconditions: [],
5523
+ postconditions: [],
5524
+ location: { file: filePath, line },
5525
+ relationships
5526
+ };
5527
+ }
4799
5528
  }
4800
- const line = ifStmt.getStartLineNumber();
4801
- const relationships = this.extractRelationshipsFromIfBlock(ifStmt, messageType);
4802
- return {
4803
- messageType,
4804
- node: context,
4805
- assignments: [],
4806
- preconditions: [],
4807
- postconditions: [],
4808
- location: { file: filePath, line },
4809
- relationships
4810
- };
5529
+ return null;
4811
5530
  } catch (_error) {
4812
5531
  return null;
4813
5532
  }
4814
5533
  }
5534
+ extractMessageTypeFromEqualityCheck(expr) {
5535
+ const operator = expr.getOperatorToken().getText();
5536
+ if (operator !== "===" && operator !== "==")
5537
+ return null;
5538
+ const left = expr.getLeft();
5539
+ const right = expr.getRight();
5540
+ const stringLiteral = Node2.isStringLiteral(right) ? right : Node2.isStringLiteral(left) ? left : null;
5541
+ const propAccess = Node2.isPropertyAccessExpression(left) ? left : Node2.isPropertyAccessExpression(right) ? right : null;
5542
+ if (!stringLiteral || !propAccess)
5543
+ return null;
5544
+ const propName = propAccess.getName();
5545
+ if (propName !== "type" && propName !== "kind" && propName !== "action") {
5546
+ return null;
5547
+ }
5548
+ return stringLiteral.getLiteralValue();
5549
+ }
4815
5550
  debugLogProcessingFunction(funcName) {
4816
5551
  if (process.env["POLLY_DEBUG"] && funcName) {
4817
5552
  console.log(`[DEBUG] Processing if condition with function: ${funcName}`);
@@ -5021,6 +5756,10 @@ class HandlerExtractor {
5021
5756
  return messageType;
5022
5757
  }
5023
5758
  inferContext(filePath) {
5759
+ for (const [pathPrefix, context] of this.contextOverrides.entries()) {
5760
+ if (filePath.startsWith(pathPrefix))
5761
+ return context;
5762
+ }
5024
5763
  const path2 = filePath.toLowerCase();
5025
5764
  return this.inferElectronContext(path2) || this.inferWorkerContext(path2) || this.inferServerAppContext(path2) || this.inferChromeExtensionContext(path2) || "unknown";
5026
5765
  }
@@ -5119,6 +5858,81 @@ class HandlerExtractor {
5119
5858
  });
5120
5859
  return constraints;
5121
5860
  }
5861
+ extractGlobalStateConstraintsFromFile(sourceFile) {
5862
+ const constraints = [];
5863
+ const filePath = sourceFile.getFilePath();
5864
+ sourceFile.forEachDescendant((node) => {
5865
+ const constraint = this.recognizeGlobalStateConstraint(node, filePath);
5866
+ if (constraint) {
5867
+ constraints.push(constraint);
5868
+ }
5869
+ });
5870
+ return constraints;
5871
+ }
5872
+ recognizeGlobalStateConstraint(node, filePath) {
5873
+ if (!Node2.isCallExpression(node)) {
5874
+ return null;
5875
+ }
5876
+ const expression = node.getExpression();
5877
+ if (!Node2.isIdentifier(expression)) {
5878
+ return null;
5879
+ }
5880
+ const functionName = expression.getText();
5881
+ if (functionName !== "stateConstraint") {
5882
+ return null;
5883
+ }
5884
+ const args = node.getArguments();
5885
+ if (args.length < 2) {
5886
+ return null;
5887
+ }
5888
+ const nameArg = args[0];
5889
+ if (!Node2.isStringLiteral(nameArg)) {
5890
+ return null;
5891
+ }
5892
+ const name = nameArg.getLiteralValue();
5893
+ const predicateArg = args[1];
5894
+ if (!Node2.isArrowFunction(predicateArg)) {
5895
+ return null;
5896
+ }
5897
+ const body = predicateArg.getBody();
5898
+ let expressionText;
5899
+ if (Node2.isBlock(body)) {
5900
+ const returnStatement = body.getStatements().find((s) => Node2.isReturnStatement(s));
5901
+ if (!returnStatement || !Node2.isReturnStatement(returnStatement)) {
5902
+ return null;
5903
+ }
5904
+ const returnExpr = returnStatement.getExpression();
5905
+ if (!returnExpr) {
5906
+ return null;
5907
+ }
5908
+ expressionText = returnExpr.getText();
5909
+ } else {
5910
+ expressionText = body.getText();
5911
+ }
5912
+ let message;
5913
+ if (args.length >= 3) {
5914
+ const optionsArg = args[2];
5915
+ if (Node2.isObjectLiteralExpression(optionsArg)) {
5916
+ for (const prop of optionsArg.getProperties()) {
5917
+ if (Node2.isPropertyAssignment(prop) && prop.getName() === "message") {
5918
+ const value = prop.getInitializer();
5919
+ if (value && Node2.isStringLiteral(value)) {
5920
+ message = value.getLiteralValue();
5921
+ }
5922
+ }
5923
+ }
5924
+ }
5925
+ }
5926
+ return {
5927
+ name,
5928
+ expression: expressionText,
5929
+ message,
5930
+ location: {
5931
+ file: filePath,
5932
+ line: node.getStartLineNumber()
5933
+ }
5934
+ };
5935
+ }
5122
5936
  recognizeStateConstraint(node, filePath) {
5123
5937
  if (!Node2.isCallExpression(node)) {
5124
5938
  return [];
@@ -5433,7 +6247,8 @@ class TypeExtractor {
5433
6247
  messageTypes: validMessageTypes,
5434
6248
  fields,
5435
6249
  handlers: completeHandlers,
5436
- stateConstraints: handlerAnalysis.stateConstraints
6250
+ stateConstraints: handlerAnalysis.stateConstraints,
6251
+ globalStateConstraints: handlerAnalysis.globalStateConstraints
5437
6252
  };
5438
6253
  }
5439
6254
  extractHandlerAnalysis() {
@@ -5864,6 +6679,10 @@ async function main() {
5864
6679
  case "validate":
5865
6680
  await validateCommand();
5866
6681
  break;
6682
+ case "--estimate":
6683
+ case "estimate":
6684
+ await estimateCommand();
6685
+ break;
5867
6686
  case "--help":
5868
6687
  case "help":
5869
6688
  showHelp();
@@ -5980,6 +6799,70 @@ async function validateCommand() {
5980
6799
  `, COLORS.red));
5981
6800
  process.exit(1);
5982
6801
  }
6802
+ async function estimateCommand() {
6803
+ const configPath = path4.join(process.cwd(), "specs", "verification.config.ts");
6804
+ console.log(color(`
6805
+ \uD83D\uDCCA Estimating state space...
6806
+ `, COLORS.blue));
6807
+ const validation = validateConfig(configPath);
6808
+ if (!validation.valid) {
6809
+ const errors = validation.issues.filter((i) => i.severity === "error");
6810
+ console.log(color(`❌ Configuration incomplete (${errors.length} error(s))
6811
+ `, COLORS.red));
6812
+ for (const error of errors.slice(0, 3)) {
6813
+ console.log(color(` • ${error.message}`, COLORS.red));
6814
+ }
6815
+ console.log(`
6816
+ Run 'polly verify --validate' to see all issues`);
6817
+ process.exit(1);
6818
+ }
6819
+ const config = await loadVerificationConfig(configPath);
6820
+ const analysis = await runCodebaseAnalysis();
6821
+ const typedConfig = config;
6822
+ const typedAnalysis = analysis;
6823
+ const exprValidation = validateExpressions(typedAnalysis.handlers, typedConfig.state);
6824
+ if (exprValidation.warnings.length > 0) {
6825
+ displayExpressionWarnings(exprValidation);
6826
+ }
6827
+ const estimate = estimateStateSpace(typedConfig, typedAnalysis);
6828
+ displayEstimate(estimate);
6829
+ }
6830
+ function displayEstimate(estimate) {
6831
+ console.log(color(`State space estimate:
6832
+ `, COLORS.blue));
6833
+ console.log(color(" Fields:", COLORS.blue));
6834
+ for (const field of estimate.fields) {
6835
+ const name = field.name.padEnd(28);
6836
+ const kind = field.kind.padEnd(18);
6837
+ const card = field.cardinality === "unbounded" ? color("(excluded — unbounded)", COLORS.yellow) : `${field.cardinality} values`;
6838
+ console.log(` ${name} ${kind} ${card}`);
6839
+ }
6840
+ console.log();
6841
+ console.log(` Field combinations: ${color(String(estimate.fieldProduct), COLORS.green)}`);
6842
+ console.log(` Handlers: ${estimate.handlerCount}`);
6843
+ console.log(` Max in-flight: ${estimate.maxInFlight}`);
6844
+ console.log(` Contexts: ${estimate.contextCount} (${estimate.contextCount - 1} tab${estimate.contextCount - 1 !== 1 ? "s" : ""} + background)`);
6845
+ console.log(` Interleaving factor: ${estimate.interleavingFactor}`);
6846
+ console.log();
6847
+ console.log(` Estimated states: ${color(`~${estimate.estimatedStates.toLocaleString()}`, COLORS.green)}`);
6848
+ const feasColor = estimate.feasibility === "trivial" || estimate.feasibility === "feasible" ? COLORS.green : estimate.feasibility === "slow" ? COLORS.yellow : COLORS.red;
6849
+ console.log();
6850
+ console.log(` Assessment: ${color(feasibilityLabel(estimate.feasibility), feasColor)}`);
6851
+ if (estimate.warnings.length > 0) {
6852
+ console.log();
6853
+ for (const w of estimate.warnings) {
6854
+ console.log(color(` ⚠️ ${w}`, COLORS.yellow));
6855
+ }
6856
+ }
6857
+ if (estimate.suggestions.length > 0) {
6858
+ console.log();
6859
+ console.log(color(" Suggestions:", COLORS.blue));
6860
+ for (const s of estimate.suggestions) {
6861
+ console.log(color(` • ${s}`, COLORS.gray));
6862
+ }
6863
+ }
6864
+ console.log();
6865
+ }
5983
6866
  function displayValidationErrors(errors) {
5984
6867
  if (errors.length === 0)
5985
6868
  return;
@@ -6011,6 +6894,19 @@ function displayValidationWarnings(warnings) {
6011
6894
  console.log();
6012
6895
  }
6013
6896
  }
6897
+ function displayExpressionWarnings(result) {
6898
+ if (result.warnings.length === 0)
6899
+ return;
6900
+ console.log(color(`⚠️ Found ${result.warnCount} expression warning(s):
6901
+ `, COLORS.yellow));
6902
+ for (const w of result.warnings) {
6903
+ console.log(color(` ⚠ ${w.message}`, COLORS.yellow));
6904
+ console.log(color(` ${w.expression}`, COLORS.gray));
6905
+ console.log(color(` at ${w.location.file}:${w.location.line}`, COLORS.gray));
6906
+ console.log(color(` → ${w.suggestion}`, COLORS.yellow));
6907
+ console.log();
6908
+ }
6909
+ }
6014
6910
  async function verifyCommand() {
6015
6911
  const configPath = path4.join(process.cwd(), "specs", "verification.config.ts");
6016
6912
  console.log(color(`
@@ -6073,15 +6969,29 @@ function getMaxDepth(config) {
6073
6969
  async function runFullVerification(configPath) {
6074
6970
  const config = await loadVerificationConfig(configPath);
6075
6971
  const analysis = await runCodebaseAnalysis();
6972
+ const typedConfig = config;
6973
+ const typedAnalysis = analysis;
6974
+ const exprValidation = validateExpressions(typedAnalysis.handlers, typedConfig.state);
6975
+ if (exprValidation.warnings.length > 0) {
6976
+ displayExpressionWarnings(exprValidation);
6977
+ }
6978
+ if (typedConfig.subsystems && Object.keys(typedConfig.subsystems).length > 0) {
6979
+ await runSubsystemVerification(typedConfig, typedAnalysis);
6980
+ return;
6981
+ }
6982
+ await runMonolithicVerification(config, analysis);
6983
+ }
6984
+ async function runMonolithicVerification(config, analysis) {
6076
6985
  const { specPath, specDir } = await generateAndWriteTLASpecs(config, analysis);
6077
6986
  findAndCopyBaseSpec(specDir);
6078
6987
  console.log(color("✓ Specification generated", COLORS.green));
6079
6988
  console.log(color(` ${specPath}`, COLORS.gray));
6080
6989
  console.log();
6081
6990
  const docker = await setupDocker();
6082
- const timeoutSeconds = getTimeout(config);
6083
- const workers = getWorkers(config);
6084
- const maxDepth = getMaxDepth(config);
6991
+ const typedConfig = config;
6992
+ const timeoutSeconds = getTimeout(typedConfig);
6993
+ const workers = getWorkers(typedConfig);
6994
+ const maxDepth = getMaxDepth(typedConfig);
6085
6995
  console.log(color("⚙️ Running TLC model checker...", COLORS.blue));
6086
6996
  if (timeoutSeconds === 0) {
6087
6997
  console.log(color(" No timeout set - will run until completion", COLORS.gray));
@@ -6101,6 +7011,122 @@ async function runFullVerification(configPath) {
6101
7011
  });
6102
7012
  displayVerificationResults(result, specDir);
6103
7013
  }
7014
+ async function runSubsystemVerification(config, analysis) {
7015
+ const subsystems = config.subsystems;
7016
+ const subsystemNames = Object.keys(subsystems);
7017
+ console.log(color(`\uD83D\uDCE6 Subsystem-scoped verification (${subsystemNames.length} subsystems)
7018
+ `, COLORS.blue));
7019
+ const { checkNonInterference: checkNonInterference2 } = await Promise.resolve().then(() => exports_non_interference);
7020
+ const interference = checkNonInterference2(subsystems, analysis.handlers);
7021
+ if (interference.valid) {
7022
+ console.log(color("✓ Non-interference: verified (no cross-subsystem state writes)", COLORS.green));
7023
+ console.log();
7024
+ } else {
7025
+ console.log(color(`⚠️ Non-interference violations detected:
7026
+ `, COLORS.yellow));
7027
+ for (const v of interference.violations) {
7028
+ console.log(color(` • Handler "${v.handler}" (${v.subsystem}) writes to "${v.writesTo}" owned by "${v.ownedBy}"`, COLORS.yellow));
7029
+ }
7030
+ console.log();
7031
+ console.log(color(" Compositional verification may not be sound. Consider restructuring subsystem boundaries.", COLORS.yellow));
7032
+ console.log();
7033
+ }
7034
+ const assignedHandlers = new Set(Object.values(subsystems).flatMap((s) => s.handlers));
7035
+ const unassigned = analysis.messageTypes.filter((mt) => !assignedHandlers.has(mt));
7036
+ if (unassigned.length > 0) {
7037
+ console.log(color(`⚠️ ${unassigned.length} handler(s) not assigned to any subsystem (will not be verified):`, COLORS.yellow));
7038
+ for (const h of unassigned.slice(0, 10)) {
7039
+ console.log(color(` • ${h}`, COLORS.yellow));
7040
+ }
7041
+ if (unassigned.length > 10) {
7042
+ console.log(color(` ... and ${unassigned.length - 10} more`, COLORS.yellow));
7043
+ }
7044
+ console.log();
7045
+ }
7046
+ const docker = await setupDocker();
7047
+ const timeoutSeconds = getTimeout(config);
7048
+ const workers = getWorkers(config);
7049
+ const maxDepth = getMaxDepth(config);
7050
+ const { generateSubsystemTLA: generateSubsystemTLA2 } = await Promise.resolve().then(() => (init_tla(), exports_tla));
7051
+ const results = [];
7052
+ for (const name of subsystemNames) {
7053
+ const sub = subsystems[name];
7054
+ const startTime = Date.now();
7055
+ console.log(color(`⚙️ Verifying subsystem: ${name}...`, COLORS.blue));
7056
+ const { spec, cfg } = await generateSubsystemTLA2(name, sub, config, analysis);
7057
+ const specDir = path4.join(process.cwd(), "specs", "tla", "generated", name);
7058
+ if (!fs4.existsSync(specDir)) {
7059
+ fs4.mkdirSync(specDir, { recursive: true });
7060
+ }
7061
+ const specPath = path4.join(specDir, `UserApp_${name}.tla`);
7062
+ const cfgPath = path4.join(specDir, `UserApp_${name}.cfg`);
7063
+ fs4.writeFileSync(specPath, spec);
7064
+ fs4.writeFileSync(cfgPath, cfg);
7065
+ findAndCopyBaseSpec(specDir);
7066
+ const result = await docker.runTLC(specPath, {
7067
+ workers,
7068
+ timeout: timeoutSeconds > 0 ? timeoutSeconds * 1000 : undefined,
7069
+ maxDepth
7070
+ });
7071
+ const elapsed = (Date.now() - startTime) / 1000;
7072
+ results.push({
7073
+ name,
7074
+ success: result.success,
7075
+ handlerCount: sub.handlers.length,
7076
+ stateCount: result.stats?.distinctStates ?? 0,
7077
+ elapsed,
7078
+ stats: result.stats,
7079
+ error: result.error
7080
+ });
7081
+ if (result.success) {
7082
+ console.log(color(` ✓ ${name} passed (${elapsed.toFixed(1)}s)`, COLORS.green));
7083
+ } else {
7084
+ console.log(color(` ✗ ${name} failed`, COLORS.red));
7085
+ if (result.violation) {
7086
+ console.log(color(` Invariant violated: ${result.violation.name}`, COLORS.red));
7087
+ } else if (result.error) {
7088
+ console.log(color(` Error: ${result.error}`, COLORS.red));
7089
+ }
7090
+ fs4.writeFileSync(path4.join(specDir, "tlc-output.log"), result.output);
7091
+ }
7092
+ }
7093
+ console.log();
7094
+ displayCompositionalReport(results, interference.valid);
7095
+ }
7096
+ function displayCompositionalReport(results, nonInterferenceValid) {
7097
+ console.log(color(`Subsystem verification results:
7098
+ `, COLORS.blue));
7099
+ for (const r of results) {
7100
+ const status = r.success ? color("✓", COLORS.green) : color("✗", COLORS.red);
7101
+ const name = r.name.padEnd(20);
7102
+ const handlers = `${r.handlerCount} handler${r.handlerCount !== 1 ? "s" : ""}`;
7103
+ const states = `${r.stateCount} states`;
7104
+ const time = `${r.elapsed.toFixed(1)}s`;
7105
+ console.log(` ${status} ${name} ${handlers.padEnd(14)} ${states.padEnd(14)} ${time}`);
7106
+ }
7107
+ console.log();
7108
+ const nonIntLabel = nonInterferenceValid ? color("✓ verified (no cross-subsystem state writes)", COLORS.green) : color("⚠ violations detected", COLORS.yellow);
7109
+ console.log(` Non-interference: ${nonIntLabel}`);
7110
+ console.log();
7111
+ const allPassed = results.every((r) => r.success);
7112
+ if (allPassed && nonInterferenceValid) {
7113
+ console.log(color("Compositional result: ✓ PASS", COLORS.green));
7114
+ console.log(color(" All subsystems verified independently. By non-interference,", COLORS.gray));
7115
+ console.log(color(" the full system satisfies all per-subsystem invariants.", COLORS.gray));
7116
+ } else if (allPassed && !nonInterferenceValid) {
7117
+ console.log(color("Compositional result: ⚠ PASS (with warnings)", COLORS.yellow));
7118
+ console.log(color(" All subsystems passed, but non-interference violations exist.", COLORS.gray));
7119
+ console.log(color(" Compositional soundness is not guaranteed.", COLORS.gray));
7120
+ } else {
7121
+ console.log(color("Compositional result: ✗ FAIL", COLORS.red));
7122
+ const failed = results.filter((r) => !r.success);
7123
+ for (const f of failed) {
7124
+ console.log(color(` Failed: ${f.name}`, COLORS.red));
7125
+ }
7126
+ process.exit(1);
7127
+ }
7128
+ console.log();
7129
+ }
6104
7130
  async function loadVerificationConfig(configPath) {
6105
7131
  const resolvedPath = path4.resolve(configPath);
6106
7132
  const configModule = await import(`file://${resolvedPath}?t=${Date.now()}`);
@@ -6260,6 +7286,9 @@ ${color("Commands:", COLORS.blue)}
6260
7286
  ${color("bun verify --validate", COLORS.green)}
6261
7287
  Validate existing configuration without running verification
6262
7288
 
7289
+ ${color("bun verify --estimate", COLORS.green)}
7290
+ Estimate state space without running TLC
7291
+
6263
7292
  ${color("bun verify --help", COLORS.green)}
6264
7293
  Show this help message
6265
7294
 
@@ -6314,4 +7343,4 @@ main().catch((error) => {
6314
7343
  process.exit(1);
6315
7344
  });
6316
7345
 
6317
- //# debugId=0CD51D5DC046B30964756E2164756E21
7346
+ //# debugId=7EE824400DEB8F7564756E2164756E21