@fairfox/polly 0.17.0 → 0.19.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.
@@ -13,6 +13,48 @@ var __export = (target, all) => {
13
13
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
14
14
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
15
15
 
16
+ // tools/verify/src/analysis/non-interference.ts
17
+ var exports_non_interference = {};
18
+ __export(exports_non_interference, {
19
+ checkNonInterference: () => checkNonInterference
20
+ });
21
+ function checkNonInterference(subsystems, handlers) {
22
+ const violations = [];
23
+ const fieldOwner = new Map;
24
+ for (const [name, sub] of Object.entries(subsystems)) {
25
+ for (const field of sub.state) {
26
+ fieldOwner.set(field, name);
27
+ }
28
+ }
29
+ const handlerSubsystem = new Map;
30
+ for (const [name, sub] of Object.entries(subsystems)) {
31
+ for (const h of sub.handlers) {
32
+ handlerSubsystem.set(h, name);
33
+ }
34
+ }
35
+ for (const handler of handlers) {
36
+ const subsystemName = handlerSubsystem.get(handler.messageType);
37
+ if (!subsystemName)
38
+ continue;
39
+ for (const assignment of handler.assignments) {
40
+ const fieldName = assignment.field;
41
+ const owner = fieldOwner.get(fieldName);
42
+ if (owner && owner !== subsystemName) {
43
+ violations.push({
44
+ handler: handler.messageType,
45
+ subsystem: subsystemName,
46
+ writesTo: fieldName,
47
+ ownedBy: owner
48
+ });
49
+ }
50
+ }
51
+ }
52
+ return {
53
+ valid: violations.length === 0,
54
+ violations
55
+ };
56
+ }
57
+
16
58
  // tools/verify/src/codegen/invariants.ts
17
59
  import { Node as Node3, Project as Project3 } from "ts-morph";
18
60
 
@@ -341,6 +383,7 @@ class TemporalTLAGenerator {
341
383
  var exports_tla = {};
342
384
  __export(exports_tla, {
343
385
  generateTLA: () => generateTLA,
386
+ generateSubsystemTLA: () => generateSubsystemTLA,
344
387
  TLAValidationError: () => TLAValidationError,
345
388
  TLAGenerator: () => TLAGenerator
346
389
  });
@@ -351,6 +394,58 @@ async function generateTLA(config, analysis) {
351
394
  const generator = new TLAGenerator;
352
395
  return await generator.generate(config, analysis);
353
396
  }
397
+ async function generateSubsystemTLA(_subsystemName, subsystem, config, analysis) {
398
+ const stateFields = new Set(subsystem.state);
399
+ const handlerNames = new Set(subsystem.handlers);
400
+ const filteredState = {};
401
+ for (const [field, fieldConfig] of Object.entries(config.state)) {
402
+ if (stateFields.has(field)) {
403
+ filteredState[field] = fieldConfig;
404
+ }
405
+ }
406
+ const filteredMessages = {
407
+ ...config.messages,
408
+ include: subsystem.handlers
409
+ };
410
+ filteredMessages.exclude = undefined;
411
+ if (filteredMessages.perMessageBounds) {
412
+ const filteredBounds = {};
413
+ for (const [msg, bound] of Object.entries(filteredMessages.perMessageBounds)) {
414
+ if (handlerNames.has(msg)) {
415
+ filteredBounds[msg] = bound;
416
+ }
417
+ }
418
+ filteredMessages.perMessageBounds = filteredBounds;
419
+ }
420
+ const filteredConfig = {
421
+ ...config,
422
+ state: filteredState,
423
+ messages: filteredMessages,
424
+ subsystems: undefined
425
+ };
426
+ if (filteredConfig.tier2?.temporalConstraints) {
427
+ filteredConfig.tier2 = {
428
+ ...filteredConfig.tier2,
429
+ temporalConstraints: filteredConfig.tier2.temporalConstraints.filter((tc) => handlerNames.has(tc.before) && handlerNames.has(tc.after))
430
+ };
431
+ }
432
+ const subsystemFieldPatterns = subsystem.state.map((field) => {
433
+ const dotIdx = field.indexOf(".");
434
+ if (dotIdx >= 0) {
435
+ return `${field.substring(0, dotIdx)}.value.${field.substring(dotIdx + 1)}`;
436
+ }
437
+ return `${field}.value`;
438
+ });
439
+ const filteredGlobalStateConstraints = (analysis.globalStateConstraints ?? []).filter((constraint) => subsystemFieldPatterns.some((pattern) => constraint.expression.includes(pattern)));
440
+ const filteredAnalysis = {
441
+ ...analysis,
442
+ messageTypes: analysis.messageTypes.filter((mt) => handlerNames.has(mt)),
443
+ handlers: analysis.handlers.filter((h) => handlerNames.has(h.messageType)),
444
+ globalStateConstraints: filteredGlobalStateConstraints
445
+ };
446
+ const generator = new TLAGenerator;
447
+ return await generator.generate(filteredConfig, filteredAnalysis, `UserApp_${_subsystemName}`);
448
+ }
354
449
  var TLAValidationError, TLAGenerator;
355
450
  var init_tla = __esm(() => {
356
451
  init_invariants();
@@ -372,6 +467,7 @@ var init_tla = __esm(() => {
372
467
  resolvedActionNames = new Map;
373
468
  tabSymmetryEnabled = false;
374
469
  tabCount = 0;
470
+ moduleName = "UserApp";
375
471
  constructor(options) {
376
472
  this.options = options;
377
473
  }
@@ -381,7 +477,10 @@ var init_tla = __esm(() => {
381
477
  }
382
478
  return /^[a-zA-Z][a-zA-Z0-9_]*$/.test(s);
383
479
  }
384
- async generate(config, analysis) {
480
+ async generate(config, analysis, moduleName) {
481
+ if (moduleName) {
482
+ this.moduleName = moduleName;
483
+ }
385
484
  this.validateInputs(config, analysis);
386
485
  this.extractInvariantsIfEnabled();
387
486
  this.generateTemporalPropertiesIfEnabled(analysis);
@@ -671,7 +770,7 @@ var init_tla = __esm(() => {
671
770
  }
672
771
  }
673
772
  addHeader() {
674
- this.line("------------------------- MODULE UserApp -------------------------");
773
+ this.line(`------------------------- MODULE ${this.moduleName} -------------------------`);
675
774
  this.line("(*");
676
775
  this.line(" Auto-generated TLA+ specification for web extension");
677
776
  this.line(" ");
@@ -1276,19 +1375,61 @@ var init_tla = __esm(() => {
1276
1375
  return assignment;
1277
1376
  }
1278
1377
  emitStateUpdates(validAssignments, preconditions) {
1279
- if (validAssignments.length > 0) {
1280
- const exceptClauses = validAssignments.map((a) => {
1378
+ if (validAssignments.length === 0) {
1379
+ if (preconditions.length === 0) {
1380
+ this.line("\\* No state changes in handler");
1381
+ }
1382
+ this.line("/\\ UNCHANGED contextStates");
1383
+ return true;
1384
+ }
1385
+ const ndetAssignments = [];
1386
+ const detAssignments = [];
1387
+ for (const a of validAssignments) {
1388
+ if (typeof a.value === "string" && a.value.startsWith("NDET:")) {
1389
+ ndetAssignments.push({ field: a.field, value: a.value });
1390
+ } else {
1391
+ detAssignments.push(a);
1392
+ }
1393
+ }
1394
+ if (ndetAssignments.length === 0) {
1395
+ const exceptClauses = detAssignments.map((a) => {
1281
1396
  const tlaValue = this.assignmentValueToTLA(a.value);
1282
1397
  return `![ctx].${this.sanitizeFieldName(a.field)} = ${tlaValue}`;
1283
1398
  });
1284
1399
  this.line(`/\\ contextStates' = [contextStates EXCEPT ${exceptClauses.join(", ")}]`);
1285
1400
  return false;
1286
1401
  }
1287
- if (preconditions.length === 0) {
1288
- this.line("\\* No state changes in handler");
1402
+ this.emitNDETStateUpdates(ndetAssignments, detAssignments);
1403
+ return false;
1404
+ }
1405
+ emitNDETStateUpdates(ndetAssignments, detAssignments) {
1406
+ const detExceptClauses = detAssignments.map((a) => {
1407
+ const tlaValue = this.assignmentValueToTLA(a.value);
1408
+ return `![ctx].${this.sanitizeFieldName(a.field)} = ${tlaValue}`;
1409
+ });
1410
+ const ndetExceptClauses = [];
1411
+ const quantifierOpeners = [];
1412
+ for (const a of ndetAssignments) {
1413
+ const fieldName = this.sanitizeFieldName(a.field);
1414
+ const fieldRef = `contextStates[ctx].${fieldName}`;
1415
+ if (a.value === "NDET:FILTER") {
1416
+ quantifierOpeners.push(`/\\ \\E newLen_${fieldName} \\in 0..Len(${fieldRef}) :`);
1417
+ ndetExceptClauses.push(`![ctx].${fieldName} = SubSeq(@, 1, newLen_${fieldName})`);
1418
+ } else if (a.value === "NDET:MAP") {
1419
+ quantifierOpeners.push(`/\\ \\E mapIdx_${fieldName} \\in 1..Len(${fieldRef}) :`);
1420
+ quantifierOpeners.push(`\\E mapVal_${fieldName} \\in Value :`);
1421
+ ndetExceptClauses.push(`![ctx].${fieldName} = [@ EXCEPT ![mapIdx_${fieldName}] = mapVal_${fieldName}]`);
1422
+ }
1423
+ }
1424
+ for (const opener of quantifierOpeners) {
1425
+ this.line(opener);
1426
+ this.indent++;
1427
+ }
1428
+ const allExceptClauses = [...detExceptClauses, ...ndetExceptClauses];
1429
+ this.line(`/\\ contextStates' = [contextStates EXCEPT ${allExceptClauses.join(", ")}]`);
1430
+ for (const _opener of quantifierOpeners) {
1431
+ this.indent--;
1289
1432
  }
1290
- this.line("/\\ UNCHANGED contextStates");
1291
- return true;
1292
1433
  }
1293
1434
  tsExpressionToTLA(expr, isPrimed = false) {
1294
1435
  if (!expr || typeof expr !== "string") {
@@ -1701,6 +1842,9 @@ var init_tla = __esm(() => {
1701
1842
  return "NULL";
1702
1843
  }
1703
1844
  if (typeof value === "string") {
1845
+ if (value.startsWith("NDET:")) {
1846
+ throw new Error(`NDET marker "${value}" reached assignmentValueToTLA — should have been partitioned in emitStateUpdates`);
1847
+ }
1704
1848
  if (value.startsWith("param:")) {
1705
1849
  const paramName = value.substring(6);
1706
1850
  return `payload.${this.sanitizeFieldName(paramName)}`;
@@ -3335,6 +3479,9 @@ class ConfigValidator {
3335
3479
  if (config.tier2) {
3336
3480
  this.validateTier2Optimizations(config.tier2);
3337
3481
  }
3482
+ if (config.subsystems) {
3483
+ this.validateSubsystems(config.subsystems, config.state);
3484
+ }
3338
3485
  }
3339
3486
  findNullPlaceholders(obj, path2) {
3340
3487
  if (obj === null || obj === undefined) {
@@ -3651,6 +3798,68 @@ class ConfigValidator {
3651
3798
  }
3652
3799
  }
3653
3800
  }
3801
+ validateSubsystems(subsystems, stateConfig) {
3802
+ const stateFieldNames = Object.keys(stateConfig);
3803
+ const allAssignedHandlers = new Map;
3804
+ const allAssignedFields = new Map;
3805
+ for (const [subsystemName, subsystem] of Object.entries(subsystems)) {
3806
+ for (const field of subsystem.state) {
3807
+ if (!stateFieldNames.includes(field)) {
3808
+ this.issues.push({
3809
+ type: "invalid_value",
3810
+ severity: "error",
3811
+ field: `subsystems.${subsystemName}.state`,
3812
+ message: `State field "${field}" does not exist in top-level state config`,
3813
+ suggestion: `Available fields: ${stateFieldNames.join(", ")}`
3814
+ });
3815
+ }
3816
+ const existingOwner = allAssignedFields.get(field);
3817
+ if (existingOwner) {
3818
+ this.issues.push({
3819
+ type: "invalid_value",
3820
+ severity: "warning",
3821
+ field: `subsystems.${subsystemName}.state`,
3822
+ message: `State field "${field}" is assigned to both "${existingOwner}" and "${subsystemName}"`,
3823
+ suggestion: "State fields should be partitioned across subsystems for non-interference"
3824
+ });
3825
+ } else {
3826
+ allAssignedFields.set(field, subsystemName);
3827
+ }
3828
+ }
3829
+ if (subsystem.handlers.length === 0) {
3830
+ this.issues.push({
3831
+ type: "invalid_value",
3832
+ severity: "warning",
3833
+ field: `subsystems.${subsystemName}.handlers`,
3834
+ message: `Subsystem "${subsystemName}" has no handlers`,
3835
+ suggestion: "Add at least one handler to the subsystem"
3836
+ });
3837
+ }
3838
+ for (const handler of subsystem.handlers) {
3839
+ const existingOwner = allAssignedHandlers.get(handler);
3840
+ if (existingOwner) {
3841
+ this.issues.push({
3842
+ type: "invalid_value",
3843
+ severity: "error",
3844
+ field: `subsystems.${subsystemName}.handlers`,
3845
+ message: `Handler "${handler}" is assigned to both "${existingOwner}" and "${subsystemName}"`,
3846
+ suggestion: "Each handler must belong to exactly one subsystem"
3847
+ });
3848
+ } else {
3849
+ allAssignedHandlers.set(handler, subsystemName);
3850
+ }
3851
+ }
3852
+ if (subsystem.state.length === 0) {
3853
+ this.issues.push({
3854
+ type: "invalid_value",
3855
+ severity: "warning",
3856
+ field: `subsystems.${subsystemName}.state`,
3857
+ message: `Subsystem "${subsystemName}" has no state fields`,
3858
+ suggestion: "Add at least one state field to the subsystem"
3859
+ });
3860
+ }
3861
+ }
3862
+ }
3654
3863
  }
3655
3864
  function validateConfig(configPath) {
3656
3865
  const validator = new ConfigValidator;
@@ -4069,7 +4278,8 @@ class HandlerExtractor {
4069
4278
  packageRoot;
4070
4279
  warnings;
4071
4280
  currentFunctionParams = [];
4072
- constructor(tsConfigPath) {
4281
+ contextOverrides;
4282
+ constructor(tsConfigPath, contextOverrides) {
4073
4283
  this.project = new Project({
4074
4284
  tsConfigFilePath: tsConfigPath
4075
4285
  });
@@ -4077,6 +4287,7 @@ class HandlerExtractor {
4077
4287
  this.relationshipExtractor = new RelationshipExtractor;
4078
4288
  this.analyzedFiles = new Set;
4079
4289
  this.warnings = [];
4290
+ this.contextOverrides = contextOverrides || new Map;
4080
4291
  this.packageRoot = this.findPackageRoot(tsConfigPath);
4081
4292
  }
4082
4293
  warnUnsupportedPattern(pattern, location, suggestion) {
@@ -4294,8 +4505,20 @@ class HandlerExtractor {
4294
4505
  handlers.push(handler);
4295
4506
  }
4296
4507
  }
4508
+ if (methodName === "ws") {
4509
+ this.extractElysiaWsHandlers(node, context, filePath, handlers);
4510
+ }
4511
+ if (this.isRestMethod(methodName) && this.isWebFrameworkFile(node.getSourceFile())) {
4512
+ const restHandler = this.extractRestHandler(node, methodName, context, filePath);
4513
+ if (restHandler) {
4514
+ handlers.push(restHandler);
4515
+ }
4516
+ }
4297
4517
  }
4298
4518
  }
4519
+ isRestMethod(name) {
4520
+ return ["get", "post", "put", "delete", "patch"].includes(name);
4521
+ }
4299
4522
  isElseIfStatement(node) {
4300
4523
  const parent = node.getParent();
4301
4524
  return parent !== undefined && Node2.isIfStatement(parent);
@@ -4361,6 +4584,135 @@ class HandlerExtractor {
4361
4584
  parameters
4362
4585
  };
4363
4586
  }
4587
+ extractElysiaWsHandlers(node, context, filePath, handlers) {
4588
+ const args = node.getArguments();
4589
+ if (args.length < 2)
4590
+ return;
4591
+ const routeArg = args[0];
4592
+ if (!routeArg || !Node2.isStringLiteral(routeArg))
4593
+ return;
4594
+ const routePath = routeArg.getLiteralValue();
4595
+ const configArg = args[1];
4596
+ if (!configArg || !Node2.isObjectLiteralExpression(configArg))
4597
+ return;
4598
+ const callbacks = ["message", "open", "close"];
4599
+ for (const cbName of callbacks) {
4600
+ const prop = configArg.getProperty(cbName);
4601
+ if (!prop)
4602
+ continue;
4603
+ let funcBody = null;
4604
+ if (Node2.isMethodDeclaration(prop)) {
4605
+ funcBody = prop;
4606
+ } else if (Node2.isPropertyAssignment(prop)) {
4607
+ const init = prop.getInitializer();
4608
+ if (init && (Node2.isArrowFunction(init) || Node2.isFunctionExpression(init))) {
4609
+ funcBody = init;
4610
+ }
4611
+ }
4612
+ if (!funcBody)
4613
+ continue;
4614
+ if (cbName === "message") {
4615
+ const body = funcBody.getBody();
4616
+ if (!body)
4617
+ continue;
4618
+ const subHandlers = this.extractSubHandlersFromBody(body, context, filePath);
4619
+ if (subHandlers.length > 0) {
4620
+ handlers.push(...subHandlers);
4621
+ } else {
4622
+ handlers.push(this.buildWsHandler(`ws_message`, routePath, context, filePath, funcBody, node.getStartLineNumber()));
4623
+ }
4624
+ } else {
4625
+ handlers.push(this.buildWsHandler(`ws_${cbName}`, routePath, context, filePath, funcBody, node.getStartLineNumber()));
4626
+ }
4627
+ }
4628
+ }
4629
+ extractSubHandlersFromBody(body, context, filePath) {
4630
+ const subHandlers = [];
4631
+ body.forEachDescendant((child) => {
4632
+ if (Node2.isIfStatement(child) && !this.isElseIfStatement(child)) {
4633
+ const typeGuardHandlers = this.extractTypeGuardHandlers(child, context, filePath);
4634
+ subHandlers.push(...typeGuardHandlers);
4635
+ }
4636
+ if (Node2.isSwitchStatement(child)) {
4637
+ const switchHandlers = this.extractSwitchCaseHandlers(child, context, filePath);
4638
+ subHandlers.push(...switchHandlers);
4639
+ }
4640
+ });
4641
+ return subHandlers;
4642
+ }
4643
+ buildWsHandler(messageType, _routePath, context, filePath, funcBody, line) {
4644
+ const assignments = [];
4645
+ const preconditions = [];
4646
+ const postconditions = [];
4647
+ this.currentFunctionParams = this.extractParameterNames(funcBody);
4648
+ this.extractAssignments(funcBody, assignments);
4649
+ this.extractVerificationConditions(funcBody, preconditions, postconditions);
4650
+ this.currentFunctionParams = [];
4651
+ return {
4652
+ messageType,
4653
+ node: context,
4654
+ assignments,
4655
+ preconditions,
4656
+ postconditions,
4657
+ location: { file: filePath, line },
4658
+ origin: "event"
4659
+ };
4660
+ }
4661
+ extractRestHandler(node, methodName, context, filePath) {
4662
+ const args = node.getArguments();
4663
+ if (args.length < 2)
4664
+ return null;
4665
+ const routeArg = args[0];
4666
+ if (!routeArg || !Node2.isStringLiteral(routeArg))
4667
+ return null;
4668
+ const routePath = routeArg.getLiteralValue();
4669
+ const httpMethod = methodName.toUpperCase();
4670
+ const messageType = `${httpMethod} ${routePath}`;
4671
+ const handlerArg = args[1];
4672
+ const assignments = [];
4673
+ const preconditions = [];
4674
+ const postconditions = [];
4675
+ let actualHandler = null;
4676
+ if (Node2.isArrowFunction(handlerArg) || Node2.isFunctionExpression(handlerArg)) {
4677
+ actualHandler = handlerArg;
4678
+ } else if (Node2.isIdentifier(handlerArg)) {
4679
+ actualHandler = this.resolveFunctionReference(handlerArg);
4680
+ }
4681
+ let parameters;
4682
+ if (actualHandler) {
4683
+ this.currentFunctionParams = this.extractParameterNames(actualHandler);
4684
+ parameters = this.currentFunctionParams.length > 0 ? [...this.currentFunctionParams] : undefined;
4685
+ this.extractAssignments(actualHandler, assignments);
4686
+ this.extractVerificationConditions(actualHandler, preconditions, postconditions);
4687
+ this.currentFunctionParams = [];
4688
+ }
4689
+ return {
4690
+ messageType,
4691
+ node: context,
4692
+ assignments,
4693
+ preconditions,
4694
+ postconditions,
4695
+ location: {
4696
+ file: filePath,
4697
+ line: node.getStartLineNumber()
4698
+ },
4699
+ origin: "event",
4700
+ parameters,
4701
+ handlerKind: "rest",
4702
+ httpMethod,
4703
+ routePath
4704
+ };
4705
+ }
4706
+ isWebFrameworkFile(sourceFile) {
4707
+ const frameworks = ["elysia", "express", "hono", "fastify", "koa", "@elysiajs/eden"];
4708
+ for (const importDecl of sourceFile.getImportDeclarations()) {
4709
+ const specifier = importDecl.getModuleSpecifierValue();
4710
+ if (frameworks.some((fw) => specifier === fw || specifier.startsWith(`${fw}/`))) {
4711
+ return true;
4712
+ }
4713
+ }
4714
+ return false;
4715
+ }
4364
4716
  extractAssignments(funcNode, assignments) {
4365
4717
  funcNode.forEachDescendant((node) => {
4366
4718
  if (Node2.isBinaryExpression(node)) {
@@ -4411,7 +4763,9 @@ class HandlerExtractor {
4411
4763
  return;
4412
4764
  if (this.tryExtractSetConstructorPattern(fieldPath, right, assignments))
4413
4765
  return;
4414
- this.tryExtractMapConstructorPattern(fieldPath, right, assignments);
4766
+ if (this.tryExtractMapConstructorPattern(fieldPath, right, assignments))
4767
+ return;
4768
+ this.tryExtractSignalDirectValuePattern(fieldPath, right, assignments);
4415
4769
  }
4416
4770
  tryExtractStateFieldPattern(fieldPath, right, assignments) {
4417
4771
  if (!fieldPath.startsWith("state."))
@@ -4530,6 +4884,25 @@ class HandlerExtractor {
4530
4884
  }
4531
4885
  return true;
4532
4886
  }
4887
+ tryExtractSignalDirectValuePattern(fieldPath, right, assignments) {
4888
+ if (!fieldPath.endsWith(".value"))
4889
+ return false;
4890
+ const signalName = fieldPath.slice(0, -6);
4891
+ const literalValue = this.extractValue(right);
4892
+ if (literalValue !== undefined) {
4893
+ assignments.push({ field: signalName, value: literalValue });
4894
+ return true;
4895
+ }
4896
+ if (Node2.isPropertyAccessExpression(right)) {
4897
+ const rightPath = this.getPropertyPath(right);
4898
+ const parts = rightPath.split(".");
4899
+ if (parts.length === 2 && parts[0] !== undefined && parts[1] !== undefined && this.currentFunctionParams.includes(parts[0])) {
4900
+ assignments.push({ field: signalName, value: `param:${parts[1]}` });
4901
+ return true;
4902
+ }
4903
+ }
4904
+ return false;
4905
+ }
4533
4906
  extractSetOperation(newExpr, fieldPath, signalName) {
4534
4907
  const args = newExpr.getArguments();
4535
4908
  if (args.length === 0) {
@@ -4646,11 +5019,11 @@ class HandlerExtractor {
4646
5019
  return null;
4647
5020
  switch (methodName) {
4648
5021
  case "filter":
4649
- return { field: signalName, value: "SelectSeq(@, LAMBDA t: TRUE)" };
5022
+ return { field: signalName, value: "NDET:FILTER" };
4650
5023
  case "map":
4651
- return { field: signalName, value: "[i \\in DOMAIN @ |-> @[i]]" };
5024
+ return { field: signalName, value: "NDET:MAP" };
4652
5025
  case "slice":
4653
- return { field: signalName, value: "SubSeq(@, 1, Len(@))" };
5026
+ return { field: signalName, value: "NDET:FILTER" };
4654
5027
  case "concat":
4655
5028
  return { field: signalName, value: "@ \\o <<payload>>" };
4656
5029
  case "reverse":
@@ -5195,32 +5568,64 @@ class HandlerExtractor {
5195
5568
  try {
5196
5569
  const ifStmt = ifNode;
5197
5570
  const condition = ifStmt.getExpression();
5198
- if (!Node2.isCallExpression(condition)) {
5199
- return null;
5571
+ if (Node2.isCallExpression(condition)) {
5572
+ const funcExpr = condition.getExpression();
5573
+ const funcName = Node2.isIdentifier(funcExpr) ? funcExpr.getText() : undefined;
5574
+ this.debugLogProcessingFunction(funcName);
5575
+ const messageType = this.resolveMessageType(funcExpr, funcName, typeGuards);
5576
+ if (!messageType) {
5577
+ this.debugLogUnresolvedMessageType(funcName);
5578
+ return null;
5579
+ }
5580
+ const line = ifStmt.getStartLineNumber();
5581
+ const relationships = this.extractRelationshipsFromIfBlock(ifStmt, messageType);
5582
+ return {
5583
+ messageType,
5584
+ node: context,
5585
+ assignments: [],
5586
+ preconditions: [],
5587
+ postconditions: [],
5588
+ location: { file: filePath, line },
5589
+ relationships
5590
+ };
5200
5591
  }
5201
- const funcExpr = condition.getExpression();
5202
- const funcName = Node2.isIdentifier(funcExpr) ? funcExpr.getText() : undefined;
5203
- this.debugLogProcessingFunction(funcName);
5204
- const messageType = this.resolveMessageType(funcExpr, funcName, typeGuards);
5205
- if (!messageType) {
5206
- this.debugLogUnresolvedMessageType(funcName);
5207
- return null;
5592
+ if (Node2.isBinaryExpression(condition)) {
5593
+ const messageType = this.extractMessageTypeFromEqualityCheck(condition);
5594
+ if (messageType) {
5595
+ const line = ifStmt.getStartLineNumber();
5596
+ const relationships = this.extractRelationshipsFromIfBlock(ifStmt, messageType);
5597
+ return {
5598
+ messageType,
5599
+ node: context,
5600
+ assignments: [],
5601
+ preconditions: [],
5602
+ postconditions: [],
5603
+ location: { file: filePath, line },
5604
+ relationships
5605
+ };
5606
+ }
5208
5607
  }
5209
- const line = ifStmt.getStartLineNumber();
5210
- const relationships = this.extractRelationshipsFromIfBlock(ifStmt, messageType);
5211
- return {
5212
- messageType,
5213
- node: context,
5214
- assignments: [],
5215
- preconditions: [],
5216
- postconditions: [],
5217
- location: { file: filePath, line },
5218
- relationships
5219
- };
5608
+ return null;
5220
5609
  } catch (_error) {
5221
5610
  return null;
5222
5611
  }
5223
5612
  }
5613
+ extractMessageTypeFromEqualityCheck(expr) {
5614
+ const operator = expr.getOperatorToken().getText();
5615
+ if (operator !== "===" && operator !== "==")
5616
+ return null;
5617
+ const left = expr.getLeft();
5618
+ const right = expr.getRight();
5619
+ const stringLiteral = Node2.isStringLiteral(right) ? right : Node2.isStringLiteral(left) ? left : null;
5620
+ const propAccess = Node2.isPropertyAccessExpression(left) ? left : Node2.isPropertyAccessExpression(right) ? right : null;
5621
+ if (!stringLiteral || !propAccess)
5622
+ return null;
5623
+ const propName = propAccess.getName();
5624
+ if (propName !== "type" && propName !== "kind" && propName !== "action") {
5625
+ return null;
5626
+ }
5627
+ return stringLiteral.getLiteralValue();
5628
+ }
5224
5629
  debugLogProcessingFunction(funcName) {
5225
5630
  if (process.env["POLLY_DEBUG"] && funcName) {
5226
5631
  console.log(`[DEBUG] Processing if condition with function: ${funcName}`);
@@ -5430,6 +5835,10 @@ class HandlerExtractor {
5430
5835
  return messageType;
5431
5836
  }
5432
5837
  inferContext(filePath) {
5838
+ for (const [pathPrefix, context] of this.contextOverrides.entries()) {
5839
+ if (filePath.startsWith(pathPrefix))
5840
+ return context;
5841
+ }
5433
5842
  const path2 = filePath.toLowerCase();
5434
5843
  return this.inferElectronContext(path2) || this.inferWorkerContext(path2) || this.inferServerAppContext(path2) || this.inferChromeExtensionContext(path2) || "unknown";
5435
5844
  }
@@ -6645,15 +7054,23 @@ async function runFullVerification(configPath) {
6645
7054
  if (exprValidation.warnings.length > 0) {
6646
7055
  displayExpressionWarnings(exprValidation);
6647
7056
  }
7057
+ if (typedConfig.subsystems && Object.keys(typedConfig.subsystems).length > 0) {
7058
+ await runSubsystemVerification(typedConfig, typedAnalysis);
7059
+ return;
7060
+ }
7061
+ await runMonolithicVerification(config, analysis);
7062
+ }
7063
+ async function runMonolithicVerification(config, analysis) {
6648
7064
  const { specPath, specDir } = await generateAndWriteTLASpecs(config, analysis);
6649
7065
  findAndCopyBaseSpec(specDir);
6650
7066
  console.log(color("✓ Specification generated", COLORS.green));
6651
7067
  console.log(color(` ${specPath}`, COLORS.gray));
6652
7068
  console.log();
6653
7069
  const docker = await setupDocker();
6654
- const timeoutSeconds = getTimeout(config);
6655
- const workers = getWorkers(config);
6656
- const maxDepth = getMaxDepth(config);
7070
+ const typedConfig = config;
7071
+ const timeoutSeconds = getTimeout(typedConfig);
7072
+ const workers = getWorkers(typedConfig);
7073
+ const maxDepth = getMaxDepth(typedConfig);
6657
7074
  console.log(color("⚙️ Running TLC model checker...", COLORS.blue));
6658
7075
  if (timeoutSeconds === 0) {
6659
7076
  console.log(color(" No timeout set - will run until completion", COLORS.gray));
@@ -6673,6 +7090,122 @@ async function runFullVerification(configPath) {
6673
7090
  });
6674
7091
  displayVerificationResults(result, specDir);
6675
7092
  }
7093
+ async function runSubsystemVerification(config, analysis) {
7094
+ const subsystems = config.subsystems;
7095
+ const subsystemNames = Object.keys(subsystems);
7096
+ console.log(color(`\uD83D\uDCE6 Subsystem-scoped verification (${subsystemNames.length} subsystems)
7097
+ `, COLORS.blue));
7098
+ const { checkNonInterference: checkNonInterference2 } = await Promise.resolve().then(() => exports_non_interference);
7099
+ const interference = checkNonInterference2(subsystems, analysis.handlers);
7100
+ if (interference.valid) {
7101
+ console.log(color("✓ Non-interference: verified (no cross-subsystem state writes)", COLORS.green));
7102
+ console.log();
7103
+ } else {
7104
+ console.log(color(`⚠️ Non-interference violations detected:
7105
+ `, COLORS.yellow));
7106
+ for (const v of interference.violations) {
7107
+ console.log(color(` • Handler "${v.handler}" (${v.subsystem}) writes to "${v.writesTo}" owned by "${v.ownedBy}"`, COLORS.yellow));
7108
+ }
7109
+ console.log();
7110
+ console.log(color(" Compositional verification may not be sound. Consider restructuring subsystem boundaries.", COLORS.yellow));
7111
+ console.log();
7112
+ }
7113
+ const assignedHandlers = new Set(Object.values(subsystems).flatMap((s) => s.handlers));
7114
+ const unassigned = analysis.messageTypes.filter((mt) => !assignedHandlers.has(mt));
7115
+ if (unassigned.length > 0) {
7116
+ console.log(color(`⚠️ ${unassigned.length} handler(s) not assigned to any subsystem (will not be verified):`, COLORS.yellow));
7117
+ for (const h of unassigned.slice(0, 10)) {
7118
+ console.log(color(` • ${h}`, COLORS.yellow));
7119
+ }
7120
+ if (unassigned.length > 10) {
7121
+ console.log(color(` ... and ${unassigned.length - 10} more`, COLORS.yellow));
7122
+ }
7123
+ console.log();
7124
+ }
7125
+ const docker = await setupDocker();
7126
+ const timeoutSeconds = getTimeout(config);
7127
+ const workers = getWorkers(config);
7128
+ const maxDepth = getMaxDepth(config);
7129
+ const { generateSubsystemTLA: generateSubsystemTLA2 } = await Promise.resolve().then(() => (init_tla(), exports_tla));
7130
+ const results = [];
7131
+ for (const name of subsystemNames) {
7132
+ const sub = subsystems[name];
7133
+ const startTime = Date.now();
7134
+ console.log(color(`⚙️ Verifying subsystem: ${name}...`, COLORS.blue));
7135
+ const { spec, cfg } = await generateSubsystemTLA2(name, sub, config, analysis);
7136
+ const specDir = path4.join(process.cwd(), "specs", "tla", "generated", name);
7137
+ if (!fs4.existsSync(specDir)) {
7138
+ fs4.mkdirSync(specDir, { recursive: true });
7139
+ }
7140
+ const specPath = path4.join(specDir, `UserApp_${name}.tla`);
7141
+ const cfgPath = path4.join(specDir, `UserApp_${name}.cfg`);
7142
+ fs4.writeFileSync(specPath, spec);
7143
+ fs4.writeFileSync(cfgPath, cfg);
7144
+ findAndCopyBaseSpec(specDir);
7145
+ const result = await docker.runTLC(specPath, {
7146
+ workers,
7147
+ timeout: timeoutSeconds > 0 ? timeoutSeconds * 1000 : undefined,
7148
+ maxDepth
7149
+ });
7150
+ const elapsed = (Date.now() - startTime) / 1000;
7151
+ results.push({
7152
+ name,
7153
+ success: result.success,
7154
+ handlerCount: sub.handlers.length,
7155
+ stateCount: result.stats?.distinctStates ?? 0,
7156
+ elapsed,
7157
+ stats: result.stats,
7158
+ error: result.error
7159
+ });
7160
+ if (result.success) {
7161
+ console.log(color(` ✓ ${name} passed (${elapsed.toFixed(1)}s)`, COLORS.green));
7162
+ } else {
7163
+ console.log(color(` ✗ ${name} failed`, COLORS.red));
7164
+ if (result.violation) {
7165
+ console.log(color(` Invariant violated: ${result.violation.name}`, COLORS.red));
7166
+ } else if (result.error) {
7167
+ console.log(color(` Error: ${result.error}`, COLORS.red));
7168
+ }
7169
+ fs4.writeFileSync(path4.join(specDir, "tlc-output.log"), result.output);
7170
+ }
7171
+ }
7172
+ console.log();
7173
+ displayCompositionalReport(results, interference.valid);
7174
+ }
7175
+ function displayCompositionalReport(results, nonInterferenceValid) {
7176
+ console.log(color(`Subsystem verification results:
7177
+ `, COLORS.blue));
7178
+ for (const r of results) {
7179
+ const status = r.success ? color("✓", COLORS.green) : color("✗", COLORS.red);
7180
+ const name = r.name.padEnd(20);
7181
+ const handlers = `${r.handlerCount} handler${r.handlerCount !== 1 ? "s" : ""}`;
7182
+ const states = `${r.stateCount} states`;
7183
+ const time = `${r.elapsed.toFixed(1)}s`;
7184
+ console.log(` ${status} ${name} ${handlers.padEnd(14)} ${states.padEnd(14)} ${time}`);
7185
+ }
7186
+ console.log();
7187
+ const nonIntLabel = nonInterferenceValid ? color("✓ verified (no cross-subsystem state writes)", COLORS.green) : color("⚠ violations detected", COLORS.yellow);
7188
+ console.log(` Non-interference: ${nonIntLabel}`);
7189
+ console.log();
7190
+ const allPassed = results.every((r) => r.success);
7191
+ if (allPassed && nonInterferenceValid) {
7192
+ console.log(color("Compositional result: ✓ PASS", COLORS.green));
7193
+ console.log(color(" All subsystems verified independently. By non-interference,", COLORS.gray));
7194
+ console.log(color(" the full system satisfies all per-subsystem invariants.", COLORS.gray));
7195
+ } else if (allPassed && !nonInterferenceValid) {
7196
+ console.log(color("Compositional result: ⚠ PASS (with warnings)", COLORS.yellow));
7197
+ console.log(color(" All subsystems passed, but non-interference violations exist.", COLORS.gray));
7198
+ console.log(color(" Compositional soundness is not guaranteed.", COLORS.gray));
7199
+ } else {
7200
+ console.log(color("Compositional result: ✗ FAIL", COLORS.red));
7201
+ const failed = results.filter((r) => !r.success);
7202
+ for (const f of failed) {
7203
+ console.log(color(` Failed: ${f.name}`, COLORS.red));
7204
+ }
7205
+ process.exit(1);
7206
+ }
7207
+ console.log();
7208
+ }
6676
7209
  async function loadVerificationConfig(configPath) {
6677
7210
  const resolvedPath = path4.resolve(configPath);
6678
7211
  const configModule = await import(`file://${resolvedPath}?t=${Date.now()}`);
@@ -6889,4 +7422,4 @@ main().catch((error) => {
6889
7422
  process.exit(1);
6890
7423
  });
6891
7424
 
6892
- //# debugId=35FC1B345D5CADC664756E2164756E21
7425
+ //# debugId=E5DA6950FD1E0C7764756E2164756E21