@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.
@@ -61,4 +61,4 @@ type UnifiedVerificationConfig = LegacyVerificationConfig | AdapterVerificationC
61
61
  * ```
62
62
  */
63
63
  export declare function defineVerification<T extends UnifiedVerificationConfig>(config: T): T;
64
- export { $constraints, ensures, hasLength, inRange, oneOf, requires } from "./primitives/index.js";
64
+ export { $constraints, ensures, hasLength, inRange, oneOf, requires, stateConstraint, } from "./primitives/index.js";
@@ -57,6 +57,7 @@ function $constraints(stateField, constraints, options) {
57
57
  }).catch(() => {});
58
58
  }
59
59
  }
60
+ function stateConstraint(name, predicate, options) {}
60
61
 
61
62
  // tools/verify/src/config.ts
62
63
  function defineVerification(config) {
@@ -80,6 +81,7 @@ function defineVerification(config) {
80
81
  return config;
81
82
  }
82
83
  export {
84
+ stateConstraint,
83
85
  requires,
84
86
  oneOf,
85
87
  inRange,
@@ -89,4 +91,4 @@ export {
89
91
  $constraints
90
92
  };
91
93
 
92
- //# debugId=C9D97C5927FA335964756E2164756E21
94
+ //# debugId=465F57F5AB1A6BBD64756E2164756E21
@@ -2,10 +2,10 @@
2
2
  "version": 3,
3
3
  "sources": ["../tools/verify/src/primitives/index.ts", "../tools/verify/src/config.ts"],
4
4
  "sourcesContent": [
5
- "// Verification primitives for formal verification\n// These are runtime no-ops but extracted during verification\n\n/**\n * Assert a precondition that must be true when the handler executes.\n *\n * In production: No-op (compiled away)\n * In verification: Translated to TLA+ assertion\n *\n * @example\n * messageBus.on(\"USER_LOGIN\", (payload) => {\n * requires(state.user.loggedIn === false, \"User must not be logged in\")\n * state.user.loggedIn = true\n * })\n */\nexport function requires(condition: boolean, message?: string): void {\n // Runtime no-op - only used during verification\n // Condition and message are checked during static analysis only\n void condition;\n void message;\n}\n\n/**\n * Assert a postcondition that must be true after the handler completes.\n *\n * In production: No-op (compiled away)\n * In verification: Translated to TLA+ assertion\n *\n * @example\n * messageBus.on(\"USER_LOGIN\", (payload) => {\n * state.user.loggedIn = true\n * ensures(state.user.loggedIn === true, \"User must be logged in\")\n * })\n */\nexport function ensures(condition: boolean, message?: string): void {\n // Runtime no-op - only used during verification\n // Condition and message are checked during static analysis only\n void condition;\n void message;\n}\n\n/**\n * Define a global invariant that must always hold.\n *\n * In production: No-op (compiled away)\n * In verification: Translated to TLA+ invariant\n *\n * @example\n * invariant(\"UserIdConsistent\", () =>\n * state.user.loggedIn === false || state.user.id !== null\n * )\n */\nexport function invariant(_name: string, condition: () => boolean): void {\n // Runtime no-op - only used during verification\n // Name and condition are checked during static analysis only\n void condition;\n}\n\n/**\n * Assert that a value is within a valid range.\n *\n * @example\n * requires(inRange(todoCount, 0, 100), \"Todo count must be 0-100\")\n */\nexport function inRange(value: number, min: number, max: number): boolean {\n return value >= min && value <= max;\n}\n\n/**\n * Assert that a value is one of the allowed values.\n *\n * @example\n * requires(oneOf(state.user.role, [\"admin\", \"user\"]), \"Role must be admin or user\")\n */\nexport function oneOf<T>(value: T, allowed: T[]): boolean {\n return allowed.includes(value);\n}\n\n/**\n * Assert that an array has a specific length constraint.\n *\n * @example\n * requires(hasLength(state.todos, { max: 10 }), \"Too many todos\")\n */\nexport function hasLength(array: unknown[], constraint: { min?: number; max?: number }): boolean {\n if (constraint.min !== undefined && array.length < constraint.min) return false;\n if (constraint.max !== undefined && array.length > constraint.max) return false;\n return true;\n}\n\n/**\n * Declare state-level constraints for verification and optional runtime checking.\n * Maps message types to preconditions on state fields.\n *\n * The parser automatically wires these constraints to handlers during verification.\n * Optionally, constraints can be enforced at runtime by passing `{ runtime: true }`.\n *\n * @example\n * // Verification only (TLA+ generation)\n * const state = { loggedIn: false };\n *\n * $constraints(\"loggedIn\", {\n * USER_LOGOUT: { requires: \"state.loggedIn === true\", message: \"Must be logged in\" },\n * BOOKMARK_ADD: { requires: \"state.loggedIn === true\", message: \"Must be logged in\" },\n * });\n *\n * @example\n * // Runtime enforcement (function predicates)\n * $constraints(\"loggedIn\", {\n * USER_LOGOUT: {\n * requires: (state) => state.loggedIn === true,\n * message: \"Must be logged in to logout\"\n * },\n * }, { runtime: true });\n */\nexport function $constraints(\n stateField: string,\n constraints: Record<\n string,\n {\n requires?: string | ((state: unknown) => boolean);\n ensures?: string | ((state: unknown) => boolean);\n message?: string;\n }\n >,\n options?: { runtime?: boolean }\n): void {\n // Register constraints for runtime checking if enabled\n if (options?.runtime) {\n // Import dynamically to avoid circular dependencies\n // This is safe because it only happens at runtime, not during static analysis\n // @ts-expect-error - Dynamic import path resolves correctly at runtime\n import(\"../../../src/shared/lib/constraints.js\")\n .then(({ registerConstraints }) => {\n registerConstraints(stateField, constraints);\n })\n .catch(() => {\n // Silently ignore - constraints module may not be available during static analysis\n });\n }\n\n // For verification: Still a no-op at runtime\n // Parser extracts these and wires them to TLA+ handlers\n}\n\n// Re-export for convenience\nexport const verify = {\n requires,\n ensures,\n invariant,\n inRange,\n oneOf,\n hasLength,\n $constraints,\n};\n",
6
- "// ═══════════════════════════════════════════════════════════════\n// Configuration Helper for @fairfox/polly/verify\n// ═══════════════════════════════════════════════════════════════\n//\n// Lightweight entry point for user configuration files.\n// Does NOT include heavy dependencies (ts-morph, analysis, etc.)\n// which are only needed by the CLI tool.\n\n// ─────────────────────────────────────────────────────────────────\n// Configuration Types (inlined to avoid heavy dependencies)\n// ─────────────────────────────────────────────────────────────────\n\n// Legacy verification configuration\ninterface LegacyVerificationConfig {\n state: Record<string, unknown>;\n messages: {\n // Basic bounds\n maxInFlight?: number;\n maxTabs?: number;\n maxClients?: number;\n maxRenderers?: number;\n maxWorkers?: number;\n maxContexts?: number;\n\n // Tier 1 Optimizations (no precision loss)\n include?: string[]; // Only verify these message types\n exclude?: string[]; // Exclude these message types (mutually exclusive with include)\n symmetry?: string[][]; // Groups of symmetric message types [[type1, type2], [type3, type4]]\n perMessageBounds?: Record<string, number>; // Different maxInFlight per message type\n };\n onBuild?: \"warn\" | \"error\" | \"off\";\n onRelease?: \"warn\" | \"error\" | \"off\";\n\n // Verification engine options\n verification?: {\n timeout?: number; // Timeout in seconds (0 = no timeout)\n workers?: number; // Number of TLC workers\n };\n\n // Tier 2 Optimizations (controlled approximations)\n tier2?: {\n // Temporal constraints: ordering requirements between messages\n temporalConstraints?: Array<{\n before: string; // Message type that must occur first\n after: string; // Message type that must occur after\n description?: string; // Human-readable description\n }>;\n\n // Bounded exploration: limit depth for specific scenarios\n boundedExploration?: {\n maxDepth?: number; // Maximum state depth to explore\n criticalPaths?: string[][]; // Sequences of message types that must be fully explored\n };\n };\n}\n\n// Adapter-based configuration (for future use)\ninterface AdapterVerificationConfig {\n adapter: unknown; // Adapter interface not exported to avoid heavy deps\n state: Record<string, unknown>;\n bounds?: {\n maxInFlight?: number;\n [key: string]: unknown;\n };\n onBuild?: \"warn\" | \"error\" | \"off\";\n}\n\n// Union type for both config formats\ntype UnifiedVerificationConfig = LegacyVerificationConfig | AdapterVerificationConfig;\n\n/**\n * Define verification configuration with type checking\n *\n * Used in generated verification.config.ts files.\n *\n * @example\n * ```typescript\n * import { defineVerification } from '@fairfox/polly/verify'\n *\n * export default defineVerification({\n * state: {\n * \"user.role\": { type: \"enum\", values: [\"admin\", \"user\", \"guest\"] },\n * },\n * messages: {\n * maxInFlight: 6,\n * maxTabs: 2,\n * },\n * })\n * ```\n */\nexport function defineVerification<T extends UnifiedVerificationConfig>(config: T): T {\n // Validate configuration structure\n if (\"adapter\" in config) {\n // New adapter-based format\n if (!config.adapter) {\n throw new Error(\"Configuration must include an adapter\");\n }\n if (!config.state) {\n throw new Error(\"Configuration must include state bounds\");\n }\n } else if (\"messages\" in config) {\n // Legacy format\n if (!config.state) {\n throw new Error(\"Configuration must include state bounds\");\n }\n if (!config.messages) {\n throw new Error(\"Legacy configuration must include messages bounds\");\n }\n } else {\n throw new Error(\n \"Invalid configuration format. Must include either 'adapter' (new format) or 'messages' (legacy format)\"\n );\n }\n\n return config;\n}\n\n// Re-export verification primitives for user code\nexport { $constraints, ensures, hasLength, inRange, oneOf, requires } from \"./primitives/index.js\";\n"
5
+ "// Verification primitives for formal verification\n// These are runtime no-ops but extracted during verification\n\n/**\n * Assert a precondition that must be true when the handler executes.\n *\n * In production: No-op (compiled away)\n * In verification: Translated to TLA+ assertion\n *\n * @example\n * messageBus.on(\"USER_LOGIN\", (payload) => {\n * requires(state.user.loggedIn === false, \"User must not be logged in\")\n * state.user.loggedIn = true\n * })\n */\nexport function requires(condition: boolean, message?: string): void {\n // Runtime no-op - only used during verification\n // Condition and message are checked during static analysis only\n void condition;\n void message;\n}\n\n/**\n * Assert a postcondition that must be true after the handler completes.\n *\n * In production: No-op (compiled away)\n * In verification: Translated to TLA+ assertion\n *\n * @example\n * messageBus.on(\"USER_LOGIN\", (payload) => {\n * state.user.loggedIn = true\n * ensures(state.user.loggedIn === true, \"User must be logged in\")\n * })\n */\nexport function ensures(condition: boolean, message?: string): void {\n // Runtime no-op - only used during verification\n // Condition and message are checked during static analysis only\n void condition;\n void message;\n}\n\n/**\n * Define a global invariant that must always hold.\n *\n * In production: No-op (compiled away)\n * In verification: Translated to TLA+ invariant\n *\n * @example\n * invariant(\"UserIdConsistent\", () =>\n * state.user.loggedIn === false || state.user.id !== null\n * )\n */\nexport function invariant(_name: string, condition: () => boolean): void {\n // Runtime no-op - only used during verification\n // Name and condition are checked during static analysis only\n void condition;\n}\n\n/**\n * Assert that a value is within a valid range.\n *\n * @example\n * requires(inRange(todoCount, 0, 100), \"Todo count must be 0-100\")\n */\nexport function inRange(value: number, min: number, max: number): boolean {\n return value >= min && value <= max;\n}\n\n/**\n * Assert that a value is one of the allowed values.\n *\n * @example\n * requires(oneOf(state.user.role, [\"admin\", \"user\"]), \"Role must be admin or user\")\n */\nexport function oneOf<T>(value: T, allowed: T[]): boolean {\n return allowed.includes(value);\n}\n\n/**\n * Assert that an array has a specific length constraint.\n *\n * @example\n * requires(hasLength(state.todos, { max: 10 }), \"Too many todos\")\n */\nexport function hasLength(array: unknown[], constraint: { min?: number; max?: number }): boolean {\n if (constraint.min !== undefined && array.length < constraint.min) return false;\n if (constraint.max !== undefined && array.length > constraint.max) return false;\n return true;\n}\n\n/**\n * Declare state-level constraints for verification and optional runtime checking.\n * Maps message types to preconditions on state fields.\n *\n * The parser automatically wires these constraints to handlers during verification.\n * Optionally, constraints can be enforced at runtime by passing `{ runtime: true }`.\n *\n * @example\n * // Verification only (TLA+ generation)\n * const state = { loggedIn: false };\n *\n * $constraints(\"loggedIn\", {\n * USER_LOGOUT: { requires: \"state.loggedIn === true\", message: \"Must be logged in\" },\n * BOOKMARK_ADD: { requires: \"state.loggedIn === true\", message: \"Must be logged in\" },\n * });\n *\n * @example\n * // Runtime enforcement (function predicates)\n * $constraints(\"loggedIn\", {\n * USER_LOGOUT: {\n * requires: (state) => state.loggedIn === true,\n * message: \"Must be logged in to logout\"\n * },\n * }, { runtime: true });\n */\nexport function $constraints(\n stateField: string,\n constraints: Record<\n string,\n {\n requires?: string | ((state: unknown) => boolean);\n ensures?: string | ((state: unknown) => boolean);\n message?: string;\n }\n >,\n options?: { runtime?: boolean }\n): void {\n // Register constraints for runtime checking if enabled\n if (options?.runtime) {\n // Import dynamically to avoid circular dependencies\n // This is safe because it only happens at runtime, not during static analysis\n // @ts-expect-error - Dynamic import path resolves correctly at runtime\n import(\"../../../src/shared/lib/constraints.js\")\n .then(({ registerConstraints }) => {\n registerConstraints(stateField, constraints);\n })\n .catch(() => {\n // Silently ignore - constraints module may not be available during static analysis\n });\n }\n\n // For verification: Still a no-op at runtime\n // Parser extracts these and wires them to TLA+ handlers\n}\n\n/**\n * Declare a global state constraint that prunes structurally impossible states.\n *\n * In production: No-op (compiled away)\n * In verification: Translated to TLC CONSTRAINT clause, discarding states\n * that violate the predicate from the exploration queue entirely.\n *\n * Unlike `invariant()` (which checks but still explores), `stateConstraint()`\n * prevents the model checker from ever reaching the pruned states.\n *\n * @example\n * stateConstraint(\"LeaderRequiresConnection\", () =>\n * !connectionState.value.isLeader || connectionState.value.status === \"connected\"\n * )\n */\nexport function stateConstraint(\n name: string,\n predicate: () => boolean,\n options?: { message?: string }\n): void {\n void name;\n void predicate;\n void options;\n}\n\n// Re-export for convenience\nexport const verify = {\n requires,\n ensures,\n invariant,\n inRange,\n oneOf,\n hasLength,\n $constraints,\n stateConstraint,\n};\n",
6
+ "// ═══════════════════════════════════════════════════════════════\n// Configuration Helper for @fairfox/polly/verify\n// ═══════════════════════════════════════════════════════════════\n//\n// Lightweight entry point for user configuration files.\n// Does NOT include heavy dependencies (ts-morph, analysis, etc.)\n// which are only needed by the CLI tool.\n\n// ─────────────────────────────────────────────────────────────────\n// Configuration Types (inlined to avoid heavy dependencies)\n// ─────────────────────────────────────────────────────────────────\n\n// Legacy verification configuration\ninterface LegacyVerificationConfig {\n state: Record<string, unknown>;\n messages: {\n // Basic bounds\n maxInFlight?: number;\n maxTabs?: number;\n maxClients?: number;\n maxRenderers?: number;\n maxWorkers?: number;\n maxContexts?: number;\n\n // Tier 1 Optimizations (no precision loss)\n include?: string[]; // Only verify these message types\n exclude?: string[]; // Exclude these message types (mutually exclusive with include)\n symmetry?: string[][]; // Groups of symmetric message types [[type1, type2], [type3, type4]]\n perMessageBounds?: Record<string, number>; // Different maxInFlight per message type\n };\n onBuild?: \"warn\" | \"error\" | \"off\";\n onRelease?: \"warn\" | \"error\" | \"off\";\n\n // Verification engine options\n verification?: {\n timeout?: number; // Timeout in seconds (0 = no timeout)\n workers?: number; // Number of TLC workers\n };\n\n // Tier 2 Optimizations (controlled approximations)\n tier2?: {\n // Temporal constraints: ordering requirements between messages\n temporalConstraints?: Array<{\n before: string; // Message type that must occur first\n after: string; // Message type that must occur after\n description?: string; // Human-readable description\n }>;\n\n // Bounded exploration: limit depth for specific scenarios\n boundedExploration?: {\n maxDepth?: number; // Maximum state depth to explore\n criticalPaths?: string[][]; // Sequences of message types that must be fully explored\n };\n };\n}\n\n// Adapter-based configuration (for future use)\ninterface AdapterVerificationConfig {\n adapter: unknown; // Adapter interface not exported to avoid heavy deps\n state: Record<string, unknown>;\n bounds?: {\n maxInFlight?: number;\n [key: string]: unknown;\n };\n onBuild?: \"warn\" | \"error\" | \"off\";\n}\n\n// Union type for both config formats\ntype UnifiedVerificationConfig = LegacyVerificationConfig | AdapterVerificationConfig;\n\n/**\n * Define verification configuration with type checking\n *\n * Used in generated verification.config.ts files.\n *\n * @example\n * ```typescript\n * import { defineVerification } from '@fairfox/polly/verify'\n *\n * export default defineVerification({\n * state: {\n * \"user.role\": { type: \"enum\", values: [\"admin\", \"user\", \"guest\"] },\n * },\n * messages: {\n * maxInFlight: 6,\n * maxTabs: 2,\n * },\n * })\n * ```\n */\nexport function defineVerification<T extends UnifiedVerificationConfig>(config: T): T {\n // Validate configuration structure\n if (\"adapter\" in config) {\n // New adapter-based format\n if (!config.adapter) {\n throw new Error(\"Configuration must include an adapter\");\n }\n if (!config.state) {\n throw new Error(\"Configuration must include state bounds\");\n }\n } else if (\"messages\" in config) {\n // Legacy format\n if (!config.state) {\n throw new Error(\"Configuration must include state bounds\");\n }\n if (!config.messages) {\n throw new Error(\"Legacy configuration must include messages bounds\");\n }\n } else {\n throw new Error(\n \"Invalid configuration format. Must include either 'adapter' (new format) or 'messages' (legacy format)\"\n );\n }\n\n return config;\n}\n\n// Re-export verification primitives for user code\nexport {\n $constraints,\n ensures,\n hasLength,\n inRange,\n oneOf,\n requires,\n stateConstraint,\n} from \"./primitives/index.js\";\n"
7
7
  ],
8
- "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAeO,SAAS,QAAQ,CAAC,WAAoB,SAAwB;AAmB9D,SAAS,OAAO,CAAC,WAAoB,SAAwB;AA8B7D,SAAS,OAAO,CAAC,OAAe,KAAa,KAAsB;AAAA,EACxE,OAAO,SAAS,OAAO,SAAS;AAAA;AAS3B,SAAS,KAAQ,CAAC,OAAU,SAAuB;AAAA,EACxD,OAAO,QAAQ,SAAS,KAAK;AAAA;AASxB,SAAS,SAAS,CAAC,OAAkB,YAAqD;AAAA,EAC/F,IAAI,WAAW,QAAQ,aAAa,MAAM,SAAS,WAAW;AAAA,IAAK,OAAO;AAAA,EAC1E,IAAI,WAAW,QAAQ,aAAa,MAAM,SAAS,WAAW;AAAA,IAAK,OAAO;AAAA,EAC1E,OAAO;AAAA;AA4BF,SAAS,YAAY,CAC1B,YACA,aAQA,SACM;AAAA,EAEN,IAAI,SAAS,SAAS;AAAA,IAIb,iDACJ,KAAK,GAAG,0BAA0B;AAAA,MACjC,oBAAoB,YAAY,WAAW;AAAA,KAC5C,EACA,MAAM,MAAM,EAEZ;AAAA,EACL;AAAA;;;ACjDK,SAAS,kBAAuD,CAAC,QAAc;AAAA,EAEpF,IAAI,aAAa,QAAQ;AAAA,IAEvB,IAAI,CAAC,OAAO,SAAS;AAAA,MACnB,MAAM,IAAI,MAAM,uCAAuC;AAAA,IACzD;AAAA,IACA,IAAI,CAAC,OAAO,OAAO;AAAA,MACjB,MAAM,IAAI,MAAM,yCAAyC;AAAA,IAC3D;AAAA,EACF,EAAO,SAAI,cAAc,QAAQ;AAAA,IAE/B,IAAI,CAAC,OAAO,OAAO;AAAA,MACjB,MAAM,IAAI,MAAM,yCAAyC;AAAA,IAC3D;AAAA,IACA,IAAI,CAAC,OAAO,UAAU;AAAA,MACpB,MAAM,IAAI,MAAM,mDAAmD;AAAA,IACrE;AAAA,EACF,EAAO;AAAA,IACL,MAAM,IAAI,MACR,wGACF;AAAA;AAAA,EAGF,OAAO;AAAA;",
9
- "debugId": "C9D97C5927FA335964756E2164756E21",
8
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAeO,SAAS,QAAQ,CAAC,WAAoB,SAAwB;AAmB9D,SAAS,OAAO,CAAC,WAAoB,SAAwB;AA8B7D,SAAS,OAAO,CAAC,OAAe,KAAa,KAAsB;AAAA,EACxE,OAAO,SAAS,OAAO,SAAS;AAAA;AAS3B,SAAS,KAAQ,CAAC,OAAU,SAAuB;AAAA,EACxD,OAAO,QAAQ,SAAS,KAAK;AAAA;AASxB,SAAS,SAAS,CAAC,OAAkB,YAAqD;AAAA,EAC/F,IAAI,WAAW,QAAQ,aAAa,MAAM,SAAS,WAAW;AAAA,IAAK,OAAO;AAAA,EAC1E,IAAI,WAAW,QAAQ,aAAa,MAAM,SAAS,WAAW;AAAA,IAAK,OAAO;AAAA,EAC1E,OAAO;AAAA;AA4BF,SAAS,YAAY,CAC1B,YACA,aAQA,SACM;AAAA,EAEN,IAAI,SAAS,SAAS;AAAA,IAIb,iDACJ,KAAK,GAAG,0BAA0B;AAAA,MACjC,oBAAoB,YAAY,WAAW;AAAA,KAC5C,EACA,MAAM,MAAM,EAEZ;AAAA,EACL;AAAA;AAqBK,SAAS,eAAe,CAC7B,MACA,WACA,SACM;;;AC1ED,SAAS,kBAAuD,CAAC,QAAc;AAAA,EAEpF,IAAI,aAAa,QAAQ;AAAA,IAEvB,IAAI,CAAC,OAAO,SAAS;AAAA,MACnB,MAAM,IAAI,MAAM,uCAAuC;AAAA,IACzD;AAAA,IACA,IAAI,CAAC,OAAO,OAAO;AAAA,MACjB,MAAM,IAAI,MAAM,yCAAyC;AAAA,IAC3D;AAAA,EACF,EAAO,SAAI,cAAc,QAAQ;AAAA,IAE/B,IAAI,CAAC,OAAO,OAAO;AAAA,MACjB,MAAM,IAAI,MAAM,yCAAyC;AAAA,IAC3D;AAAA,IACA,IAAI,CAAC,OAAO,UAAU;AAAA,MACpB,MAAM,IAAI,MAAM,mDAAmD;AAAA,IACrE;AAAA,EACF,EAAO;AAAA,IACL,MAAM,IAAI,MACR,wGACF;AAAA;AAAA,EAGF,OAAO;AAAA;",
9
+ "debugId": "465F57F5AB1A6BBD64756E2164756E21",
10
10
  "names": []
11
11
  }
@@ -92,6 +92,24 @@ export declare function $constraints(stateField: string, constraints: Record<str
92
92
  }>, options?: {
93
93
  runtime?: boolean;
94
94
  }): void;
