@fairfox/polly 0.14.1 → 0.15.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.
Files changed (32) hide show
  1. package/dist/src/background/index.js +342 -3
  2. package/dist/src/background/index.js.map +7 -4
  3. package/dist/src/background/message-router.js +342 -3
  4. package/dist/src/background/message-router.js.map +7 -4
  5. package/dist/src/index.js +402 -99
  6. package/dist/src/index.js.map +8 -5
  7. package/dist/src/shared/adapters/index.d.ts +3 -0
  8. package/dist/src/shared/adapters/index.js +356 -4
  9. package/dist/src/shared/adapters/index.js.map +7 -4
  10. package/dist/src/shared/lib/adapter-factory.d.ts +80 -0
  11. package/dist/src/shared/lib/context-helpers.js +342 -3
  12. package/dist/src/shared/lib/context-helpers.js.map +7 -4
  13. package/dist/src/shared/lib/message-bus.js +342 -3
  14. package/dist/src/shared/lib/message-bus.js.map +7 -4
  15. package/dist/src/shared/lib/state.d.ts +5 -1
  16. package/dist/src/shared/lib/state.js +274 -1173
  17. package/dist/src/shared/lib/state.js.map +6 -19
  18. package/dist/src/shared/lib/storage-adapter.d.ts +42 -0
  19. package/dist/src/shared/lib/sync-adapter.d.ts +79 -0
  20. package/dist/src/shared/state/app-state.js +294 -1173
  21. package/dist/src/shared/state/app-state.js.map +6 -18
  22. package/dist/tools/analysis/src/extract/handlers.d.ts +48 -1
  23. package/dist/tools/analysis/src/types/core.d.ts +20 -0
  24. package/dist/tools/teach/src/cli.js +376 -7
  25. package/dist/tools/teach/src/cli.js.map +4 -4
  26. package/dist/tools/teach/src/index.js +232 -7
  27. package/dist/tools/teach/src/index.js.map +3 -3
  28. package/dist/tools/verify/src/cli.js +232 -7
  29. package/dist/tools/verify/src/cli.js.map +3 -3
  30. package/dist/tools/visualize/src/cli.js +232 -7
  31. package/dist/tools/visualize/src/cli.js.map +3 -3
  32. package/package.json +1 -1
@@ -5502,21 +5502,45 @@ class HandlerExtractor {
5502
5502
  const messageTypes = new Set;
5503
5503
  const invalidMessageTypes = new Set;
5504
5504
  const stateConstraints = [];
5505
+ const verifiedStates = [];
5505
5506
  const allSourceFiles = this.project.getSourceFiles();
5506
5507
  const entryPoints = allSourceFiles.filter((f) => this.isWithinPackage(f.getFilePath()));
5507
5508
  this.debugLogSourceFiles(allSourceFiles, entryPoints);
5508
5509
  for (const entryPoint of entryPoints) {
5509
- this.analyzeFileAndImports(entryPoint, handlers, messageTypes, invalidMessageTypes, stateConstraints);
5510
+ this.analyzeFileAndImports(entryPoint, handlers, messageTypes, invalidMessageTypes, stateConstraints, verifiedStates);
5511
+ }
5512
+ if (verifiedStates.length > 0) {
5513
+ if (process.env["POLLY_DEBUG"]) {
5514
+ console.log(`[DEBUG] Found ${verifiedStates.length} verified state(s), scanning for mutating functions...`);
5515
+ }
5516
+ for (const filePath of this.analyzedFiles) {
5517
+ const sourceFile = this.project.getSourceFile(filePath);
5518
+ if (!sourceFile)
5519
+ continue;
5520
+ const mutatingHandlers = this.findStateMutatingFunctions(sourceFile, verifiedStates);
5521
+ for (const handler of mutatingHandlers) {
5522
+ const exists = handlers.some((h) => h.messageType === handler.messageType && h.location.file === handler.location.file);
5523
+ if (!exists) {
5524
+ handlers.push(handler);
5525
+ if (this.isValidTLAIdentifier(handler.messageType)) {
5526
+ messageTypes.add(handler.messageType);
5527
+ } else {
5528
+ invalidMessageTypes.add(handler.messageType);
5529
+ }
5530
+ }
5531
+ }
5532
+ }
5510
5533
  }
5511
5534
  this.debugLogExtractionResults(handlers.length, invalidMessageTypes.size);
5512
5535
  this.debugLogAnalysisStats(allSourceFiles.length, entryPoints.length);
5513
5536
  return {
5514
5537
  handlers,
5515
5538
  messageTypes,
5516
- stateConstraints
5539
+ stateConstraints,
5540
+ verifiedStates
5517
5541
  };
5518
5542
  }
