@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.
@@ -1,3 +1,7 @@
1
+ interface SubsystemConfig {
2
+ state: string[];
3
+ handlers: string[];
4
+ }
1
5
  interface LegacyVerificationConfig {
2
6
  state: Record<string, unknown>;
3
7
  messages: {
@@ -18,6 +22,7 @@ interface LegacyVerificationConfig {
18
22
  timeout?: number;
19
23
  workers?: number;
20
24
  };
25
+ subsystems?: Record<string, SubsystemConfig>;
21
26
  tier2?: {
22
27
  temporalConstraints?: Array<{
23
28
  before: string;
@@ -3,9 +3,9 @@
3
3
  "sources": ["../tools/verify/src/primitives/index.ts", "../tools/verify/src/config.ts"],
4
4
  "sourcesContent": [
5
5
  "// Verification primitives for formal verification\n// These are runtime no-ops but extracted during verification\n\n/**\n * Assert a precondition that must be true when the handler executes.\n *\n * In production: No-op (compiled away)\n * In verification: Translated to TLA+ assertion\n *\n * @example\n * messageBus.on(\"USER_LOGIN\", (payload) => {\n * requires(state.user.loggedIn === false, \"User must not be logged in\")\n * state.user.loggedIn = true\n * })\n */\nexport function requires(condition: boolean, message?: string): void {\n // Runtime no-op - only used during verification\n // Condition and message are checked during static analysis only\n void condition;\n void message;\n}\n\n/**\n * Assert a postcondition that must be true after the handler completes.\n *\n * In production: No-op (compiled away)\n * In verification: Translated to TLA+ assertion\n *\n * @example\n * messageBus.on(\"USER_LOGIN\", (payload) => {\n * state.user.loggedIn = true\n * ensures(state.user.loggedIn === true, \"User must be logged in\")\n * })\n */\nexport function ensures(condition: boolean, message?: string): void {\n // Runtime no-op - only used during verification\n // Condition and message are checked during static analysis only\n void condition;\n void message;\n}\n\n/**\n * Define a global invariant that must always hold.\n *\n * In production: No-op (compiled away)\n * In verification: Translated to TLA+ invariant\n *\n * @example\n * invariant(\"UserIdConsistent\", () =>\n * state.user.loggedIn === false || state.user.id !== null\n * )\n */\nexport function invariant(_name: string, condition: () => boolean): void {\n // Runtime no-op - only used during verification\n // Name and condition are checked during static analysis only\n void condition;\n}\n\n/**\n * Assert that a value is within a valid range.\n *\n * @example\n * requires(inRange(todoCount, 0, 100), \"Todo count must be 0-100\")\n */\nexport function inRange(value: number, min: number, max: number): boolean {\n return value >= min && value <= max;\n}\n\n/**\n * Assert that a value is one of the allowed values.\n *\n * @example\n * requires(oneOf(state.user.role, [\"admin\", \"user\"]), \"Role must be admin or user\")\n */\nexport function oneOf<T>(value: T, allowed: T[]): boolean {\n return allowed.includes(value);\n}\n\n/**\n * Assert that an array has a specific length constraint.\n *\n * @example\n * requires(hasLength(state.todos, { max: 10 }), \"Too many todos\")\n */\nexport function hasLength(array: unknown[], constraint: { min?: number; max?: number }): boolean {\n if (constraint.min !== undefined && array.length < constraint.min) return false;\n if (constraint.max !== undefined && array.length > constraint.max) return false;\n return true;\n}\n\n/**\n * Declare state-level constraints for verification and optional runtime checking.\n * Maps message types to preconditions on state fields.\n *\n * The parser automatically wires these constraints to handlers during verification.\n * Optionally, constraints can be enforced at runtime by passing `{ runtime: true }`.\n *\n * @example\n * // Verification only (TLA+ generation)\n * const state = { loggedIn: false };\n *\n * $constraints(\"loggedIn\", {\n * USER_LOGOUT: { requires: \"state.loggedIn === true\", message: \"Must be logged in\" },\n * BOOKMARK_ADD: { requires: \"state.loggedIn === true\", message: \"Must be logged in\" },\n * });\n *\n * @example\n * // Runtime enforcement (function predicates)\n * $constraints(\"loggedIn\", {\n * USER_LOGOUT: {\n * requires: (state) => state.loggedIn === true,\n * message: \"Must be logged in to logout\"\n * },\n * }, { runtime: true });\n */\nexport function $constraints(\n stateField: string,\n constraints: Record<\n string,\n {\n requires?: string | ((state: unknown) => boolean);\n ensures?: string | ((state: unknown) => boolean);\n message?: string;\n }\n >,\n options?: { runtime?: boolean }\n): void {\n // Register constraints for runtime checking if enabled\n if (options?.runtime) {\n // Import dynamically to avoid circular dependencies\n // This is safe because it only happens at runtime, not during static analysis\n // @ts-expect-error - Dynamic import path resolves correctly at runtime\n import(\"../../../src/shared/lib/constraints.js\")\n .then(({ registerConstraints }) => {\n registerConstraints(stateField, constraints);\n })\n .catch(() => {\n // Silently ignore - constraints module may not be available during static analysis\n });\n }\n\n // For verification: Still a no-op at runtime\n // Parser extracts these and wires them to TLA+ handlers\n}\n\n/**\n * Declare a global state constraint that prunes structurally impossible states.\n *\n * In production: No-op (compiled away)\n * In verification: Translated to TLC CONSTRAINT clause, discarding states\n * that violate the predicate from the exploration queue entirely.\n *\n * Unlike `invariant()` (which checks but still explores), `stateConstraint()`\n * prevents the model checker from ever reaching the pruned states.\n *\n * @example\n * stateConstraint(\"LeaderRequiresConnection\", () =>\n * !connectionState.value.isLeader || connectionState.value.status === \"connected\"\n * )\n */\nexport function stateConstraint(\n name: string,\n predicate: () => boolean,\n options?: { message?: string }\n): void {\n void name;\n void predicate;\n void options;\n}\n\n// Re-export for convenience\nexport const verify = {\n requires,\n ensures,\n invariant,\n inRange,\n oneOf,\n hasLength,\n $constraints,\n stateConstraint,\n};\n",
6
- "// ═══════════════════════════════════════════════════════════════\n// Configuration Helper for @fairfox/polly/verify\n// ═══════════════════════════════════════════════════════════════\n//\n// Lightweight entry point for user configuration files.\n// Does NOT include heavy dependencies (ts-morph, analysis, etc.)\n// which are only needed by the CLI tool.\n\n// ─────────────────────────────────────────────────────────────────\n// Configuration Types (inlined to avoid heavy dependencies)\n// ─────────────────────────────────────────────────────────────────\n\n// Legacy verification configuration\ninterface LegacyVerificationConfig {\n state: Record<string, unknown>;\n messages: {\n // Basic bounds\n maxInFlight?: number;\n maxTabs?: number;\n maxClients?: number;\n maxRenderers?: number;\n maxWorkers?: number;\n maxContexts?: number;\n\n // Tier 1 Optimizations (no precision loss)\n include?: string[]; // Only verify these message types\n exclude?: string[]; // Exclude these message types (mutually exclusive with include)\n symmetry?: string[][]; // Groups of symmetric message types [[type1, type2], [type3, type4]]\n perMessageBounds?: Record<string, number>; // Different maxInFlight per message type\n };\n onBuild?: \"warn\" | \"error\" | \"off\";\n onRelease?: \"warn\" | \"error\" | \"off\";\n\n // Verification engine options\n verification?: {\n timeout?: number; // Timeout in seconds (0 = no timeout)\n workers?: number; // Number of TLC workers\n };\n\n // Tier 2 Optimizations (controlled approximations)\n tier2?: {\n // Temporal constraints: ordering requirements between messages\n temporalConstraints?: Array<{\n before: string; // Message type that must occur first\n after: string; // Message type that must occur after\n description?: string; // Human-readable description\n }>;\n\n // Bounded exploration: limit depth for specific scenarios\n boundedExploration?: {\n maxDepth?: number; // Maximum state depth to explore\n criticalPaths?: string[][]; // Sequences of message types that must be fully explored\n };\n };\n}\n\n// Adapter-based configuration (for future use)\ninterface AdapterVerificationConfig {\n adapter: unknown; // Adapter interface not exported to avoid heavy deps\n state: Record<string, unknown>;\n bounds?: {\n maxInFlight?: number;\n [key: string]: unknown;\n };\n onBuild?: \"warn\" | \"error\" | \"off\";\n}\n\n// Union type for both config formats\ntype UnifiedVerificationConfig = LegacyVerificationConfig | AdapterVerificationConfig;\n\n/**\n * Define verification configuration with type checking\n *\n * Used in generated verification.config.ts files.\n *\n * @example\n * ```typescript\n * import { defineVerification } from '@fairfox/polly/verify'\n *\n * export default defineVerification({\n * state: {\n * \"user.role\": { type: \"enum\", values: [\"admin\", \"user\", \"guest\"] },\n * },\n * messages: {\n * maxInFlight: 6,\n * maxTabs: 2,\n * },\n * })\n * ```\n */\nexport function defineVerification<T extends UnifiedVerificationConfig>(config: T): T {\n // Validate configuration structure\n if (\"adapter\" in config) {\n // New adapter-based format\n if (!config.adapter) {\n throw new Error(\"Configuration must include an adapter\");\n }\n if (!config.state) {\n throw new Error(\"Configuration must include state bounds\");\n }\n } else if (\"messages\" in config) {\n // Legacy format\n if (!config.state) {\n throw new Error(\"Configuration must include state bounds\");\n }\n if (!config.messages) {\n throw new Error(\"Legacy configuration must include messages bounds\");\n }\n } else {\n throw new Error(\n \"Invalid configuration format. Must include either 'adapter' (new format) or 'messages' (legacy format)\"\n );\n }\n\n return config;\n}\n\n// Re-export verification primitives for user code\nexport {\n $constraints,\n ensures,\n hasLength,\n inRange,\n oneOf,\n requires,\n stateConstraint,\n} from \"./primitives/index.js\";\n"
6
+ "// ═══════════════════════════════════════════════════════════════\n// Configuration Helper for @fairfox/polly/verify\n// ═══════════════════════════════════════════════════════════════\n//\n// Lightweight entry point for user configuration files.\n// Does NOT include heavy dependencies (ts-morph, analysis, etc.)\n// which are only needed by the CLI tool.\n\n// ─────────────────────────────────────────────────────────────────\n// Configuration Types (inlined to avoid heavy dependencies)\n// ─────────────────────────────────────────────────────────────────\n\n// Subsystem configuration for compositional verification\ninterface SubsystemConfig {\n state: string[]; // Field names from parent state config\n handlers: string[]; // Message type names\n}\n\n// Legacy verification configuration\ninterface LegacyVerificationConfig {\n state: Record<string, unknown>;\n messages: {\n // Basic bounds\n maxInFlight?: number;\n maxTabs?: number;\n maxClients?: number;\n maxRenderers?: number;\n maxWorkers?: number;\n maxContexts?: number;\n\n // Tier 1 Optimizations (no precision loss)\n include?: string[]; // Only verify these message types\n exclude?: string[]; // Exclude these message types (mutually exclusive with include)\n symmetry?: string[][]; // Groups of symmetric message types [[type1, type2], [type3, type4]]\n perMessageBounds?: Record<string, number>; // Different maxInFlight per message type\n };\n onBuild?: \"warn\" | \"error\" | \"off\";\n onRelease?: \"warn\" | \"error\" | \"off\";\n\n // Verification engine options\n verification?: {\n timeout?: number; // Timeout in seconds (0 = no timeout)\n workers?: number; // Number of TLC workers\n };\n\n // Subsystem-scoped verification (compositional)\n subsystems?: Record<string, SubsystemConfig>;\n\n // Tier 2 Optimizations (controlled approximations)\n tier2?: {\n // Temporal constraints: ordering requirements between messages\n temporalConstraints?: Array<{\n before: string; // Message type that must occur first\n after: string; // Message type that must occur after\n description?: string; // Human-readable description\n }>;\n\n // Bounded exploration: limit depth for specific scenarios\n boundedExploration?: {\n maxDepth?: number; // Maximum state depth to explore\n criticalPaths?: string[][]; // Sequences of message types that must be fully explored\n };\n };\n}\n\n// Adapter-based configuration (for future use)\ninterface AdapterVerificationConfig {\n adapter: unknown; // Adapter interface not exported to avoid heavy deps\n state: Record<string, unknown>;\n bounds?: {\n maxInFlight?: number;\n [key: string]: unknown;\n };\n onBuild?: \"warn\" | \"error\" | \"off\";\n}\n\n// Union type for both config formats\ntype UnifiedVerificationConfig = LegacyVerificationConfig | AdapterVerificationConfig;\n\n/**\n * Define verification configuration with type checking\n *\n * Used in generated verification.config.ts files.\n *\n * @example\n * ```typescript\n * import { defineVerification } from '@fairfox/polly/verify'\n *\n * export default defineVerification({\n * state: {\n * \"user.role\": { type: \"enum\", values: [\"admin\", \"user\", \"guest\"] },\n * },\n * messages: {\n * maxInFlight: 6,\n * maxTabs: 2,\n * },\n * })\n * ```\n */\nexport function defineVerification<T extends UnifiedVerificationConfig>(config: T): T {\n // Validate configuration structure\n if (\"adapter\" in config) {\n // New adapter-based format\n if (!config.adapter) {\n throw new Error(\"Configuration must include an adapter\");\n }\n if (!config.state) {\n throw new Error(\"Configuration must include state bounds\");\n }\n } else if (\"messages\" in config) {\n // Legacy format\n if (!config.state) {\n throw new Error(\"Configuration must include state bounds\");\n }\n if (!config.messages) {\n throw new Error(\"Legacy configuration must include messages bounds\");\n }\n } else {\n throw new Error(\n \"Invalid configuration format. Must include either 'adapter' (new format) or 'messages' (legacy format)\"\n );\n }\n\n return config;\n}\n\n// Re-export verification primitives for user code\nexport {\n $constraints,\n ensures,\n hasLength,\n inRange,\n oneOf,\n requires,\n stateConstraint,\n} from \"./primitives/index.js\";\n"
7
7
  ],
8
- "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAeO,SAAS,QAAQ,CAAC,WAAoB,SAAwB;AAmB9D,SAAS,OAAO,CAAC,WAAoB,SAAwB;AA8B7D,SAAS,OAAO,CAAC,OAAe,KAAa,KAAsB;AAAA,EACxE,OAAO,SAAS,OAAO,SAAS;AAAA;AAS3B,SAAS,KAAQ,CAAC,OAAU,SAAuB;AAAA,EACxD,OAAO,QAAQ,SAAS,KAAK;AAAA;AASxB,SAAS,SAAS,CAAC,OAAkB,YAAqD;AAAA,EAC/F,IAAI,WAAW,QAAQ,aAAa,MAAM,SAAS,WAAW;AAAA,IAAK,OAAO;AAAA,EAC1E,IAAI,WAAW,QAAQ,aAAa,MAAM,SAAS,WAAW;AAAA,IAAK,OAAO;AAAA,EAC1E,OAAO;AAAA;AA4BF,SAAS,YAAY,CAC1B,YACA,aAQA,SACM;AAAA,EAEN,IAAI,SAAS,SAAS;AAAA,IAIb,iDACJ,KAAK,GAAG,0BAA0B;AAAA,MACjC,oBAAoB,YAAY,WAAW;AAAA,KAC5C,EACA,MAAM,MAAM,EAEZ;AAAA,EACL;AAAA;AAqBK,SAAS,eAAe,CAC7B,MACA,WACA,SACM;;;AC1ED,SAAS,kBAAuD,CAAC,QAAc;AAAA,EAEpF,IAAI,aAAa,QAAQ;AAAA,IAEvB,IAAI,CAAC,OAAO,SAAS;AAAA,MACnB,MAAM,IAAI,MAAM,uCAAuC;AAAA,IACzD;AAAA,IACA,IAAI,CAAC,OAAO,OAAO;AAAA,MACjB,MAAM,IAAI,MAAM,yCAAyC;AAAA,IAC3D;AAAA,EACF,EAAO,SAAI,cAAc,QAAQ;AAAA,IAE/B,IAAI,CAAC,OAAO,OAAO;AAAA,MACjB,MAAM,IAAI,MAAM,yCAAyC;AAAA,IAC3D;AAAA,IACA,IAAI,CAAC,OAAO,UAAU;AAAA,MACpB,MAAM,IAAI,MAAM,mDAAmD;AAAA,IACrE;AAAA,EACF,EAAO;AAAA,IACL,MAAM,IAAI,MACR,wGACF;AAAA;AAAA,EAGF,OAAO;AAAA;",
8
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAeO,SAAS,QAAQ,CAAC,WAAoB,SAAwB;AAmB9D,SAAS,OAAO,CAAC,WAAoB,SAAwB;AA8B7D,SAAS,OAAO,CAAC,OAAe,KAAa,KAAsB;AAAA,EACxE,OAAO,SAAS,OAAO,SAAS;AAAA;AAS3B,SAAS,KAAQ,CAAC,OAAU,SAAuB;AAAA,EACxD,OAAO,QAAQ,SAAS,KAAK;AAAA;AASxB,SAAS,SAAS,CAAC,OAAkB,YAAqD;AAAA,EAC/F,IAAI,WAAW,QAAQ,aAAa,MAAM,SAAS,WAAW;AAAA,IAAK,OAAO;AAAA,EAC1E,IAAI,WAAW,QAAQ,aAAa,MAAM,SAAS,WAAW;AAAA,IAAK,OAAO;AAAA,EAC1E,OAAO;AAAA;AA4BF,SAAS,YAAY,CAC1B,YACA,aAQA,SACM;AAAA,EAEN,IAAI,SAAS,SAAS;AAAA,IAIb,iDACJ,KAAK,GAAG,0BAA0B;AAAA,MACjC,oBAAoB,YAAY,WAAW;AAAA,KAC5C,EACA,MAAM,MAAM,EAEZ;AAAA,EACL;AAAA;AAqBK,SAAS,eAAe,CAC7B,MACA,WACA,SACM;;;ACjED,SAAS,kBAAuD,CAAC,QAAc;AAAA,EAEpF,IAAI,aAAa,QAAQ;AAAA,IAEvB,IAAI,CAAC,OAAO,SAAS;AAAA,MACnB,MAAM,IAAI,MAAM,uCAAuC;AAAA,IACzD;AAAA,IACA,IAAI,CAAC,OAAO,OAAO;AAAA,MACjB,MAAM,IAAI,MAAM,yCAAyC;AAAA,IAC3D;AAAA,EACF,EAAO,SAAI,cAAc,QAAQ;AAAA,IAE/B,IAAI,CAAC,OAAO,OAAO;AAAA,MACjB,MAAM,IAAI,MAAM,yCAAyC;AAAA,IAC3D;AAAA,IACA,IAAI,CAAC,OAAO,UAAU;AAAA,MACpB,MAAM,IAAI,MAAM,mDAAmD;AAAA,IACrE;AAAA,EACF,EAAO;AAAA,IACL,MAAM,IAAI,MACR,wGACF;AAAA;AAAA,EAGF,OAAO;AAAA;",
9
9
  "debugId": "465F57F5AB1A6BBD64756E2164756E21",
10
10
  "names": []
11
11
  }
@@ -39,6 +39,12 @@ class ProjectDetector {
39
39
  const packageJsonPath = path3.join(this.projectRoot, "package.json");
40
40
  if (fs3.existsSync(packageJsonPath)) {
41
41
  const packageJson = JSON.parse(fs3.readFileSync(packageJsonPath, "utf-8"));
42
+ if (packageJson.workspaces) {
43
+ const monorepoConfig = this.detectMonorepo(packageJson);
44
+ if (monorepoConfig) {
45
+ return monorepoConfig;
46
+ }
47
+ }
42
48
  if (packageJson.dependencies?.electron || packageJson.devDependencies?.electron) {
43
49
  return this.detectElectron(packageJson);
44
50
  }
@@ -268,10 +274,11 @@ class ProjectDetector {
268
274
  }
269
275
  };
270
276
  }
271
- scoreServerCandidates(candidates) {
277
+ scoreServerCandidates(candidates, baseDir) {
278
+ const root = baseDir || this.projectRoot;
272
279
  const scored = [];
273
280
  for (const candidate of candidates) {
274
- const fullPath = path3.join(this.projectRoot, candidate);
281
+ const fullPath = path3.join(root, candidate);
275
282
  if (!fs3.existsSync(fullPath))
276
283
  continue;
277
284
  try {
@@ -426,6 +433,135 @@ class ProjectDetector {
426
433
  }
427
434
  return score;
428
435
  }
436
+ detectMonorepo(packageJson) {
437
+ const workspacesRaw = packageJson.workspaces;
438
+ const patterns = Array.isArray(workspacesRaw) ? workspacesRaw : Array.isArray(workspacesRaw?.packages) ? workspacesRaw.packages : [];
439
+ if (patterns.length === 0)
440
+ return null;
441
+ const workspaceDirs = this.resolveWorkspaceGlobs(patterns);
442
+ if (workspaceDirs.length === 0)
443
+ return null;
444
+ const entryPoints = {};
445
+ const contextMapping = {};
446
+ const workspacePackages = [];
447
+ for (const wsDir of workspaceDirs) {
448
+ const wsPkgPath = path3.join(wsDir, "package.json");
449
+ if (!fs3.existsSync(wsPkgPath))
450
+ continue;
451
+ let wsPkg;
452
+ try {
453
+ wsPkg = JSON.parse(fs3.readFileSync(wsPkgPath, "utf-8"));
454
+ } catch {
455
+ continue;
456
+ }
457
+ const pkgName = wsPkg.name || path3.basename(wsDir);
458
+ const context = this.inferContextFromPackageName(pkgName);
459
+ const serverCandidates = [
460
+ "src/server.ts",
461
+ "src/server.js",
462
+ "src/index.ts",
463
+ "src/index.js",
464
+ "src/main.ts",
465
+ "src/main.js",
466
+ "src/app.ts",
467
+ "src/app.js",
468
+ "server.ts",
469
+ "server.js",
470
+ "index.ts",
471
+ "index.js"
472
+ ];
473
+ const scoredServers = this.scoreServerCandidates(serverCandidates, wsDir);
474
+ if (scoredServers.length > 0) {
475
+ const best = scoredServers[0];
476
+ if (best) {
477
+ entryPoints[context] = best.path;
478
+ if (best.hasWebSocket) {
479
+ contextMapping[context] = "WebSocket Server";
480
+ } else if (best.hasHTTP) {
481
+ contextMapping[context] = "HTTP Server";
482
+ } else {
483
+ contextMapping[context] = this.capitalize(context);
484
+ }
485
+ }
486
+ } else {
487
+ const commonEntries = [
488
+ "src/index.ts",
489
+ "src/index.tsx",
490
+ "src/main.ts",
491
+ "src/main.tsx",
492
+ "src/app.ts",
493
+ "src/app.tsx",
494
+ "index.ts",
495
+ "index.tsx"
496
+ ];
497
+ for (const candidate of commonEntries) {
498
+ const fullPath = path3.join(wsDir, candidate);
499
+ if (fs3.existsSync(fullPath)) {
500
+ entryPoints[context] = fullPath;
501
+ contextMapping[context] = this.capitalize(context);
502
+ break;
503
+ }
504
+ }
505
+ }
506
+ workspacePackages.push({ name: pkgName, path: wsDir, context });
507
+ }
508
+ if (Object.keys(entryPoints).length === 0)
509
+ return null;
510
+ return {
511
+ type: "monorepo",
512
+ entryPoints,
513
+ contextMapping,
514
+ workspacePackages,
515
+ metadata: {
516
+ name: packageJson.name,
517
+ version: packageJson.version,
518
+ description: packageJson.description
519
+ }
520
+ };
521
+ }
522
+ resolveWorkspaceGlobs(patterns) {
523
+ const dirs = [];
524
+ for (const pattern of patterns) {
525
+ if (pattern.includes("*")) {
526
+ const base = pattern.replace(/\/?\*.*$/, "");
527
+ const baseDir = path3.join(this.projectRoot, base);
528
+ if (fs3.existsSync(baseDir) && fs3.statSync(baseDir).isDirectory()) {
529
+ try {
530
+ const entries = fs3.readdirSync(baseDir, { withFileTypes: true });
531
+ for (const entry of entries) {
532
+ if (entry.isDirectory() && !entry.name.startsWith(".")) {
533
+ dirs.push(path3.join(baseDir, entry.name));
534
+ }
535
+ }
536
+ } catch {}
537
+ }
538
+ } else {
539
+ const fullPath = path3.join(this.projectRoot, pattern);
540
+ if (fs3.existsSync(fullPath) && fs3.statSync(fullPath).isDirectory()) {
541
+ dirs.push(fullPath);
542
+ }
543
+ }
544
+ }
545
+ return dirs;
546
+ }
547
+ inferContextFromPackageName(name) {
548
+ const parts = name.split("/");
549
+ const baseName = name.includes("/") ? parts[parts.length - 1] || name : name;
550
+ const lower = baseName.toLowerCase();
551
+ const serverAliases = ["api", "server", "backend"];
552
+ if (serverAliases.includes(lower))
553
+ return "server";
554
+ const clientAliases = ["web", "app", "frontend", "client", "ui"];
555
+ if (clientAliases.includes(lower))
556
+ return "client";
557
+ const sharedAliases = ["shared", "common", "lib", "core", "utils"];
558
+ if (sharedAliases.includes(lower))
559
+ return "shared";
560
+ return lower;
561
+ }
562
+ capitalize(str) {
563
+ return str.charAt(0).toUpperCase() + str.slice(1);
564
+ }
429
565
  detectGenericProject() {
430
566
  const entryPoints = {};
431
567
  const tsConfigPath = path3.join(this.projectRoot, "tsconfig.json");
@@ -915,11 +1051,13 @@ import { Node as Node2, Project as Project2 } from "ts-morph";
915
1051
  class FlowAnalyzer {
916
1052
  project;
917
1053
  handlers;
918
- constructor(tsConfigPath, handlers) {
1054
+ contextOverrides;
1055
+ constructor(tsConfigPath, handlers, contextOverrides) {
919
1056
  this.project = new Project2({
920
1057
  tsConfigFilePath: tsConfigPath
921
1058
  });
922
1059
  this.handlers = handlers;
1060
+ this.contextOverrides = contextOverrides || new Map;
923
1061
  }
924
1062
  analyzeFlows() {
925
1063
  const flows = [];
@@ -1155,6 +1293,10 @@ class FlowAnalyzer {
1155
1293
  return { trigger, flowName, description };
1156
1294
  }
1157
1295
  inferContext(filePath) {
1296
+ for (const [pathPrefix, context] of this.contextOverrides.entries()) {
1297
+ if (filePath.startsWith(pathPrefix))
1298
+ return context;
1299
+ }
1158
1300
  const path2 = filePath.toLowerCase();
1159
1301
  const contextPatterns = [
1160
1302
  { context: "background", patterns: ["/background/", "\\background\\"] },
@@ -1585,7 +1727,8 @@ class HandlerExtractor {
1585
1727
  packageRoot;
1586
1728
  warnings;
1587
1729
  currentFunctionParams = [];
1588
- constructor(tsConfigPath) {
1730
+ contextOverrides;
1731
+ constructor(tsConfigPath, contextOverrides) {
1589
1732
  this.project = new Project3({
1590
1733
  tsConfigFilePath: tsConfigPath
1591
1734
  });
@@ -1593,6 +1736,7 @@ class HandlerExtractor {
1593
1736
  this.relationshipExtractor = new RelationshipExtractor;
1594
1737
  this.analyzedFiles = new Set;
1595
1738
  this.warnings = [];
1739
+ this.contextOverrides = contextOverrides || new Map;
1596
1740
  this.packageRoot = this.findPackageRoot(tsConfigPath);
1597
1741
  }
1598
1742
  warnUnsupportedPattern(pattern, location, suggestion) {
@@ -1810,8 +1954,20 @@ class HandlerExtractor {
1810
1954
  handlers.push(handler);
1811
1955
  }
1812
1956
  }
1957
+ if (methodName === "ws") {
1958
+ this.extractElysiaWsHandlers(node, context, filePath, handlers);
1959
+ }
1960
+ if (this.isRestMethod(methodName) && this.isWebFrameworkFile(node.getSourceFile())) {
1961
+ const restHandler = this.extractRestHandler(node, methodName, context, filePath);
1962
+ if (restHandler) {
1963
+ handlers.push(restHandler);
1964
+ }
1965
+ }
1813
1966
  }
1814
1967
  }
1968
+ isRestMethod(name) {
1969
+ return ["get", "post", "put", "delete", "patch"].includes(name);
1970
+ }
1815
1971
  isElseIfStatement(node) {
1816
1972
  const parent = node.getParent();
1817
1973
  return parent !== undefined && Node4.isIfStatement(parent);
@@ -1877,6 +2033,135 @@ class HandlerExtractor {
1877
2033
  parameters
1878
2034
  };
1879
2035
  }
2036
+ extractElysiaWsHandlers(node, context, filePath, handlers) {
2037
+ const args = node.getArguments();
2038
+ if (args.length < 2)
2039
+ return;
2040
+ const routeArg = args[0];
2041
+ if (!routeArg || !Node4.isStringLiteral(routeArg))
2042
+ return;
2043
+ const routePath = routeArg.getLiteralValue();
2044
+ const configArg = args[1];
2045
+ if (!configArg || !Node4.isObjectLiteralExpression(configArg))
2046
+ return;
2047
+ const callbacks = ["message", "open", "close"];
2048
+ for (const cbName of callbacks) {
2049
+ const prop = configArg.getProperty(cbName);
2050
+ if (!prop)
2051
+ continue;
2052
+ let funcBody = null;
2053
+ if (Node4.isMethodDeclaration(prop)) {
2054
+ funcBody = prop;
2055
+ } else if (Node4.isPropertyAssignment(prop)) {
2056
+ const init = prop.getInitializer();
2057
+ if (init && (Node4.isArrowFunction(init) || Node4.isFunctionExpression(init))) {
2058
+ funcBody = init;
2059
+ }
2060
+ }
2061
+ if (!funcBody)
2062
+ continue;
2063
+ if (cbName === "message") {
2064
+ const body = funcBody.getBody();
2065
+ if (!body)
2066
+ continue;
2067
+ const subHandlers = this.extractSubHandlersFromBody(body, context, filePath);
2068
+ if (subHandlers.length > 0) {
2069
+ handlers.push(...subHandlers);
2070
+ } else {
2071
+ handlers.push(this.buildWsHandler(`ws_message`, routePath, context, filePath, funcBody, node.getStartLineNumber()));
2072
+ }
2073
+ } else {
2074
+ handlers.push(this.buildWsHandler(`ws_${cbName}`, routePath, context, filePath, funcBody, node.getStartLineNumber()));
2075
+ }
2076
+ }
2077
+ }
2078
+ extractSubHandlersFromBody(body, context, filePath) {
2079
+ const subHandlers = [];
2080
+ body.forEachDescendant((child) => {
2081
+ if (Node4.isIfStatement(child) && !this.isElseIfStatement(child)) {
2082
+ const typeGuardHandlers = this.extractTypeGuardHandlers(child, context, filePath);
2083
+ subHandlers.push(...typeGuardHandlers);
2084
+ }
2085
+ if (Node4.isSwitchStatement(child)) {
2086
+ const switchHandlers = this.extractSwitchCaseHandlers(child, context, filePath);
2087
+ subHandlers.push(...switchHandlers);
2088
+ }
2089
+ });
2090
+ return subHandlers;
2091
+ }
2092
+ buildWsHandler(messageType, _routePath, context, filePath, funcBody, line) {
2093
+ const assignments = [];
2094
+ const preconditions = [];
2095
+ const postconditions = [];
2096
+ this.currentFunctionParams = this.extractParameterNames(funcBody);
2097
+ this.extractAssignments(funcBody, assignments);
2098
+ this.extractVerificationConditions(funcBody, preconditions, postconditions);
2099
+ this.currentFunctionParams = [];
2100
+ return {
2101
+ messageType,
2102
+ node: context,
2103
+ assignments,
2104
+ preconditions,
2105
+ postconditions,
2106
+ location: { file: filePath, line },
2107
+ origin: "event"
2108
+ };
2109
+ }
2110
+ extractRestHandler(node, methodName, context, filePath) {
2111
+ const args = node.getArguments();
2112
+ if (args.length < 2)
2113
+ return null;
2114
+ const routeArg = args[0];
2115
+ if (!routeArg || !Node4.isStringLiteral(routeArg))
2116
+ return null;
2117
+ const routePath = routeArg.getLiteralValue();
2118
+ const httpMethod = methodName.toUpperCase();
2119
+ const messageType = `${httpMethod} ${routePath}`;
2120
+ const handlerArg = args[1];
2121
+ const assignments = [];
2122
+ const preconditions = [];
2123
+ const postconditions = [];
2124
+ let actualHandler = null;
2125
+ if (Node4.isArrowFunction(handlerArg) || Node4.isFunctionExpression(handlerArg)) {
2126
+ actualHandler = handlerArg;
2127
+ } else if (Node4.isIdentifier(handlerArg)) {
2128
+ actualHandler = this.resolveFunctionReference(handlerArg);
2129
+ }
2130
+ let parameters;
2131
+ if (actualHandler) {
2132
+ this.currentFunctionParams = this.extractParameterNames(actualHandler);
2133
+ parameters = this.currentFunctionParams.length > 0 ? [...this.currentFunctionParams] : undefined;
2134
+ this.extractAssignments(actualHandler, assignments);
2135
+ this.extractVerificationConditions(actualHandler, preconditions, postconditions);
2136
+ this.currentFunctionParams = [];
2137
+ }
2138
+ return {
2139
+ messageType,
2140
+ node: context,
2141
+ assignments,
2142
+ preconditions,
2143
+ postconditions,
2144
+ location: {
2145
+ file: filePath,
2146
+ line: node.getStartLineNumber()
2147
+ },
2148
+ origin: "event",
2149
+ parameters,
2150
+ handlerKind: "rest",
2151
+ httpMethod,
2152
+ routePath
2153
+ };
2154
+ }
2155
+ isWebFrameworkFile(sourceFile) {
2156
+ const frameworks = ["elysia", "express", "hono", "fastify", "koa", "@elysiajs/eden"];
2157
+ for (const importDecl of sourceFile.getImportDeclarations()) {
2158
+ const specifier = importDecl.getModuleSpecifierValue();
2159
+ if (frameworks.some((fw) => specifier === fw || specifier.startsWith(`${fw}/`))) {
2160
+ return true;
2161
+ }
2162
+ }
2163
+ return false;
2164
+ }
1880
2165
  extractAssignments(funcNode, assignments) {
1881
2166
  funcNode.forEachDescendant((node) => {
1882
2167
  if (Node4.isBinaryExpression(node)) {
@@ -2711,32 +2996,64 @@ class HandlerExtractor {
2711
2996
  try {
2712
2997
  const ifStmt = ifNode;
2713
2998
  const condition = ifStmt.getExpression();
2714
- if (!Node4.isCallExpression(condition)) {
2715
- return null;
2999
+ if (Node4.isCallExpression(condition)) {
3000
+ const funcExpr = condition.getExpression();
3001
+ const funcName = Node4.isIdentifier(funcExpr) ? funcExpr.getText() : undefined;
3002
+ this.debugLogProcessingFunction(funcName);
3003
+ const messageType = this.resolveMessageType(funcExpr, funcName, typeGuards);
3004
+ if (!messageType) {
3005
+ this.debugLogUnresolvedMessageType(funcName);
3006
+ return null;
3007
+ }
3008
+ const line = ifStmt.getStartLineNumber();
3009
+ const relationships = this.extractRelationshipsFromIfBlock(ifStmt, messageType);
3010
+ return {
3011
+ messageType,
3012
+ node: context,
3013
+ assignments: [],
3014
+ preconditions: [],
3015
+ postconditions: [],
3016
+ location: { file: filePath, line },
3017
+ relationships
3018
+ };
2716
3019
  }
2717
- const funcExpr = condition.getExpression();
2718
- const funcName = Node4.isIdentifier(funcExpr) ? funcExpr.getText() : undefined;
2719
- this.debugLogProcessingFunction(funcName);
2720
- const messageType = this.resolveMessageType(funcExpr, funcName, typeGuards);
2721
- if (!messageType) {
2722
- this.debugLogUnresolvedMessageType(funcName);
2723
- return null;
3020
+ if (Node4.isBinaryExpression(condition)) {
3021
+ const messageType = this.extractMessageTypeFromEqualityCheck(condition);
3022
+ if (messageType) {
3023
+ const line = ifStmt.getStartLineNumber();
3024
+ const relationships = this.extractRelationshipsFromIfBlock(ifStmt, messageType);
3025
+ return {
3026
+ messageType,
3027
+ node: context,
3028
+ assignments: [],
3029
+ preconditions: [],
3030
+ postconditions: [],
3031
+ location: { file: filePath, line },
3032
+ relationships
3033
+ };
3034
+ }
2724
3035
  }
2725
- const line = ifStmt.getStartLineNumber();
2726
- const relationships = this.extractRelationshipsFromIfBlock(ifStmt, messageType);
2727
- return {
2728
- messageType,
2729
- node: context,
2730
- assignments: [],
2731
- preconditions: [],
2732
- postconditions: [],
2733
- location: { file: filePath, line },
2734
- relationships
2735
- };
3036
+ return null;
2736
3037
  } catch (_error) {
2737
3038
  return null;
2738
3039
  }
2739
3040
  }
3041
+ extractMessageTypeFromEqualityCheck(expr) {
3042
+ const operator = expr.getOperatorToken().getText();
3043
+ if (operator !== "===" && operator !== "==")
3044
+ return null;
3045
+ const left = expr.getLeft();
3046
+ const right = expr.getRight();
3047
+ const stringLiteral = Node4.isStringLiteral(right) ? right : Node4.isStringLiteral(left) ? left : null;
3048
+ const propAccess = Node4.isPropertyAccessExpression(left) ? left : Node4.isPropertyAccessExpression(right) ? right : null;
3049
+ if (!stringLiteral || !propAccess)
3050
+ return null;
3051
+ const propName = propAccess.getName();
3052
+ if (propName !== "type" && propName !== "kind" && propName !== "action") {
3053
+ return null;
3054
+ }
3055
+ return stringLiteral.getLiteralValue();
3056
+ }
2740
3057
  debugLogProcessingFunction(funcName) {
2741
3058
  if (process.env["POLLY_DEBUG"] && funcName) {
2742
3059
  console.log(`[DEBUG] Processing if condition with function: ${funcName}`);
@@ -2946,6 +3263,10 @@ class HandlerExtractor {
2946
3263
  return messageType;
2947
3264
  }
2948
3265
  inferContext(filePath) {
3266
+ for (const [pathPrefix, context] of this.contextOverrides.entries()) {
3267
+ if (filePath.startsWith(pathPrefix))
3268
+ return context;
3269
+ }
2949
3270
  const path2 = filePath.toLowerCase();
2950
3271
  return this.inferElectronContext(path2) || this.inferWorkerContext(path2) || this.inferServerAppContext(path2) || this.inferChromeExtensionContext(path2) || "unknown";
2951
3272
  }
@@ -3894,10 +4215,16 @@ class ArchitectureAnalyzer {
3894
4215
  }
3895
4216
  async analyze() {
3896
4217
  const { manifest, projectConfig, entryPoints, systemInfo } = await this.initializeSystemInfo();
3897
- const handlerExtractor = new HandlerExtractor(this.options.tsConfigPath);
4218
+ const contextOverrides = new Map;
4219
+ if (projectConfig?.workspacePackages) {
4220
+ for (const pkg of projectConfig.workspacePackages) {
4221
+ contextOverrides.set(pkg.path, pkg.context);
4222
+ }
4223
+ }
4224
+ const handlerExtractor = new HandlerExtractor(this.options.tsConfigPath, contextOverrides.size > 0 ? contextOverrides : undefined);
3898
4225
  const { handlers } = handlerExtractor.extractHandlers();
3899
4226
  const contexts = this.analyzeContexts(entryPoints, handlers);
3900
- const flowAnalyzer = new FlowAnalyzer(this.options.tsConfigPath, handlers);
4227
+ const flowAnalyzer = new FlowAnalyzer(this.options.tsConfigPath, handlers, contextOverrides.size > 0 ? contextOverrides : undefined);
3901
4228
  const messageFlows = flowAnalyzer.analyzeFlows();
3902
4229
  const integrationAnalyzer = new IntegrationAnalyzer(this.options.tsConfigPath);
3903
4230
  const integrations = integrationAnalyzer.analyzeIntegrations();
@@ -4229,12 +4556,16 @@ class StructurizrDSLGenerator {
4229
4556
  handlersByType.get(handler.messageType)?.push(handler);
4230
4557
  }
4231
4558
  for (const [messageType, handlers] of handlersByType) {
4232
- const componentName = this.toComponentName(messageType);
4233
4559
  const firstHandler = handlers[0];
4234
4560
  if (!firstHandler)
4235
4561
  continue;
4562
+ const isRest = firstHandler.handlerKind === "rest";
4563
+ const componentName = isRest ? messageType : this.toComponentName(messageType);
4236
4564
  const description = this.generateComponentDescription(messageType, firstHandler);
4237
4565
  const tags = this.getComponentTags(messageType, firstHandler);
4566
+ if (isRest) {
4567
+ tags.push("REST Endpoint");
4568
+ }
4238
4569
  const properties = this.getComponentProperties(messageType, firstHandler, contextType);
4239
4570
  componentDefs.push({
4240
4571
  id: this.toId(componentName),
@@ -5066,9 +5397,12 @@ class StructurizrDSLGenerator {
5066
5397
  popup: "Browser Action Popup",
5067
5398
  devtools: "DevTools Panel",
5068
5399
  options: "Options Page",
5069
- offscreen: "Offscreen Document"
5400
+ offscreen: "Offscreen Document",
5401
+ server: "Server Application",
5402
+ client: "Client Application",
5403
+ shared: "Shared Library"
5070
5404
  };
5071
- return technologies[contextType] || "Extension Context";
5405
+ return technologies[contextType] || "Application Context";
5072
5406
  }
5073
5407
  toComponentName(messageType) {
5074
5408
  return `${messageType.split("_").map((part) => this.capitalize(part.toLowerCase())).join(" ")} Handler`;
@@ -5718,4 +6052,4 @@ main().catch((_error) => {
5718
6052
  process.exit(1);
5719
6053
  });
5720
6054
 
5721
- //# debugId=6E19C4E7D7F05D9564756E2164756E21
6055
+ //# debugId=345DE519CB00B03D64756E2164756E21