@fairfox/polly 0.16.0 → 0.17.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.
@@ -1832,7 +1832,7 @@ var init_tla = __esm(() => {
1832
1832
  this.addTemporalConstraints(config.tier2.temporalConstraints);
1833
1833
  }
1834
1834
  this.line("\\* State constraint to bound state space");
1835
- this.addStateConstraint(config);
1835
+ this.addStateConstraint(config, _analysis);
1836
1836
  this.line("=============================================================================");
1837
1837
  }
1838
1838
  addTemporalConstraints(constraints) {
@@ -1862,10 +1862,11 @@ var init_tla = __esm(() => {
1862
1862
  });
1863
1863
  }
1864
1864
  }
1865
- addStateConstraint(config) {
1865
+ addStateConstraint(config, analysis) {
1866
1866
  const hasPerMessageBounds = config.messages.perMessageBounds && Object.keys(config.messages.perMessageBounds).length > 0;
1867
1867
  const hasBoundedExploration = config.tier2?.boundedExploration?.maxDepth !== undefined;
1868
- const needsConjunction = hasPerMessageBounds || hasBoundedExploration;
1868
+ const hasUserConstraints = (analysis.globalStateConstraints?.length ?? 0) > 0;
1869
+ const needsConjunction = hasPerMessageBounds || hasBoundedExploration || hasUserConstraints;
1869
1870
  this.line("StateConstraint ==");
1870
1871
  this.indent++;
1871
1872
  if (needsConjunction) {
@@ -1879,6 +1880,13 @@ var init_tla = __esm(() => {
1879
1880
  if (hasBoundedExploration && config.tier2?.boundedExploration?.maxDepth) {
1880
1881
  this.line(`/\\ TLCGet("level") <= ${config.tier2.boundedExploration.maxDepth} \\* Tier 2: Bounded exploration`);
1881
1882
  }
1883
+ if (hasUserConstraints && analysis.globalStateConstraints) {
1884
+ for (const constraint of analysis.globalStateConstraints) {
1885
+ const tlaExpr = this.tsExpressionToTLA(constraint.expression, false);
1886
+ this.line(`\\* ${constraint.name}${constraint.message ? `: ${constraint.message}` : ""}`);
1887
+ this.line(`/\\ \\A ctx \\in Contexts : (${tlaExpr})`);
1888
+ }
1889
+ }
1882
1890
  } else {
1883
1891
  this.line("Len(messages) <= MaxMessages");
1884
1892
  }
@@ -1887,6 +1895,9 @@ var init_tla = __esm(() => {
1887
1895
  if (hasBoundedExploration && config.tier2?.boundedExploration?.maxDepth) {
1888
1896
  console.log(`[INFO] [TLAGenerator] Tier 2: Bounded exploration with maxDepth = ${config.tier2.boundedExploration.maxDepth}`);
1889
1897
  }
1898
+ if (hasUserConstraints && analysis.globalStateConstraints) {
1899
+ console.log(`[INFO] [TLAGenerator] ${analysis.globalStateConstraints.length} user-defined state constraint(s) added to CONSTRAINT clause`);
1900
+ }
1890
1901
  }
1891
1902
  addDeliveredTracking() {
1892
1903
  this.line("");
@@ -2366,6 +2377,400 @@ var init_docker = () => {};
2366
2377
  import * as fs4 from "node:fs";
2367
2378
  import * as path4 from "node:path";
2368
2379
 
2380
+ // tools/verify/src/analysis/expression-validator.ts
2381
+ var SIGNAL_VALUE_FIELD = /([a-zA-Z_]\w*)\.value\.([a-zA-Z_][\w.]*)/g;
2382
+ var SIGNAL_VALUE_BARE = /([a-zA-Z_]\w*)\.value\b(?!\.)/g;
2383
+ var STATE_DOT_FIELD = /state\.([a-zA-Z_][\w.]*)/g;
2384
+ var PAYLOAD_REF = /payload\.\w+/;
2385
+ function extractFieldRefs(expression) {
2386
+ const refs = [];
2387
+ for (const m of expression.matchAll(SIGNAL_VALUE_FIELD)) {
2388
+ const prefix = m[1];
2389
+ const suffix = m[2];
2390
+ refs.push(`${prefix}.${suffix}`);
2391
+ }
2392
+ for (const m of expression.matchAll(SIGNAL_VALUE_BARE)) {
2393
+ refs.push(m[1]);
2394
+ }
2395
+ for (const m of expression.matchAll(STATE_DOT_FIELD)) {
2396
+ refs.push(m[1]);
2397
+ }
2398
+ return [...new Set(refs)];
2399
+ }
2400
+ function fieldInConfig(fieldRef, configKeys, stateConfig) {
2401
+ if (configKeys.has(fieldRef))
2402
+ return true;
2403
+ const underscored = fieldRef.replace(/\./g, "_");
2404
+ if (configKeys.has(underscored))
2405
+ return true;
2406
+ const dotted = fieldRef.replace(/_/g, ".");
2407
+ if (configKeys.has(dotted))
2408
+ return true;
2409
+ if (stateConfig && fieldRef.endsWith(".length")) {
2410
+ const parent = fieldRef.slice(0, -".length".length);
2411
+ const parentEntry = resolveConfigEntry(parent, stateConfig);
2412
+ if (parentEntry !== undefined && isArrayLike(parentEntry))
2413
+ return true;
2414
+ }
2415
+ return false;
2416
+ }
2417
+ function resolveConfigEntry(fieldRef, stateConfig) {
2418
+ if (fieldRef in stateConfig)
2419
+ return stateConfig[fieldRef];
2420
+ const underscored = fieldRef.replace(/\./g, "_");
2421
+ if (underscored in stateConfig)
2422
+ return stateConfig[underscored];
2423
+ const dotted = fieldRef.replace(/_/g, ".");
2424
+ if (dotted in stateConfig)
2425
+ return stateConfig[dotted];
2426
+ return;
2427
+ }
2428
+ function isArrayLike(configEntry) {
2429
+ if (configEntry && typeof configEntry === "object") {
2430
+ const obj = configEntry;
2431
+ if (obj.type === "array")
2432
+ return true;
2433
+ if ("maxLength" in obj)
2434
+ return true;
2435
+ if ("maxSize" in obj)
2436
+ return true;
2437
+ }
2438
+ return false;
2439
+ }
2440
+ function isNullable(configEntry) {
2441
+ if (Array.isArray(configEntry)) {
2442
+ return configEntry.includes(null);
2443
+ }
2444
+ if (configEntry && typeof configEntry === "object") {
2445
+ const obj = configEntry;
2446
+ if (obj.abstract === true)
2447
+ return true;
2448
+ if (Array.isArray(obj.values)) {
2449
+ return obj.values.includes(null);
2450
+ }
2451
+ }
2452
+ return false;
2453
+ }
2454
+ var UNSUPPORTED_METHODS = /\.(some|every|find|filter)\s*\(/;
2455
+ var OPTIONAL_CHAIN = /\?\./;
2456
+ var NULL_COMPARISON = /(===?\s*null|!==?\s*null|null\s*===?|null\s*!==?)/;
2457
+ function checkUnmodeledFields(expression, configKeys, stateConfig, messageType, conditionType, location) {
2458
+ const warnings = [];
2459
+ const refs = extractFieldRefs(expression);
2460
+ for (const ref of refs) {
2461
+ if (!fieldInConfig(ref, configKeys, stateConfig)) {
2462
+ warnings.push({
2463
+ kind: "unmodeled_field",
2464
+ message: `${conditionType}() in ${messageType} references unmodeled field '${ref}'`,
2465
+ messageType,
2466
+ conditionType,
2467
+ expression,
2468
+ location,
2469
+ suggestion: `Add '${ref}' to state config or remove from ${conditionType}()`
2470
+ });
2471
+ }
2472
+ }
2473
+ return warnings;
2474
+ }
2475
+ function checkUnsupportedMethods(expression, configKeys, stateConfig, messageType, conditionType, location) {
2476
+ const match = expression.match(UNSUPPORTED_METHODS);
2477
+ if (!match)
2478
+ return [];
2479
+ const refs = extractFieldRefs(expression);
2480
+ const allUnmodeled = refs.length > 0 && refs.every((r) => !fieldInConfig(r, configKeys, stateConfig));
2481
+ if (allUnmodeled)
2482
+ return [];
2483
+ return [
2484
+ {
2485
+ kind: "unsupported_method",
2486
+ message: `${conditionType}() in ${messageType} uses .${match[1]}() which cannot be translated to TLA+`,
2487
+ messageType,
2488
+ conditionType,
2489
+ expression,
2490
+ location,
2491
+ suggestion: `Rewrite without .${match[1]}() or model the check as a boolean state field`
2492
+ }
2493
+ ];
2494
+ }
2495
+ function checkOptionalChaining(expression, messageType, conditionType, location) {
2496
+ if (!OPTIONAL_CHAIN.test(expression))
2497
+ return [];
2498
+ return [
2499
+ {
2500
+ kind: "optional_chaining",
2501
+ message: `${conditionType}() in ${messageType} uses optional chaining (?.)`,
2502
+ messageType,
2503
+ conditionType,
2504
+ expression,
2505
+ location,
2506
+ suggestion: "Optional chaining translation is fragile — consider explicit null checks or restructuring"
2507
+ }
2508
+ ];
2509
+ }
2510
+ function checkNullComparisons(expression, stateConfig, configKeys, messageType, conditionType, location) {
2511
+ if (!NULL_COMPARISON.test(expression))
2512
+ return [];
2513
+ const warnings = [];
2514
+ const refs = extractFieldRefs(expression);
2515
+ for (const ref of refs) {
2516
+ if (!fieldInConfig(ref, configKeys, stateConfig))
2517
+ continue;
2518
+ const entry = resolveConfigEntry(ref, stateConfig);
2519
+ if (entry !== undefined && !isNullable(entry)) {
2520
+ warnings.push({
2521
+ kind: "null_comparison",
2522
+ message: `${conditionType}() in ${messageType} compares '${ref}' to null but field is not nullable`,
2523
+ messageType,
2524
+ conditionType,
2525
+ expression,
2526
+ location,
2527
+ suggestion: `Add null to '${ref}' values in state config, or remove the null check`
2528
+ });
2529
+ }
2530
+ }
2531
+ return warnings;
2532
+ }
2533
+ var WEAK_NEGATION = /([a-zA-Z_][\w.]*(?:\.value\.[\w.]+|\.value\b|))?\s*!==?\s*("[^"]*"|'[^']*'|\d+|true|false|null)/;
2534
+ function checkWeakPostconditions(expression, handler, messageType, conditionType, location) {
2535
+ if (conditionType !== "ensures")
2536
+ return [];
2537
+ const match = expression.match(WEAK_NEGATION);
2538
+ if (!match)
2539
+ return [];
2540
+ const refs = extractFieldRefs(expression);
2541
+ if (refs.length === 0)
2542
+ return [];
2543
+ for (const ref of refs) {
2544
+ const underscoredRef = ref.replace(/\./g, "_");
2545
+ const hasAssignment = handler.assignments.some((a) => a.field === ref || a.field === underscoredRef || a.field.replace(/_/g, ".") === ref);
2546
+ if (hasAssignment) {
2547
+ const assignment = handler.assignments.find((a) => a.field === ref || a.field === underscoredRef || a.field.replace(/_/g, ".") === ref);
2548
+ const assignedValue = assignment ? JSON.stringify(assignment.value) : "<value>";
2549
+ return [
2550
+ {
2551
+ kind: "weak_postcondition",
2552
+ message: `ensures() in ${messageType} uses !== for '${ref}' which has a concrete assignment`,
2553
+ messageType,
2554
+ conditionType,
2555
+ expression,
2556
+ location,
2557
+ suggestion: `Consider ensures(${ref} === ${assignedValue}) for a stronger postcondition`
2558
+ }
2559
+ ];
2560
+ }
2561
+ }
2562
+ return [];
2563
+ }
2564
+ function validateExpressions(handlers, stateConfig) {
2565
+ const warnings = [];
2566
+ let validCount = 0;
2567
+ const configKeys = new Set(Object.keys(stateConfig));
2568
+ for (const handler of handlers) {
2569
+ const conditions = [
2570
+ ...handler.preconditions.map((c) => ({
2571
+ cond: c,
2572
+ type: "requires"
2573
+ })),
2574
+ ...handler.postconditions.map((c) => ({
2575
+ cond: c,
2576
+ type: "ensures"
2577
+ }))
2578
+ ];
2579
+ for (const { cond, type } of conditions) {
2580
+ const refs = extractFieldRefs(cond.expression);
2581
+ const isPayloadOnly = refs.length === 0 && PAYLOAD_REF.test(cond.expression);
2582
+ const loc = {
2583
+ file: handler.location.file,
2584
+ line: cond.location.line,
2585
+ column: cond.location.column
2586
+ };
2587
+ const condWarnings = [];
2588
+ if (!isPayloadOnly) {
2589
+ condWarnings.push(...checkUnmodeledFields(cond.expression, configKeys, stateConfig, handler.messageType, type, loc));
2590
+ }
2591
+ condWarnings.push(...checkUnsupportedMethods(cond.expression, configKeys, stateConfig, handler.messageType, type, loc));
2592
+ condWarnings.push(...checkOptionalChaining(cond.expression, handler.messageType, type, loc));
2593
+ condWarnings.push(...checkNullComparisons(cond.expression, stateConfig, configKeys, handler.messageType, type, loc));
2594
+ condWarnings.push(...checkWeakPostconditions(cond.expression, handler, handler.messageType, type, loc));
2595
+ if (condWarnings.length === 0) {
2596
+ validCount++;
2597
+ } else {
2598
+ warnings.push(...condWarnings);
2599
+ }
2600
+ }
2601
+ }
2602
+ return { warnings, validCount, warnCount: warnings.length };
2603
+ }
2604
+
2605
+ // tools/verify/src/config/types.ts
2606
+ function isAdapterConfig(config) {
2607
+ return "adapter" in config;
2608
+ }
2609
+ function isLegacyConfig(config) {
2610
+ return "messages" in config && !("adapter" in config);
2611
+ }
2612
+
2613
+ // tools/verify/src/analysis/state-space-estimator.ts
2614
+ function typedFieldCardinality(name, obj) {
2615
+ if (!("type" in obj))
2616
+ return null;
2617
+ switch (obj.type) {
2618
+ case "boolean":
2619
+ return { name, cardinality: 2, kind: "boolean" };
2620
+ case "enum":
2621
+ if (Array.isArray(obj.values)) {
2622
+ return { name, cardinality: obj.values.length, kind: "enum" };
2623
+ }
2624
+ return { name, cardinality: "unbounded", kind: "enum" };
2625
+ case "number":
2626
+ if (typeof obj.min === "number" && typeof obj.max === "number") {
2627
+ return { name, cardinality: obj.max - obj.min + 1, kind: "number" };
2628
+ }
2629
+ return { name, cardinality: "unbounded", kind: "number" };
2630
+ case "array":
2631
+ return { name, cardinality: "unbounded", kind: "array" };
2632
+ case "string":
2633
+ return { name, cardinality: "unbounded", kind: "string" };
2634
+ default:
2635
+ return null;
2636
+ }
2637
+ }
2638
+ function legacyFieldCardinality(name, obj) {
2639
+ if ("values" in obj && Array.isArray(obj.values)) {
2640
+ const base = obj.values.length;
2641
+ const extra = obj.abstract === true ? 1 : 0;
2642
+ return {
2643
+ name,
2644
+ cardinality: base + extra,
2645
+ kind: obj.abstract ? "enum (abstract)" : "enum (values)"
2646
+ };
2647
+ }
2648
+ if ("maxLength" in obj && !("type" in obj)) {
2649
+ return { name, cardinality: "unbounded", kind: "array" };
2650
+ }
2651
+ if ("min" in obj && "max" in obj && typeof obj.min === "number" && typeof obj.max === "number" && !("type" in obj)) {
2652
+ return { name, cardinality: obj.max - obj.min + 1, kind: "number" };
2653
+ }
2654
+ return null;
2655
+ }
2656
+ function fieldCardinality(name, value) {
2657
+ if (Array.isArray(value)) {
2658
+ return { name, cardinality: value.length, kind: "enum (literal)" };
2659
+ }
2660
+ if (typeof value !== "object" || value === null) {
2661
+ return { name, cardinality: "unbounded", kind: "unknown" };
2662
+ }
2663
+ const obj = value;
2664
+ return typedFieldCardinality(name, obj) ?? legacyFieldCardinality(name, obj) ?? { name, cardinality: "unbounded", kind: "unknown" };
2665
+ }
2666
+ function permutations(n, k) {
2667
+ if (k > n)
2668
+ return 1;
2669
+ let result = 1;
2670
+ for (let i = 0;i < k; i++) {
2671
+ result *= n - i;
2672
+ }
2673
+ return result;
2674
+ }
2675
+ function getHandlerCount(config, analysis) {
2676
+ if (isLegacyConfig(config)) {
2677
+ const msgs = config.messages;
2678
+ if (msgs.include) {
2679
+ return msgs.include.length;
2680
+ }
2681
+ if (msgs.exclude) {
2682
+ return Math.max(0, analysis.handlers.length - msgs.exclude.length);
2683
+ }
2684
+ }
2685
+ return analysis.handlers.length;
2686
+ }
2687
+ function getMaxInFlight(config) {
2688
+ if (isLegacyConfig(config)) {
2689
+ return config.messages.maxInFlight ?? 1;
2690
+ }
2691
+ if (isAdapterConfig(config)) {
2692
+ return config.bounds?.maxInFlight ?? 1;
2693
+ }
2694
+ return 1;
2695
+ }
2696
+ function getMaxTabs(config) {
2697
+ if (isLegacyConfig(config)) {
2698
+ return config.messages.maxTabs ?? 1;
2699
+ }
2700
+ return 1;
2701
+ }
2702
+ function getFeasibility(states) {
2703
+ if (states < 1e5)
2704
+ return "trivial";
2705
+ if (states <= 1e6)
2706
+ return "feasible";
2707
+ if (states <= 1e7)
2708
+ return "slow";
2709
+ return "infeasible";
2710
+ }
2711
+ function feasibilityLabel(f) {
2712
+ switch (f) {
2713
+ case "trivial":
2714
+ return "trivial (should complete in seconds)";
2715
+ case "feasible":
2716
+ return "feasible (should complete in minutes)";
2717
+ case "slow":
2718
+ return "slow (may take 10–30 minutes)";
2719
+ case "infeasible":
2720
+ return "infeasible (likely won't terminate)";
2721
+ }
2722
+ }
2723
+ function estimateStateSpace(config, analysis) {
2724
+ const state = config.state;
2725
+ const fields = [];
2726
+ const warnings = [];
2727
+ const suggestions = [];
2728
+ for (const [name, value] of Object.entries(state)) {
2729
+ fields.push(fieldCardinality(name, value));
2730
+ }
2731
+ const unboundedFields = fields.filter((f) => f.cardinality === "unbounded");
2732
+ const boundedFields = fields.filter((f) => f.cardinality !== "unbounded");
2733
+ const fieldProduct = boundedFields.length > 0 ? boundedFields.reduce((acc, f) => acc * f.cardinality, 1) : 1;
2734
+ if (unboundedFields.length > 0) {
2735
+ warnings.push(`${unboundedFields.length} field(s) have unbounded domains: ${unboundedFields.map((f) => f.name).join(", ")}`);
2736
+ suggestions.push(`Fields ${unboundedFields.map((f) => f.name).join(", ")} have unbounded domains — their actual impact depends on handler logic`);
2737
+ }
2738
+ const handlerCount = getHandlerCount(config, analysis);
2739
+ const maxInFlight = getMaxInFlight(config);
2740
+ const maxTabs = getMaxTabs(config);
2741
+ const contextCount = maxTabs + 1;
2742
+ const totalStateSpace = fieldProduct ** contextCount;
2743
+ const interleavingFactor = permutations(handlerCount, maxInFlight);
2744
+ const estimatedStates = totalStateSpace * interleavingFactor;
2745
+ const feasibility = getFeasibility(estimatedStates);
2746
+ if (handlerCount > 15) {
2747
+ suggestions.push("Consider splitting into subsystems");
2748
+ }
2749
+ for (const f of boundedFields) {
2750
+ if (f.cardinality > 50) {
2751
+ suggestions.push(`Consider reducing bounds for field "${f.name}" (${f.cardinality} values)`);
2752
+ }
2753
+ }
2754
+ if (maxInFlight > 2) {
2755
+ const reducedInterleaving = permutations(handlerCount, 2);
2756
+ const reduction = reducedInterleaving > 0 ? Math.round(interleavingFactor / reducedInterleaving) : 1;
2757
+ suggestions.push(`Reducing maxInFlight from ${maxInFlight} to 2 would reduce interleaving by ~${reduction}x`);
2758
+ }
2759
+ return {
2760
+ fields,
2761
+ fieldProduct,
2762
+ handlerCount,
2763
+ maxInFlight,
2764
+ contextCount,
2765
+ totalStateSpace,
2766
+ interleavingFactor,
2767
+ estimatedStates,
2768
+ feasibility,
2769
+ warnings,
2770
+ suggestions
2771
+ };
2772
+ }
2773
+
2369
2774
  // tools/verify/src/codegen/config.ts
2370
2775
  class ConfigGenerator {
2371
2776
  lines = [];
@@ -3717,13 +4122,14 @@ class HandlerExtractor {
3717
4122
  const messageTypes = new Set;
3718
4123
  const invalidMessageTypes = new Set;
3719
4124
  const stateConstraints = [];
4125
+ const globalStateConstraints = [];
3720
4126
  const verifiedStates = [];
3721
4127
  this.warnings = [];
3722
4128
  const allSourceFiles = this.project.getSourceFiles();
3723
4129
  const entryPoints = allSourceFiles.filter((f) => this.isWithinPackage(f.getFilePath()));
3724
4130
  this.debugLogSourceFiles(allSourceFiles, entryPoints);
3725
4131
  for (const entryPoint of entryPoints) {
3726
- this.analyzeFileAndImports(entryPoint, handlers, messageTypes, invalidMessageTypes, stateConstraints, verifiedStates);
4132
+ this.analyzeFileAndImports(entryPoint, handlers, messageTypes, invalidMessageTypes, stateConstraints, globalStateConstraints, verifiedStates);
3727
4133
  }
3728
4134
  if (verifiedStates.length > 0) {
3729
4135
  if (process.env["POLLY_DEBUG"]) {
@@ -3753,11 +4159,12 @@ class HandlerExtractor {
3753
4159
  handlers,
3754
4160
  messageTypes,
3755
4161
  stateConstraints,
4162
+ globalStateConstraints,
3756
4163
  verifiedStates,
3757
4164
  warnings: this.warnings
3758
4165
  };
3759
4166
  }
3760
- analyzeFileAndImports(sourceFile, handlers, messageTypes, invalidMessageTypes, stateConstraints, verifiedStates) {
4167
+ analyzeFileAndImports(sourceFile, handlers, messageTypes, invalidMessageTypes, stateConstraints, globalStateConstraints, verifiedStates) {
3761
4168
  const filePath = sourceFile.getFilePath();
3762
4169
  if (this.analyzedFiles.has(filePath)) {
3763
4170
  return;
@@ -3771,6 +4178,8 @@ class HandlerExtractor {
3771
4178
  this.categorizeHandlerMessageTypes(fileHandlers, messageTypes, invalidMessageTypes);
3772
4179
  const fileConstraints = this.extractStateConstraintsFromFile(sourceFile);
3773
4180
  stateConstraints.push(...fileConstraints);
4181
+ const fileGlobalConstraints = this.extractGlobalStateConstraintsFromFile(sourceFile);
4182
+ globalStateConstraints.push(...fileGlobalConstraints);
3774
4183
  const fileVerifiedStates = this.extractVerifiedStatesFromFile(sourceFile);
3775
4184
  verifiedStates.push(...fileVerifiedStates);
3776
4185
  const importDeclarations = sourceFile.getImportDeclarations();
@@ -3784,7 +4193,7 @@ class HandlerExtractor {
3784
4193
  }
3785
4194
  continue;
3786
4195
  }
3787
- this.analyzeFileAndImports(importedFile, handlers, messageTypes, invalidMessageTypes, stateConstraints, verifiedStates);
4196
+ this.analyzeFileAndImports(importedFile, handlers, messageTypes, invalidMessageTypes, stateConstraints, globalStateConstraints, verifiedStates);
3788
4197
  } else if (process.env["POLLY_DEBUG"]) {
3789
4198
  const specifier = importDecl.getModuleSpecifierValue();
3790
4199
  if (!specifier.startsWith("node:") && !this.isNodeModuleImport(specifier)) {
@@ -5119,6 +5528,81 @@ class HandlerExtractor {
5119
5528
  });
5120
5529
  return constraints;
5121
5530
  }
5531
+ extractGlobalStateConstraintsFromFile(sourceFile) {
5532
+ const constraints = [];
5533
+ const filePath = sourceFile.getFilePath();
5534
+ sourceFile.forEachDescendant((node) => {
5535
+ const constraint = this.recognizeGlobalStateConstraint(node, filePath);
5536
+ if (constraint) {
5537
+ constraints.push(constraint);
5538
+ }
5539
+ });
5540
+ return constraints;
5541
+ }
5542
+ recognizeGlobalStateConstraint(node, filePath) {
5543
+ if (!Node2.isCallExpression(node)) {
5544
+ return null;
5545
+ }
5546
+ const expression = node.getExpression();
5547
+ if (!Node2.isIdentifier(expression)) {
5548
+ return null;
5549
+ }
5550
+ const functionName = expression.getText();
5551
+ if (functionName !== "stateConstraint") {
5552
+ return null;
5553
+ }
5554
+ const args = node.getArguments();
5555
+ if (args.length < 2) {
5556
+ return null;
5557
+ }
5558
+ const nameArg = args[0];
5559
+ if (!Node2.isStringLiteral(nameArg)) {
5560
+ return null;
5561
+ }
5562
+ const name = nameArg.getLiteralValue();
5563
+ const predicateArg = args[1];
5564
+ if (!Node2.isArrowFunction(predicateArg)) {
5565
+ return null;
5566
+ }
5567
+ const body = predicateArg.getBody();
5568
+ let expressionText;
5569
+ if (Node2.isBlock(body)) {
5570
+ const returnStatement = body.getStatements().find((s) => Node2.isReturnStatement(s));
5571
+ if (!returnStatement || !Node2.isReturnStatement(returnStatement)) {
5572
+ return null;
5573
+ }
5574
+ const returnExpr = returnStatement.getExpression();
5575
+ if (!returnExpr) {
5576
+ return null;
5577
+ }
5578
+ expressionText = returnExpr.getText();
5579
+ } else {
5580
+ expressionText = body.getText();
5581
+ }
5582
+ let message;
5583
+ if (args.length >= 3) {
5584
+ const optionsArg = args[2];
5585
+ if (Node2.isObjectLiteralExpression(optionsArg)) {
5586
+ for (const prop of optionsArg.getProperties()) {
5587
+ if (Node2.isPropertyAssignment(prop) && prop.getName() === "message") {
5588
+ const value = prop.getInitializer();
5589
+ if (value && Node2.isStringLiteral(value)) {
5590
+ message = value.getLiteralValue();
5591
+ }
5592
+ }
5593
+ }
5594
+ }
5595
+ }
5596
+ return {
5597
+ name,
5598
+ expression: expressionText,
5599
+ message,
5600
+ location: {
5601
+ file: filePath,
5602
+ line: node.getStartLineNumber()
5603
+ }
5604
+ };
5605
+ }
5122
5606
  recognizeStateConstraint(node, filePath) {
5123
5607
  if (!Node2.isCallExpression(node)) {
5124
5608
  return [];
@@ -5433,7 +5917,8 @@ class TypeExtractor {
5433
5917
  messageTypes: validMessageTypes,
5434
5918
  fields,
5435
5919
  handlers: completeHandlers,
5436
- stateConstraints: handlerAnalysis.stateConstraints
5920
+ stateConstraints: handlerAnalysis.stateConstraints,
5921
+ globalStateConstraints: handlerAnalysis.globalStateConstraints
5437
5922
  };
5438
5923
  }
5439
5924
  extractHandlerAnalysis() {
@@ -5864,6 +6349,10 @@ async function main() {
5864
6349
  case "validate":
5865
6350
  await validateCommand();
5866
6351
  break;
6352
+ case "--estimate":
6353
+ case "estimate":
6354
+ await estimateCommand();
6355
+ break;
5867
6356
  case "--help":
5868
6357
  case "help":
5869
6358
  showHelp();
@@ -5980,6 +6469,70 @@ async function validateCommand() {
5980
6469
  `, COLORS.red));
5981
6470
  process.exit(1);
5982
6471
  }
6472
+ async function estimateCommand() {
6473
+ const configPath = path4.join(process.cwd(), "specs", "verification.config.ts");
6474
+ console.log(color(`
6475
+ \uD83D\uDCCA Estimating state space...
6476
+ `, COLORS.blue));
6477
+ const validation = validateConfig(configPath);
6478
+ if (!validation.valid) {
6479
+ const errors = validation.issues.filter((i) => i.severity === "error");
6480
+ console.log(color(`❌ Configuration incomplete (${errors.length} error(s))
6481
+ `, COLORS.red));
6482
+ for (const error of errors.slice(0, 3)) {
6483
+ console.log(color(` • ${error.message}`, COLORS.red));
6484
+ }
6485
+ console.log(`
6486
+ Run 'polly verify --validate' to see all issues`);
6487
+ process.exit(1);
6488
+ }
6489
+ const config = await loadVerificationConfig(configPath);
6490
+ const analysis = await runCodebaseAnalysis();
6491
+ const typedConfig = config;
6492
+ const typedAnalysis = analysis;
6493
+ const exprValidation = validateExpressions(typedAnalysis.handlers, typedConfig.state);
6494
+ if (exprValidation.warnings.length > 0) {
6495
+ displayExpressionWarnings(exprValidation);
6496
+ }
6497
+ const estimate = estimateStateSpace(typedConfig, typedAnalysis);
6498
+ displayEstimate(estimate);
6499
+ }
6500
+ function displayEstimate(estimate) {
6501
+ console.log(color(`State space estimate:
6502
+ `, COLORS.blue));
6503
+ console.log(color(" Fields:", COLORS.blue));
6504
+ for (const field of estimate.fields) {
6505
+ const name = field.name.padEnd(28);
6506
+ const kind = field.kind.padEnd(18);
6507
+ const card = field.cardinality === "unbounded" ? color("(excluded — unbounded)", COLORS.yellow) : `${field.cardinality} values`;
6508
+ console.log(` ${name} ${kind} ${card}`);
6509
+ }
6510
+ console.log();
6511
+ console.log(` Field combinations: ${color(String(estimate.fieldProduct), COLORS.green)}`);
6512
+ console.log(` Handlers: ${estimate.handlerCount}`);
6513
+ console.log(` Max in-flight: ${estimate.maxInFlight}`);
6514
+ console.log(` Contexts: ${estimate.contextCount} (${estimate.contextCount - 1} tab${estimate.contextCount - 1 !== 1 ? "s" : ""} + background)`);
6515
+ console.log(` Interleaving factor: ${estimate.interleavingFactor}`);
6516
+ console.log();
6517
+ console.log(` Estimated states: ${color(`~${estimate.estimatedStates.toLocaleString()}`, COLORS.green)}`);
6518
+ const feasColor = estimate.feasibility === "trivial" || estimate.feasibility === "feasible" ? COLORS.green : estimate.feasibility === "slow" ? COLORS.yellow : COLORS.red;
6519
+ console.log();
6520
+ console.log(` Assessment: ${color(feasibilityLabel(estimate.feasibility), feasColor)}`);
6521
+ if (estimate.warnings.length > 0) {
6522
+ console.log();
6523
+ for (const w of estimate.warnings) {
6524
+ console.log(color(` ⚠️ ${w}`, COLORS.yellow));
6525
+ }
6526
+ }
6527
+ if (estimate.suggestions.length > 0) {
6528
+ console.log();
6529
+ console.log(color(" Suggestions:", COLORS.blue));
6530
+ for (const s of estimate.suggestions) {
6531
+ console.log(color(` • ${s}`, COLORS.gray));
6532
+ }
6533
+ }
6534
+ console.log();
6535
+ }
5983
6536
  function displayValidationErrors(errors) {
5984
6537
  if (errors.length === 0)
5985
6538
  return;
@@ -6011,6 +6564,19 @@ function displayValidationWarnings(warnings) {
6011
6564
  console.log();
6012
6565
  }
6013
6566
  }
6567
+ function displayExpressionWarnings(result) {
6568
+ if (result.warnings.length === 0)
6569
+ return;
6570
+ console.log(color(`⚠️ Found ${result.warnCount} expression warning(s):
6571
+ `, COLORS.yellow));
6572
+ for (const w of result.warnings) {
6573
+ console.log(color(` ⚠ ${w.message}`, COLORS.yellow));
6574
+ console.log(color(` ${w.expression}`, COLORS.gray));
6575
+ console.log(color(` at ${w.location.file}:${w.location.line}`, COLORS.gray));
6576
+ console.log(color(` → ${w.suggestion}`, COLORS.yellow));
6577
+ console.log();
6578
+ }
6579
+ }
6014
6580
  async function verifyCommand() {
6015
6581
  const configPath = path4.join(process.cwd(), "specs", "verification.config.ts");
6016
6582
  console.log(color(`
@@ -6073,6 +6639,12 @@ function getMaxDepth(config) {
6073
6639
  async function runFullVerification(configPath) {
6074
6640
  const config = await loadVerificationConfig(configPath);
6075
6641
  const analysis = await runCodebaseAnalysis();
6642
+ const typedConfig = config;
6643
+ const typedAnalysis = analysis;
6644
+ const exprValidation = validateExpressions(typedAnalysis.handlers, typedConfig.state);
6645
+ if (exprValidation.warnings.length > 0) {
6646
+ displayExpressionWarnings(exprValidation);
6647
+ }
6076
6648
  const { specPath, specDir } = await generateAndWriteTLASpecs(config, analysis);
6077
6649
  findAndCopyBaseSpec(specDir);
6078
6650
  console.log(color("✓ Specification generated", COLORS.green));
@@ -6260,6 +6832,9 @@ ${color("Commands:", COLORS.blue)}
6260
6832
  ${color("bun verify --validate", COLORS.green)}
6261
6833
  Validate existing configuration without running verification
6262
6834
 
6835
+ ${color("bun verify --estimate", COLORS.green)}
6836
+ Estimate state space without running TLC
6837
+
6263
6838
  ${color("bun verify --help", COLORS.green)}
6264
6839
  Show this help message
6265
6840
 
@@ -6314,4 +6889,4 @@ main().catch((error) => {
6314
6889
  process.exit(1);
6315
6890
  });
6316
6891
 
6317
- //# debugId=0CD51D5DC046B30964756E2164756E21
6892
+ //# debugId=35FC1B345D5CADC664756E2164756E21