@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
@@ -3419,21 +3419,45 @@ class HandlerExtractor {
3419
3419
  const messageTypes = new Set;
3420
3420
  const invalidMessageTypes = new Set;
3421
3421
  const stateConstraints = [];
3422
+ const verifiedStates = [];
3422
3423
  const allSourceFiles = this.project.getSourceFiles();
3423
3424
  const entryPoints = allSourceFiles.filter((f) => this.isWithinPackage(f.getFilePath()));
3424
3425
  this.debugLogSourceFiles(allSourceFiles, entryPoints);
3425
3426
  for (const entryPoint of entryPoints) {
3426
- this.analyzeFileAndImports(entryPoint, handlers, messageTypes, invalidMessageTypes, stateConstraints);
3427
+ this.analyzeFileAndImports(entryPoint, handlers, messageTypes, invalidMessageTypes, stateConstraints, verifiedStates);
3428
+ }
3429
+ if (verifiedStates.length > 0) {
3430
+ if (process.env["POLLY_DEBUG"]) {
3431
+ console.log(`[DEBUG] Found ${verifiedStates.length} verified state(s), scanning for mutating functions...`);
3432
+ }
3433
+ for (const filePath of this.analyzedFiles) {
3434
+ const sourceFile = this.project.getSourceFile(filePath);
3435
+ if (!sourceFile)
3436
+ continue;
3437
+ const mutatingHandlers = this.findStateMutatingFunctions(sourceFile, verifiedStates);
3438
+ for (const handler of mutatingHandlers) {
3439
+ const exists = handlers.some((h) => h.messageType === handler.messageType && h.location.file === handler.location.file);
3440
+ if (!exists) {
3441
+ handlers.push(handler);
3442
+ if (this.isValidTLAIdentifier(handler.messageType)) {
3443
+ messageTypes.add(handler.messageType);
3444
+ } else {
3445
+ invalidMessageTypes.add(handler.messageType);
3446
+ }
3447
+ }
3448
+ }
3449
+ }
3427
3450
  }
3428
3451
  this.debugLogExtractionResults(handlers.length, invalidMessageTypes.size);
3429
3452
  this.debugLogAnalysisStats(allSourceFiles.length, entryPoints.length);
3430
3453
  return {
3431
3454
  handlers,
3432
3455
  messageTypes,
3433
- stateConstraints
3456
+ stateConstraints,
3457
+ verifiedStates
3434
3458
  };
3435
3459
  }