95
+ /**
96
+ * Declare a global state constraint that prunes structurally impossible states.
97
+ *
98
+ * In production: No-op (compiled away)
99
+ * In verification: Translated to TLC CONSTRAINT clause, discarding states
100
+ * that violate the predicate from the exploration queue entirely.
101
+ *
102
+ * Unlike `invariant()` (which checks but still explores), `stateConstraint()`
103
+ * prevents the model checker from ever reaching the pruned states.
104
+ *
105
+ * @example
106
+ * stateConstraint("LeaderRequiresConnection", () =>
107
+ * !connectionState.value.isLeader || connectionState.value.status === "connected"
108
+ * )
109
+ */
110
+ export declare function stateConstraint(name: string, predicate: () => boolean, options?: {
111
+ message?: string;
112
+ }): void;
95
113
  export declare const verify: {
96
114
  requires: typeof requires;
97
115
  ensures: typeof ensures;
@@ -100,4 +118,5 @@ export declare const verify: {
100
118
  oneOf: typeof oneOf;
101
119
  hasLength: typeof hasLength;
102
120
  $constraints: typeof $constraints;
121
+ stateConstraint: typeof stateConstraint;
103
122
  };
@@ -1638,13 +1638,14 @@ class HandlerExtractor {
1638
1638
  const messageTypes = new Set;
1639
1639
  const invalidMessageTypes = new Set;
1640
1640
  const stateConstraints = [];
1641
+ const globalStateConstraints = [];
1641
1642
  const verifiedStates = [];
1642
1643
  this.warnings = [];
1643
1644
  const allSourceFiles = this.project.getSourceFiles();
1644
1645
  const entryPoints = allSourceFiles.filter((f) => this.isWithinPackage(f.getFilePath()));
1645
1646
  this.debugLogSourceFiles(allSourceFiles, entryPoints);
1646
1647
  for (const entryPoint of entryPoints) {
1647
- this.analyzeFileAndImports(entryPoint, handlers, messageTypes, invalidMessageTypes, stateConstraints, verifiedStates);
1648
+ this.analyzeFileAndImports(entryPoint, handlers, messageTypes, invalidMessageTypes, stateConstraints, globalStateConstraints, verifiedStates);
1648
1649
  }
1649
1650
  if (verifiedStates.length > 0) {
1650
1651
  if (process.env["POLLY_DEBUG"]) {
@@ -1674,11 +1675,12 @@ class HandlerExtractor {
1674
1675
  handlers,
1675
1676
  messageTypes,
1676
1677
  stateConstraints,
1678
+ globalStateConstraints,
1677
1679
  verifiedStates,
1678
1680
  warnings: this.warnings
1679
1681
  };
1680
1682
  }
1681
- analyzeFileAndImports(sourceFile, handlers, messageTypes, invalidMessageTypes, stateConstraints, verifiedStates) {
1683
+ analyzeFileAndImports(sourceFile, handlers, messageTypes, invalidMessageTypes, stateConstraints, globalStateConstraints, verifiedStates) {
1682
1684
  const filePath = sourceFile.getFilePath();
1683
1685
  if (this.analyzedFiles.has(filePath)) {
1684
1686
  return;
@@ -1692,6 +1694,8 @@ class HandlerExtractor {
1692
1694
  this.categorizeHandlerMessageTypes(fileHandlers, messageTypes, invalidMessageTypes);
1693
1695
  const fileConstraints = this.extractStateConstraintsFromFile(sourceFile);
1694
1696
  stateConstraints.push(...fileConstraints);
1697
+ const fileGlobalConstraints = this.extractGlobalStateConstraintsFromFile(sourceFile);
1698
+ globalStateConstraints.push(...fileGlobalConstraints);
1695
1699
  const fileVerifiedStates = this.extractVerifiedStatesFromFile(sourceFile);
1696
1700
  verifiedStates.push(...fileVerifiedStates);
1697
1701
  const importDeclarations = sourceFile.getImportDeclarations();
@@ -1705,7 +1709,7 @@ class HandlerExtractor {
1705
1709
  }
1706
1710
  continue;
1707
1711
  }
1708
- this.analyzeFileAndImports(importedFile, handlers, messageTypes, invalidMessageTypes, stateConstraints, verifiedStates);
1712
+ this.analyzeFileAndImports(importedFile, handlers, messageTypes, invalidMessageTypes, stateConstraints, globalStateConstraints, verifiedStates);
1709
1713
  } else if (process.env["POLLY_DEBUG"]) {
1710
1714
  const specifier = importDecl.getModuleSpecifierValue();
1711
1715
  if (!specifier.startsWith("node:") && !this.isNodeModuleImport(specifier)) {
@@ -3040,6 +3044,81 @@ class HandlerExtractor {
3040
3044
  });
3041
3045
  return constraints;
3042
3046
  }
3047
+ extractGlobalStateConstraintsFromFile(sourceFile) {
3048
+ const constraints = [];
3049
+ const filePath = sourceFile.getFilePath();
3050
+ sourceFile.forEachDescendant((node) => {
3051
+ const constraint = this.recognizeGlobalStateConstraint(node, filePath);
3052
+ if (constraint) {
3053
+ constraints.push(constraint);
3054
+ }
3055
+ });
3056
+ return constraints;
3057
+ }
3058
+ recognizeGlobalStateConstraint(node, filePath) {
3059
+ if (!Node4.isCallExpression(node)) {
3060
+ return null;
3061
+ }
3062
+ const expression = node.getExpression();
3063
+ if (!Node4.isIdentifier(expression)) {
3064
+ return null;
3065
+ }
3066
+ const functionName = expression.getText();
3067
+ if (functionName !== "stateConstraint") {
3068
+ return null;
3069
+ }
3070
+ const args = node.getArguments();
3071
+ if (args.length < 2) {
3072
+ return null;
3073
+ }
3074
+ const nameArg = args[0];
3075
+ if (!Node4.isStringLiteral(nameArg)) {
3076
+ return null;
3077
+ }
3078
+ const name = nameArg.getLiteralValue();
3079
+ const predicateArg = args[1];
3080
+ if (!Node4.isArrowFunction(predicateArg)) {
3081
+ return null;
3082
+ }
3083
+ const body = predicateArg.getBody();
3084
+ let expressionText;
3085
+ if (Node4.isBlock(body)) {
3086
+ const returnStatement = body.getStatements().find((s) => Node4.isReturnStatement(s));
3087
+ if (!returnStatement || !Node4.isReturnStatement(returnStatement)) {
3088
+ return null;
3089
+ }
3090
+ const returnExpr = returnStatement.getExpression();
3091
+ if (!returnExpr) {
3092
+ return null;
3093
+ }
3094
+ expressionText = returnExpr.getText();
3095
+ } else {
3096
+ expressionText = body.getText();
3097
+ }
3098
+ let message;
3099
+ if (args.length >= 3) {
3100
+ const optionsArg = args[2];
3101
+ if (Node4.isObjectLiteralExpression(optionsArg)) {
3102
+ for (const prop of optionsArg.getProperties()) {
3103
+ if (Node4.isPropertyAssignment(prop) && prop.getName() === "message") {
3104
+ const value = prop.getInitializer();
3105
+ if (value && Node4.isStringLiteral(value)) {
3106
+ message = value.getLiteralValue();
3107
+ }
3108
+ }
3109
+ }
3110
+ }
3111
+ }
3112
+ return {
3113
+ name,
3114
+ expression: expressionText,
3115
+ message,
3116
+ location: {
3117
+ file: filePath,
3118
+ line: node.getStartLineNumber()
3119
+ }
3120
+ };
3121
+ }
3043
3122
  recognizeStateConstraint(node, filePath) {
3044
3123
  if (!Node4.isCallExpression(node)) {
3045
3124
  return [];
@@ -5639,4 +5718,4 @@ main().catch((_error) => {
5639
5718
  process.exit(1);
5640
5719
  });
5641
5720
 
5642
- //# debugId=E7AAE0901ACF898964756E2164756E21
5721
+ //# debugId=6E19C4E7D7F05D9564756E2164756E21