5519
- analyzeFileAndImports(sourceFile, handlers, messageTypes, invalidMessageTypes, stateConstraints) {
5543
+ analyzeFileAndImports(sourceFile, handlers, messageTypes, invalidMessageTypes, stateConstraints, verifiedStates) {
5520
5544
  const filePath = sourceFile.getFilePath();
5521
5545
  if (this.analyzedFiles.has(filePath)) {
5522
5546
  return;
@@ -5530,6 +5554,8 @@ class HandlerExtractor {
5530
5554
  this.categorizeHandlerMessageTypes(fileHandlers, messageTypes, invalidMessageTypes);
5531
5555
  const fileConstraints = this.extractStateConstraintsFromFile(sourceFile);
5532
5556
  stateConstraints.push(...fileConstraints);
5557
+ const fileVerifiedStates = this.extractVerifiedStatesFromFile(sourceFile);
5558
+ verifiedStates.push(...fileVerifiedStates);
5533
5559
  const importDeclarations = sourceFile.getImportDeclarations();
5534
5560
  for (const importDecl of importDeclarations) {
5535
5561
  const importedFile = importDecl.getModuleSpecifierSourceFile();
@@ -5541,7 +5567,7 @@ class HandlerExtractor {
5541
5567
  }
5542
5568
  continue;
5543
5569
  }
5544
- this.analyzeFileAndImports(importedFile, handlers, messageTypes, invalidMessageTypes, stateConstraints);
5570
+ this.analyzeFileAndImports(importedFile, handlers, messageTypes, invalidMessageTypes, stateConstraints, verifiedStates);
5545
5571
  } else if (process.env["POLLY_DEBUG"]) {
5546
5572
  const specifier = importDecl.getModuleSpecifierValue();
5547
5573
  if (!specifier.startsWith("node:") && !this.isNodeModuleImport(specifier)) {
@@ -5750,7 +5776,7 @@ class HandlerExtractor {
5750
5776
  return;
5751
5777
  }
5752
5778
  const valueMatch = fieldPath.match(/\.value\.(.+)$/);
5753
- if (valueMatch && valueMatch[1]) {
5779
+ if (valueMatch?.[1]) {
5754
5780
  const field = valueMatch[1];
5755
5781
  const value = this.extractValue(right);
5756
5782
  if (value !== undefined) {
@@ -5808,7 +5834,7 @@ class HandlerExtractor {
5808
5834
  return fieldPath.substring(6);
5809
5835
  }
5810
5836
  const valueMatch = fieldPath.match(/\.value\.(.+)$/);
5811
- if (valueMatch && valueMatch[1]) {
5837
+ if (valueMatch?.[1]) {
5812
5838
  return valueMatch[1];
5813
5839
  }
5814
5840
  return null;
@@ -6616,6 +6642,205 @@ class HandlerExtractor {
6616
6642
  }
6617
6643
  return results;
6618
6644
  }
6645
+ extractVerifiedStatesFromFile(sourceFile) {
6646
+ const verifiedStates = [];
6647
+ const filePath = sourceFile.getFilePath();
6648
+ sourceFile.forEachDescendant((node) => {
6649
+ if (!Node4.isCallExpression(node))
6650
+ return;
6651
+ const stateInfo = this.recognizeVerifiedStateCall(node, filePath);
6652
+ if (stateInfo) {
6653
+ verifiedStates.push(stateInfo);
6654
+ }
6655
+ });
6656
+ return verifiedStates;
6657
+ }
6658
+ recognizeVerifiedStateCall(node, filePath) {
6659
+ if (!Node4.isCallExpression(node))
6660
+ return null;
6661
+ const expression = node.getExpression();
6662
+ if (!Node4.isIdentifier(expression))
6663
+ return null;
6664
+ const funcName = expression.getText();
6665
+ if (!["$sharedState", "$syncedState", "$persistedState"].includes(funcName)) {
6666
+ return null;
6667
+ }
6668
+ const args = node.getArguments();
6669
+ if (args.length < 2)
6670
+ return null;
6671
+ const optionsArg = args[2];
6672
+ if (!optionsArg || !this.hasVerifyTrue(optionsArg))
6673
+ return null;
6674
+ const keyArg = args[0];
6675
+ if (!keyArg || !Node4.isStringLiteral(keyArg))
6676
+ return null;
6677
+ const key = keyArg.getLiteralValue();
6678
+ const variableName = this.getVariableNameFromParent(node) || key;
6679
+ const initialValueArg = args[1];
6680
+ const fields = initialValueArg ? this.extractFieldNames(initialValueArg) : [];
6681
+ if (process.env["POLLY_DEBUG"]) {
6682
+ console.log(`[DEBUG] Found verified state: ${variableName} (key: "${key}") with fields: [${fields.join(", ")}]`);
6683
+ }
6684
+ return {
6685
+ key,
6686
+ variableName,
6687
+ filePath,
6688
+ line: node.getStartLineNumber(),
6689
+ fields
6690
+ };
6691
+ }
6692
+ hasVerifyTrue(optionsNode) {
6693
+ if (!Node4.isObjectLiteralExpression(optionsNode))
6694
+ return false;
6695
+ for (const prop of optionsNode.getProperties()) {
6696
+ if (!Node4.isPropertyAssignment(prop))
6697
+ continue;
6698
+ const name = prop.getName();
6699
+ if (name !== "verify")
6700
+ continue;
6701
+ const initializer = prop.getInitializer();
6702
+ if (initializer && initializer.getKind() === SyntaxKind.TrueKeyword) {
6703
+ return true;
6704
+ }
6705
+ }
6706
+ return false;
6707
+ }
6708
+ getVariableNameFromParent(node) {
6709
+ const parent = node.getParent();
6710
+ if (Node4.isVariableDeclaration(parent)) {
6711
+ return parent.getName();
6712
+ }
6713
+ return null;
6714
+ }
6715
+ extractFieldNames(node) {
6716
+ const fields = [];
6717
+ if (Node4.isObjectLiteralExpression(node)) {
6718
+ for (const prop of node.getProperties()) {
6719
+ if (Node4.isPropertyAssignment(prop) || Node4.isShorthandPropertyAssignment(prop)) {
6720
+ fields.push(prop.getName());
6721
+ }
6722
+ }
6723
+ } else if (Node4.isIdentifier(node)) {
6724
+ const definitions = node.getDefinitionNodes();
6725
+ for (const def of definitions) {
6726
+ if (Node4.isVariableDeclaration(def)) {
6727
+ const initializer = def.getInitializer();
6728
+ if (initializer && Node4.isObjectLiteralExpression(initializer)) {
6729
+ return this.extractFieldNames(initializer);
6730
+ }
6731
+ }
6732
+ }
6733
+ }
6734
+ return fields;
6735
+ }
6736
+ findStateMutatingFunctions(sourceFile, verifiedStates) {
6737
+ const handlers = [];
6738
+ const stateVarNames = new Set(verifiedStates.map((s) => s.variableName));
6739
+ const filePath = sourceFile.getFilePath();
6740
+ const context = this.inferContext(filePath);
6741
+ for (const func of sourceFile.getFunctions()) {
6742
+ if (!func.isExported())
6743
+ continue;
6744
+ const funcName = func.getName();
6745
+ if (!funcName)
6746
+ continue;
6747
+ const assignments = this.findStateMutationsInFunction(func, stateVarNames);
6748
+ if (assignments.length === 0)
6749
+ continue;
6750
+ const preconditions = [];
6751
+ const postconditions = [];
6752
+ this.extractVerificationConditions(func, preconditions, postconditions);
6753
+ const messageType = this.functionNameToMessageType(funcName);
6754
+ if (process.env["POLLY_DEBUG"]) {
6755
+ console.log(`[DEBUG] Found state-mutating function: ${funcName} → ${messageType} ` + `(${assignments.length} assignments, ${preconditions.length} preconditions, ${postconditions.length} postconditions)`);
6756
+ }
6757
+ handlers.push({
6758
+ messageType,
6759
+ node: context,
6760
+ assignments,
6761
+ preconditions,
6762
+ postconditions,
6763
+ location: {
6764
+ file: filePath,
6765
+ line: func.getStartLineNumber()
6766
+ }
6767
+ });
6768
+ }
6769
+ for (const varStmt of sourceFile.getVariableStatements()) {
6770
+ if (!varStmt.isExported())
6771
+ continue;
6772
+ for (const decl of varStmt.getDeclarations()) {
6773
+ const initializer = decl.getInitializer();
6774
+ if (!initializer)
6775
+ continue;
6776
+ if (!Node4.isArrowFunction(initializer) && !Node4.isFunctionExpression(initializer))
6777
+ continue;
6778
+ const funcName = decl.getName();
6779
+ if (!funcName)
6780
+ continue;
6781
+ const assignments = this.findStateMutationsInFunction(initializer, stateVarNames);
6782
+ if (assignments.length === 0)
6783
+ continue;
6784
+ const preconditions = [];
6785
+ const postconditions = [];
6786
+ this.extractVerificationConditions(initializer, preconditions, postconditions);
6787
+ const messageType = this.functionNameToMessageType(funcName);
6788
+ if (process.env["POLLY_DEBUG"]) {
6789
+ console.log(`[DEBUG] Found state-mutating arrow function: ${funcName} → ${messageType}`);
6790
+ }
6791
+ handlers.push({
6792
+ messageType,
6793
+ node: context,
6794
+ assignments,
6795
+ preconditions,
6796
+ postconditions,
6797
+ location: {
6798
+ file: filePath,
6799
+ line: decl.getStartLineNumber()
6800
+ }
6801
+ });
6802
+ }
6803
+ }
6804
+ return handlers;
6805
+ }
6806
+ findStateMutationsInFunction(func, stateVarNames) {
6807
+ const mutations = [];
6808
+ func.forEachDescendant((node) => {
6809
+ if (!Node4.isBinaryExpression(node))
6810
+ return;
6811
+ const operator = node.getOperatorToken().getText();
6812
+ if (operator !== "=")
6813
+ return;
6814
+ const left = node.getLeft();
6815
+ if (!Node4.isPropertyAccessExpression(left))
6816
+ return;
6817
+ const path3 = this.getPropertyPath(left);
6818
+ for (const varName of stateVarNames) {
6819
+ if (path3 === `${varName}.value`) {
6820
+ const right = node.getRight();
6821
+ if (Node4.isObjectLiteralExpression(right)) {
6822
+ this.extractObjectLiteralAssignments(right, mutations);
6823
+ }
6824
+ break;
6825
+ }
6826
+ const fieldPrefix = `${varName}.value.`;
6827
+ if (path3.startsWith(fieldPrefix)) {
6828
+ const field = path3.substring(fieldPrefix.length);
6829
+ const value = this.extractValue(node.getRight());
6830
+ mutations.push({ field, value: value ?? "@" });
6831
+ break;
6832
+ }
6833
+ }
6834
+ });
6835
+ return mutations;
6836
+ }
6837
+ functionNameToMessageType(funcName) {
6838
+ let name = funcName.replace(/^handle/, "").replace(/^on/, "").replace(/^set/, "Set").replace(/^update/, "Update").replace(/^do/, "");
6839
+ if (name.length > 0) {
6840
+ name = name.charAt(0).toUpperCase() + name.slice(1);
6841
+ }
6842
+ return name || funcName;
6843
+ }
6619
6844
  }
6620
6845
 
6621
6846
  // tools/analysis/src/extract/integrations.ts
@@ -9080,9 +9305,11 @@ ${generateVerificationSection(context)}
9080
9305
  - **Translation**: How TypeScript code becomes TLA+ specifications
9081
9306
  - **Verification**: What properties are being verified and what they mean
9082
9307
  - **State-Level Constraints**: Using $constraints() to declare verification constraints alongside state
9308
+ - **Verified State Discovery**: Using \`{ verify: true }\` on $sharedState to enable automatic handler discovery
9083
9309
  - **Performance**: How to optimize verification speed and state space exploration
9084
9310
  - **Debugging**: Interpreting counterexamples and fixing violations
9085
9311
  - **Configuration**: Understanding maxInFlight, bounds, and other verification parameters
9312
+ - **Universal State Management**: Using the same state code in Chrome extensions, web apps, and Node.js
9086
9313
  - **Elysia Integration**: Using Polly with Elysia/Bun servers for full-stack distributed systems verification
9087
9314
  - **Testing & Adapters**: Using mock adapters for testing and verification in Node.js environments
9088
9315
 
@@ -9101,6 +9328,23 @@ ${generateVerificationSection(context)}
9101
9328
  - **Transitive Discovery**: The analyzer uses transitive import following to discover constraints
9102
9329
  - Files outside src/ are automatically found if imported from handler files
9103
9330
  - This enables clean separation of verification code from runtime code
9331
+ - **Verified State Discovery**: When \`{ verify: true }\` is set on $sharedState/$syncedState/$persistedState:
9332
+ - The analyzer automatically discovers exported functions that modify that state signal
9333
+ - Functions with \`requires()\` and \`ensures()\` annotations get those extracted as pre/postconditions
9334
+ - State assignments like \`authState.value = { ... }\` or \`authState.value.field = x\` are detected
9335
+ - Message types are derived from function names: \`handleAuthSuccess\` → \`AuthSuccess\`
9336
+ - This enables verification for applications that don't use \`messageBus.on()\` handlers
9337
+ - Ideal for: multi-tab PWAs, WebSocket apps, reactive effect-driven architectures
9338
+ - Example:
9339
+ \`\`\`typescript
9340
+ export const authState = $sharedState('auth', { isAuthenticated: false }, { verify: true });
9341
+
9342
+ export function handleLogin(): void {
9343
+ requires(!authState.value.isAuthenticated);
9344
+ authState.value = { ...authState.value, isAuthenticated: true };
9345
+ ensures(authState.value.isAuthenticated);
9346
+ }
9347
+ \`\`\`
9104
9348
 
9105
9349
  # Verification Parameters Explained
9106
9350
 
@@ -9434,6 +9678,126 @@ All context creation functions now follow the adapter pattern:
9434
9678
  - \`getMessageBus(context, adapters?)\` ✓
9435
9679
  - \`new MessageBus(context, adapters?)\` ✓
9436
9680
 
9681
+ # Universal State Management
9682
+
9683
+ Polly's state primitives (\`$sharedState\`, \`$syncedState\`, \`$persistedState\`, \`$state\`) now work universally across Chrome extensions, web applications/PWAs, and Node.js environments using an **adapter pattern**.
9684
+
9685
+ ## The Adapter System
9686
+
9687
+ State management is decoupled into two independent concerns via adapters:
9688
+
9689
+ 1. **Storage Adapter** - Where state persists (chrome.storage, IndexedDB, or in-memory)
9690
+ 2. **Sync Adapter** - How state synchronizes across contexts (chrome.runtime, BroadcastChannel, or NoOp)
9691
+
9692
+ ## Automatic Environment Detection
9693
+
9694
+ The framework automatically detects the environment and selects appropriate adapters:
9695
+
9696
+ | Environment | Storage | Sync Transport | Use Case |
9697
+ |------------|---------|----------------|----------|
9698
+ | **Chrome Extension** | \`chrome.storage.local\` | \`chrome.runtime\` messaging | Multi-context extensions |
9699
+ | **Web App / PWA** | IndexedDB | BroadcastChannel | Multi-tab web applications |
9700
+ | **Single-Tab Web App** | IndexedDB | None (NoOp) | Single-tab applications |
9701
+ | **Node.js / Testing** | In-memory Map | None (NoOp) | Verification & unit tests |
9702
+
9703
+ **Example:**
9704
+ \`\`\`typescript
9705
+ // Same code works everywhere!
9706
+ import { $sharedState } from '@fairfox/polly/state';
9707
+
9708
+ const settings = $sharedState('settings', { theme: 'dark' });
9709
+
9710
+ // In Chrome extension:
9711
+ // → Uses chrome.storage.local + chrome.runtime messaging
9712
+ // In web app:
9713
+ // → Uses IndexedDB + BroadcastChannel
9714
+ // In Node.js test:
9715
+ // → Uses in-memory Map + NoOp sync
9716
+ \`\`\`
9717
+
9718
+ ## Why BroadcastChannel for Web Apps?
9719
+
9720
+ **Architecture Decision**: BroadcastChannel was chosen over SharedWorker for web app sync because:
9721
+ - Simpler API with no lifecycle management complexity
9722
+ - Decentralized (aligns with local-first/offline-first architecture)
9723
+ - Better browser support (especially Safari and mobile)
9724
+ - Perfect for message-passing with Lamport clock conflict resolution
9725
+ - No single point of failure
9726
+
9727
+ SharedWorker could be added as an optional adapter in the future for use cases requiring:
9728
+ - Central coordination point for complex multi-tab workflows
9729
+ - Shared WebSocket connections (one connection for all tabs)
9730
+ - Heavy computation done once and shared across tabs
9731
+ - Persistent background work when tabs are closed
9732
+
9733
+ See \`src/shared/lib/sync-adapter.ts\` for detailed architectural rationale.
9734
+
9735
+ ## Available Adapters
9736
+
9737
+ ### Storage Adapters
9738
+ - \`ChromeStorageAdapter\` - Uses \`chrome.storage.local\` (auto-detected in extensions)
9739
+ - \`IndexedDBAdapter\` - Uses IndexedDB (auto-detected in web apps)
9740
+ - \`MemoryStorageAdapter\` - Uses in-memory Map (auto-detected in Node.js)
9741
+
9742
+ ### Sync Adapters
9743
+ - \`ChromeRuntimeSyncAdapter\` - Uses \`chrome.runtime.sendMessage\` (auto-detected in extensions)
9744
+ - \`BroadcastChannelSyncAdapter\` - Uses BroadcastChannel API (auto-detected in web apps)
9745
+ - \`NoOpSyncAdapter\` - No synchronization (auto-detected for single-context scenarios)
9746
+
9747
+ ## Custom Adapters
9748
+
9749
+ Users can provide custom adapters for specialized scenarios:
9750
+
9751
+ \`\`\`typescript
9752
+ import { $sharedState } from '@fairfox/polly/state';
9753
+ import { IndexedDBAdapter, BroadcastChannelSyncAdapter } from '@fairfox/polly/adapters';
9754
+
9755
+ const settings = $sharedState('settings', defaultSettings, {
9756
+ storage: new IndexedDBAdapter('custom-db-name'),
9757
+ sync: new BroadcastChannelSyncAdapter('custom-channel'),
9758
+ });
9759
+ \`\`\`
9760
+
9761
+ ## Key Features
9762
+
9763
+ - **Write once, run anywhere**: Same state code works in extensions, web apps, and Node.js
9764
+ - **Environment-optimized**: Uses the best available APIs for each platform
9765
+ - **No conditional logic**: Framework handles detection and selection
9766
+ - **Testable**: Mock adapters enable testing without Chrome APIs
9767
+ - **Extensible**: Custom adapters for specialized use cases
9768
+
9769
+ ## Common Use Cases
9770
+
9771
+ ### Multi-Tab Web Application
9772
+ \`\`\`typescript
9773
+ // State automatically syncs across tabs using BroadcastChannel
9774
+ const todos = $sharedState('todos', []);
9775
+ \`\`\`
9776
+
9777
+ ### Chrome Extension
9778
+ \`\`\`typescript
9779
+ // State syncs across extension contexts using chrome.runtime
9780
+ const settings = $sharedState('settings', { theme: 'dark' });
9781
+ \`\`\`
9782
+
9783
+ ### Single-Tab Web App (PWA)
9784
+ \`\`\`typescript
9785
+ // State persists to IndexedDB but doesn't sync (no other tabs)
9786
+ const user = $sharedState('user', null, {
9787
+ sync: new NoOpSyncAdapter() // Explicitly disable sync
9788
+ });
9789
+ \`\`\`
9790
+
9791
+ ### Node.js Testing
9792
+ \`\`\`typescript
9793
+ // State uses in-memory storage, no persistence or sync
9794
+ const mockState = $sharedState('test-state', defaultValue);
9795
+ \`\`\`
9796
+
9797
+ ## Migration from Chrome-Only
9798
+
9799
+ Existing Chrome extension code requires **no changes** - the framework is backward compatible. Web apps can now use the same primitives that previously only worked in extensions.
9800
+
9437
9801
  Begin by understanding their question and providing a clear, precise answer based on their project context.`;
9438
9802
  }
9439
9803
  function generateProjectSection(context, contexts2, handlers2, flows2) {
@@ -9583,6 +9947,11 @@ confidence that they will work when users apply them to their configuration.
9583
9947
  Constraints and type guards can be organized in separate files (e.g., specs/constraints.ts) and will
9584
9948
  be automatically discovered via imports. Files outside src/ are fully supported.
9585
9949
 
9950
+ **Verified State Discovery**: When \`{ verify: true }\` is set on $sharedState/$syncedState/$persistedState,
9951
+ the analyzer automatically discovers exported functions that modify that state and extracts their
9952
+ \`requires()\`/\`ensures()\` annotations. This enables verification for apps that don't use \`messageBus.on()\`
9953
+ handlers, such as multi-tab PWAs or WebSocket applications with reactive effect-driven state changes.
9954
+
9586
9955
  # Communication Style
9587
9956
 
9588
9957
  - Direct and precise - no fluff
@@ -9925,4 +10294,4 @@ Goodbye!`);
9925
10294
  }
9926
10295
  main();
9927
10296
 
9928
- //# debugId=0C5B6EFE146F7BE464756E2164756E21
10297
+ //# debugId=4FDE5BCEB24CA52B64756E2164756E21