@fairfox/polly 0.15.1 → 0.15.4

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 } from "./primitives/index.js";
64
+ export { $constraints, ensures, hasLength, inRange, oneOf, requires } from "./primitives/index.js";
@@ -49,6 +49,21 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
49
49
  });
50
50
 
51
51
  // tools/verify/src/primitives/index.ts
52
+ function requires(condition, message) {}
53
+ function ensures(condition, message) {}
54
+ function inRange(value, min, max) {
55
+ return value >= min && value <= max;
56
+ }
57
+ function oneOf(value, allowed) {
58
+ return allowed.includes(value);
59
+ }
60
+ function hasLength(array, constraint) {
61
+ if (constraint.min !== undefined && array.length < constraint.min)
62
+ return false;
63
+ if (constraint.max !== undefined && array.length > constraint.max)
64
+ return false;
65
+ return true;
66
+ }
52
67
  function $constraints(stateField, constraints, options) {
53
68
  if (options?.runtime) {
54
69
  import("../../../src/shared/lib/constraints.js").then(({ registerConstraints }) => {
@@ -79,8 +94,13 @@ function defineVerification(config) {
79
94
  return config;
80
95
  }
81
96
  export {
97
+ requires,
98
+ oneOf,
99
+ inRange,
100
+ hasLength,
101
+ ensures,
82
102
  defineVerification,
83
103
  $constraints
84
104
  };
85
105
 
86
- //# debugId=B7658306F59988EF64756E2164756E21
106
+ //# debugId=477FC1CD67CF195864756E2164756E21
@@ -3,9 +3,9 @@
3
3
  "sources": ["../tools/verify/src/primitives/index.ts", "../tools/verify/src/config.ts"],
4
4
  "sourcesContent": [
5
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 $constraints from primitives for user code\nexport { $constraints } from \"./primitives/index.js\";\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"
7
7
  ],
8
- "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmHO,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": "B7658306F59988EF64756E2164756E21",
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": "477FC1CD67CF195864756E2164756E21",
10
10
  "names": []
11
11
  }
@@ -1598,6 +1598,7 @@ class HandlerExtractor {
1598
1598
  relationshipExtractor;
1599
1599
  analyzedFiles;
1600
1600
  packageRoot;
1601
+ warnings;
1601
1602
  constructor(tsConfigPath) {
1602
1603
  this.project = new Project3({
1603
1604
  tsConfigFilePath: tsConfigPath
@@ -1605,8 +1606,27 @@ class HandlerExtractor {
1605
1606
  this.typeGuardCache = new WeakMap;
1606
1607
  this.relationshipExtractor = new RelationshipExtractor;
1607
1608
  this.analyzedFiles = new Set;
1609
+ this.warnings = [];
1608
1610
  this.packageRoot = this.findPackageRoot(tsConfigPath);
1609
1611
  }
1612
+ warnUnsupportedPattern(pattern, location, suggestion) {
1613
+ const exists = this.warnings.some((w) => w.pattern === pattern && w.location === location);
1614
+ if (!exists) {
1615
+ this.warnings.push({
1616
+ type: "unsupported_pattern",
1617
+ pattern,
1618
+ location,
1619
+ suggestion
1620
+ });
1621
+ }
1622
+ }
1623
+ getNodeLocation(node) {
1624
+ const sourceFile = node.getSourceFile();
1625
+ const lineAndCol = sourceFile.getLineAndColumnAtPos(node.getStart());
1626
+ const filePath = sourceFile.getFilePath();
1627
+ const relativePath = filePath.startsWith(this.packageRoot) ? filePath.substring(this.packageRoot.length + 1) : filePath;
1628
+ return `${relativePath}:${lineAndCol.line}`;
1629
+ }
1610
1630
  findPackageRoot(tsConfigPath) {
1611
1631
  let dir = tsConfigPath.substring(0, tsConfigPath.lastIndexOf("/"));
1612
1632
  while (dir.length > 1) {
@@ -1630,6 +1650,7 @@ class HandlerExtractor {
1630
1650
  const invalidMessageTypes = new Set;
1631
1651
  const stateConstraints = [];
1632
1652
  const verifiedStates = [];
1653
+ this.warnings = [];
1633
1654
  const allSourceFiles = this.project.getSourceFiles();
1634
1655
  const entryPoints = allSourceFiles.filter((f) => this.isWithinPackage(f.getFilePath()));
1635
1656
  this.debugLogSourceFiles(allSourceFiles, entryPoints);
@@ -1664,7 +1685,8 @@ class HandlerExtractor {
1664
1685
  handlers,
1665
1686
  messageTypes,
1666
1687
  stateConstraints,
1667
- verifiedStates
1688
+ verifiedStates,
1689
+ warnings: this.warnings
1668
1690
  };
1669
1691
  }
1670
1692
  analyzeFileAndImports(sourceFile, handlers, messageTypes, invalidMessageTypes, stateConstraints, verifiedStates) {
@@ -1894,46 +1916,364 @@ class HandlerExtractor {
1894
1916
  if (!Node4.isPropertyAccessExpression(left))
1895
1917
  return;
1896
1918
  const fieldPath = this.getPropertyPath(left);
1897
- if (fieldPath.startsWith("state.")) {
1898
- const field = fieldPath.substring(6);
1899
- const value = this.extractValue(right);
1900
- if (value !== undefined) {
1901
- assignments.push({ field, value });
1902
- }
1919
+ if (this.tryExtractStateFieldPattern(fieldPath, right, assignments))
1903
1920
  return;
1904
- }
1905
- const valueMatch = fieldPath.match(/\.value\.(.+)$/);
1906
- if (valueMatch?.[1]) {
1907
- const field = valueMatch[1];
1908
- const value = this.extractValue(right);
1909
- if (value !== undefined) {
1910
- assignments.push({ field, value });
1911
- }
1921
+ if (this.tryExtractSignalNestedFieldPattern(fieldPath, right, assignments))
1922
+ return;
1923
+ if (this.tryExtractSignalObjectPattern(fieldPath, right, assignments))
1924
+ return;
1925
+ if (this.tryExtractSignalArrayPattern(fieldPath, right, assignments))
1926
+ return;
1927
+ if (this.tryExtractSignalMethodPattern(fieldPath, right, assignments))
1928
+ return;
1929
+ if (this.tryExtractSetConstructorPattern(fieldPath, right, assignments))
1912
1930
  return;
1931
+ this.tryExtractMapConstructorPattern(fieldPath, right, assignments);
1932
+ }
1933
+ tryExtractStateFieldPattern(fieldPath, right, assignments) {
1934
+ if (!fieldPath.startsWith("state."))
1935
+ return false;
1936
+ const field = fieldPath.substring(6);
1937
+ const value = this.extractValue(right);
1938
+ if (value !== undefined) {
1939
+ assignments.push({ field, value });
1913
1940
  }
1914
- if (fieldPath.endsWith(".value") && Node4.isObjectLiteralExpression(right)) {
1915
- this.extractObjectLiteralAssignments(right, assignments);
1941
+ return true;
1942
+ }
1943
+ tryExtractSignalNestedFieldPattern(fieldPath, right, assignments) {
1944
+ const valueFieldMatch = fieldPath.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\.value\.(.+)$/);
1945
+ if (!valueFieldMatch?.[1] || !valueFieldMatch?.[2])
1946
+ return false;
1947
+ const signalName = valueFieldMatch[1];
1948
+ const fieldName = valueFieldMatch[2];
1949
+ const value = this.extractValue(right);
1950
+ if (value !== undefined) {
1951
+ assignments.push({ field: `${signalName}_${fieldName}`, value });
1916
1952
  }
1953
+ return true;
1917
1954
  }
1918
- extractObjectLiteralAssignments(objectLiteral, assignments) {
1919
- if (!Node4.isObjectLiteralExpression(objectLiteral))
1955
+ tryExtractSignalObjectPattern(fieldPath, right, assignments) {
1956
+ if (!fieldPath.endsWith(".value") || !Node4.isObjectLiteralExpression(right))
1957
+ return false;
1958
+ const signalName = fieldPath.slice(0, -6);
1959
+ if (this.isSpreadUpdatePattern(right, fieldPath)) {
1960
+ this.extractSpreadUpdateAssignments(right, assignments, signalName);
1961
+ } else {
1962
+ this.extractObjectLiteralAssignments(right, assignments, signalName);
1963
+ }
1964
+ return true;
1965
+ }
1966
+ isSpreadUpdatePattern(objectLiteral, fieldPath) {
1967
+ const properties = objectLiteral.getProperties();
1968
+ if (properties.length === 0)
1969
+ return false;
1970
+ const firstProp = properties[0];
1971
+ if (!firstProp || !Node4.isSpreadAssignment(firstProp))
1972
+ return false;
1973
+ const spreadExpr = firstProp.getExpression();
1974
+ if (!spreadExpr)
1975
+ return false;
1976
+ return this.getPropertyPath(spreadExpr) === fieldPath;
1977
+ }
1978
+ tryExtractSignalArrayPattern(fieldPath, right, assignments) {
1979
+ if (!fieldPath.endsWith(".value") || !Node4.isArrayLiteralExpression(right))
1980
+ return false;
1981
+ const signalName = fieldPath.slice(0, -6);
1982
+ const arrayAssignment = this.extractArraySpreadOperation(right, fieldPath, signalName);
1983
+ if (arrayAssignment) {
1984
+ assignments.push(arrayAssignment);
1985
+ }
1986
+ return true;
1987
+ }
1988
+ tryExtractSignalMethodPattern(fieldPath, right, assignments) {
1989
+ if (!fieldPath.endsWith(".value") || !Node4.isCallExpression(right))
1990
+ return false;
1991
+ const signalName = fieldPath.slice(0, -6);
1992
+ this.checkForMutatingCollectionMethods(right);
1993
+ const methodAssignment = this.extractArrayMethodOperation(right, fieldPath, signalName);
1994
+ if (methodAssignment) {
1995
+ assignments.push(methodAssignment);
1996
+ }
1997
+ return true;
1998
+ }
1999
+ checkForMutatingCollectionMethods(callExpr) {
2000
+ const expression = callExpr.getExpression();
2001
+ if (!Node4.isPropertyAccessExpression(expression))
1920
2002
  return;
1921
- for (const prop of objectLiteral.getProperties()) {
1922
- if (Node4.isPropertyAssignment(prop)) {
1923
- const name = prop.getName();
1924
- const initializer = prop.getInitializer();
1925
- if (!name || !initializer)
1926
- continue;
1927
- const value = this.extractValue(initializer);
1928
- if (value !== undefined) {
1929
- assignments.push({ field: name, value });
2003
+ const methodName = expression.getName();
2004
+ const sourceExpr = expression.getExpression();
2005
+ const setMethods = ["add", "delete", "clear"];
2006
+ if (setMethods.includes(methodName)) {
2007
+ const sourceText = sourceExpr.getText();
2008
+ if (sourceText.includes("Set") || this.looksLikeSetOrMap(sourceExpr, "Set")) {
2009
+ this.warnUnsupportedPattern(`set.${methodName}()`, this.getNodeLocation(callExpr), `Set.${methodName}() mutates in place. Use: new Set([...set, item]) for add, new Set([...set].filter(...)) for delete.`);
2010
+ }
2011
+ }
2012
+ const mapMethods = ["set", "delete", "clear"];
2013
+ if (mapMethods.includes(methodName)) {
2014
+ const sourceText = sourceExpr.getText();
2015
+ if (sourceText.includes("Map") || this.looksLikeSetOrMap(sourceExpr, "Map")) {
2016
+ this.warnUnsupportedPattern(`map.${methodName}()`, this.getNodeLocation(callExpr), `Map.${methodName}() mutates in place. Use: new Map([...map, [key, value]]) for set, new Map([...map].filter(...)) for delete.`);
2017
+ }
2018
+ }
2019
+ }
2020
+ looksLikeSetOrMap(expr, collectionType) {
2021
+ const text = expr.getText().toLowerCase();
2022
+ return text.includes(collectionType.toLowerCase());
2023
+ }
2024
+ tryExtractSetConstructorPattern(fieldPath, right, assignments) {
2025
+ if (!fieldPath.endsWith(".value") || !Node4.isNewExpression(right))
2026
+ return false;
2027
+ const constructorExpr = right.getExpression();
2028
+ if (!Node4.isIdentifier(constructorExpr) || constructorExpr.getText() !== "Set")
2029
+ return false;
2030
+ const signalName = fieldPath.slice(0, -6);
2031
+ const setAssignment = this.extractSetOperation(right, fieldPath, signalName);
2032
+ if (setAssignment) {
2033
+ assignments.push(setAssignment);
2034
+ }
2035
+ return true;
2036
+ }
2037
+ tryExtractMapConstructorPattern(fieldPath, right, assignments) {
2038
+ if (!fieldPath.endsWith(".value") || !Node4.isNewExpression(right))
2039
+ return false;
2040
+ const constructorExpr = right.getExpression();
2041
+ if (!Node4.isIdentifier(constructorExpr) || constructorExpr.getText() !== "Map")
2042
+ return false;
2043
+ const signalName = fieldPath.slice(0, -6);
2044
+ const mapAssignment = this.extractMapOperation(right, fieldPath, signalName);
2045
+ if (mapAssignment) {
2046
+ assignments.push(mapAssignment);
2047
+ }
2048
+ return true;
2049
+ }
2050
+ extractSetOperation(newExpr, fieldPath, signalName) {
2051
+ const args = newExpr.getArguments();
2052
+ if (args.length === 0) {
2053
+ return { field: signalName, value: "{}" };
2054
+ }
2055
+ const firstArg = args[0];
2056
+ if (!firstArg)
2057
+ return null;
2058
+ if (Node4.isArrayLiteralExpression(firstArg)) {
2059
+ return this.extractSetArrayOperation(firstArg, fieldPath, signalName);
2060
+ }
2061
+ if (Node4.isCallExpression(firstArg)) {
2062
+ return this.extractSetMethodChainOperation(firstArg, fieldPath, signalName);
2063
+ }
2064
+ return null;
2065
+ }
2066
+ extractSetArrayOperation(arrayLiteral, fieldPath, signalName) {
2067
+ const elements = arrayLiteral.getElements();
2068
+ if (elements.length < 1)
2069
+ return null;
2070
+ const firstElement = elements[0];
2071
+ const lastElement = elements[elements.length - 1];
2072
+ if (firstElement && Node4.isSpreadElement(firstElement)) {
2073
+ const spreadExpr = firstElement.getExpression();
2074
+ if (spreadExpr && this.getPropertyPath(spreadExpr) === fieldPath) {
2075
+ return { field: signalName, value: "@ \\union {payload}" };
2076
+ }
2077
+ }
2078
+ if (lastElement && Node4.isSpreadElement(lastElement)) {
2079
+ const spreadExpr = lastElement.getExpression();
2080
+ if (spreadExpr && this.getPropertyPath(spreadExpr) === fieldPath) {
2081
+ return { field: signalName, value: "{payload} \\union @" };
2082
+ }
2083
+ }
2084
+ return null;
2085
+ }
2086
+ extractSetMethodChainOperation(callExpr, fieldPath, signalName) {
2087
+ const expression = callExpr.getExpression();
2088
+ if (!Node4.isPropertyAccessExpression(expression))
2089
+ return null;
2090
+ const methodName = expression.getName();
2091
+ const sourceExpr = expression.getExpression();
2092
+ if (methodName === "filter" && Node4.isArrayLiteralExpression(sourceExpr)) {
2093
+ const elements = sourceExpr.getElements();
2094
+ if (elements.length === 1) {
2095
+ const spreadEl = elements[0];
2096
+ if (spreadEl && Node4.isSpreadElement(spreadEl)) {
2097
+ const spreadExpr = spreadEl.getExpression();
2098
+ if (spreadExpr && this.getPropertyPath(spreadExpr) === fieldPath) {
2099
+ return { field: signalName, value: "@ \\ {payload}" };
2100
+ }
1930
2101
  }
1931
2102
  }
1932
- if (Node4.isShorthandPropertyAssignment(prop)) {
1933
- const name = prop.getName();
1934
- assignments.push({ field: name, value: "@" });
2103
+ }
2104
+ return null;
2105
+ }
2106
+ extractMapOperation(newExpr, fieldPath, signalName) {
2107
+ const args = newExpr.getArguments();
2108
+ if (args.length === 0) {
2109
+ return { field: signalName, value: "<<>>" };
2110
+ }
2111
+ const firstArg = args[0];
2112
+ if (!firstArg)
2113
+ return null;
2114
+ if (Node4.isArrayLiteralExpression(firstArg)) {
2115
+ return this.extractMapArrayOperation(firstArg, fieldPath, signalName);
2116
+ }
2117
+ if (Node4.isCallExpression(firstArg)) {
2118
+ return this.extractMapMethodChainOperation(firstArg, fieldPath, signalName);
2119
+ }
2120
+ return null;
2121
+ }
2122
+ extractMapArrayOperation(arrayLiteral, fieldPath, signalName) {
2123
+ const elements = arrayLiteral.getElements();
2124
+ if (elements.length < 1)
2125
+ return null;
2126
+ const firstElement = elements[0];
2127
+ if (firstElement && Node4.isSpreadElement(firstElement)) {
2128
+ const spreadExpr = firstElement.getExpression();
2129
+ if (spreadExpr && this.getPropertyPath(spreadExpr) === fieldPath) {
2130
+ return { field: signalName, value: "[@ EXCEPT ![payload.key] = payload.value]" };
2131
+ }
2132
+ }
2133
+ return null;
2134
+ }
2135
+ extractMapMethodChainOperation(callExpr, fieldPath, signalName) {
2136
+ const expression = callExpr.getExpression();
2137
+ if (!Node4.isPropertyAccessExpression(expression))
2138
+ return null;
2139
+ const methodName = expression.getName();
2140
+ const sourceExpr = expression.getExpression();
2141
+ if (methodName === "filter" && Node4.isArrayLiteralExpression(sourceExpr)) {
2142
+ const elements = sourceExpr.getElements();
2143
+ if (elements.length === 1) {
2144
+ const spreadEl = elements[0];
2145
+ if (spreadEl && Node4.isSpreadElement(spreadEl)) {
2146
+ const spreadExpr = spreadEl.getExpression();
2147
+ if (spreadExpr && this.getPropertyPath(spreadExpr) === fieldPath) {
2148
+ return { field: signalName, value: "[k \\in DOMAIN @ \\ {payload.key} |-> @[k]]" };
2149
+ }
2150
+ }
1935
2151
  }
1936
2152
  }
2153
+ return null;
2154
+ }
2155
+ extractArrayMethodOperation(callExpr, fieldPath, signalName) {
2156
+ const expression = callExpr.getExpression();
2157
+ if (!Node4.isPropertyAccessExpression(expression))
2158
+ return null;
2159
+ const methodName = expression.getName();
2160
+ const sourceExpr = expression.getExpression();
2161
+ const sourcePath = this.getPropertyPath(sourceExpr);
2162
+ if (sourcePath !== fieldPath)
2163
+ return null;
2164
+ switch (methodName) {
2165
+ case "filter":
2166
+ return { field: signalName, value: "SelectSeq(@, LAMBDA t: TRUE)" };
2167
+ case "map":
2168
+ return { field: signalName, value: "[i \\in DOMAIN @ |-> @[i]]" };
2169
+ case "slice":
2170
+ return { field: signalName, value: "SubSeq(@, 1, Len(@))" };
2171
+ case "concat":
2172
+ return { field: signalName, value: "@ \\o <<payload>>" };
2173
+ case "reverse":
2174
+ return { field: signalName, value: "[i \\in DOMAIN @ |-> @[Len(@) - i + 1]]" };
2175
+ default:
2176
+ this.warnUnsupportedArrayMethod(methodName, callExpr);
2177
+ return null;
2178
+ }
2179
+ }
2180
+ warnUnsupportedArrayMethod(methodName, node) {
2181
+ const mutatingMethods = [
2182
+ "push",
2183
+ "pop",
2184
+ "shift",
2185
+ "unshift",
2186
+ "splice",
2187
+ "sort",
2188
+ "fill",
2189
+ "copyWithin"
2190
+ ];
2191
+ const queryMethods = [
2192
+ "find",
2193
+ "findIndex",
2194
+ "reduce",
2195
+ "reduceRight",
2196
+ "some",
2197
+ "every",
2198
+ "includes",
2199
+ "indexOf",
2200
+ "lastIndexOf"
2201
+ ];
2202
+ const otherMethods = ["flat", "flatMap", "join", "toString", "toLocaleString"];
2203
+ if (mutatingMethods.includes(methodName)) {
2204
+ this.warnUnsupportedPattern(`array.${methodName}()`, this.getNodeLocation(node), `'${methodName}' mutates in place. Use spread syntax: [...arr, item] for append, arr.filter() for removal.`);
2205
+ } else if (queryMethods.includes(methodName)) {
2206
+ this.warnUnsupportedPattern(`array.${methodName}()`, this.getNodeLocation(node), `'${methodName}' returns a single value, not a new array. State assignment won't be extracted.`);
2207
+ } else if (otherMethods.includes(methodName)) {
2208
+ this.warnUnsupportedPattern(`array.${methodName}()`, this.getNodeLocation(node), `'${methodName}' is not supported for state extraction. Consider using map/filter instead.`);
2209
+ }
2210
+ }
2211
+ extractSpreadUpdateAssignments(objectLiteral, assignments, signalName) {
2212
+ for (const prop of objectLiteral.getProperties()) {
2213
+ if (Node4.isSpreadAssignment(prop))
2214
+ continue;
2215
+ this.extractPropertyAssignment(prop, assignments, signalName);
2216
+ }
2217
+ }
2218
+ extractArraySpreadOperation(arrayLiteral, fieldPath, signalName) {
2219
+ const elements = arrayLiteral.getElements();
2220
+ if (elements.length < 1)
2221
+ return null;
2222
+ return this.tryExtractAppendOperation(elements, fieldPath, signalName) ?? this.tryExtractPrependOperation(elements, fieldPath, signalName);
2223
+ }
2224
+ tryExtractAppendOperation(elements, fieldPath, signalName) {
2225
+ const firstElement = elements[0];
2226
+ if (!firstElement || !Node4.isSpreadElement(firstElement))
2227
+ return null;
2228
+ const spreadExpr = firstElement.getExpression();
2229
+ if (!spreadExpr || this.getPropertyPath(spreadExpr) !== fieldPath)
2230
+ return null;
2231
+ if (elements.length === 2) {
2232
+ return { field: signalName, value: "Append(@, payload)" };
2233
+ }
2234
+ const placeholders = Array(elements.length - 1).fill("payload").join(", ");
2235
+ return { field: signalName, value: `@ \\o <<${placeholders}>>` };
2236
+ }
2237
+ tryExtractPrependOperation(elements, fieldPath, signalName) {
2238
+ if (elements.length < 2)
2239
+ return null;
2240
+ const lastElement = elements[elements.length - 1];
2241
+ if (!lastElement || !Node4.isSpreadElement(lastElement))
2242
+ return null;
2243
+ const spreadExpr = lastElement.getExpression();
2244
+ if (!spreadExpr || this.getPropertyPath(spreadExpr) !== fieldPath)
2245
+ return null;
2246
+ if (elements.length === 2) {
2247
+ return { field: signalName, value: "<<payload>> \\o @" };
2248
+ }
2249
+ const placeholders = Array(elements.length - 1).fill("payload").join(", ");
2250
+ return { field: signalName, value: `<<${placeholders}>> \\o @` };
2251
+ }
2252
+ extractObjectLiteralAssignments(objectLiteral, assignments, signalName) {
2253
+ if (!Node4.isObjectLiteralExpression(objectLiteral))
2254
+ return;
2255
+ for (const prop of objectLiteral.getProperties()) {
2256
+ this.extractPropertyAssignment(prop, assignments, signalName);
2257
+ }
2258
+ }
2259
+ extractPropertyAssignment(prop, assignments, signalName) {
2260
+ if (Node4.isPropertyAssignment(prop)) {
2261
+ const name = prop.getName();
2262
+ const initializer = prop.getInitializer();
2263
+ if (!name || !initializer)
2264
+ return;
2265
+ const value = this.extractValue(initializer);
2266
+ if (value === undefined)
2267
+ return;
2268
+ const field = signalName ? `${signalName}_${name}` : name;
2269
+ assignments.push({ field, value });
2270
+ return;
2271
+ }
2272
+ if (Node4.isShorthandPropertyAssignment(prop)) {
2273
+ const name = prop.getName();
2274
+ const field = signalName ? `${signalName}_${name}` : name;
2275
+ assignments.push({ field, value: "@" });
2276
+ }
1937
2277
  }
1938
2278
  extractElementAccessAssignment(left, right, assignments) {
1939
2279
  if (!Node4.isElementAccessExpression(left))
@@ -2946,15 +3286,15 @@ class HandlerExtractor {
2946
3286
  if (path2 === `${varName}.value`) {
2947
3287
  const right = node.getRight();
2948
3288
  if (Node4.isObjectLiteralExpression(right)) {
2949
- this.extractObjectLiteralAssignments(right, mutations);
3289
+ this.extractObjectLiteralAssignments(right, mutations, varName);
2950
3290
  }
2951
3291
  break;
2952
3292
  }
2953
3293
  const fieldPrefix = `${varName}.value.`;
2954
3294
  if (path2.startsWith(fieldPrefix)) {
2955
- const field = path2.substring(fieldPrefix.length);
3295
+ const fieldName = path2.substring(fieldPrefix.length);
2956
3296
  const value = this.extractValue(node.getRight());
2957
- mutations.push({ field, value: value ?? "@" });
3297
+ mutations.push({ field: `${varName}_${fieldName}`, value: value ?? "@" });
2958
3298
  break;
2959
3299
  }
2960
3300
  }
@@ -5285,4 +5625,4 @@ main().catch((_error) => {
5285
5625
  process.exit(1);
5286
5626
  });
5287
5627
 
5288
- //# debugId=AD2A9496CB1D22A064756E2164756E21
5628
+ //# debugId=3513B8B8E90D943B64756E2164756E21