3436
- analyzeFileAndImports(sourceFile, handlers, messageTypes, invalidMessageTypes, stateConstraints) {
3460
+ analyzeFileAndImports(sourceFile, handlers, messageTypes, invalidMessageTypes, stateConstraints, verifiedStates) {
3437
3461
  const filePath = sourceFile.getFilePath();
3438
3462
  if (this.analyzedFiles.has(filePath)) {
3439
3463
  return;
@@ -3447,6 +3471,8 @@ class HandlerExtractor {
3447
3471
  this.categorizeHandlerMessageTypes(fileHandlers, messageTypes, invalidMessageTypes);
3448
3472
  const fileConstraints = this.extractStateConstraintsFromFile(sourceFile);
3449
3473
  stateConstraints.push(...fileConstraints);
3474
+ const fileVerifiedStates = this.extractVerifiedStatesFromFile(sourceFile);
3475
+ verifiedStates.push(...fileVerifiedStates);
3450
3476
  const importDeclarations = sourceFile.getImportDeclarations();
3451
3477
  for (const importDecl of importDeclarations) {
3452
3478
  const importedFile = importDecl.getModuleSpecifierSourceFile();
@@ -3458,7 +3484,7 @@ class HandlerExtractor {
3458
3484
  }
3459
3485
  continue;
3460
3486
  }
3461
- this.analyzeFileAndImports(importedFile, handlers, messageTypes, invalidMessageTypes, stateConstraints);
3487
+ this.analyzeFileAndImports(importedFile, handlers, messageTypes, invalidMessageTypes, stateConstraints, verifiedStates);
3462
3488
  } else if (process.env["POLLY_DEBUG"]) {
3463
3489
  const specifier = importDecl.getModuleSpecifierValue();
3464
3490
  if (!specifier.startsWith("node:") && !this.isNodeModuleImport(specifier)) {
@@ -3667,7 +3693,7 @@ class HandlerExtractor {
3667
3693
  return;
3668
3694
  }
3669
3695
  const valueMatch = fieldPath.match(/\.value\.(.+)$/);
3670
- if (valueMatch && valueMatch[1]) {
3696
+ if (valueMatch?.[1]) {
3671
3697
  const field = valueMatch[1];
3672
3698
  const value = this.extractValue(right);
3673
3699
  if (value !== undefined) {
@@ -3725,7 +3751,7 @@ class HandlerExtractor {
3725
3751
  return fieldPath.substring(6);
3726
3752
  }
3727
3753
  const valueMatch = fieldPath.match(/\.value\.(.+)$/);
3728
- if (valueMatch && valueMatch[1]) {
3754
+ if (valueMatch?.[1]) {
3729
3755
  return valueMatch[1];
3730
3756
  }
3731
3757
  return null;
@@ -4533,6 +4559,205 @@ class HandlerExtractor {
4533
4559
  }
4534
4560
  return results;
4535
4561
  }
4562
+ extractVerifiedStatesFromFile(sourceFile) {
4563
+ const verifiedStates = [];
4564
+ const filePath = sourceFile.getFilePath();
4565
+ sourceFile.forEachDescendant((node) => {
4566
+ if (!Node2.isCallExpression(node))
4567
+ return;
4568
+ const stateInfo = this.recognizeVerifiedStateCall(node, filePath);
4569
+ if (stateInfo) {
4570
+ verifiedStates.push(stateInfo);
4571
+ }
4572
+ });
4573
+ return verifiedStates;
4574
+ }
4575
+ recognizeVerifiedStateCall(node, filePath) {
4576
+ if (!Node2.isCallExpression(node))
4577
+ return null;
4578
+ const expression = node.getExpression();
4579
+ if (!Node2.isIdentifier(expression))
4580
+ return null;
4581
+ const funcName = expression.getText();
4582
+ if (!["$sharedState", "$syncedState", "$persistedState"].includes(funcName)) {
4583
+ return null;
4584
+ }
4585
+ const args = node.getArguments();
4586
+ if (args.length < 2)
4587
+ return null;
4588
+ const optionsArg = args[2];
4589
+ if (!optionsArg || !this.hasVerifyTrue(optionsArg))
4590
+ return null;
4591
+ const keyArg = args[0];
4592
+ if (!keyArg || !Node2.isStringLiteral(keyArg))
4593
+ return null;
4594
+ const key = keyArg.getLiteralValue();
4595
+ const variableName = this.getVariableNameFromParent(node) || key;
4596
+ const initialValueArg = args[1];
4597
+ const fields = initialValueArg ? this.extractFieldNames(initialValueArg) : [];
4598
+ if (process.env["POLLY_DEBUG"]) {
4599
+ console.log(`[DEBUG] Found verified state: ${variableName} (key: "${key}") with fields: [${fields.join(", ")}]`);
4600
+ }
4601
+ return {
4602
+ key,
4603
+ variableName,
4604
+ filePath,
4605
+ line: node.getStartLineNumber(),
4606
+ fields
4607
+ };
4608
+ }
4609
+ hasVerifyTrue(optionsNode) {
4610
+ if (!Node2.isObjectLiteralExpression(optionsNode))
4611
+ return false;
4612
+ for (const prop of optionsNode.getProperties()) {
4613
+ if (!Node2.isPropertyAssignment(prop))
4614
+ continue;
4615
+ const name = prop.getName();
4616
+ if (name !== "verify")
4617
+ continue;
4618
+ const initializer = prop.getInitializer();
4619
+ if (initializer && initializer.getKind() === SyntaxKind.TrueKeyword) {
4620
+ return true;
4621
+ }
4622
+ }
4623
+ return false;
4624
+ }
4625
+ getVariableNameFromParent(node) {
4626
+ const parent = node.getParent();
4627
+ if (Node2.isVariableDeclaration(parent)) {
4628
+ return parent.getName();
4629
+ }
4630
+ return null;
4631
+ }
4632
+ extractFieldNames(node) {
4633
+ const fields = [];
4634
+ if (Node2.isObjectLiteralExpression(node)) {
4635
+ for (const prop of node.getProperties()) {
4636
+ if (Node2.isPropertyAssignment(prop) || Node2.isShorthandPropertyAssignment(prop)) {
4637
+ fields.push(prop.getName());
4638
+ }
4639
+ }
4640
+ } else if (Node2.isIdentifier(node)) {
4641
+ const definitions = node.getDefinitionNodes();
4642
+ for (const def of definitions) {
4643
+ if (Node2.isVariableDeclaration(def)) {
4644
+ const initializer = def.getInitializer();
4645
+ if (initializer && Node2.isObjectLiteralExpression(initializer)) {
4646
+ return this.extractFieldNames(initializer);
4647
+ }
4648
+ }
4649
+ }
4650
+ }
4651
+ return fields;
4652
+ }
4653
+ findStateMutatingFunctions(sourceFile, verifiedStates) {
4654
+ const handlers = [];
4655
+ const stateVarNames = new Set(verifiedStates.map((s) => s.variableName));
4656
+ const filePath = sourceFile.getFilePath();
4657
+ const context = this.inferContext(filePath);
4658
+ for (const func of sourceFile.getFunctions()) {
4659
+ if (!func.isExported())
4660
+ continue;
4661
+ const funcName = func.getName();
4662
+ if (!funcName)
4663
+ continue;
4664
+ const assignments = this.findStateMutationsInFunction(func, stateVarNames);
4665
+ if (assignments.length === 0)
4666
+ continue;
4667
+ const preconditions = [];
4668
+ const postconditions = [];
4669
+ this.extractVerificationConditions(func, preconditions, postconditions);
4670
+ const messageType = this.functionNameToMessageType(funcName);
4671
+ if (process.env["POLLY_DEBUG"]) {
4672
+ console.log(`[DEBUG] Found state-mutating function: ${funcName} → ${messageType} ` + `(${assignments.length} assignments, ${preconditions.length} preconditions, ${postconditions.length} postconditions)`);
4673
+ }
4674
+ handlers.push({
4675
+ messageType,
4676
+ node: context,
4677
+ assignments,
4678
+ preconditions,
4679
+ postconditions,
4680
+ location: {
4681
+ file: filePath,
4682
+ line: func.getStartLineNumber()
4683
+ }
4684
+ });
4685
+ }
4686
+ for (const varStmt of sourceFile.getVariableStatements()) {
4687
+ if (!varStmt.isExported())
4688
+ continue;
4689
+ for (const decl of varStmt.getDeclarations()) {
4690
+ const initializer = decl.getInitializer();
4691
+ if (!initializer)
4692
+ continue;
4693
+ if (!Node2.isArrowFunction(initializer) && !Node2.isFunctionExpression(initializer))
4694
+ continue;
4695
+ const funcName = decl.getName();
4696
+ if (!funcName)
4697
+ continue;
4698
+ const assignments = this.findStateMutationsInFunction(initializer, stateVarNames);
4699
+ if (assignments.length === 0)
4700
+ continue;
4701
+ const preconditions = [];
4702
+ const postconditions = [];
4703
+ this.extractVerificationConditions(initializer, preconditions, postconditions);
4704
+ const messageType = this.functionNameToMessageType(funcName);
4705
+ if (process.env["POLLY_DEBUG"]) {
4706
+ console.log(`[DEBUG] Found state-mutating arrow function: ${funcName} → ${messageType}`);
4707
+ }
4708
+ handlers.push({
4709
+ messageType,
4710
+ node: context,
4711
+ assignments,
4712
+ preconditions,
4713
+ postconditions,
4714
+ location: {
4715
+ file: filePath,
4716
+ line: decl.getStartLineNumber()
4717
+ }
4718
+ });
4719
+ }
4720
+ }
4721
+ return handlers;
4722
+ }
4723
+ findStateMutationsInFunction(func, stateVarNames) {
4724
+ const mutations = [];
4725
+ func.forEachDescendant((node) => {
4726
+ if (!Node2.isBinaryExpression(node))
4727
+ return;
4728
+ const operator = node.getOperatorToken().getText();
4729
+ if (operator !== "=")
4730
+ return;
4731
+ const left = node.getLeft();
4732
+ if (!Node2.isPropertyAccessExpression(left))
4733
+ return;
4734
+ const path2 = this.getPropertyPath(left);
4735
+ for (const varName of stateVarNames) {
4736
+ if (path2 === `${varName}.value`) {
4737
+ const right = node.getRight();
4738
+ if (Node2.isObjectLiteralExpression(right)) {
4739
+ this.extractObjectLiteralAssignments(right, mutations);
4740
+ }
4741
+ break;
4742
+ }
4743
+ const fieldPrefix = `${varName}.value.`;
4744
+ if (path2.startsWith(fieldPrefix)) {
4745
+ const field = path2.substring(fieldPrefix.length);
4746
+ const value = this.extractValue(node.getRight());
4747
+ mutations.push({ field, value: value ?? "@" });
4748
+ break;
4749
+ }
4750
+ }
4751
+ });
4752
+ return mutations;
4753
+ }
4754
+ functionNameToMessageType(funcName) {
4755
+ let name = funcName.replace(/^handle/, "").replace(/^on/, "").replace(/^set/, "Set").replace(/^update/, "Update").replace(/^do/, "");
4756
+ if (name.length > 0) {
4757
+ name = name.charAt(0).toUpperCase() + name.slice(1);
4758
+ }
4759
+ return name || funcName;
4760
+ }
4536
4761
  }
4537
4762
 
4538
4763
  // tools/analysis/src/extract/types.ts
@@ -5434,4 +5659,4 @@ main().catch((error) => {
5434
5659
  process.exit(1);
5435
5660
  });
5436
5661
 
5437
- //# debugId=6B12EC08F58C418A64756E2164756E21
5662
+ //# debugId=B1C82DC4E0975F9164756E2164756E21