@fairfox/polly 0.17.0 → 0.18.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,49 @@ 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 filteredAnalysis = {
433
+ ...analysis,
434
+ messageTypes: analysis.messageTypes.filter((mt) => handlerNames.has(mt)),
435
+ handlers: analysis.handlers.filter((h) => handlerNames.has(h.messageType))
436
+ };
437
+ const generator = new TLAGenerator;
438
+ return await generator.generate(filteredConfig, filteredAnalysis);
439
+ }
354
440
  var TLAValidationError, TLAGenerator;
355
441
  var init_tla = __esm(() => {
356
442
  init_invariants();
@@ -3335,6 +3421,9 @@ class ConfigValidator {
3335
3421
  if (config.tier2) {
3336
3422
  this.validateTier2Optimizations(config.tier2);
3337
3423
  }
3424
+ if (config.subsystems) {
3425
+ this.validateSubsystems(config.subsystems, config.state);
3426
+ }
3338
3427
  }
3339
3428
  findNullPlaceholders(obj, path2) {
3340
3429
  if (obj === null || obj === undefined) {
@@ -3651,6 +3740,68 @@ class ConfigValidator {
3651
3740
  }
3652
3741
  }
3653
3742
  }
3743
+ validateSubsystems(subsystems, stateConfig) {
3744
+ const stateFieldNames = Object.keys(stateConfig);
3745
+ const allAssignedHandlers = new Map;
3746
+ const allAssignedFields = new Map;
3747
+ for (const [subsystemName, subsystem] of Object.entries(subsystems)) {
3748
+ for (const field of subsystem.state) {
3749
+ if (!stateFieldNames.includes(field)) {
3750
+ this.issues.push({
3751
+ type: "invalid_value",
3752
+ severity: "error",
3753
+ field: `subsystems.${subsystemName}.state`,
3754
+ message: `State field "${field}" does not exist in top-level state config`,
3755
+ suggestion: `Available fields: ${stateFieldNames.join(", ")}`
3756
+ });
3757
+ }
3758
+ const existingOwner = allAssignedFields.get(field);
3759
+ if (existingOwner) {
3760
+ this.issues.push({
3761
+ type: "invalid_value",
3762
+ severity: "warning",
3763
+ field: `subsystems.${subsystemName}.state`,
3764
+ message: `State field "${field}" is assigned to both "${existingOwner}" and "${subsystemName}"`,
3765
+ suggestion: "State fields should be partitioned across subsystems for non-interference"
3766
+ });
3767
+ } else {
3768
+ allAssignedFields.set(field, subsystemName);
3769
+ }
3770
+ }
3771
+ if (subsystem.handlers.length === 0) {
3772
+ this.issues.push({
3773
+ type: "invalid_value",
3774
+ severity: "warning",
3775
+ field: `subsystems.${subsystemName}.handlers`,
3776
+ message: `Subsystem "${subsystemName}" has no handlers`,
3777
+ suggestion: "Add at least one handler to the subsystem"
3778
+ });
3779
+ }
3780
+ for (const handler of subsystem.handlers) {
3781
+ const existingOwner = allAssignedHandlers.get(handler);
3782
+ if (existingOwner) {
3783
+ this.issues.push({
3784
+ type: "invalid_value",
3785
+ severity: "error",
3786
+ field: `subsystems.${subsystemName}.handlers`,
3787
+ message: `Handler "${handler}" is assigned to both "${existingOwner}" and "${subsystemName}"`,
3788
+ suggestion: "Each handler must belong to exactly one subsystem"
3789
+ });
3790
+ } else {
3791
+ allAssignedHandlers.set(handler, subsystemName);
3792
+ }
3793
+ }
3794
+ if (subsystem.state.length === 0) {
3795
+ this.issues.push({
3796
+ type: "invalid_value",
3797
+ severity: "warning",
3798
+ field: `subsystems.${subsystemName}.state`,
3799
+ message: `Subsystem "${subsystemName}" has no state fields`,
3800
+ suggestion: "Add at least one state field to the subsystem"
3801
+ });
3802
+ }
3803
+ }
3804
+ }
3654
3805
  }
3655
3806
  function validateConfig(configPath) {
3656
3807
  const validator = new ConfigValidator;
@@ -4069,7 +4220,8 @@ class HandlerExtractor {
4069
4220
  packageRoot;
4070
4221
  warnings;
4071
4222
  currentFunctionParams = [];
4072
- constructor(tsConfigPath) {
4223
+ contextOverrides;
4224
+ constructor(tsConfigPath, contextOverrides) {
4073
4225
  this.project = new Project({
4074
4226
  tsConfigFilePath: tsConfigPath
4075
4227
  });
@@ -4077,6 +4229,7 @@ class HandlerExtractor {
4077
4229
  this.relationshipExtractor = new RelationshipExtractor;
4078
4230
  this.analyzedFiles = new Set;
4079
4231
  this.warnings = [];
4232
+ this.contextOverrides = contextOverrides || new Map;
4080
4233
  this.packageRoot = this.findPackageRoot(tsConfigPath);
4081
4234
  }
4082
4235
  warnUnsupportedPattern(pattern, location, suggestion) {
@@ -4294,8 +4447,20 @@ class HandlerExtractor {
4294
4447
  handlers.push(handler);
4295
4448
  }
4296
4449
  }
4450
+ if (methodName === "ws") {
4451
+ this.extractElysiaWsHandlers(node, context, filePath, handlers);
4452
+ }
4453
+ if (this.isRestMethod(methodName) && this.isWebFrameworkFile(node.getSourceFile())) {
4454
+ const restHandler = this.extractRestHandler(node, methodName, context, filePath);
4455
+ if (restHandler) {
4456
+ handlers.push(restHandler);
4457
+ }
4458
+ }
4297
4459
  }
4298
4460
  }
4461
+ isRestMethod(name) {
4462
+ return ["get", "post", "put", "delete", "patch"].includes(name);
4463
+ }
4299
4464
  isElseIfStatement(node) {
4300
4465
  const parent = node.getParent();
4301
4466
  return parent !== undefined && Node2.isIfStatement(parent);
@@ -4361,6 +4526,135 @@ class HandlerExtractor {
4361
4526
  parameters
4362
4527
  };
4363
4528
  }
4529
+ extractElysiaWsHandlers(node, context, filePath, handlers) {
4530
+ const args = node.getArguments();
4531
+ if (args.length < 2)
4532
+ return;
4533
+ const routeArg = args[0];
4534
+ if (!routeArg || !Node2.isStringLiteral(routeArg))
4535
+ return;
4536
+ const routePath = routeArg.getLiteralValue();
4537
+ const configArg = args[1];
4538
+ if (!configArg || !Node2.isObjectLiteralExpression(configArg))
4539
+ return;
4540
+ const callbacks = ["message", "open", "close"];
4541
+ for (const cbName of callbacks) {
4542
+ const prop = configArg.getProperty(cbName);
4543
+ if (!prop)
4544
+ continue;
4545
+ let funcBody = null;
4546
+ if (Node2.isMethodDeclaration(prop)) {
4547
+ funcBody = prop;
4548
+ } else if (Node2.isPropertyAssignment(prop)) {
4549
+ const init = prop.getInitializer();
4550
+ if (init && (Node2.isArrowFunction(init) || Node2.isFunctionExpression(init))) {
4551
+ funcBody = init;
4552
+ }
4553
+ }
4554
+ if (!funcBody)
4555
+ continue;
4556
+ if (cbName === "message") {
4557
+ const body = funcBody.getBody();
4558
+ if (!body)
4559
+ continue;
4560
+ const subHandlers = this.extractSubHandlersFromBody(body, context, filePath);
4561
+ if (subHandlers.length > 0) {
4562
+ handlers.push(...subHandlers);
4563
+ } else {
4564
+ handlers.push(this.buildWsHandler(`ws_message`, routePath, context, filePath, funcBody, node.getStartLineNumber()));
4565
+ }
4566
+ } else {
4567
+ handlers.push(this.buildWsHandler(`ws_${cbName}`, routePath, context, filePath, funcBody, node.getStartLineNumber()));
4568
+ }
4569
+ }
4570
+ }
4571
+ extractSubHandlersFromBody(body, context, filePath) {
4572
+ const subHandlers = [];
4573
+ body.forEachDescendant((child) => {
4574
+ if (Node2.isIfStatement(child) && !this.isElseIfStatement(child)) {
4575
+ const typeGuardHandlers = this.extractTypeGuardHandlers(child, context, filePath);
4576
+ subHandlers.push(...typeGuardHandlers);
4577
+ }
4578
+ if (Node2.isSwitchStatement(child)) {
4579
+ const switchHandlers = this.extractSwitchCaseHandlers(child, context, filePath);
4580
+ subHandlers.push(...switchHandlers);
4581
+ }
4582
+ });
4583
+ return subHandlers;
4584
+ }
4585
+ buildWsHandler(messageType, _routePath, context, filePath, funcBody, line) {
4586
+ const assignments = [];
4587
+ const preconditions = [];
4588
+ const postconditions = [];
4589
+ this.currentFunctionParams = this.extractParameterNames(funcBody);
4590
+ this.extractAssignments(funcBody, assignments);
4591
+ this.extractVerificationConditions(funcBody, preconditions, postconditions);
4592
+ this.currentFunctionParams = [];
4593
+ return {
4594
+ messageType,
4595
+ node: context,
4596
+ assignments,
4597
+ preconditions,
4598
+ postconditions,
4599
+ location: { file: filePath, line },
4600
+ origin: "event"
4601
+ };
4602
+ }
4603
+ extractRestHandler(node, methodName, context, filePath) {
4604
+ const args = node.getArguments();
4605
+ if (args.length < 2)
4606
+ return null;
4607
+ const routeArg = args[0];
4608
+ if (!routeArg || !Node2.isStringLiteral(routeArg))
4609
+ return null;
4610
+ const routePath = routeArg.getLiteralValue();
4611
+ const httpMethod = methodName.toUpperCase();
4612
+ const messageType = `${httpMethod} ${routePath}`;
4613
+ const handlerArg = args[1];
4614
+ const assignments = [];
4615
+ const preconditions = [];
4616
+ const postconditions = [];
4617
+ let actualHandler = null;
4618
+ if (Node2.isArrowFunction(handlerArg) || Node2.isFunctionExpression(handlerArg)) {
4619
+ actualHandler = handlerArg;
4620
+ } else if (Node2.isIdentifier(handlerArg)) {
4621
+ actualHandler = this.resolveFunctionReference(handlerArg);
4622
+ }
4623
+ let parameters;
4624
+ if (actualHandler) {
4625
+ this.currentFunctionParams = this.extractParameterNames(actualHandler);
4626
+ parameters = this.currentFunctionParams.length > 0 ? [...this.currentFunctionParams] : undefined;
4627
+ this.extractAssignments(actualHandler, assignments);
4628
+ this.extractVerificationConditions(actualHandler, preconditions, postconditions);
4629
+ this.currentFunctionParams = [];
4630
+ }
4631
+ return {
4632
+ messageType,
4633
+ node: context,
4634
+ assignments,
4635
+ preconditions,
4636
+ postconditions,
4637
+ location: {
4638
+ file: filePath,
4639
+ line: node.getStartLineNumber()
4640
+ },
4641
+ origin: "event",
4642
+ parameters,
4643
+ handlerKind: "rest",
4644
+ httpMethod,
4645
+ routePath
4646
+ };
4647
+ }
4648
+ isWebFrameworkFile(sourceFile) {
4649
+ const frameworks = ["elysia", "express", "hono", "fastify", "koa", "@elysiajs/eden"];
4650
+ for (const importDecl of sourceFile.getImportDeclarations()) {
4651
+ const specifier = importDecl.getModuleSpecifierValue();
4652
+ if (frameworks.some((fw) => specifier === fw || specifier.startsWith(`${fw}/`))) {
4653
+ return true;
4654
+ }
4655
+ }
4656
+ return false;
4657
+ }
4364
4658
  extractAssignments(funcNode, assignments) {
4365
4659
  funcNode.forEachDescendant((node) => {
4366
4660
  if (Node2.isBinaryExpression(node)) {
@@ -5195,32 +5489,64 @@ class HandlerExtractor {
5195
5489
  try {
5196
5490
  const ifStmt = ifNode;
5197
5491
  const condition = ifStmt.getExpression();
5198
- if (!Node2.isCallExpression(condition)) {
5199
- return null;
5492
+ if (Node2.isCallExpression(condition)) {
5493
+ const funcExpr = condition.getExpression();
5494
+ const funcName = Node2.isIdentifier(funcExpr) ? funcExpr.getText() : undefined;
5495
+ this.debugLogProcessingFunction(funcName);
5496
+ const messageType = this.resolveMessageType(funcExpr, funcName, typeGuards);
5497
+ if (!messageType) {
5498
+ this.debugLogUnresolvedMessageType(funcName);
5499
+ return null;
5500
+ }
5501
+ const line = ifStmt.getStartLineNumber();
5502
+ const relationships = this.extractRelationshipsFromIfBlock(ifStmt, messageType);
5503
+ return {
5504
+ messageType,
5505
+ node: context,
5506
+ assignments: [],
5507
+ preconditions: [],
5508
+ postconditions: [],
5509
+ location: { file: filePath, line },
5510
+ relationships
5511
+ };
5200
5512
  }
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;
5513
+ if (Node2.isBinaryExpression(condition)) {
5514
+ const messageType = this.extractMessageTypeFromEqualityCheck(condition);
5515
+ if (messageType) {
5516
+ const line = ifStmt.getStartLineNumber();
5517
+ const relationships = this.extractRelationshipsFromIfBlock(ifStmt, messageType);
5518
+ return {
5519
+ messageType,
5520
+ node: context,
5521
+ assignments: [],
5522
+ preconditions: [],
5523
+ postconditions: [],
5524
+ location: { file: filePath, line },
5525
+ relationships
5526
+ };
5527
+ }
5208
5528
  }
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
- };
5529
+ return null;
5220
5530
  } catch (_error) {
5221
5531
  return null;
5222
5532
  }
5223
5533
  }
5534
+ extractMessageTypeFromEqualityCheck(expr) {
5535
+ const operator = expr.getOperatorToken().getText();
5536
+ if (operator !== "===" && operator !== "==")
5537
+ return null;
5538
+ const left = expr.getLeft();
5539
+ const right = expr.getRight();
5540
+ const stringLiteral = Node2.isStringLiteral(right) ? right : Node2.isStringLiteral(left) ? left : null;
5541
+ const propAccess = Node2.isPropertyAccessExpression(left) ? left : Node2.isPropertyAccessExpression(right) ? right : null;
5542
+ if (!stringLiteral || !propAccess)
5543
+ return null;
5544
+ const propName = propAccess.getName();
5545
+ if (propName !== "type" && propName !== "kind" && propName !== "action") {
5546
+ return null;
5547
+ }
5548
+ return stringLiteral.getLiteralValue();
5549
+ }
5224
5550
  debugLogProcessingFunction(funcName) {
5225
5551
  if (process.env["POLLY_DEBUG"] && funcName) {
5226
5552
  console.log(`[DEBUG] Processing if condition with function: ${funcName}`);
@@ -5430,6 +5756,10 @@ class HandlerExtractor {
5430
5756
  return messageType;
5431
5757
  }
5432
5758
  inferContext(filePath) {
5759
+ for (const [pathPrefix, context] of this.contextOverrides.entries()) {
5760
+ if (filePath.startsWith(pathPrefix))
5761
+ return context;
5762
+ }
5433
5763
  const path2 = filePath.toLowerCase();
5434
5764
  return this.inferElectronContext(path2) || this.inferWorkerContext(path2) || this.inferServerAppContext(path2) || this.inferChromeExtensionContext(path2) || "unknown";
5435
5765
  }
@@ -6645,15 +6975,23 @@ async function runFullVerification(configPath) {
6645
6975
  if (exprValidation.warnings.length > 0) {
6646
6976
  displayExpressionWarnings(exprValidation);
6647
6977
  }
6978
+ if (typedConfig.subsystems && Object.keys(typedConfig.subsystems).length > 0) {
6979
+ await runSubsystemVerification(typedConfig, typedAnalysis);
6980
+ return;
6981
+ }
6982
+ await runMonolithicVerification(config, analysis);
6983
+ }
6984
+ async function runMonolithicVerification(config, analysis) {
6648
6985
  const { specPath, specDir } = await generateAndWriteTLASpecs(config, analysis);
6649
6986
  findAndCopyBaseSpec(specDir);
6650
6987
  console.log(color("✓ Specification generated", COLORS.green));
6651
6988
  console.log(color(` ${specPath}`, COLORS.gray));
6652
6989
  console.log();
6653
6990
  const docker = await setupDocker();
6654
- const timeoutSeconds = getTimeout(config);
6655
- const workers = getWorkers(config);
6656
- const maxDepth = getMaxDepth(config);
6991
+ const typedConfig = config;
6992
+ const timeoutSeconds = getTimeout(typedConfig);
6993
+ const workers = getWorkers(typedConfig);
6994
+ const maxDepth = getMaxDepth(typedConfig);
6657
6995
  console.log(color("⚙️ Running TLC model checker...", COLORS.blue));
6658
6996
  if (timeoutSeconds === 0) {
6659
6997
  console.log(color(" No timeout set - will run until completion", COLORS.gray));
@@ -6673,6 +7011,122 @@ async function runFullVerification(configPath) {
6673
7011
  });
6674
7012
  displayVerificationResults(result, specDir);
6675
7013
  }
7014
+ async function runSubsystemVerification(config, analysis) {
7015
+ const subsystems = config.subsystems;
7016
+ const subsystemNames = Object.keys(subsystems);
7017
+ console.log(color(`\uD83D\uDCE6 Subsystem-scoped verification (${subsystemNames.length} subsystems)
7018
+ `, COLORS.blue));
7019
+ const { checkNonInterference: checkNonInterference2 } = await Promise.resolve().then(() => exports_non_interference);
7020
+ const interference = checkNonInterference2(subsystems, analysis.handlers);
7021
+ if (interference.valid) {
7022
+ console.log(color("✓ Non-interference: verified (no cross-subsystem state writes)", COLORS.green));
7023
+ console.log();
7024
+ } else {
7025
+ console.log(color(`⚠️ Non-interference violations detected:
7026
+ `, COLORS.yellow));
7027
+ for (const v of interference.violations) {
7028
+ console.log(color(` • Handler "${v.handler}" (${v.subsystem}) writes to "${v.writesTo}" owned by "${v.ownedBy}"`, COLORS.yellow));
7029
+ }
7030
+ console.log();
7031
+ console.log(color(" Compositional verification may not be sound. Consider restructuring subsystem boundaries.", COLORS.yellow));
7032
+ console.log();
7033
+ }
7034
+ const assignedHandlers = new Set(Object.values(subsystems).flatMap((s) => s.handlers));
7035
+ const unassigned = analysis.messageTypes.filter((mt) => !assignedHandlers.has(mt));
7036
+ if (unassigned.length > 0) {
7037
+ console.log(color(`⚠️ ${unassigned.length} handler(s) not assigned to any subsystem (will not be verified):`, COLORS.yellow));
7038
+ for (const h of unassigned.slice(0, 10)) {
7039
+ console.log(color(` • ${h}`, COLORS.yellow));
7040
+ }
7041
+ if (unassigned.length > 10) {
7042
+ console.log(color(` ... and ${unassigned.length - 10} more`, COLORS.yellow));
7043
+ }
7044
+ console.log();
7045
+ }
7046
+ const docker = await setupDocker();
7047
+ const timeoutSeconds = getTimeout(config);
7048
+ const workers = getWorkers(config);
7049
+ const maxDepth = getMaxDepth(config);
7050
+ const { generateSubsystemTLA: generateSubsystemTLA2 } = await Promise.resolve().then(() => (init_tla(), exports_tla));
7051
+ const results = [];
7052
+ for (const name of subsystemNames) {
7053
+ const sub = subsystems[name];
7054
+ const startTime = Date.now();
7055
+ console.log(color(`⚙️ Verifying subsystem: ${name}...`, COLORS.blue));
7056
+ const { spec, cfg } = await generateSubsystemTLA2(name, sub, config, analysis);
7057
+ const specDir = path4.join(process.cwd(), "specs", "tla", "generated", name);
7058
+ if (!fs4.existsSync(specDir)) {
7059
+ fs4.mkdirSync(specDir, { recursive: true });
7060
+ }
7061
+ const specPath = path4.join(specDir, `UserApp_${name}.tla`);
7062
+ const cfgPath = path4.join(specDir, `UserApp_${name}.cfg`);
7063
+ fs4.writeFileSync(specPath, spec);
7064
+ fs4.writeFileSync(cfgPath, cfg);
7065
+ findAndCopyBaseSpec(specDir);
7066
+ const result = await docker.runTLC(specPath, {
7067
+ workers,
7068
+ timeout: timeoutSeconds > 0 ? timeoutSeconds * 1000 : undefined,
7069
+ maxDepth
7070
+ });
7071
+ const elapsed = (Date.now() - startTime) / 1000;
7072
+ results.push({
7073
+ name,
7074
+ success: result.success,
7075
+ handlerCount: sub.handlers.length,
7076
+ stateCount: result.stats?.distinctStates ?? 0,
7077
+ elapsed,
7078
+ stats: result.stats,
7079
+ error: result.error
7080
+ });
7081
+ if (result.success) {
7082
+ console.log(color(` ✓ ${name} passed (${elapsed.toFixed(1)}s)`, COLORS.green));
7083
+ } else {
7084
+ console.log(color(` ✗ ${name} failed`, COLORS.red));
7085
+ if (result.violation) {
7086
+ console.log(color(` Invariant violated: ${result.violation.name}`, COLORS.red));
7087
+ } else if (result.error) {
7088
+ console.log(color(` Error: ${result.error}`, COLORS.red));
7089
+ }
7090
+ fs4.writeFileSync(path4.join(specDir, "tlc-output.log"), result.output);
7091
+ }
7092
+ }
7093
+ console.log();
7094
+ displayCompositionalReport(results, interference.valid);
7095
+ }
7096
+ function displayCompositionalReport(results, nonInterferenceValid) {
7097
+ console.log(color(`Subsystem verification results:
7098
+ `, COLORS.blue));
7099
+ for (const r of results) {
7100
+ const status = r.success ? color("✓", COLORS.green) : color("✗", COLORS.red);
7101
+ const name = r.name.padEnd(20);
7102
+ const handlers = `${r.handlerCount} handler${r.handlerCount !== 1 ? "s" : ""}`;
7103
+ const states = `${r.stateCount} states`;
7104
+ const time = `${r.elapsed.toFixed(1)}s`;
7105
+ console.log(` ${status} ${name} ${handlers.padEnd(14)} ${states.padEnd(14)} ${time}`);
7106
+ }
7107
+ console.log();
7108
+ const nonIntLabel = nonInterferenceValid ? color("✓ verified (no cross-subsystem state writes)", COLORS.green) : color("⚠ violations detected", COLORS.yellow);
7109
+ console.log(` Non-interference: ${nonIntLabel}`);
7110
+ console.log();
7111
+ const allPassed = results.every((r) => r.success);
7112
+ if (allPassed && nonInterferenceValid) {
7113
+ console.log(color("Compositional result: ✓ PASS", COLORS.green));
7114
+ console.log(color(" All subsystems verified independently. By non-interference,", COLORS.gray));
7115
+ console.log(color(" the full system satisfies all per-subsystem invariants.", COLORS.gray));
7116
+ } else if (allPassed && !nonInterferenceValid) {
7117
+ console.log(color("Compositional result: ⚠ PASS (with warnings)", COLORS.yellow));
7118
+ console.log(color(" All subsystems passed, but non-interference violations exist.", COLORS.gray));
7119
+ console.log(color(" Compositional soundness is not guaranteed.", COLORS.gray));
7120
+ } else {
7121
+ console.log(color("Compositional result: ✗ FAIL", COLORS.red));
7122
+ const failed = results.filter((r) => !r.success);
7123
+ for (const f of failed) {
7124
+ console.log(color(` Failed: ${f.name}`, COLORS.red));
7125
+ }
7126
+ process.exit(1);
7127
+ }
7128
+ console.log();
7129
+ }
6676
7130
  async function loadVerificationConfig(configPath) {
6677
7131
  const resolvedPath = path4.resolve(configPath);
6678
7132
  const configModule = await import(`file://${resolvedPath}?t=${Date.now()}`);
@@ -6889,4 +7343,4 @@ main().catch((error) => {
6889
7343
  process.exit(1);
6890
7344
  });
6891
7345
 
6892
- //# debugId=35FC1B345D5CADC664756E2164756E21
7346
+ //# debugId=7EE824400DEB8F7564756E2164756E21