@defai.digital/automatosx 12.7.0 → 12.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +74 -85
  2. package/dist/index.js +12796 -8906
  3. package/dist/mcp/index.js +2981 -157
  4. package/package.json +12 -4
package/dist/mcp/index.js CHANGED
@@ -868,9 +868,9 @@ function createSafeTimeout(callback, delayMs, options = {}) {
868
868
  return cleanup;
869
869
  }
870
870
  async function sleep(ms, signal) {
871
- return new Promise((resolve5, reject) => {
871
+ return new Promise((resolve6, reject) => {
872
872
  const timeoutId = setTimeout(() => {
873
- resolve5();
873
+ resolve6();
874
874
  }, ms);
875
875
  if (timeoutId.unref) {
876
876
  timeoutId.unref();
@@ -1007,7 +1007,7 @@ var init_process_manager = __esm({
1007
1007
  if (child.killed || child.exitCode !== null) {
1008
1008
  continue;
1009
1009
  }
1010
- killPromises.push(new Promise((resolve5) => {
1010
+ killPromises.push(new Promise((resolve6) => {
1011
1011
  let mainTimeoutId = null;
1012
1012
  let fallbackTimeoutId = null;
1013
1013
  let resolved = false;
@@ -1022,7 +1022,7 @@ var init_process_manager = __esm({
1022
1022
  }
1023
1023
  if (!resolved) {
1024
1024
  resolved = true;
1025
- resolve5();
1025
+ resolve6();
1026
1026
  }
1027
1027
  };
1028
1028
  child.once("exit", () => {
@@ -1047,8 +1047,8 @@ var init_process_manager = __esm({
1047
1047
  try {
1048
1048
  await Promise.race([
1049
1049
  Promise.all(killPromises),
1050
- new Promise((resolve5) => {
1051
- finalTimeoutId = setTimeout(resolve5, timeout);
1050
+ new Promise((resolve6) => {
1051
+ finalTimeoutId = setTimeout(resolve6, timeout);
1052
1052
  })
1053
1053
  ]);
1054
1054
  } finally {
@@ -2704,7 +2704,7 @@ var init_base_provider = __esm({
2704
2704
  * @returns Promise resolving to stdout and stderr
2705
2705
  */
2706
2706
  async executeWithSpawn(command, cliCommand) {
2707
- return new Promise((resolve5, reject) => {
2707
+ return new Promise((resolve6, reject) => {
2708
2708
  const child = spawn(command, [], {
2709
2709
  shell: true,
2710
2710
  // Auto-detects: cmd.exe on Windows, /bin/sh on Unix
@@ -2795,7 +2795,7 @@ var init_base_provider = __esm({
2795
2795
  if (progressParser) {
2796
2796
  progressParser.succeed(`${cliCommand} completed successfully`);
2797
2797
  }
2798
- resolve5({ stdout, stderr });
2798
+ resolve6({ stdout, stderr });
2799
2799
  } else if (signal) {
2800
2800
  if (progressParser) {
2801
2801
  progressParser.fail(`${cliCommand} killed by signal ${signal}`);
@@ -2855,7 +2855,7 @@ var init_base_provider = __esm({
2855
2855
  * @returns Promise resolving to stdout and stderr
2856
2856
  */
2857
2857
  async executeWithStdin(cliCommand, cliArgs, prompt) {
2858
- return new Promise((resolve5, reject) => {
2858
+ return new Promise((resolve6, reject) => {
2859
2859
  logger.debug(`Executing ${cliCommand} CLI with stdin`, {
2860
2860
  command: cliCommand,
2861
2861
  args: cliArgs,
@@ -2983,7 +2983,7 @@ var init_base_provider = __esm({
2983
2983
  if (progressParser) {
2984
2984
  progressParser.succeed(`${cliCommand} completed successfully`);
2985
2985
  }
2986
- resolve5({ stdout, stderr });
2986
+ resolve6({ stdout, stderr });
2987
2987
  } else if (signal) {
2988
2988
  if (progressParser) {
2989
2989
  progressParser.fail(`${cliCommand} killed by signal ${signal}`);
@@ -3779,7 +3779,7 @@ var init_cli_wrapper = __esm({
3779
3779
  * Spawn codex process
3780
3780
  */
3781
3781
  async spawnProcess(args2, prompt, timeout) {
3782
- return new Promise((resolve5, reject) => {
3782
+ return new Promise((resolve6, reject) => {
3783
3783
  let stdout = "";
3784
3784
  let stderr = "";
3785
3785
  let hasTimedOut = false;
@@ -3836,7 +3836,7 @@ var init_cli_wrapper = __esm({
3836
3836
  return;
3837
3837
  }
3838
3838
  if (code === 0) {
3839
- resolve5({ stdout, stderr, exitCode: code });
3839
+ resolve6({ stdout, stderr, exitCode: code });
3840
3840
  } else {
3841
3841
  reject(
3842
3842
  new CodexError(
@@ -3869,7 +3869,7 @@ var init_cli_wrapper = __esm({
3869
3869
  * Spawn codex process with streaming support
3870
3870
  */
3871
3871
  async spawnProcessWithStreaming(args2, prompt, timeout, renderer) {
3872
- return new Promise((resolve5, reject) => {
3872
+ return new Promise((resolve6, reject) => {
3873
3873
  let stdout = "";
3874
3874
  let stderr = "";
3875
3875
  let hasTimedOut = false;
@@ -3949,7 +3949,7 @@ var init_cli_wrapper = __esm({
3949
3949
  if (renderer) {
3950
3950
  renderer.succeed("Execution complete");
3951
3951
  }
3952
- resolve5({ stdout, stderr, exitCode: code });
3952
+ resolve6({ stdout, stderr, exitCode: code });
3953
3953
  } else {
3954
3954
  if (renderer) {
3955
3955
  renderer.fail(`Process exited with code ${code}`);
@@ -6086,7 +6086,7 @@ var init_cli_wrapper2 = __esm({
6086
6086
  * Spawn CLI process and get output
6087
6087
  */
6088
6088
  spawnCLI(args2, prompt) {
6089
- return new Promise((resolve5, reject) => {
6089
+ return new Promise((resolve6, reject) => {
6090
6090
  const process2 = spawn(this.config.command, args2, {
6091
6091
  stdio: ["pipe", "pipe", "pipe"],
6092
6092
  timeout: this.config.timeout
@@ -6104,7 +6104,7 @@ var init_cli_wrapper2 = __esm({
6104
6104
  });
6105
6105
  process2.on("close", (code) => {
6106
6106
  if (code === 0) {
6107
- resolve5(stdout);
6107
+ resolve6(stdout);
6108
6108
  } else {
6109
6109
  reject(new Error(
6110
6110
  `CLI exited with code ${code}: ${stderr || "No error message"}`
@@ -7046,7 +7046,7 @@ var init_cli_wrapper3 = __esm({
7046
7046
  * Spawn CLI process and get output
7047
7047
  */
7048
7048
  spawnCLI(args2, prompt) {
7049
- return new Promise((resolve5, reject) => {
7049
+ return new Promise((resolve6, reject) => {
7050
7050
  const process2 = spawn(this.config.command, args2, {
7051
7051
  stdio: ["pipe", "pipe", "pipe"],
7052
7052
  timeout: this.config.timeout
@@ -7064,7 +7064,7 @@ var init_cli_wrapper3 = __esm({
7064
7064
  });
7065
7065
  process2.on("close", (code) => {
7066
7066
  if (code === 0) {
7067
- resolve5(stdout);
7067
+ resolve6(stdout);
7068
7068
  } else {
7069
7069
  reject(new Error(
7070
7070
  `CLI exited with code ${code}: ${stderr || "No error message"}`
@@ -7951,7 +7951,7 @@ var init_cli_wrapper4 = __esm({
7951
7951
  return this.createMockResponse(request.prompt);
7952
7952
  }
7953
7953
  const startTime = Date.now();
7954
- return new Promise((resolve5, reject) => {
7954
+ return new Promise((resolve6, reject) => {
7955
7955
  const args2 = [];
7956
7956
  if (this.config.vlmSwitchMode !== "once") {
7957
7957
  args2.push("--vlm-switch-mode", this.config.vlmSwitchMode);
@@ -8045,7 +8045,7 @@ ${request.prompt}`;
8045
8045
  }
8046
8046
  if ((code === 0 || code === null) && !signal) {
8047
8047
  const content = this.parseResponse(stdout);
8048
- resolve5({
8048
+ resolve6({
8049
8049
  content: content.trim(),
8050
8050
  model: "qwen-code-cli",
8051
8051
  tokensUsed: {
@@ -9713,7 +9713,7 @@ var PRECOMPILED_CONFIG = {
9713
9713
  "enableFreeTierPrioritization": true,
9714
9714
  "enableWorkloadAwareRouting": true
9715
9715
  },
9716
- "version": "12.7.0"
9716
+ "version": "12.8.2"
9717
9717
  };
9718
9718
 
9719
9719
  // src/core/config/schemas.ts
@@ -12447,6 +12447,778 @@ var CircuitBreaker = class {
12447
12447
  }
12448
12448
  };
12449
12449
 
12450
+ // src/core/router/affinity-manager.ts
12451
+ init_esm_shims();
12452
+ init_logger();
12453
+ var RouterAffinityManager = class {
12454
+ routingConfig;
12455
+ agentAffinities;
12456
+ constructor(routingConfig) {
12457
+ this.routingConfig = routingConfig;
12458
+ this.agentAffinities = routingConfig?.agentAffinities ?? {};
12459
+ logger.debug("RouterAffinityManager initialized", {
12460
+ hasConfig: !!routingConfig,
12461
+ agentCount: Object.keys(this.agentAffinities).length,
12462
+ autoConfigured: routingConfig?.autoConfigured
12463
+ });
12464
+ }
12465
+ /**
12466
+ * Update routing configuration at runtime
12467
+ */
12468
+ updateConfig(routingConfig) {
12469
+ this.routingConfig = routingConfig;
12470
+ this.agentAffinities = routingConfig.agentAffinities ?? {};
12471
+ logger.debug("RouterAffinityManager config updated", {
12472
+ agentCount: Object.keys(this.agentAffinities).length,
12473
+ strategy: routingConfig.strategy
12474
+ });
12475
+ }
12476
+ /**
12477
+ * Get affinity configuration for a specific agent
12478
+ *
12479
+ * @param agentName - Name of the agent
12480
+ * @returns AffinityLookupResult with primary provider and fallback chain
12481
+ */
12482
+ getAgentAffinity(agentName) {
12483
+ const affinity = this.agentAffinities[agentName];
12484
+ if (affinity) {
12485
+ return {
12486
+ hasAffinity: true,
12487
+ primary: affinity.primary,
12488
+ fallback: affinity.fallback,
12489
+ source: "config"
12490
+ };
12491
+ }
12492
+ return {
12493
+ hasAffinity: false,
12494
+ primary: null,
12495
+ fallback: [],
12496
+ source: "default"
12497
+ };
12498
+ }
12499
+ /**
12500
+ * Reorder providers based on agent affinity
12501
+ *
12502
+ * If an agent has configured affinities, reorders the available providers
12503
+ * to prioritize the primary provider and fallback chain.
12504
+ *
12505
+ * @param options - Agent name and available providers
12506
+ * @returns Reordered provider names, or original order if no affinity
12507
+ */
12508
+ reorderByAffinity(options) {
12509
+ const { agentName, availableProviders } = options;
12510
+ if (availableProviders.length <= 1) {
12511
+ return availableProviders;
12512
+ }
12513
+ const affinity = this.getAgentAffinity(agentName);
12514
+ if (!affinity.hasAffinity || !affinity.primary) {
12515
+ logger.debug("No affinity configured, using default order", {
12516
+ agentName,
12517
+ source: affinity.source
12518
+ });
12519
+ return availableProviders;
12520
+ }
12521
+ const reordered = [];
12522
+ const used = /* @__PURE__ */ new Set();
12523
+ if (affinity.primary && availableProviders.includes(affinity.primary)) {
12524
+ reordered.push(affinity.primary);
12525
+ used.add(affinity.primary);
12526
+ }
12527
+ for (const fallback of affinity.fallback) {
12528
+ if (availableProviders.includes(fallback) && !used.has(fallback)) {
12529
+ reordered.push(fallback);
12530
+ used.add(fallback);
12531
+ }
12532
+ }
12533
+ for (const provider of availableProviders) {
12534
+ if (!used.has(provider)) {
12535
+ reordered.push(provider);
12536
+ used.add(provider);
12537
+ }
12538
+ }
12539
+ logger.debug("Providers reordered by affinity", {
12540
+ agentName,
12541
+ original: availableProviders,
12542
+ reordered,
12543
+ primary: affinity.primary,
12544
+ fallbackChain: affinity.fallback
12545
+ });
12546
+ return reordered;
12547
+ }
12548
+ /**
12549
+ * Check if affinity-based routing is enabled
12550
+ */
12551
+ isEnabled() {
12552
+ return !!this.routingConfig?.autoConfigured && Object.keys(this.agentAffinities).length > 0;
12553
+ }
12554
+ /**
12555
+ * Get all configured agent affinities
12556
+ */
12557
+ getAllAffinities() {
12558
+ return { ...this.agentAffinities };
12559
+ }
12560
+ /**
12561
+ * Get affinity statistics for observability
12562
+ */
12563
+ getStats() {
12564
+ return {
12565
+ enabled: this.isEnabled(),
12566
+ agentCount: Object.keys(this.agentAffinities).length,
12567
+ strategy: this.routingConfig?.strategy,
12568
+ lastConfiguredAt: this.routingConfig?.lastConfiguredAt
12569
+ };
12570
+ }
12571
+ };
12572
+
12573
+ // src/core/router/ability-manager.ts
12574
+ init_esm_shims();
12575
+ init_logger();
12576
+ var ABILITY_TYPES = [
12577
+ "code-generation",
12578
+ "code-review",
12579
+ "security-audit",
12580
+ "documentation",
12581
+ "data-analysis",
12582
+ "architecture",
12583
+ "testing",
12584
+ "devops",
12585
+ "research",
12586
+ "creative"
12587
+ ];
12588
+ var DEFAULT_ABILITY_ROUTING = {
12589
+ "code-generation": ["claude-code", "codex", "qwen", "gemini-cli"],
12590
+ "code-review": ["gemini-cli", "claude-code", "grok", "codex"],
12591
+ "security-audit": ["claude-code", "grok", "codex"],
12592
+ "documentation": ["gemini-cli", "claude-code", "grok"],
12593
+ "data-analysis": ["gemini-cli", "claude-code", "grok", "qwen"],
12594
+ "architecture": ["claude-code", "gemini-cli", "grok"],
12595
+ "testing": ["gemini-cli", "claude-code", "codex"],
12596
+ "devops": ["codex", "claude-code", "gemini-cli"],
12597
+ "research": ["grok", "gemini-cli", "claude-code"],
12598
+ "creative": ["grok", "gemini-cli", "claude-code"]
12599
+ };
12600
+ var RouterAbilityManager = class {
12601
+ routingConfig;
12602
+ abilityRouting;
12603
+ constructor(routingConfig) {
12604
+ this.routingConfig = routingConfig;
12605
+ this.abilityRouting = routingConfig?.abilityRouting ?? {};
12606
+ logger.debug("RouterAbilityManager initialized", {
12607
+ hasConfig: !!routingConfig,
12608
+ abilityCount: Object.keys(this.abilityRouting).length,
12609
+ autoConfigured: routingConfig?.autoConfigured
12610
+ });
12611
+ }
12612
+ /**
12613
+ * Update routing configuration at runtime
12614
+ */
12615
+ updateConfig(routingConfig) {
12616
+ this.routingConfig = routingConfig;
12617
+ this.abilityRouting = routingConfig.abilityRouting ?? {};
12618
+ logger.debug("RouterAbilityManager config updated", {
12619
+ abilityCount: Object.keys(this.abilityRouting).length,
12620
+ strategy: routingConfig.strategy
12621
+ });
12622
+ }
12623
+ /**
12624
+ * Get routing configuration for a specific ability type
12625
+ *
12626
+ * @param abilityType - Type of ability (e.g., 'code-generation')
12627
+ * @returns AbilityLookupResult with preferred providers
12628
+ */
12629
+ getAbilityRouting(abilityType) {
12630
+ const routing = this.abilityRouting[abilityType];
12631
+ if (routing) {
12632
+ return {
12633
+ hasRouting: true,
12634
+ preferredProviders: routing.preferredProviders,
12635
+ source: "config"
12636
+ };
12637
+ }
12638
+ const defaultRouting = DEFAULT_ABILITY_ROUTING[abilityType];
12639
+ if (defaultRouting) {
12640
+ return {
12641
+ hasRouting: true,
12642
+ preferredProviders: defaultRouting,
12643
+ source: "default"
12644
+ };
12645
+ }
12646
+ return {
12647
+ hasRouting: false,
12648
+ preferredProviders: [],
12649
+ source: "default"
12650
+ };
12651
+ }
12652
+ /**
12653
+ * Reorder providers based on ability routing
12654
+ *
12655
+ * If an ability has configured routing, reorders the available providers
12656
+ * to prioritize those best suited for the ability type.
12657
+ *
12658
+ * @param options - Ability type and available providers
12659
+ * @returns Reordered provider names, or original order if no routing
12660
+ */
12661
+ reorderByAbility(options) {
12662
+ const { abilityType, availableProviders } = options;
12663
+ if (availableProviders.length <= 1) {
12664
+ return availableProviders;
12665
+ }
12666
+ const routing = this.getAbilityRouting(abilityType);
12667
+ if (!routing.hasRouting || routing.preferredProviders.length === 0) {
12668
+ logger.debug("No ability routing configured, using default order", {
12669
+ abilityType,
12670
+ source: routing.source
12671
+ });
12672
+ return availableProviders;
12673
+ }
12674
+ const reordered = [];
12675
+ const used = /* @__PURE__ */ new Set();
12676
+ for (const provider of routing.preferredProviders) {
12677
+ if (availableProviders.includes(provider) && !used.has(provider)) {
12678
+ reordered.push(provider);
12679
+ used.add(provider);
12680
+ }
12681
+ }
12682
+ for (const provider of availableProviders) {
12683
+ if (!used.has(provider)) {
12684
+ reordered.push(provider);
12685
+ used.add(provider);
12686
+ }
12687
+ }
12688
+ logger.debug("Providers reordered by ability", {
12689
+ abilityType,
12690
+ original: availableProviders,
12691
+ reordered,
12692
+ preferredProviders: routing.preferredProviders
12693
+ });
12694
+ return reordered;
12695
+ }
12696
+ /**
12697
+ * Check if ability-based routing is enabled
12698
+ */
12699
+ isEnabled() {
12700
+ return !!this.routingConfig?.autoConfigured && Object.keys(this.abilityRouting).length > 0;
12701
+ }
12702
+ /**
12703
+ * Get all configured ability routings
12704
+ */
12705
+ getAllRoutings() {
12706
+ return { ...this.abilityRouting };
12707
+ }
12708
+ /**
12709
+ * Get ability routing statistics for observability
12710
+ */
12711
+ getStats() {
12712
+ return {
12713
+ enabled: this.isEnabled(),
12714
+ abilityCount: Object.keys(this.abilityRouting).length,
12715
+ strategy: this.routingConfig?.strategy,
12716
+ lastConfiguredAt: this.routingConfig?.lastConfiguredAt,
12717
+ configuredAbilities: Object.keys(this.abilityRouting)
12718
+ };
12719
+ }
12720
+ /**
12721
+ * Check if an ability type is valid/known
12722
+ */
12723
+ isKnownAbility(abilityType) {
12724
+ return ABILITY_TYPES.includes(abilityType);
12725
+ }
12726
+ /**
12727
+ * Get all known ability types
12728
+ */
12729
+ getKnownAbilities() {
12730
+ return ABILITY_TYPES;
12731
+ }
12732
+ };
12733
+
12734
+ // src/core/router/dynamic-optimizer.ts
12735
+ init_esm_shims();
12736
+ init_logger();
12737
+ var DEFAULT_CONFIG3 = {
12738
+ enableAdaptivePriorities: true,
12739
+ enableCostOptimization: true,
12740
+ enableHealthReordering: true,
12741
+ minRequestsForOptimization: 20,
12742
+ optimizationIntervalMs: 5 * 60 * 1e3,
12743
+ // 5 minutes
12744
+ maxHistorySnapshots: 100,
12745
+ weights: {
12746
+ latency: 0.3,
12747
+ quality: 0.35,
12748
+ cost: 0.2,
12749
+ availability: 0.15
12750
+ }
12751
+ };
12752
+ var STATE_VERSION = 1;
12753
+ var DynamicOptimizer = class extends EventEmitter {
12754
+ config;
12755
+ state;
12756
+ metricsTracker;
12757
+ optimizationInterval;
12758
+ isInitialized = false;
12759
+ constructor(config = {}) {
12760
+ super();
12761
+ this.config = { ...DEFAULT_CONFIG3, ...config };
12762
+ this.metricsTracker = getProviderMetricsTracker();
12763
+ this.state = this.createEmptyState();
12764
+ logger.debug("DynamicOptimizer created", {
12765
+ enableAdaptivePriorities: this.config.enableAdaptivePriorities,
12766
+ enableCostOptimization: this.config.enableCostOptimization,
12767
+ enableHealthReordering: this.config.enableHealthReordering,
12768
+ optimizationIntervalMs: this.config.optimizationIntervalMs
12769
+ });
12770
+ }
12771
+ /**
12772
+ * Initialize the optimizer (load persisted state, start optimization loop)
12773
+ */
12774
+ async initialize() {
12775
+ if (this.isInitialized) return;
12776
+ if (this.config.statePath) {
12777
+ await this.loadState();
12778
+ }
12779
+ this.startOptimizationLoop();
12780
+ this.metricsTracker.on("metrics-updated", (provider) => {
12781
+ this.handleMetricsUpdate(provider);
12782
+ });
12783
+ this.isInitialized = true;
12784
+ logger.info("DynamicOptimizer initialized", {
12785
+ hasPersistedState: !!this.config.statePath,
12786
+ providersTracked: Object.keys(this.state.performanceHistory).length
12787
+ });
12788
+ }
12789
+ /**
12790
+ * Get current performance snapshot for a provider
12791
+ */
12792
+ async getPerformanceSnapshot(provider) {
12793
+ const metrics = await this.metricsTracker.getMetrics(provider);
12794
+ if (!metrics) return null;
12795
+ const priorityScore = await this.calculatePriorityScore(provider, metrics);
12796
+ const qualityScore = metrics.quality.successRate * 0.7 + metrics.quality.properStopRate * 0.3;
12797
+ return {
12798
+ provider,
12799
+ avgLatencyMs: metrics.latency.avg,
12800
+ p95LatencyMs: metrics.latency.p95,
12801
+ latency: {
12802
+ avg: metrics.latency.avg,
12803
+ p95: metrics.latency.p95
12804
+ },
12805
+ successRate: metrics.quality.successRate,
12806
+ qualityScore,
12807
+ avgCostPer1M: metrics.cost.avgCostPer1MTokens,
12808
+ totalRequests: metrics.quality.totalRequests,
12809
+ consecutiveFailures: metrics.availability.consecutiveFailures,
12810
+ lastSuccessAt: metrics.availability.lastSuccess,
12811
+ priorityScore,
12812
+ timestamp: Date.now(),
12813
+ lastUpdatedAt: new Date(metrics.lastUpdated).toISOString()
12814
+ };
12815
+ }
12816
+ /**
12817
+ * Get all provider performance snapshots
12818
+ */
12819
+ async getAllPerformanceSnapshots(providers) {
12820
+ const snapshots = [];
12821
+ for (const provider of providers) {
12822
+ const snapshot = await this.getPerformanceSnapshot(provider);
12823
+ if (snapshot) {
12824
+ snapshots.push(snapshot);
12825
+ }
12826
+ }
12827
+ return snapshots.sort((a, b) => b.priorityScore - a.priorityScore);
12828
+ }
12829
+ /**
12830
+ * Calculate adaptive priority for a provider based on performance
12831
+ */
12832
+ async calculatePriorityScore(provider, metrics) {
12833
+ const { weights } = this.config;
12834
+ const latencyScore = this.normalizeLatency(metrics.latency.p95);
12835
+ const qualityScore = metrics.quality.successRate * 0.7 + metrics.quality.properStopRate * 0.3;
12836
+ const costScore = this.normalizeCost(metrics.cost.avgCostPer1MTokens);
12837
+ const availabilityScore = metrics.availability.uptime * (1 - Math.min(metrics.availability.consecutiveFailures * 0.1, 0.5));
12838
+ const score = weights.latency * latencyScore + weights.quality * qualityScore + weights.cost * costScore + weights.availability * availabilityScore;
12839
+ return Math.round(score * 100) / 100;
12840
+ }
12841
+ /**
12842
+ * Normalize latency to 0-1 score (lower latency = higher score)
12843
+ */
12844
+ normalizeLatency(p95Ms) {
12845
+ if (p95Ms < 1e3) return 1;
12846
+ if (p95Ms < 2e3) return 0.9;
12847
+ if (p95Ms < 3e3) return 0.7;
12848
+ if (p95Ms < 5e3) return 0.5;
12849
+ if (p95Ms < 1e4) return 0.3;
12850
+ return 0.1;
12851
+ }
12852
+ /**
12853
+ * Normalize cost to 0-1 score (lower cost = higher score)
12854
+ */
12855
+ normalizeCost(costPer1M) {
12856
+ if (costPer1M === 0) return 1;
12857
+ if (costPer1M < 0.5) return 0.9;
12858
+ if (costPer1M < 2) return 0.7;
12859
+ if (costPer1M < 5) return 0.5;
12860
+ if (costPer1M < 15) return 0.3;
12861
+ return 0.1;
12862
+ }
12863
+ /**
12864
+ * Generate optimization recommendations based on current performance
12865
+ */
12866
+ async generateRecommendations(providers) {
12867
+ const recommendations = [];
12868
+ const snapshots = await this.getAllPerformanceSnapshots(providers);
12869
+ if (snapshots.length === 0) {
12870
+ return recommendations;
12871
+ }
12872
+ const currentPriorities = { ...this.state.adaptivePriorities };
12873
+ for (const snapshot of snapshots) {
12874
+ const { provider, priorityScore, successRate, consecutiveFailures, avgCostPer1M } = snapshot;
12875
+ if (consecutiveFailures >= 3) {
12876
+ recommendations.push({
12877
+ type: "health_warning",
12878
+ provider,
12879
+ currentValue: consecutiveFailures,
12880
+ recommendedValue: 0,
12881
+ reason: `Provider has ${consecutiveFailures} consecutive failures`,
12882
+ confidence: 0.9,
12883
+ impact: consecutiveFailures >= 5 ? "high" : "medium"
12884
+ });
12885
+ }
12886
+ if (successRate < 0.5 && snapshot.totalRequests >= this.config.minRequestsForOptimization) {
12887
+ recommendations.push({
12888
+ type: "disable_provider",
12889
+ provider,
12890
+ currentValue: successRate,
12891
+ recommendedValue: "disabled",
12892
+ reason: `Success rate (${(successRate * 100).toFixed(1)}%) is below 50%`,
12893
+ confidence: 0.85,
12894
+ impact: "high"
12895
+ });
12896
+ }
12897
+ if (this.config.enableAdaptivePriorities) {
12898
+ const currentPriority = currentPriorities[provider] ?? 50;
12899
+ const recommendedPriority = this.calculateRecommendedPriority(priorityScore);
12900
+ if (Math.abs(recommendedPriority - currentPriority) >= 5) {
12901
+ recommendations.push({
12902
+ type: "priority_adjustment",
12903
+ provider,
12904
+ currentValue: currentPriority,
12905
+ recommendedValue: recommendedPriority,
12906
+ reason: `Performance score suggests priority change (score: ${priorityScore.toFixed(2)})`,
12907
+ confidence: Math.min(snapshot.totalRequests / 100, 0.95),
12908
+ impact: Math.abs(recommendedPriority - currentPriority) >= 20 ? "high" : "medium"
12909
+ });
12910
+ }
12911
+ }
12912
+ if (this.config.enableCostOptimization && avgCostPer1M > 10) {
12913
+ const cheaperAlternatives = snapshots.filter(
12914
+ (s) => s.provider !== provider && s.avgCostPer1M < avgCostPer1M * 0.5 && s.successRate >= successRate * 0.9
12915
+ );
12916
+ if (cheaperAlternatives.length > 0) {
12917
+ const best = cheaperAlternatives[0];
12918
+ if (best) {
12919
+ recommendations.push({
12920
+ type: "cost_optimization",
12921
+ provider,
12922
+ currentValue: avgCostPer1M,
12923
+ recommendedValue: best.provider,
12924
+ reason: `${best.provider} offers similar quality at ${((1 - best.avgCostPer1M / avgCostPer1M) * 100).toFixed(0)}% lower cost`,
12925
+ confidence: 0.7,
12926
+ impact: "medium"
12927
+ });
12928
+ }
12929
+ }
12930
+ }
12931
+ }
12932
+ return recommendations.sort((a, b) => {
12933
+ const impactOrder = { high: 3, medium: 2, low: 1 };
12934
+ const impactDiff = impactOrder[b.impact] - impactOrder[a.impact];
12935
+ if (impactDiff !== 0) return impactDiff;
12936
+ return b.confidence - a.confidence;
12937
+ });
12938
+ }
12939
+ /**
12940
+ * Calculate recommended priority based on score
12941
+ */
12942
+ calculateRecommendedPriority(score) {
12943
+ return Math.round(100 - score * 90);
12944
+ }
12945
+ /**
12946
+ * Apply an optimization recommendation
12947
+ */
12948
+ async applyRecommendation(recommendation) {
12949
+ try {
12950
+ switch (recommendation.type) {
12951
+ case "priority_adjustment":
12952
+ this.state.adaptivePriorities[recommendation.provider] = recommendation.recommendedValue;
12953
+ break;
12954
+ case "disable_provider":
12955
+ this.state.adaptivePriorities[recommendation.provider] = 999;
12956
+ break;
12957
+ case "health_warning":
12958
+ case "cost_optimization":
12959
+ break;
12960
+ }
12961
+ this.state.appliedOptimizations.push({
12962
+ recommendation,
12963
+ appliedAt: Date.now()
12964
+ });
12965
+ await this.saveState();
12966
+ this.emit("optimization-applied", recommendation);
12967
+ logger.info("Optimization recommendation applied", {
12968
+ type: recommendation.type,
12969
+ provider: recommendation.provider,
12970
+ recommendedValue: recommendation.recommendedValue
12971
+ });
12972
+ return true;
12973
+ } catch (error) {
12974
+ logger.error("Failed to apply optimization", {
12975
+ recommendation,
12976
+ error: error.message
12977
+ });
12978
+ return false;
12979
+ }
12980
+ }
12981
+ /**
12982
+ * Get adaptive priorities for provider reordering
12983
+ */
12984
+ getAdaptivePriorities() {
12985
+ return { ...this.state.adaptivePriorities };
12986
+ }
12987
+ /**
12988
+ * Reorder providers based on adaptive priorities and health
12989
+ */
12990
+ async reorderByPerformance(providers, healthStatus) {
12991
+ if (!this.config.enableHealthReordering && !this.config.enableAdaptivePriorities) {
12992
+ return providers;
12993
+ }
12994
+ const snapshots = await this.getAllPerformanceSnapshots(providers);
12995
+ const snapshotMap = new Map(snapshots.map((s) => [s.provider, s]));
12996
+ const sorted = [...providers].sort((a, b) => {
12997
+ if (this.config.enableHealthReordering) {
12998
+ const aHealthy = healthStatus.get(a) ?? true;
12999
+ const bHealthy = healthStatus.get(b) ?? true;
13000
+ if (aHealthy !== bHealthy) {
13001
+ return aHealthy ? -1 : 1;
13002
+ }
13003
+ }
13004
+ if (this.config.enableAdaptivePriorities) {
13005
+ const aPriority = this.state.adaptivePriorities[a] ?? 50;
13006
+ const bPriority = this.state.adaptivePriorities[b] ?? 50;
13007
+ if (aPriority !== bPriority) {
13008
+ return aPriority - bPriority;
13009
+ }
13010
+ }
13011
+ const aSnapshot = snapshotMap.get(a);
13012
+ const bSnapshot = snapshotMap.get(b);
13013
+ const aScore = aSnapshot?.priorityScore ?? 0.5;
13014
+ const bScore = bSnapshot?.priorityScore ?? 0.5;
13015
+ return bScore - aScore;
13016
+ });
13017
+ logger.debug("Providers reordered by performance", {
13018
+ original: providers,
13019
+ reordered: sorted
13020
+ });
13021
+ return sorted;
13022
+ }
13023
+ /**
13024
+ * Track cost for a request
13025
+ */
13026
+ trackCost(provider, costUsd) {
13027
+ this.state.costTracking.totalCostUsd += costUsd;
13028
+ this.state.costTracking.costByProvider[provider] = (this.state.costTracking.costByProvider[provider] ?? 0) + costUsd;
13029
+ }
13030
+ /**
13031
+ * Get cost tracking summary
13032
+ */
13033
+ getCostSummary() {
13034
+ const costByProvider = { ...this.state.costTracking.costByProvider };
13035
+ const topSpenders = Object.entries(costByProvider).map(([provider, costUsd]) => ({ provider, costUsd })).sort((a, b) => b.costUsd - a.costUsd).slice(0, 5);
13036
+ return {
13037
+ totalCostUsd: this.state.costTracking.totalCostUsd,
13038
+ costByProvider,
13039
+ topSpenders
13040
+ };
13041
+ }
13042
+ /**
13043
+ * Get optimizer statistics
13044
+ */
13045
+ getStats() {
13046
+ return {
13047
+ isInitialized: this.isInitialized,
13048
+ lastOptimizationAt: this.state.lastOptimizationAt,
13049
+ providersTracked: Object.keys(this.state.performanceHistory).length,
13050
+ totalAppliedOptimizations: this.state.appliedOptimizations.length,
13051
+ adaptivePriorities: { ...this.state.adaptivePriorities },
13052
+ costTracking: {
13053
+ totalCostUsd: this.state.costTracking.totalCostUsd,
13054
+ sinceReset: new Date(this.state.costTracking.lastResetAt).toISOString()
13055
+ }
13056
+ };
13057
+ }
13058
+ /**
13059
+ * Reset cost tracking
13060
+ */
13061
+ resetCostTracking() {
13062
+ this.state.costTracking = {
13063
+ totalCostUsd: 0,
13064
+ costByProvider: {},
13065
+ lastResetAt: Date.now()
13066
+ };
13067
+ this.emit("cost-tracking-reset");
13068
+ }
13069
+ /**
13070
+ * Handle metrics update event
13071
+ */
13072
+ async handleMetricsUpdate(provider) {
13073
+ try {
13074
+ const snapshot = await this.getPerformanceSnapshot(provider);
13075
+ if (!snapshot) return;
13076
+ if (!this.state.performanceHistory[provider]) {
13077
+ this.state.performanceHistory[provider] = [];
13078
+ }
13079
+ const history = this.state.performanceHistory[provider];
13080
+ history.push(snapshot);
13081
+ if (history.length > this.config.maxHistorySnapshots) {
13082
+ history.splice(0, history.length - this.config.maxHistorySnapshots);
13083
+ }
13084
+ this.emit("performance-updated", provider, snapshot);
13085
+ } catch (error) {
13086
+ logger.warn("Failed to handle metrics update", {
13087
+ provider,
13088
+ error: error.message
13089
+ });
13090
+ }
13091
+ }
13092
+ /**
13093
+ * Start the optimization loop
13094
+ */
13095
+ startOptimizationLoop() {
13096
+ if (this.optimizationInterval) return;
13097
+ this.optimizationInterval = setInterval(async () => {
13098
+ try {
13099
+ await this.runOptimization();
13100
+ } catch (error) {
13101
+ logger.error("Optimization loop failed", {
13102
+ error: error.message
13103
+ });
13104
+ }
13105
+ }, this.config.optimizationIntervalMs);
13106
+ this.optimizationInterval.unref();
13107
+ logger.debug("Optimization loop started", {
13108
+ intervalMs: this.config.optimizationIntervalMs
13109
+ });
13110
+ }
13111
+ /**
13112
+ * Run optimization analysis
13113
+ */
13114
+ async runOptimization() {
13115
+ const providers = Object.keys(this.state.performanceHistory);
13116
+ if (providers.length === 0) return;
13117
+ const hasEnoughData = providers.some((p) => {
13118
+ const history = this.state.performanceHistory[p];
13119
+ return history && history.length >= this.config.minRequestsForOptimization;
13120
+ });
13121
+ if (!hasEnoughData) return;
13122
+ const recommendations = await this.generateRecommendations(providers);
13123
+ this.state.lastOptimizationAt = Date.now();
13124
+ if (recommendations.length > 0) {
13125
+ this.emit("recommendations-generated", recommendations);
13126
+ logger.info("Optimization recommendations generated", {
13127
+ count: recommendations.length,
13128
+ types: recommendations.map((r) => r.type)
13129
+ });
13130
+ for (const rec of recommendations) {
13131
+ if (rec.type === "priority_adjustment" && rec.confidence >= 0.8 && this.config.enableAdaptivePriorities) {
13132
+ await this.applyRecommendation(rec);
13133
+ }
13134
+ }
13135
+ }
13136
+ await this.saveState();
13137
+ }
13138
+ /**
13139
+ * Create empty state
13140
+ */
13141
+ createEmptyState() {
13142
+ return {
13143
+ version: STATE_VERSION,
13144
+ lastOptimizationAt: 0,
13145
+ performanceHistory: {},
13146
+ appliedOptimizations: [],
13147
+ adaptivePriorities: {},
13148
+ costTracking: {
13149
+ totalCostUsd: 0,
13150
+ costByProvider: {},
13151
+ lastResetAt: Date.now()
13152
+ }
13153
+ };
13154
+ }
13155
+ /**
13156
+ * Load state from disk
13157
+ */
13158
+ async loadState() {
13159
+ if (!this.config.statePath) return;
13160
+ try {
13161
+ const data = await readFile(this.config.statePath, "utf-8");
13162
+ const loaded = JSON.parse(data);
13163
+ if (loaded.version !== STATE_VERSION) {
13164
+ logger.warn("State version mismatch, starting fresh", {
13165
+ expected: STATE_VERSION,
13166
+ found: loaded.version
13167
+ });
13168
+ return;
13169
+ }
13170
+ this.state = loaded;
13171
+ logger.info("DynamicOptimizer state loaded", {
13172
+ providersTracked: Object.keys(this.state.performanceHistory).length,
13173
+ appliedOptimizations: this.state.appliedOptimizations.length
13174
+ });
13175
+ } catch (error) {
13176
+ if (error.code !== "ENOENT") {
13177
+ logger.warn("Failed to load optimizer state", {
13178
+ error: error.message
13179
+ });
13180
+ }
13181
+ }
13182
+ }
13183
+ /**
13184
+ * Save state to disk
13185
+ */
13186
+ async saveState() {
13187
+ if (!this.config.statePath) return;
13188
+ try {
13189
+ await mkdir(dirname(this.config.statePath), { recursive: true });
13190
+ const tmpPath = `${this.config.statePath}.tmp`;
13191
+ await writeFile(tmpPath, JSON.stringify(this.state, null, 2));
13192
+ const { rename: rename2 } = await import('fs/promises');
13193
+ await rename2(tmpPath, this.config.statePath);
13194
+ logger.debug("DynamicOptimizer state saved");
13195
+ } catch (error) {
13196
+ logger.warn("Failed to save optimizer state", {
13197
+ error: error.message
13198
+ });
13199
+ }
13200
+ }
13201
+ /**
13202
+ * Stop the optimizer and clean up
13203
+ */
13204
+ destroy() {
13205
+ if (this.optimizationInterval) {
13206
+ clearInterval(this.optimizationInterval);
13207
+ this.optimizationInterval = void 0;
13208
+ }
13209
+ this.removeAllListeners();
13210
+ this.isInitialized = false;
13211
+ logger.debug("DynamicOptimizer destroyed");
13212
+ }
13213
+ };
13214
+ var globalOptimizer = null;
13215
+ function getDynamicOptimizer(config) {
13216
+ if (!globalOptimizer) {
13217
+ globalOptimizer = new DynamicOptimizer(config);
13218
+ }
13219
+ return globalOptimizer;
13220
+ }
13221
+
12450
13222
  // src/core/router/trace-logger.ts
12451
13223
  init_esm_shims();
12452
13224
  init_logger();
@@ -12843,6 +13615,12 @@ var Router = class {
12843
13615
  tracer;
12844
13616
  // v9.0.2: Extracted circuit breaker (better modularity and testability)
12845
13617
  circuitBreaker;
13618
+ // v13.1.0: Agent-provider affinity routing
13619
+ affinityManager;
13620
+ // v13.2.0: Ability-based provider routing
13621
+ abilityManager;
13622
+ // v14.0.0: Dynamic performance optimization
13623
+ dynamicOptimizer;
12846
13624
  constructor(config) {
12847
13625
  this.routerConfig = config;
12848
13626
  this.providers = [...config.providers].sort((a, b) => {
@@ -12874,6 +13652,37 @@ var Router = class {
12874
13652
  weights: strategyManager.getWeights()
12875
13653
  });
12876
13654
  }
13655
+ this.affinityManager = new RouterAffinityManager(config.agentAffinities);
13656
+ if (this.affinityManager.isEnabled()) {
13657
+ const stats = this.affinityManager.getStats();
13658
+ logger.info("Agent affinity routing enabled", {
13659
+ agentCount: stats.agentCount,
13660
+ strategy: stats.strategy,
13661
+ lastConfiguredAt: stats.lastConfiguredAt
13662
+ });
13663
+ }
13664
+ this.abilityManager = new RouterAbilityManager(config.agentAffinities);
13665
+ if (this.abilityManager.isEnabled()) {
13666
+ const stats = this.abilityManager.getStats();
13667
+ logger.info("Ability routing enabled", {
13668
+ abilityCount: stats.abilityCount,
13669
+ configuredAbilities: stats.configuredAbilities
13670
+ });
13671
+ }
13672
+ if (config.enableDynamicOptimization) {
13673
+ this.dynamicOptimizer = getDynamicOptimizer(config.dynamicOptimizer);
13674
+ void this.dynamicOptimizer.initialize().then(() => {
13675
+ const stats = this.dynamicOptimizer?.getStats();
13676
+ logger.info("Dynamic optimization enabled", {
13677
+ isInitialized: stats?.isInitialized,
13678
+ providersTracked: stats?.providersTracked
13679
+ });
13680
+ }).catch((err) => {
13681
+ logger.warn("Failed to initialize DynamicOptimizer", {
13682
+ error: err instanceof Error ? err.message : String(err)
13683
+ });
13684
+ });
13685
+ }
12877
13686
  if (config.workspacePath && config.enableTracing !== false) {
12878
13687
  this.tracer = createTraceLogger(config.workspacePath, config.enableTracing ?? true);
12879
13688
  logger.debug("Router trace logging enabled", {
@@ -12936,8 +13745,12 @@ var Router = class {
12936
13745
  }
12937
13746
  /**
12938
13747
  * Execute request with automatic provider fallback
13748
+ * v13.1.0: Added options parameter for agent-specific affinity routing
13749
+ *
13750
+ * @param request - Execution request
13751
+ * @param options - Optional execution options including agentName for affinity routing
12939
13752
  */
12940
- async execute(request) {
13753
+ async execute(request, options) {
12941
13754
  const timer = new PerformanceTimer(
12942
13755
  "Router" /* ROUTER */,
12943
13756
  "execute",
@@ -12997,6 +13810,42 @@ var Router = class {
12997
13810
  throw ProviderError.noAvailableProviders();
12998
13811
  }
12999
13812
  let providersToTry = availableProviders;
13813
+ const agentName = options?.agentName;
13814
+ if (agentName && this.affinityManager.isEnabled()) {
13815
+ const providerNames = availableProviders.map((p) => p.name);
13816
+ const reorderedNames = this.affinityManager.reorderByAffinity({
13817
+ agentName,
13818
+ availableProviders: providerNames
13819
+ });
13820
+ providersToTry = reorderedNames.map((name) => availableProviders.find((p) => p.name === name)).filter((p) => p !== void 0);
13821
+ const firstProvider = providersToTry[0];
13822
+ if (providersToTry.length > 0 && firstProvider) {
13823
+ logger.debug("Provider order adjusted by agent affinity", {
13824
+ agentName,
13825
+ originalOrder: providerNames,
13826
+ affinityOrder: reorderedNames,
13827
+ selected: firstProvider.name
13828
+ });
13829
+ }
13830
+ }
13831
+ const abilityType = options?.abilityType;
13832
+ if (abilityType && this.abilityManager.isEnabled()) {
13833
+ const providerNames = providersToTry.map((p) => p.name);
13834
+ const reorderedNames = this.abilityManager.reorderByAbility({
13835
+ abilityType,
13836
+ availableProviders: providerNames
13837
+ });
13838
+ providersToTry = reorderedNames.map((name) => availableProviders.find((p) => p.name === name)).filter((p) => p !== void 0);
13839
+ const firstProvider = providersToTry[0];
13840
+ if (providersToTry.length > 0 && firstProvider) {
13841
+ logger.debug("Provider order adjusted by ability routing", {
13842
+ abilityType,
13843
+ originalOrder: providerNames,
13844
+ abilityOrder: reorderedNames,
13845
+ selected: firstProvider.name
13846
+ });
13847
+ }
13848
+ }
13000
13849
  if (this.useMultiFactorRouting) {
13001
13850
  const strategyManager = getRoutingStrategyManager();
13002
13851
  const providerNames = providersToTry.map((p) => p.name);
@@ -13031,6 +13880,34 @@ var Router = class {
13031
13880
  });
13032
13881
  }
13033
13882
  }
13883
+ if (this.dynamicOptimizer) {
13884
+ try {
13885
+ const providerNames = providersToTry.map((p) => p.name);
13886
+ const healthStatus = /* @__PURE__ */ new Map();
13887
+ for (const provider of providersToTry) {
13888
+ const isHealthy = !this.circuitBreaker.isOpen(provider.name);
13889
+ healthStatus.set(provider.name, isHealthy);
13890
+ }
13891
+ const optimizedOrder = await this.dynamicOptimizer.reorderByPerformance(
13892
+ providerNames,
13893
+ healthStatus
13894
+ );
13895
+ const originalOrder = providersToTry.map((p) => p.name);
13896
+ providersToTry = optimizedOrder.map((name) => availableProviders.find((p) => p.name === name)).filter((p) => p !== void 0);
13897
+ const firstProvider = providersToTry[0];
13898
+ if (providersToTry.length > 0 && firstProvider && originalOrder[0] !== optimizedOrder[0]) {
13899
+ logger.debug("Provider order adjusted by dynamic optimization", {
13900
+ originalOrder,
13901
+ optimizedOrder,
13902
+ selected: firstProvider.name
13903
+ });
13904
+ }
13905
+ } catch (optimizerError) {
13906
+ logger.warn("Dynamic optimization failed, continuing with current order", {
13907
+ error: optimizerError.message
13908
+ });
13909
+ }
13910
+ }
13034
13911
  let lastError;
13035
13912
  let attemptNumber = 0;
13036
13913
  for (const provider of providersToTry) {
@@ -13485,12 +14362,32 @@ Run 'ax doctor' to diagnose provider setup.` : "";
13485
14362
  }
13486
14363
  /**
13487
14364
  * Select best provider based on health and availability
14365
+ * v13.1.0: Added optional agentName for affinity-based selection
14366
+ *
14367
+ * @param agentName - Optional agent name for affinity-based selection
13488
14368
  */
13489
- async selectProvider() {
14369
+ async selectProvider(agentName) {
13490
14370
  const availableProviders = await this.getAvailableProviders();
13491
14371
  if (availableProviders.length === 0) {
13492
14372
  return null;
13493
14373
  }
14374
+ if (agentName && this.affinityManager.isEnabled()) {
14375
+ const providerNames = availableProviders.map((p) => p.name);
14376
+ const reorderedNames = this.affinityManager.reorderByAffinity({
14377
+ agentName,
14378
+ availableProviders: providerNames
14379
+ });
14380
+ for (const name of reorderedNames) {
14381
+ const provider = availableProviders.find((p) => p.name === name);
14382
+ if (provider) {
14383
+ logger.debug("Provider selected by agent affinity", {
14384
+ agentName,
14385
+ selected: provider.name
14386
+ });
14387
+ return provider;
14388
+ }
14389
+ }
14390
+ }
13494
14391
  return availableProviders[0] ?? null;
13495
14392
  }
13496
14393
  /**
@@ -13598,6 +14495,10 @@ Run 'ax doctor' to diagnose provider setup.` : "";
13598
14495
  this.tracer.close();
13599
14496
  logger.debug("Router trace logger closed");
13600
14497
  }
14498
+ if (this.dynamicOptimizer) {
14499
+ this.dynamicOptimizer.destroy();
14500
+ logger.debug("Dynamic optimizer destroyed");
14501
+ }
13601
14502
  }
13602
14503
  /**
13603
14504
  * Get circuit breaker stats for observability
@@ -13643,6 +14544,183 @@ Run 'ax doctor' to diagnose provider setup.` : "";
13643
14544
  }
13644
14545
  return Math.min(...limitedProviders.map((p) => p.resetAtMs));
13645
14546
  }
14547
+ /**
14548
+ * Get agent affinity routing stats
14549
+ * v13.1.0: New API for affinity routing observability
14550
+ */
14551
+ getAffinityStats() {
14552
+ return this.affinityManager.getStats();
14553
+ }
14554
+ /**
14555
+ * Update agent affinity configuration at runtime
14556
+ * v13.1.0: Allows updating affinities without recreating the router
14557
+ *
14558
+ * @param routingConfig - New routing configuration
14559
+ */
14560
+ updateAffinityConfig(routingConfig) {
14561
+ this.affinityManager.updateConfig(routingConfig);
14562
+ logger.info("Router affinity config updated", {
14563
+ agentCount: Object.keys(routingConfig.agentAffinities ?? {}).length,
14564
+ strategy: routingConfig.strategy
14565
+ });
14566
+ }
14567
+ /**
14568
+ * Get agent affinity for a specific agent
14569
+ * v13.1.0: For debugging and inspection
14570
+ *
14571
+ * @param agentName - Name of the agent
14572
+ */
14573
+ getAgentAffinity(agentName) {
14574
+ return this.affinityManager.getAgentAffinity(agentName);
14575
+ }
14576
+ /**
14577
+ * Get ability routing stats
14578
+ * v13.2.0: New API for ability routing observability
14579
+ */
14580
+ getAbilityStats() {
14581
+ return this.abilityManager.getStats();
14582
+ }
14583
+ /**
14584
+ * Update ability routing configuration at runtime
14585
+ * v13.2.0: Allows updating ability routing without recreating the router
14586
+ *
14587
+ * @param routingConfig - New routing configuration
14588
+ */
14589
+ updateAbilityConfig(routingConfig) {
14590
+ this.abilityManager.updateConfig(routingConfig);
14591
+ logger.info("Router ability config updated", {
14592
+ abilityCount: Object.keys(routingConfig.abilityRouting ?? {}).length,
14593
+ strategy: routingConfig.strategy
14594
+ });
14595
+ }
14596
+ /**
14597
+ * Get ability routing for a specific ability type
14598
+ * v13.2.0: For debugging and inspection
14599
+ *
14600
+ * @param abilityType - Type of ability (e.g., 'code-generation')
14601
+ */
14602
+ getAbilityRouting(abilityType) {
14603
+ return this.abilityManager.getAbilityRouting(abilityType);
14604
+ }
14605
+ /**
14606
+ * Get all known ability types
14607
+ * v13.2.0: For discovery and validation
14608
+ */
14609
+ getKnownAbilities() {
14610
+ return this.abilityManager.getKnownAbilities();
14611
+ }
14612
+ /**
14613
+ * Check if an ability type is valid/known
14614
+ * v13.2.0: For validation
14615
+ */
14616
+ isKnownAbility(abilityType) {
14617
+ return this.abilityManager.isKnownAbility(abilityType);
14618
+ }
14619
+ // ============================================================================
14620
+ // v14.0.0: Dynamic Optimization API
14621
+ // ============================================================================
14622
+ /**
14623
+ * Check if dynamic optimization is enabled
14624
+ * v14.0.0: For feature detection
14625
+ */
14626
+ isDynamicOptimizationEnabled() {
14627
+ return this.dynamicOptimizer !== void 0;
14628
+ }
14629
+ /**
14630
+ * Get dynamic optimizer stats
14631
+ * v14.0.0: For monitoring and debugging
14632
+ */
14633
+ getDynamicOptimizerStats() {
14634
+ if (!this.dynamicOptimizer) {
14635
+ return null;
14636
+ }
14637
+ const stats = this.dynamicOptimizer.getStats();
14638
+ const costSummary = this.dynamicOptimizer.getCostSummary();
14639
+ return {
14640
+ enabled: true,
14641
+ isInitialized: stats.isInitialized,
14642
+ lastOptimizationAt: stats.lastOptimizationAt > 0 ? new Date(stats.lastOptimizationAt).toISOString() : null,
14643
+ providersTracked: stats.providersTracked,
14644
+ totalCostUsd: costSummary.totalCostUsd,
14645
+ adaptivePriorities: this.dynamicOptimizer.getAdaptivePriorities()
14646
+ };
14647
+ }
14648
+ /**
14649
+ * Get optimization recommendations for providers
14650
+ * v14.0.0: Returns suggestions for improving provider performance
14651
+ *
14652
+ * @param providers - Optional list of providers to get recommendations for
14653
+ */
14654
+ async getOptimizationRecommendations(providers) {
14655
+ if (!this.dynamicOptimizer) {
14656
+ return [];
14657
+ }
14658
+ const targetProviders = providers ?? this.providers.map((p) => p.name);
14659
+ return this.dynamicOptimizer.generateRecommendations(targetProviders);
14660
+ }
14661
+ /**
14662
+ * Get performance snapshot for a specific provider
14663
+ * v14.0.0: For detailed provider performance analysis
14664
+ *
14665
+ * @param providerName - Name of the provider
14666
+ */
14667
+ async getProviderPerformanceSnapshot(providerName) {
14668
+ if (!this.dynamicOptimizer) {
14669
+ return null;
14670
+ }
14671
+ return this.dynamicOptimizer.getPerformanceSnapshot(providerName);
14672
+ }
14673
+ /**
14674
+ * Track cost for a provider
14675
+ * v14.0.0: For cost optimization tracking
14676
+ *
14677
+ * @param providerName - Name of the provider
14678
+ * @param costUsd - Cost in USD
14679
+ */
14680
+ trackProviderCost(providerName, costUsd) {
14681
+ if (this.dynamicOptimizer) {
14682
+ this.dynamicOptimizer.trackCost(providerName, costUsd);
14683
+ }
14684
+ }
14685
+ /**
14686
+ * Get cost summary across all providers
14687
+ * v14.0.0: For cost monitoring
14688
+ */
14689
+ getCostSummary() {
14690
+ if (!this.dynamicOptimizer) {
14691
+ return null;
14692
+ }
14693
+ return this.dynamicOptimizer.getCostSummary();
14694
+ }
14695
+ /**
14696
+ * Apply an optimization recommendation
14697
+ * v14.0.0: Manually apply a generated recommendation
14698
+ *
14699
+ * @param recommendation - The recommendation to apply
14700
+ */
14701
+ async applyOptimizationRecommendation(recommendation) {
14702
+ if (!this.dynamicOptimizer) {
14703
+ return false;
14704
+ }
14705
+ return this.dynamicOptimizer.applyRecommendation(recommendation);
14706
+ }
14707
+ /**
14708
+ * Update dynamic optimizer configuration at runtime
14709
+ * v14.0.0: Allows updating optimizer settings without recreating the router
14710
+ *
14711
+ * @param config - Partial configuration to update
14712
+ */
14713
+ updateDynamicOptimizerConfig(config) {
14714
+ if (!this.dynamicOptimizer) {
14715
+ logger.warn("Cannot update optimizer config - dynamic optimization not enabled");
14716
+ return;
14717
+ }
14718
+ logger.info("Dynamic optimizer config update requested", {
14719
+ enableAdaptivePriorities: config.enableAdaptivePriorities,
14720
+ enableCostOptimization: config.enableCostOptimization,
14721
+ enableHealthReordering: config.enableHealthReordering
14722
+ });
14723
+ }
13646
14724
  };
13647
14725
 
13648
14726
  // src/core/memory/lazy-manager.ts
@@ -14891,9 +15969,9 @@ var MemoryManager = class _MemoryManager {
14891
15969
  throw new MemoryError("Memory manager not initialized", "DATABASE_ERROR");
14892
15970
  }
14893
15971
  try {
14894
- const { mkdir: mkdir5 } = await import('fs/promises');
15972
+ const { mkdir: mkdir6 } = await import('fs/promises');
14895
15973
  const destDir = dirname3(destPath);
14896
- await mkdir5(destDir, { recursive: true });
15974
+ await mkdir6(destDir, { recursive: true });
14897
15975
  await this.db.backup(destPath);
14898
15976
  logger.info("Database backup created", { destPath: normalizePath(destPath) });
14899
15977
  } catch (error) {
@@ -15049,9 +16127,9 @@ var MemoryManager = class _MemoryManager {
15049
16127
  },
15050
16128
  entries
15051
16129
  };
15052
- const { writeFile: writeFile7 } = await import('fs/promises');
16130
+ const { writeFile: writeFile8 } = await import('fs/promises');
15053
16131
  const json = pretty ? JSON.stringify(exportData, null, 2) : JSON.stringify(exportData);
15054
- await writeFile7(filePath, json, "utf-8");
16132
+ await writeFile8(filePath, json, "utf-8");
15055
16133
  const sizeBytes = Buffer.byteLength(json, "utf-8");
15056
16134
  logger.info("Memory exported to JSON", {
15057
16135
  filePath: normalizePath(filePath),
@@ -15100,8 +16178,8 @@ var MemoryManager = class _MemoryManager {
15100
16178
  { filePath }
15101
16179
  );
15102
16180
  }
15103
- const { readFile: readFile13 } = await import('fs/promises');
15104
- const content = await readFile13(filePath, "utf-8");
16181
+ const { readFile: readFile14 } = await import('fs/promises');
16182
+ const content = await readFile14(filePath, "utf-8");
15105
16183
  const importData = JSON.parse(content);
15106
16184
  const SUPPORTED_VERSIONS = ["1.0", "4.0.0", "4.11.0"];
15107
16185
  if (!importData.version || !SUPPORTED_VERSIONS.includes(importData.version)) {
@@ -16750,7 +17828,8 @@ init_esm_shims();
16750
17828
  init_esm_shims();
16751
17829
  init_logger();
16752
17830
  var MAX_CONTEXT_SIZE = 100 * 1024;
16753
- var DEFAULT_CACHE_TTL = 3e5;
17831
+ var MAX_SUMMARY_SIZE = 10 * 1024;
17832
+ var DEFAULT_CACHE_TTL = 6e4;
16754
17833
  var STALE_THRESHOLD_MS = 24 * 60 * 60 * 1e3;
16755
17834
  var ProjectContextLoader = class {
16756
17835
  constructor(projectRoot, options) {
@@ -16762,6 +17841,11 @@ var ProjectContextLoader = class {
16762
17841
  cacheTTL;
16763
17842
  /**
16764
17843
  * Load project context with caching
17844
+ *
17845
+ * Token-saving strategy:
17846
+ * 1. First try ax.summary.json (fast path, ~500 tokens)
17847
+ * 2. Fall back to generating summary from ax.index.json if missing
17848
+ * 3. Context prompt tells AI to read ax.index.json for full details
16765
17849
  */
16766
17850
  async load() {
16767
17851
  if (this.cache && this.cacheExpiry && Date.now() < this.cacheExpiry) {
@@ -16774,37 +17858,98 @@ var ProjectContextLoader = class {
16774
17858
  projectRoot: this.projectRoot
16775
17859
  });
16776
17860
  const context = {};
17861
+ const summaryLoaded = await this.loadSummary(context);
17862
+ await this.loadIndex(context);
17863
+ if (!summaryLoaded && context.index) {
17864
+ context.summary = this.generateDynamicSummary(context.index);
17865
+ logger.info("Generated dynamic summary from ax.index.json");
17866
+ }
17867
+ await this.loadCustomInstructions(context);
17868
+ context.contextPrompt = this.buildContextPrompt(context);
17869
+ this.cache = context;
17870
+ this.cacheExpiry = Date.now() + this.cacheTTL;
17871
+ logger.info("Project context loaded", {
17872
+ hasSummary: !!context.summary,
17873
+ hasIndex: !!context.index,
17874
+ hasCustomInstructions: !!context.customInstructions,
17875
+ guardrails: context.guardrails?.length ?? 0,
17876
+ guardrailConflicts: context.guardrailConflicts?.length ?? 0,
17877
+ commands: Object.keys(context.commands ?? {}).length,
17878
+ isStale: context.isStale
17879
+ });
17880
+ return context;
17881
+ }
17882
+ /**
17883
+ * Load ax.summary.json (fast path)
17884
+ * Returns true if summary was loaded successfully
17885
+ */
17886
+ async loadSummary(context) {
17887
+ try {
17888
+ const summaryPath = path4__default.join(this.projectRoot, "ax.summary.json");
17889
+ const resolvedPath = await realpath(summaryPath).catch(() => null);
17890
+ if (!resolvedPath) return false;
17891
+ const rel = path4__default.relative(this.projectRoot, resolvedPath);
17892
+ if (rel.startsWith("..") || path4__default.isAbsolute(rel)) return false;
17893
+ const stats = await stat(resolvedPath);
17894
+ if (stats.size > MAX_SUMMARY_SIZE) {
17895
+ logger.warn("ax.summary.json too large, ignoring", {
17896
+ size: stats.size,
17897
+ limit: MAX_SUMMARY_SIZE
17898
+ });
17899
+ return false;
17900
+ }
17901
+ const summaryContent = await readFile(resolvedPath, "utf-8");
17902
+ context.summary = JSON.parse(summaryContent);
17903
+ if (context.summary.commands) {
17904
+ context.commands = { ...context.summary.commands };
17905
+ }
17906
+ logger.info("Loaded ax.summary.json (fast path)", {
17907
+ projectName: context.summary.project.name,
17908
+ projectType: context.summary.project.type,
17909
+ techStack: context.summary.techStack.length
17910
+ });
17911
+ return true;
17912
+ } catch (error) {
17913
+ if (error && typeof error === "object" && "code" in error && error.code !== "ENOENT") {
17914
+ logger.warn("Error loading ax.summary.json", { error });
17915
+ }
17916
+ return false;
17917
+ }
17918
+ }
17919
+ /**
17920
+ * Load ax.index.json (full analysis)
17921
+ * Used for staleness check and fallback summary generation
17922
+ */
17923
+ async loadIndex(context) {
16777
17924
  try {
16778
17925
  const indexPath = path4__default.join(this.projectRoot, "ax.index.json");
16779
17926
  const resolvedPath = await realpath(indexPath).catch(() => null);
16780
- if (resolvedPath) {
16781
- const rel = path4__default.relative(this.projectRoot, resolvedPath);
16782
- if (!rel.startsWith("..") && !path4__default.isAbsolute(rel)) {
16783
- const stats = await stat(resolvedPath);
16784
- if (stats.size > MAX_CONTEXT_SIZE) {
16785
- logger.warn("ax.index.json too large, ignoring", {
16786
- size: stats.size,
16787
- limit: MAX_CONTEXT_SIZE
16788
- });
16789
- } else {
16790
- const indexContent = await readFile(resolvedPath, "utf-8");
16791
- context.index = JSON.parse(indexContent);
16792
- context.lastUpdated = stats.mtime;
16793
- const age = Date.now() - stats.mtime.getTime();
16794
- context.isStale = age > STALE_THRESHOLD_MS;
16795
- logger.info("Loaded ax.index.json", {
16796
- projectName: context.index.projectName,
16797
- projectType: context.index.projectType,
16798
- isStale: context.isStale,
16799
- ageHours: Math.floor(age / (1e3 * 60 * 60))
16800
- });
16801
- if (context.index.commands) {
16802
- context.commands = {};
16803
- for (const [name, cmd] of Object.entries(context.index.commands)) {
16804
- context.commands[name] = cmd.script;
16805
- }
16806
- }
16807
- }
17927
+ if (!resolvedPath) return;
17928
+ const rel = path4__default.relative(this.projectRoot, resolvedPath);
17929
+ if (rel.startsWith("..") || path4__default.isAbsolute(rel)) return;
17930
+ const stats = await stat(resolvedPath);
17931
+ if (stats.size > MAX_CONTEXT_SIZE) {
17932
+ logger.warn("ax.index.json too large, ignoring", {
17933
+ size: stats.size,
17934
+ limit: MAX_CONTEXT_SIZE
17935
+ });
17936
+ return;
17937
+ }
17938
+ const indexContent = await readFile(resolvedPath, "utf-8");
17939
+ context.index = JSON.parse(indexContent);
17940
+ context.lastUpdated = stats.mtime;
17941
+ const age = Date.now() - stats.mtime.getTime();
17942
+ context.isStale = age > STALE_THRESHOLD_MS;
17943
+ logger.debug("Loaded ax.index.json", {
17944
+ projectName: context.index.projectName,
17945
+ projectType: context.index.projectType,
17946
+ isStale: context.isStale,
17947
+ ageHours: Math.floor(age / (1e3 * 60 * 60))
17948
+ });
17949
+ if (!context.commands && context.index.commands) {
17950
+ context.commands = {};
17951
+ for (const [name, cmd] of Object.entries(context.index.commands)) {
17952
+ context.commands[name] = cmd.script;
16808
17953
  }
16809
17954
  }
16810
17955
  } catch (error) {
@@ -16812,26 +17957,38 @@ var ProjectContextLoader = class {
16812
17957
  logger.warn("Error loading ax.index.json", { error });
16813
17958
  }
16814
17959
  }
17960
+ }
17961
+ /**
17962
+ * Load .automatosx/CUSTOM.md
17963
+ */
17964
+ async loadCustomInstructions(context) {
16815
17965
  try {
16816
17966
  const customMdPath = path4__default.join(this.projectRoot, ".automatosx", "CUSTOM.md");
16817
17967
  const resolvedPath = await realpath(customMdPath).catch(() => null);
16818
- if (resolvedPath) {
16819
- const rel = path4__default.relative(this.projectRoot, resolvedPath);
16820
- if (!rel.startsWith("..") && !path4__default.isAbsolute(rel)) {
16821
- const stats = await stat(resolvedPath);
16822
- if (stats.size > MAX_CONTEXT_SIZE) {
16823
- logger.warn("CUSTOM.md too large, ignoring", {
16824
- size: stats.size,
16825
- limit: MAX_CONTEXT_SIZE
16826
- });
16827
- } else {
16828
- context.customInstructions = await readFile(resolvedPath, "utf-8");
16829
- logger.info("Loaded CUSTOM.md", {
16830
- size: stats.size,
16831
- lines: context.customInstructions.split("\n").length
16832
- });
16833
- context.guardrails = this.parseGuardrails(context.customInstructions);
16834
- }
17968
+ if (!resolvedPath) return;
17969
+ const rel = path4__default.relative(this.projectRoot, resolvedPath);
17970
+ if (rel.startsWith("..") || path4__default.isAbsolute(rel)) return;
17971
+ const stats = await stat(resolvedPath);
17972
+ if (stats.size > MAX_CONTEXT_SIZE) {
17973
+ logger.warn("CUSTOM.md too large, ignoring", {
17974
+ size: stats.size,
17975
+ limit: MAX_CONTEXT_SIZE
17976
+ });
17977
+ return;
17978
+ }
17979
+ context.customInstructions = await readFile(resolvedPath, "utf-8");
17980
+ logger.info("Loaded CUSTOM.md", {
17981
+ size: stats.size,
17982
+ lines: context.customInstructions.split("\n").length
17983
+ });
17984
+ context.guardrails = this.parseGuardrails(context.customInstructions);
17985
+ if (context.guardrails && context.guardrails.length > 1) {
17986
+ context.guardrailConflicts = this.detectGuardrailConflicts(context.guardrails);
17987
+ if (context.guardrailConflicts.length > 0) {
17988
+ logger.warn("Guardrail conflicts detected", {
17989
+ count: context.guardrailConflicts.length,
17990
+ conflicts: context.guardrailConflicts.map((c) => c.description)
17991
+ });
16835
17992
  }
16836
17993
  }
16837
17994
  } catch (error) {
@@ -16839,17 +17996,52 @@ var ProjectContextLoader = class {
16839
17996
  logger.warn("Error loading CUSTOM.md", { error });
16840
17997
  }
16841
17998
  }
16842
- context.contextPrompt = this.buildContextPrompt(context);
16843
- this.cache = context;
16844
- this.cacheExpiry = Date.now() + this.cacheTTL;
16845
- logger.info("Project context loaded", {
16846
- hasIndex: !!context.index,
16847
- hasCustomInstructions: !!context.customInstructions,
16848
- guardrails: context.guardrails?.length ?? 0,
16849
- commands: Object.keys(context.commands ?? {}).length,
16850
- isStale: context.isStale
16851
- });
16852
- return context;
17999
+ }
18000
+ /**
18001
+ * Generate a dynamic summary from ax.index.json
18002
+ * Used when ax.summary.json is missing
18003
+ */
18004
+ generateDynamicSummary(index) {
18005
+ const techStack = [];
18006
+ if (index.language) techStack.push(index.language);
18007
+ if (index.framework) techStack.push(index.framework);
18008
+ if (index.buildTool) techStack.push(index.buildTool);
18009
+ if (index.testFramework) techStack.push(index.testFramework);
18010
+ if (index.packageManager) techStack.push(index.packageManager);
18011
+ const directories = {};
18012
+ for (const mod of index.modules.slice(0, 5)) {
18013
+ directories[mod.path] = mod.purpose;
18014
+ }
18015
+ const commands = {};
18016
+ const cmdEntries = Object.entries(index.commands).slice(0, 5);
18017
+ for (const [name, cmd] of cmdEntries) {
18018
+ commands[name] = cmd.script;
18019
+ }
18020
+ const gotchas = [];
18021
+ if (index.hasTypeScript) {
18022
+ gotchas.push("TypeScript strict mode is enabled");
18023
+ }
18024
+ if (index.isMonorepo) {
18025
+ gotchas.push("Monorepo structure - run commands from package directory");
18026
+ }
18027
+ if (index.packageManager === "pnpm") {
18028
+ gotchas.push("Uses pnpm - ensure dependencies are installed with pnpm");
18029
+ }
18030
+ return {
18031
+ schemaVersion: "1.0",
18032
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
18033
+ project: {
18034
+ name: index.projectName,
18035
+ type: index.projectType,
18036
+ language: index.language,
18037
+ version: index.version
18038
+ },
18039
+ directories,
18040
+ commands,
18041
+ techStack,
18042
+ gotchas: gotchas.slice(0, 5),
18043
+ indexFile: "ax.index.json"
18044
+ };
16853
18045
  }
16854
18046
  /**
16855
18047
  * Clear cache (useful for testing or file watching)
@@ -16931,15 +18123,163 @@ var ProjectContextLoader = class {
16931
18123
  }
16932
18124
  return guardrails;
16933
18125
  }
18126
+ /**
18127
+ * Detect semantic conflicts between guardrails
18128
+ *
18129
+ * Uses keyword-based heuristics to identify potential contradictions:
18130
+ * - "never X" vs "always X"
18131
+ * - "do X" vs "don't X"
18132
+ * - Conflicting action verbs on same subject
18133
+ *
18134
+ * @param guardrails - List of guardrail rules
18135
+ * @returns Detected conflicts
18136
+ *
18137
+ * @since v12.7.1
18138
+ */
18139
+ detectGuardrailConflicts(guardrails) {
18140
+ const conflicts = [];
18141
+ const contradictionPairs = [
18142
+ [/\balways\b/i, /\bnever\b/i, "always/never"],
18143
+ [/\bmust\b/i, /\bmust\s+not\b/i, "must/must not"],
18144
+ [/\bshould\b/i, /\bshould\s+not\b/i, "should/should not"],
18145
+ [/\bdo\b/i, /\bdon'?t\b/i, "do/don't"],
18146
+ [/\buse\b/i, /\bnever\s+use\b/i, "use/never use"],
18147
+ [/\brequire[ds]?\b/i, /\bforbidden\b/i, "required/forbidden"],
18148
+ [/\ballow(ed)?\b/i, /\bprohibit(ed)?\b/i, "allowed/prohibited"],
18149
+ [/\benable[ds]?\b/i, /\bdisable[ds]?\b/i, "enable/disable"]
18150
+ ];
18151
+ const extractSubject = (rule) => {
18152
+ const subjects = [];
18153
+ const patterns = [
18154
+ /(?:use|run|execute|call|import|require|include|add|create|edit|modify|delete|remove)\s+[`"']?(\w+(?:[._-]\w+)*)[`"']?/gi,
18155
+ /(?:never|always|must|should|do|don't)\s+(?:\w+\s+)?[`"']?(\w+(?:[._-]\w+)*)[`"']?/gi,
18156
+ /[`"'](\w+(?:[._-]\w+)*)[`"']/g
18157
+ // Backtick/quoted terms
18158
+ ];
18159
+ for (const pattern of patterns) {
18160
+ let match;
18161
+ while ((match = pattern.exec(rule)) !== null) {
18162
+ if (match[1] && match[1].length > 2) {
18163
+ subjects.push(match[1].toLowerCase());
18164
+ }
18165
+ }
18166
+ }
18167
+ return [...new Set(subjects)];
18168
+ };
18169
+ for (let i = 0; i < guardrails.length; i++) {
18170
+ for (let j = i + 1; j < guardrails.length; j++) {
18171
+ const rule1 = guardrails[i];
18172
+ const rule2 = guardrails[j];
18173
+ if (!rule1 || !rule2) continue;
18174
+ const subjects1 = extractSubject(rule1);
18175
+ const subjects2 = extractSubject(rule2);
18176
+ const sharedSubjects = subjects1.filter((s) => subjects2.includes(s));
18177
+ if (sharedSubjects.length === 0) {
18178
+ continue;
18179
+ }
18180
+ for (const [positivePattern, negativePattern, patternName] of contradictionPairs) {
18181
+ const rule1Positive = positivePattern.test(rule1);
18182
+ const rule1Negative = negativePattern.test(rule1);
18183
+ const rule2Positive = positivePattern.test(rule2);
18184
+ const rule2Negative = negativePattern.test(rule2);
18185
+ if (rule1Positive && rule2Negative || rule1Negative && rule2Positive) {
18186
+ conflicts.push({
18187
+ rule1,
18188
+ rule2,
18189
+ type: "contradiction",
18190
+ description: `Potential ${patternName} conflict on subject: ${sharedSubjects.join(", ")}`
18191
+ });
18192
+ break;
18193
+ }
18194
+ }
18195
+ if (sharedSubjects.length > 0 && conflicts.every((c) => c.rule1 !== rule1 || c.rule2 !== rule2)) {
18196
+ const actionVerbs = /\b(use|run|edit|create|delete|modify|add|remove|enable|disable|require|allow|block)\b/gi;
18197
+ const verbs1 = rule1.match(actionVerbs)?.map((v) => v.toLowerCase()) || [];
18198
+ const verbs2 = rule2.match(actionVerbs)?.map((v) => v.toLowerCase()) || [];
18199
+ const conflictingVerbs = verbs1.filter(
18200
+ (v) => verbs2.some((v2) => {
18201
+ const conflicts2 = {
18202
+ "create": ["delete", "remove"],
18203
+ "add": ["remove", "delete"],
18204
+ "enable": ["disable", "block"],
18205
+ "allow": ["block", "disable"],
18206
+ "use": ["remove", "delete"]
18207
+ };
18208
+ return conflicts2[v]?.includes(v2) || conflicts2[v2]?.includes(v);
18209
+ })
18210
+ );
18211
+ if (conflictingVerbs.length > 0) {
18212
+ conflicts.push({
18213
+ rule1,
18214
+ rule2,
18215
+ type: "ambiguity",
18216
+ description: `Ambiguous actions (${conflictingVerbs.join(", ")}) on subject: ${sharedSubjects.join(", ")}`
18217
+ });
18218
+ }
18219
+ }
18220
+ }
18221
+ }
18222
+ return conflicts;
18223
+ }
16934
18224
  /**
16935
18225
  * Build formatted context prompt for agent injection
18226
+ *
18227
+ * Uses ax.summary.json (compact ~500 tokens) for fast prompt injection.
18228
+ * Tells AI to read ax.index.json for full project analysis if needed.
18229
+ *
18230
+ * @since v12.10.0 - Now uses summary instead of full index
16936
18231
  */
16937
18232
  buildContextPrompt(context) {
16938
- if (!context.index && !context.customInstructions) {
18233
+ if (!context.summary && !context.index && !context.customInstructions) {
16939
18234
  return "";
16940
18235
  }
16941
18236
  let prompt = "\n# PROJECT CONTEXT\n\n";
16942
- if (context.index) {
18237
+ if (context.summary) {
18238
+ const summary = context.summary;
18239
+ prompt += `**Project:** ${summary.project.name}`;
18240
+ if (summary.project.version) {
18241
+ prompt += ` v${summary.project.version}`;
18242
+ }
18243
+ prompt += "\n";
18244
+ prompt += `**Type:** ${summary.project.type}
18245
+ `;
18246
+ prompt += `**Language:** ${summary.project.language}
18247
+ `;
18248
+ if (summary.techStack.length > 0) {
18249
+ prompt += `**Tech:** ${summary.techStack.join(", ")}
18250
+ `;
18251
+ }
18252
+ prompt += "\n";
18253
+ const dirEntries = Object.entries(summary.directories);
18254
+ if (dirEntries.length > 0) {
18255
+ prompt += "## Directories:\n";
18256
+ for (const [dir, purpose] of dirEntries) {
18257
+ prompt += `- \`${dir}/\` - ${purpose}
18258
+ `;
18259
+ }
18260
+ prompt += "\n";
18261
+ }
18262
+ const cmdEntries = Object.entries(summary.commands);
18263
+ if (cmdEntries.length > 0) {
18264
+ prompt += "## Commands:\n";
18265
+ for (const [name, script] of cmdEntries) {
18266
+ prompt += `- ${name}: \`${script}\`
18267
+ `;
18268
+ }
18269
+ prompt += "\n";
18270
+ }
18271
+ if (summary.gotchas.length > 0) {
18272
+ prompt += "## Gotchas:\n";
18273
+ for (const gotcha of summary.gotchas) {
18274
+ prompt += `- ${gotcha}
18275
+ `;
18276
+ }
18277
+ prompt += "\n";
18278
+ }
18279
+ prompt += `**For full project analysis, read:** \`${summary.indexFile}\`
18280
+
18281
+ `;
18282
+ } else if (context.index) {
16943
18283
  prompt += `**Project:** ${context.index.projectName} v${context.index.version}
16944
18284
  `;
16945
18285
  prompt += `**Type:** ${context.index.projectType}
@@ -16950,32 +18290,46 @@ var ProjectContextLoader = class {
16950
18290
  }
16951
18291
  prompt += "\n\n";
16952
18292
  if (context.index.modules.length > 0) {
16953
- prompt += "## Project Structure:\n\n";
16954
- for (const mod of context.index.modules.slice(0, 10)) {
18293
+ prompt += "## Directories:\n";
18294
+ for (const mod of context.index.modules.slice(0, 5)) {
16955
18295
  prompt += `- \`${mod.path}/\` - ${mod.purpose}
16956
18296
  `;
16957
18297
  }
16958
18298
  prompt += "\n";
16959
18299
  }
16960
18300
  if (context.commands && Object.keys(context.commands).length > 0) {
16961
- prompt += "## Available Commands:\n\n";
16962
- for (const [name, script] of Object.entries(context.commands).slice(0, 10)) {
18301
+ prompt += "## Commands:\n";
18302
+ for (const [name, script] of Object.entries(context.commands).slice(0, 5)) {
16963
18303
  prompt += `- ${name}: \`${script}\`
16964
18304
  `;
16965
18305
  }
16966
18306
  prompt += "\n";
16967
18307
  }
18308
+ prompt += "**For full project analysis, read:** `ax.index.json`\n\n";
16968
18309
  }
16969
18310
  if (context.guardrails && context.guardrails.length > 0) {
16970
- prompt += "## CRITICAL RULES (NEVER VIOLATE):\n\n";
18311
+ prompt += "## CRITICAL RULES (NEVER VIOLATE):\n";
16971
18312
  for (const rule of context.guardrails) {
16972
18313
  prompt += `- ${rule}
16973
18314
  `;
16974
18315
  }
16975
18316
  prompt += "\n";
18317
+ if (context.guardrailConflicts && context.guardrailConflicts.length > 0) {
18318
+ prompt += "### Guardrail Conflicts Detected:\n";
18319
+ prompt += "The following rules may conflict. Use your best judgment:\n";
18320
+ for (const conflict of context.guardrailConflicts) {
18321
+ prompt += `- **${conflict.type}**: ${conflict.description}
18322
+ `;
18323
+ prompt += ` - Rule 1: "${conflict.rule1}"
18324
+ `;
18325
+ prompt += ` - Rule 2: "${conflict.rule2}"
18326
+ `;
18327
+ }
18328
+ prompt += "\n";
18329
+ }
16976
18330
  }
16977
18331
  if (context.isStale) {
16978
- prompt += "\u26A0\uFE0F **Note:** Project index is stale (>24h old). Run `ax init` to update.\n\n";
18332
+ prompt += "**Note:** Project index is stale (>24h old). Run `ax init` to update.\n\n";
16979
18333
  }
16980
18334
  return prompt;
16981
18335
  }
@@ -17161,23 +18515,24 @@ var ContextManager = class {
17161
18515
  }
17162
18516
  const searchQuery = query || context.task;
17163
18517
  const MAX_QUERY_LENGTH = 1e3;
17164
- const truncatedQuery = searchQuery.length > MAX_QUERY_LENGTH ? searchQuery.substring(0, MAX_QUERY_LENGTH) : searchQuery;
17165
- if (truncatedQuery !== searchQuery) {
17166
- logger.debug("Memory search query truncated", {
18518
+ const optimizedQuery = this.extractSearchKeywords(searchQuery, MAX_QUERY_LENGTH);
18519
+ if (optimizedQuery !== searchQuery) {
18520
+ logger.debug("Memory search query optimized", {
17167
18521
  originalLength: searchQuery.length,
17168
- truncatedLength: truncatedQuery.length,
17169
- maxLength: MAX_QUERY_LENGTH
18522
+ optimizedLength: optimizedQuery.length,
18523
+ maxLength: MAX_QUERY_LENGTH,
18524
+ method: searchQuery.length > MAX_QUERY_LENGTH ? "keyword_extraction" : "passthrough"
17170
18525
  });
17171
18526
  }
17172
18527
  try {
17173
18528
  const results = await this.config.memoryManager.search({
17174
- text: truncatedQuery,
17175
- // Use truncated query
18529
+ text: optimizedQuery,
18530
+ // Use optimized query with keywords
17176
18531
  limit
17177
18532
  });
17178
18533
  context.memory = results.map((r) => r.entry);
17179
18534
  logger.debug("Memory injected", {
17180
- query: truncatedQuery.substring(0, 100) + (truncatedQuery.length > 100 ? "..." : ""),
18535
+ query: optimizedQuery.substring(0, 100) + (optimizedQuery.length > 100 ? "..." : ""),
17181
18536
  count: context.memory.length
17182
18537
  });
17183
18538
  } catch (error) {
@@ -17187,6 +18542,156 @@ var ContextManager = class {
17187
18542
  context.memory = [];
17188
18543
  }
17189
18544
  }
18545
+ /**
18546
+ * Extract meaningful keywords from a query for memory search
18547
+ *
18548
+ * Instead of truncating long queries (which loses context at the end),
18549
+ * this extracts the most meaningful terms for FTS5 search.
18550
+ *
18551
+ * @param query - The full query text
18552
+ * @param maxLength - Maximum output length
18553
+ * @returns Optimized search query with keywords
18554
+ *
18555
+ * @since v12.7.1
18556
+ */
18557
+ extractSearchKeywords(query, maxLength) {
18558
+ if (query.length <= maxLength) {
18559
+ return query;
18560
+ }
18561
+ const stopWords = /* @__PURE__ */ new Set([
18562
+ "the",
18563
+ "a",
18564
+ "an",
18565
+ "and",
18566
+ "or",
18567
+ "but",
18568
+ "in",
18569
+ "on",
18570
+ "at",
18571
+ "to",
18572
+ "for",
18573
+ "of",
18574
+ "with",
18575
+ "by",
18576
+ "from",
18577
+ "as",
18578
+ "is",
18579
+ "was",
18580
+ "are",
18581
+ "were",
18582
+ "been",
18583
+ "be",
18584
+ "have",
18585
+ "has",
18586
+ "had",
18587
+ "do",
18588
+ "does",
18589
+ "did",
18590
+ "will",
18591
+ "would",
18592
+ "could",
18593
+ "should",
18594
+ "may",
18595
+ "might",
18596
+ "must",
18597
+ "shall",
18598
+ "can",
18599
+ "need",
18600
+ "dare",
18601
+ "ought",
18602
+ "used",
18603
+ "this",
18604
+ "that",
18605
+ "these",
18606
+ "those",
18607
+ "i",
18608
+ "you",
18609
+ "he",
18610
+ "she",
18611
+ "it",
18612
+ "we",
18613
+ "they",
18614
+ "what",
18615
+ "which",
18616
+ "who",
18617
+ "whom",
18618
+ "when",
18619
+ "where",
18620
+ "why",
18621
+ "how",
18622
+ "all",
18623
+ "each",
18624
+ "every",
18625
+ "both",
18626
+ "few",
18627
+ "more",
18628
+ "most",
18629
+ "other",
18630
+ "some",
18631
+ "such",
18632
+ "no",
18633
+ "nor",
18634
+ "not",
18635
+ "only",
18636
+ "own",
18637
+ "same",
18638
+ "so",
18639
+ "than",
18640
+ "too",
18641
+ "very",
18642
+ "just",
18643
+ "also",
18644
+ "now",
18645
+ "here",
18646
+ "there",
18647
+ "then",
18648
+ "once",
18649
+ "please",
18650
+ "want",
18651
+ "need",
18652
+ "help",
18653
+ "me",
18654
+ "my",
18655
+ "your",
18656
+ "our",
18657
+ "their",
18658
+ "its"
18659
+ ]);
18660
+ const words = query.match(/[a-zA-Z0-9_-]+/g) || [];
18661
+ const wordScores = /* @__PURE__ */ new Map();
18662
+ for (const word of words) {
18663
+ const lower = word.toLowerCase();
18664
+ if (stopWords.has(lower) || word.length < 3) {
18665
+ continue;
18666
+ }
18667
+ let score = wordScores.get(lower) || 0;
18668
+ if (/[A-Z]/.test(word) && /[a-z]/.test(word)) score += 3;
18669
+ if (word.includes("_") || word.includes("-")) score += 2;
18670
+ if (/\d/.test(word)) score += 2;
18671
+ if (word.length > 8) score += 2;
18672
+ if (word.length > 5) score += 1;
18673
+ score += 1;
18674
+ wordScores.set(lower, score);
18675
+ }
18676
+ const sortedWords = Array.from(wordScores.entries()).sort((a, b) => b[1] - a[1]).map(([word]) => word);
18677
+ const result = [];
18678
+ let currentLength = 0;
18679
+ for (const word of sortedWords) {
18680
+ const wordWithSpace = result.length > 0 ? ` ${word}` : word;
18681
+ if (currentLength + wordWithSpace.length > maxLength) {
18682
+ break;
18683
+ }
18684
+ result.push(word);
18685
+ currentLength += wordWithSpace.length;
18686
+ }
18687
+ if (result.length < 5) {
18688
+ const prefix = query.substring(0, Math.min(200, maxLength - currentLength - 1));
18689
+ if (prefix && currentLength + prefix.length + 1 <= maxLength) {
18690
+ return `${prefix} ${result.join(" ")}`.trim();
18691
+ }
18692
+ }
18693
+ return result.join(" ");
18694
+ }
17190
18695
  /**
17191
18696
  * Select abilities based on task keywords (smart selection)
17192
18697
  */
@@ -22680,7 +24185,7 @@ ${context.task}`;
22680
24185
  * @param signal - Optional AbortSignal to cancel the sleep
22681
24186
  */
22682
24187
  sleep(ms, signal) {
22683
- return new Promise((resolve5, reject) => {
24188
+ return new Promise((resolve6, reject) => {
22684
24189
  if (signal?.aborted) {
22685
24190
  reject(new Error("Sleep cancelled"));
22686
24191
  return;
@@ -22690,7 +24195,7 @@ ${context.task}`;
22690
24195
  if (abortHandler && signal) {
22691
24196
  signal.removeEventListener("abort", abortHandler);
22692
24197
  }
22693
- resolve5();
24198
+ resolve6();
22694
24199
  }, ms);
22695
24200
  if (signal) {
22696
24201
  abortHandler = () => {
@@ -25125,6 +26630,968 @@ init_logger();
25125
26630
  // src/core/bugfix/bug-detector.ts
25126
26631
  init_esm_shims();
25127
26632
  init_logger();
26633
+
26634
+ // src/core/bugfix/ast-analyzer.ts
26635
+ init_esm_shims();
26636
+ var ts = null;
26637
+ async function loadTypeScript() {
26638
+ if (!ts) {
26639
+ ts = await import('typescript');
26640
+ }
26641
+ return ts;
26642
+ }
26643
+ function getTS() {
26644
+ if (!ts) {
26645
+ throw new Error("TypeScript not loaded. Call ASTAnalyzer.init() first.");
26646
+ }
26647
+ return ts;
26648
+ }
26649
+ var ASTAnalyzer = class {
26650
+ cache = /* @__PURE__ */ new Map();
26651
+ maxCacheSize;
26652
+ initialized = false;
26653
+ constructor(maxCacheSize = 100) {
26654
+ this.maxCacheSize = maxCacheSize;
26655
+ }
26656
+ /**
26657
+ * Initialize the analyzer by loading TypeScript.
26658
+ * Must be called before any parse or analysis methods.
26659
+ */
26660
+ async init() {
26661
+ if (!this.initialized) {
26662
+ await loadTypeScript();
26663
+ this.initialized = true;
26664
+ }
26665
+ }
26666
+ /**
26667
+ * Check if the analyzer is initialized
26668
+ */
26669
+ isInitialized() {
26670
+ return this.initialized;
26671
+ }
26672
+ /**
26673
+ * Parse a TypeScript/JavaScript file into an AST.
26674
+ * Note: init() must be called before this method.
26675
+ */
26676
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
26677
+ parseFile(content, filePath) {
26678
+ const hash = this.hashContent(content);
26679
+ const cacheKey = filePath;
26680
+ const cached = this.cache.get(cacheKey);
26681
+ if (cached && cached.hash === hash) {
26682
+ cached.accessedAt = Date.now();
26683
+ return cached.sourceFile;
26684
+ }
26685
+ const typescript = getTS();
26686
+ const sourceFile = typescript.createSourceFile(
26687
+ filePath,
26688
+ content,
26689
+ typescript.ScriptTarget.Latest,
26690
+ true,
26691
+ // setParentNodes - needed for traversal
26692
+ this.getScriptKind(filePath)
26693
+ );
26694
+ this.cache.set(cacheKey, {
26695
+ sourceFile,
26696
+ hash,
26697
+ accessedAt: Date.now()
26698
+ });
26699
+ this.evictOldest();
26700
+ return sourceFile;
26701
+ }
26702
+ /**
26703
+ * Find all class declarations in the file
26704
+ */
26705
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
26706
+ findClasses(sourceFile) {
26707
+ const typescript = getTS();
26708
+ const classes = [];
26709
+ const visit = (node) => {
26710
+ if (typescript.isClassDeclaration(node) && node.name) {
26711
+ classes.push(this.extractClassInfo(node, sourceFile));
26712
+ }
26713
+ typescript.forEachChild(node, visit);
26714
+ };
26715
+ visit(sourceFile);
26716
+ return classes;
26717
+ }
26718
+ /**
26719
+ * Find classes that extend a specific base class
26720
+ */
26721
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
26722
+ findClassesExtending(sourceFile, baseClassName) {
26723
+ const allClasses = this.findClasses(sourceFile);
26724
+ return allClasses.filter((cls) => {
26725
+ if (typeof baseClassName === "string") {
26726
+ return cls.extendsClause.includes(baseClassName);
26727
+ }
26728
+ return cls.extendsClause.some((name) => baseClassName.test(name));
26729
+ });
26730
+ }
26731
+ /**
26732
+ * Check if a class has a specific method
26733
+ */
26734
+ classHasMethod(classInfo, methodName) {
26735
+ return classInfo.methods.some((method) => {
26736
+ if (typeof methodName === "string") {
26737
+ return method.name === methodName;
26738
+ }
26739
+ return methodName.test(method.name);
26740
+ });
26741
+ }
26742
+ /**
26743
+ * Check if a class has destroy, dispose, or cleanup method
26744
+ */
26745
+ classHasDestroyMethod(classInfo) {
26746
+ const destroyMethodPattern = /^(destroy|dispose|cleanup|close|shutdown|teardown)$/i;
26747
+ return this.classHasMethod(classInfo, destroyMethodPattern);
26748
+ }
26749
+ /**
26750
+ * Find all function calls matching a pattern
26751
+ */
26752
+ findCalls(sourceFile, functionName) {
26753
+ const calls = [];
26754
+ const visit = (node) => {
26755
+ if (getTS().isCallExpression(node)) {
26756
+ const callName = this.getCallExpressionName(node);
26757
+ const matches = typeof functionName === "string" ? callName === functionName : functionName.test(callName);
26758
+ if (matches) {
26759
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
26760
+ const isReturnCaptured = this.isReturnValueCaptured(node);
26761
+ const hasChainedUnref = this.hasChainedUnrefCall(node);
26762
+ calls.push({
26763
+ name: callName,
26764
+ line: line + 1,
26765
+ column: character + 1,
26766
+ arguments: node.arguments.map((arg) => arg.getText(sourceFile)),
26767
+ isReturnCaptured,
26768
+ capturedVariable: isReturnCaptured ? this.getCapturedVariableName(node) : void 0,
26769
+ hasChainedUnref
26770
+ });
26771
+ }
26772
+ }
26773
+ getTS().forEachChild(node, visit);
26774
+ };
26775
+ visit(sourceFile);
26776
+ return calls;
26777
+ }
26778
+ /**
26779
+ * Find the enclosing function for a given line number
26780
+ */
26781
+ findEnclosingFunction(sourceFile, lineNumber) {
26782
+ let result = null;
26783
+ const position = sourceFile.getPositionOfLineAndCharacter(lineNumber - 1, 0);
26784
+ const visit = (node) => {
26785
+ sourceFile.getLineAndCharacterOfPosition(node.getStart());
26786
+ sourceFile.getLineAndCharacterOfPosition(node.getEnd());
26787
+ if (position >= node.getStart() && position <= node.getEnd()) {
26788
+ if (getTS().isFunctionDeclaration(node) || getTS().isFunctionExpression(node) || getTS().isArrowFunction(node) || getTS().isMethodDeclaration(node)) {
26789
+ const funcInfo = this.extractFunctionInfo(node, sourceFile);
26790
+ if (!result || funcInfo.startLine >= result.startLine) {
26791
+ result = funcInfo;
26792
+ }
26793
+ }
26794
+ getTS().forEachChild(node, visit);
26795
+ }
26796
+ };
26797
+ visit(sourceFile);
26798
+ return result;
26799
+ }
26800
+ /**
26801
+ * Track variable usage throughout the file
26802
+ */
26803
+ trackVariableUsage(sourceFile, variableName) {
26804
+ let declarationLine = -1;
26805
+ const usageLines = [];
26806
+ let usedInCleanup = false;
26807
+ let cleanupMethod;
26808
+ const cleanupMethods = ["clearInterval", "clearTimeout", "removeListener", "removeEventListener", "off", "destroy", "dispose", "close"];
26809
+ const visit = (node) => {
26810
+ if (getTS().isVariableDeclaration(node)) {
26811
+ if (node.name.getText(sourceFile) === variableName) {
26812
+ const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
26813
+ declarationLine = line + 1;
26814
+ }
26815
+ }
26816
+ if (getTS().isIdentifier(node) && node.text === variableName) {
26817
+ if (!getTS().isVariableDeclaration(node.parent) || node.parent.name !== node) {
26818
+ const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
26819
+ usageLines.push(line + 1);
26820
+ const parent = node.parent;
26821
+ if (getTS().isCallExpression(parent)) {
26822
+ const callName = this.getCallExpressionName(parent);
26823
+ if (cleanupMethods.includes(callName)) {
26824
+ usedInCleanup = true;
26825
+ cleanupMethod = callName;
26826
+ }
26827
+ } else if (getTS().isCallExpression(parent?.parent)) {
26828
+ const callName = this.getCallExpressionName(parent.parent);
26829
+ if (cleanupMethods.includes(callName)) {
26830
+ usedInCleanup = true;
26831
+ cleanupMethod = callName;
26832
+ }
26833
+ }
26834
+ }
26835
+ }
26836
+ getTS().forEachChild(node, visit);
26837
+ };
26838
+ visit(sourceFile);
26839
+ if (declarationLine === -1) {
26840
+ return null;
26841
+ }
26842
+ return {
26843
+ name: variableName,
26844
+ declarationLine,
26845
+ usageLines,
26846
+ usedInCleanup,
26847
+ cleanupMethod
26848
+ };
26849
+ }
26850
+ /**
26851
+ * Check if a method in a class calls clearInterval/clearTimeout on a variable
26852
+ */
26853
+ methodClearsTimer(sourceFile, classInfo, methodName, variableName) {
26854
+ const method = classInfo.methods.find((m) => m.name === methodName);
26855
+ if (!method) return false;
26856
+ const startPos = sourceFile.getPositionOfLineAndCharacter(method.startLine - 1, 0);
26857
+ const endPos = sourceFile.getPositionOfLineAndCharacter(method.endLine - 1, 0);
26858
+ const methodText = sourceFile.text.substring(startPos, endPos);
26859
+ const clearPatterns = [
26860
+ new RegExp(`clearInterval\\s*\\(\\s*(?:this\\.)?${variableName}\\s*\\)`),
26861
+ new RegExp(`clearTimeout\\s*\\(\\s*(?:this\\.)?${variableName}\\s*\\)`)
26862
+ ];
26863
+ return clearPatterns.some((pattern) => pattern.test(methodText));
26864
+ }
26865
+ /**
26866
+ * Check if a file is a test file
26867
+ */
26868
+ isTestFile(filePath) {
26869
+ const testPatterns = [
26870
+ /\.test\.[tj]sx?$/,
26871
+ /\.spec\.[tj]sx?$/,
26872
+ /\.bench\.[tj]sx?$/,
26873
+ /__tests__\//,
26874
+ /test\/fixtures\//,
26875
+ /tests?\//
26876
+ ];
26877
+ return testPatterns.some((pattern) => pattern.test(filePath));
26878
+ }
26879
+ /**
26880
+ * Analyze Promise+setTimeout patterns for potential leaks
26881
+ * v12.8.0: Phase 3 - Reduced false positives for simple sleep utilities
26882
+ */
26883
+ analyzePromiseTimeouts(sourceFile, content) {
26884
+ const results = [];
26885
+ for (const safePattern of ALLOWLISTS.promiseTimeout.safePatterns) {
26886
+ if (safePattern.test(content)) ;
26887
+ }
26888
+ const visit = (node) => {
26889
+ if (getTS().isNewExpression(node) && getTS().isIdentifier(node.expression) && node.expression.text === "Promise") {
26890
+ const promisePos = sourceFile.getLineAndCharacterOfPosition(node.getStart());
26891
+ const promiseLine = promisePos.line + 1;
26892
+ const findTimerCalls = (innerNode) => {
26893
+ if (getTS().isCallExpression(innerNode)) {
26894
+ const callName = this.getCallExpressionName(innerNode);
26895
+ if (callName === "setTimeout" || callName === "setInterval") {
26896
+ const timerPos = sourceFile.getLineAndCharacterOfPosition(innerNode.getStart());
26897
+ const timerLine = timerPos.line + 1;
26898
+ const timeoutIdCaptured = this.isReturnValueCaptured(innerNode);
26899
+ const hasCleanup = this.hasCleanupInPromise(node, sourceFile);
26900
+ const enclosingFunc = this.findEnclosingFunction(sourceFile, promiseLine);
26901
+ const { isAllowlisted, reason } = this.checkPromiseTimeoutAllowlist(
26902
+ enclosingFunc?.name || "",
26903
+ node,
26904
+ sourceFile
26905
+ );
26906
+ results.push({
26907
+ promiseLine,
26908
+ timeoutLine: timerLine,
26909
+ isAllowlisted,
26910
+ allowlistReason: reason,
26911
+ hasCleanup,
26912
+ enclosingFunction: enclosingFunc?.name,
26913
+ timeoutIdCaptured
26914
+ });
26915
+ }
26916
+ }
26917
+ getTS().forEachChild(innerNode, findTimerCalls);
26918
+ };
26919
+ getTS().forEachChild(node, findTimerCalls);
26920
+ }
26921
+ getTS().forEachChild(node, visit);
26922
+ };
26923
+ visit(sourceFile);
26924
+ return results;
26925
+ }
26926
+ /**
26927
+ * Check if a Promise has cleanup (finally/catch/then with clearTimeout, or clearTimeout in executor)
26928
+ */
26929
+ hasCleanupInPromise(promiseNode, sourceFile) {
26930
+ const promiseText = promiseNode.getText(sourceFile);
26931
+ if (/clearTimeout/i.test(promiseText)) {
26932
+ return true;
26933
+ }
26934
+ let current = promiseNode;
26935
+ while (current.parent) {
26936
+ const parent = current.parent;
26937
+ if (getTS().isCallExpression(parent) && getTS().isPropertyAccessExpression(parent.expression)) {
26938
+ const propName = parent.expression.name.text;
26939
+ if (propName === "finally" || propName === "catch" || propName === "then") {
26940
+ const callText = parent.getText(sourceFile);
26941
+ if (/clearTimeout/i.test(callText)) {
26942
+ return true;
26943
+ }
26944
+ }
26945
+ }
26946
+ if (getTS().isTryStatement(parent)) {
26947
+ if (parent.finallyBlock) {
26948
+ const finallyText = parent.finallyBlock.getText(sourceFile);
26949
+ if (/clearTimeout/i.test(finallyText)) {
26950
+ return true;
26951
+ }
26952
+ }
26953
+ }
26954
+ current = parent;
26955
+ }
26956
+ return false;
26957
+ }
26958
+ /**
26959
+ * Check if a Promise+setTimeout pattern is allowlisted
26960
+ */
26961
+ checkPromiseTimeoutAllowlist(functionName, promiseNode, sourceFile) {
26962
+ if (functionName) {
26963
+ const normalizedName = functionName.toLowerCase();
26964
+ for (const allowedName of ALLOWLISTS.promiseTimeout.functionNames) {
26965
+ const lowerAllowed = allowedName.toLowerCase();
26966
+ if (normalizedName === lowerAllowed || normalizedName.startsWith(lowerAllowed)) {
26967
+ return {
26968
+ isAllowlisted: true,
26969
+ reason: `Function name '${functionName}' matches allowlisted pattern '${allowedName}'`
26970
+ };
26971
+ }
26972
+ }
26973
+ }
26974
+ const promiseText = promiseNode.getText(sourceFile);
26975
+ for (const safePattern of ALLOWLISTS.promiseTimeout.safePatterns) {
26976
+ if (safePattern.test(promiseText)) {
26977
+ return {
26978
+ isAllowlisted: true,
26979
+ reason: "Simple sleep/delay pattern - timeout cleanup not needed"
26980
+ };
26981
+ }
26982
+ }
26983
+ const promiseArgs = promiseNode.arguments;
26984
+ if (promiseArgs && promiseArgs.length > 0) {
26985
+ const promiseArg = promiseArgs[0];
26986
+ if (promiseArg && getTS().isArrowFunction(promiseArg)) {
26987
+ const body = promiseArg.body;
26988
+ if (getTS().isCallExpression(body)) {
26989
+ const callName = this.getCallExpressionName(body);
26990
+ if (callName === "setTimeout") {
26991
+ const args2 = body.arguments;
26992
+ if (args2.length >= 1) {
26993
+ const firstArg = args2[0];
26994
+ const params = promiseArg.parameters;
26995
+ if (params.length >= 1) {
26996
+ const resolveParam = params[0];
26997
+ if (resolveParam && getTS().isIdentifier(resolveParam.name)) {
26998
+ const resolveParamName = resolveParam.name.text;
26999
+ if (firstArg && getTS().isIdentifier(firstArg) && firstArg.text === resolveParamName) {
27000
+ return {
27001
+ isAllowlisted: true,
27002
+ reason: "Simple Promise with direct resolve to setTimeout - no cleanup needed"
27003
+ };
27004
+ }
27005
+ }
27006
+ }
27007
+ }
27008
+ }
27009
+ }
27010
+ }
27011
+ }
27012
+ if (getTS().isAwaitExpression(promiseNode.parent)) {
27013
+ if (promiseArgs && promiseArgs.length > 0) {
27014
+ const promiseArg = promiseArgs[0];
27015
+ if (promiseArg && getTS().isArrowFunction(promiseArg)) {
27016
+ const body = promiseArg.body;
27017
+ if (getTS().isCallExpression(body)) {
27018
+ const callName = this.getCallExpressionName(body);
27019
+ if (callName === "setTimeout") {
27020
+ return {
27021
+ isAllowlisted: true,
27022
+ reason: "Awaited Promise with setTimeout - no leak possible when awaited"
27023
+ };
27024
+ }
27025
+ }
27026
+ }
27027
+ }
27028
+ }
27029
+ return { isAllowlisted: false };
27030
+ }
27031
+ /**
27032
+ * Analyze code for unreachable code patterns
27033
+ * v12.8.0: Phase 4 - Control flow analysis to reduce false positives
27034
+ */
27035
+ analyzeUnreachableCode(sourceFile) {
27036
+ const results = [];
27037
+ const visit = (node) => {
27038
+ if (getTS().isBlock(node)) {
27039
+ this.analyzeBlockForUnreachable(node, sourceFile, results);
27040
+ }
27041
+ getTS().forEachChild(node, visit);
27042
+ };
27043
+ visit(sourceFile);
27044
+ return results;
27045
+ }
27046
+ /**
27047
+ * Analyze a block for unreachable statements
27048
+ */
27049
+ analyzeBlockForUnreachable(block, sourceFile, results) {
27050
+ const statements = block.statements;
27051
+ let foundTerminator = false;
27052
+ let terminatorLine = 0;
27053
+ let terminatorReason = "return";
27054
+ for (let i = 0; i < statements.length; i++) {
27055
+ const stmt = statements[i];
27056
+ if (!stmt) continue;
27057
+ const stmtPos = sourceFile.getLineAndCharacterOfPosition(stmt.getStart());
27058
+ const stmtLine = stmtPos.line + 1;
27059
+ if (foundTerminator) {
27060
+ if (getTS().isCaseClause(stmt.parent) || getTS().isDefaultClause(stmt.parent)) {
27061
+ results.push({
27062
+ line: stmtLine,
27063
+ code: stmt.getText(sourceFile).substring(0, 80),
27064
+ reason: terminatorReason,
27065
+ controlFlowLine: terminatorLine,
27066
+ isFalsePositive: true,
27067
+ falsePositiveReason: "Case/default clause in switch statement"
27068
+ });
27069
+ continue;
27070
+ }
27071
+ if (this.isInSwitchCase(stmt)) {
27072
+ results.push({
27073
+ line: stmtLine,
27074
+ code: stmt.getText(sourceFile).substring(0, 80),
27075
+ reason: terminatorReason,
27076
+ controlFlowLine: terminatorLine,
27077
+ isFalsePositive: true,
27078
+ falsePositiveReason: "Statement in switch case after break belongs to next case"
27079
+ });
27080
+ continue;
27081
+ }
27082
+ const parentCase = this.findParentSwitchCase(block);
27083
+ if (parentCase) {
27084
+ foundTerminator = false;
27085
+ continue;
27086
+ }
27087
+ results.push({
27088
+ line: stmtLine,
27089
+ code: stmt.getText(sourceFile).substring(0, 80),
27090
+ reason: terminatorReason,
27091
+ controlFlowLine: terminatorLine,
27092
+ isFalsePositive: false
27093
+ });
27094
+ }
27095
+ if (getTS().isReturnStatement(stmt)) {
27096
+ foundTerminator = true;
27097
+ terminatorLine = stmtLine;
27098
+ terminatorReason = "return";
27099
+ } else if (getTS().isThrowStatement(stmt)) {
27100
+ foundTerminator = true;
27101
+ terminatorLine = stmtLine;
27102
+ terminatorReason = "throw";
27103
+ } else if (getTS().isBreakStatement(stmt)) {
27104
+ if (this.isInSwitchOrLoop(stmt)) ; else {
27105
+ foundTerminator = true;
27106
+ terminatorLine = stmtLine;
27107
+ terminatorReason = "break";
27108
+ }
27109
+ } else if (getTS().isContinueStatement(stmt)) {
27110
+ if (!this.isInLoop(stmt)) {
27111
+ foundTerminator = true;
27112
+ terminatorLine = stmtLine;
27113
+ terminatorReason = "continue";
27114
+ }
27115
+ }
27116
+ }
27117
+ }
27118
+ /**
27119
+ * Check if a node is inside a switch case
27120
+ */
27121
+ isInSwitchCase(node) {
27122
+ let current = node.parent;
27123
+ while (current) {
27124
+ if (getTS().isCaseClause(current) || getTS().isDefaultClause(current)) {
27125
+ return true;
27126
+ }
27127
+ if (getTS().isSwitchStatement(current)) {
27128
+ return false;
27129
+ }
27130
+ current = current.parent;
27131
+ }
27132
+ return false;
27133
+ }
27134
+ /**
27135
+ * Find parent switch case clause
27136
+ */
27137
+ findParentSwitchCase(node) {
27138
+ let current = node.parent;
27139
+ while (current) {
27140
+ if (getTS().isCaseClause(current) || getTS().isDefaultClause(current)) {
27141
+ return current;
27142
+ }
27143
+ current = current.parent;
27144
+ }
27145
+ return null;
27146
+ }
27147
+ /**
27148
+ * Check if a node is inside a switch statement or loop
27149
+ */
27150
+ isInSwitchOrLoop(node) {
27151
+ let current = node.parent;
27152
+ while (current) {
27153
+ if (getTS().isSwitchStatement(current) || getTS().isForStatement(current) || getTS().isForInStatement(current) || getTS().isForOfStatement(current) || getTS().isWhileStatement(current) || getTS().isDoStatement(current)) {
27154
+ return true;
27155
+ }
27156
+ if (getTS().isFunctionDeclaration(current) || getTS().isFunctionExpression(current) || getTS().isArrowFunction(current)) {
27157
+ return false;
27158
+ }
27159
+ current = current.parent;
27160
+ }
27161
+ return false;
27162
+ }
27163
+ /**
27164
+ * Check if a node is inside a loop
27165
+ */
27166
+ isInLoop(node) {
27167
+ let current = node.parent;
27168
+ while (current) {
27169
+ if (getTS().isForStatement(current) || getTS().isForInStatement(current) || getTS().isForOfStatement(current) || getTS().isWhileStatement(current) || getTS().isDoStatement(current)) {
27170
+ return true;
27171
+ }
27172
+ if (getTS().isFunctionDeclaration(current) || getTS().isFunctionExpression(current) || getTS().isArrowFunction(current)) {
27173
+ return false;
27174
+ }
27175
+ current = current.parent;
27176
+ }
27177
+ return false;
27178
+ }
27179
+ /**
27180
+ * Analyze setInterval/setTimeout calls for potential timer leaks
27181
+ * v12.8.0: Phase 5 - Variable tracking and destroy() method analysis
27182
+ */
27183
+ analyzeTimerLeaks(sourceFile, content) {
27184
+ const results = [];
27185
+ const intervalCalls = this.findCalls(sourceFile, "setInterval");
27186
+ for (const call of intervalCalls) {
27187
+ const info = this.analyzeTimerCall(call, sourceFile, content, "setInterval");
27188
+ results.push(info);
27189
+ }
27190
+ return results;
27191
+ }
27192
+ /**
27193
+ * Analyze a single timer call for leak potential
27194
+ */
27195
+ analyzeTimerCall(call, sourceFile, content, timerType) {
27196
+ content.split("\n");
27197
+ const enclosingFunc = this.findEnclosingFunction(sourceFile, call.line);
27198
+ const enclosingClass = this.findEnclosingClass(sourceFile, call.line);
27199
+ const info = {
27200
+ line: call.line,
27201
+ timerType,
27202
+ isValueCaptured: call.isReturnCaptured,
27203
+ capturedVariable: call.capturedVariable,
27204
+ hasClearCall: false,
27205
+ hasUnref: false,
27206
+ hasDestroyCleanup: false,
27207
+ enclosingClass: enclosingClass?.name,
27208
+ enclosingFunction: enclosingFunc?.name,
27209
+ isFalsePositive: false,
27210
+ falsePositiveReason: void 0
27211
+ };
27212
+ if (!call.isReturnCaptured) {
27213
+ if (call.hasChainedUnref) {
27214
+ info.hasUnref = true;
27215
+ info.isFalsePositive = true;
27216
+ info.falsePositiveReason = "Timer has .unref() chained directly";
27217
+ return info;
27218
+ }
27219
+ return info;
27220
+ }
27221
+ const variableName = call.capturedVariable;
27222
+ const variableUsage = this.trackVariableUsage(sourceFile, variableName);
27223
+ if (variableUsage) {
27224
+ if (variableUsage.usedInCleanup) {
27225
+ info.hasClearCall = true;
27226
+ info.isFalsePositive = true;
27227
+ info.falsePositiveReason = `Timer is cleared via ${variableUsage.cleanupMethod}()`;
27228
+ return info;
27229
+ }
27230
+ }
27231
+ const unrefPattern = new RegExp(`${this.escapeRegex(variableName)}\\s*\\.\\s*unref\\s*\\(\\s*\\)`, "g");
27232
+ if (unrefPattern.test(content)) {
27233
+ info.hasUnref = true;
27234
+ info.isFalsePositive = true;
27235
+ info.falsePositiveReason = "Timer has .unref() called on it";
27236
+ return info;
27237
+ }
27238
+ const optionalUnrefPattern = new RegExp(`${this.escapeRegex(variableName)}\\s*\\?\\.\\s*unref\\s*\\(\\s*\\)`, "g");
27239
+ if (optionalUnrefPattern.test(content)) {
27240
+ info.hasUnref = true;
27241
+ info.isFalsePositive = true;
27242
+ info.falsePositiveReason = "Timer has optional .unref?.() called on it";
27243
+ return info;
27244
+ }
27245
+ if (enclosingClass) {
27246
+ const extendsAllowlistedBase = enclosingClass.extendsClause.some(
27247
+ (base) => ALLOWLISTS.timerLeak.baseClasses.includes(base)
27248
+ );
27249
+ if (extendsAllowlistedBase) {
27250
+ for (const methodName of ALLOWLISTS.timerLeak.cleanupMethods) {
27251
+ if (this.methodClearsTimer(sourceFile, enclosingClass, methodName, variableName)) {
27252
+ info.hasDestroyCleanup = true;
27253
+ info.isFalsePositive = true;
27254
+ info.falsePositiveReason = `Timer is cleared in ${methodName}() method`;
27255
+ return info;
27256
+ }
27257
+ }
27258
+ }
27259
+ for (const methodName of ALLOWLISTS.timerLeak.cleanupMethods) {
27260
+ if (this.methodClearsTimer(sourceFile, enclosingClass, methodName, variableName)) {
27261
+ info.hasDestroyCleanup = true;
27262
+ info.isFalsePositive = true;
27263
+ info.falsePositiveReason = `Timer is cleared in ${methodName}() method`;
27264
+ return info;
27265
+ }
27266
+ }
27267
+ }
27268
+ if (enclosingFunc) {
27269
+ const clearPattern = timerType === "setInterval" ? new RegExp(`clearInterval\\s*\\(\\s*${this.escapeRegex(variableName)}\\s*\\)`) : new RegExp(`clearTimeout\\s*\\(\\s*${this.escapeRegex(variableName)}\\s*\\)`);
27270
+ if (clearPattern.test(enclosingFunc.body)) {
27271
+ info.hasClearCall = true;
27272
+ info.isFalsePositive = true;
27273
+ info.falsePositiveReason = "Timer is cleared in the same function";
27274
+ return info;
27275
+ }
27276
+ }
27277
+ return info;
27278
+ }
27279
+ /**
27280
+ * Find the enclosing class for a given line
27281
+ */
27282
+ findEnclosingClass(sourceFile, lineNumber) {
27283
+ const allClasses = this.findClasses(sourceFile);
27284
+ let result = null;
27285
+ for (const cls of allClasses) {
27286
+ if (lineNumber >= cls.startLine && lineNumber <= cls.endLine) {
27287
+ if (!result || cls.startLine >= result.startLine) {
27288
+ result = cls;
27289
+ }
27290
+ }
27291
+ }
27292
+ return result;
27293
+ }
27294
+ /**
27295
+ * Escape special regex characters in a string
27296
+ */
27297
+ escapeRegex(str) {
27298
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
27299
+ }
27300
+ /**
27301
+ * Clear the AST cache
27302
+ */
27303
+ clearCache() {
27304
+ this.cache.clear();
27305
+ }
27306
+ /**
27307
+ * Get cache statistics
27308
+ */
27309
+ getCacheStats() {
27310
+ return {
27311
+ size: this.cache.size,
27312
+ maxSize: this.maxCacheSize
27313
+ };
27314
+ }
27315
+ // Private helper methods
27316
+ hashContent(content) {
27317
+ let hash = 0;
27318
+ for (let i = 0; i < content.length; i++) {
27319
+ const char = content.charCodeAt(i);
27320
+ hash = (hash << 5) - hash + char;
27321
+ hash = hash & hash;
27322
+ }
27323
+ return hash.toString(16);
27324
+ }
27325
+ getScriptKind(filePath) {
27326
+ if (filePath.endsWith(".tsx")) return getTS().ScriptKind.TSX;
27327
+ if (filePath.endsWith(".ts")) return getTS().ScriptKind.TS;
27328
+ if (filePath.endsWith(".jsx")) return getTS().ScriptKind.JSX;
27329
+ if (filePath.endsWith(".mjs") || filePath.endsWith(".mts")) return getTS().ScriptKind.TS;
27330
+ return getTS().ScriptKind.JS;
27331
+ }
27332
+ evictOldest() {
27333
+ if (this.cache.size <= this.maxCacheSize) return;
27334
+ let oldestKey = null;
27335
+ let oldestTime = Infinity;
27336
+ for (const [key, entry] of this.cache) {
27337
+ if (entry.accessedAt < oldestTime) {
27338
+ oldestTime = entry.accessedAt;
27339
+ oldestKey = key;
27340
+ }
27341
+ }
27342
+ if (oldestKey) {
27343
+ this.cache.delete(oldestKey);
27344
+ }
27345
+ }
27346
+ extractClassInfo(node, sourceFile) {
27347
+ const name = node.name?.getText(sourceFile) || "<anonymous>";
27348
+ const { line: startLine } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
27349
+ const { line: endLine } = sourceFile.getLineAndCharacterOfPosition(node.getEnd());
27350
+ const extendsClause = [];
27351
+ const implementsClause = [];
27352
+ if (node.heritageClauses) {
27353
+ for (const clause of node.heritageClauses) {
27354
+ if (clause.token === getTS().SyntaxKind.ExtendsKeyword) {
27355
+ for (const type of clause.types) {
27356
+ extendsClause.push(type.expression.getText(sourceFile));
27357
+ }
27358
+ } else if (clause.token === getTS().SyntaxKind.ImplementsKeyword) {
27359
+ for (const type of clause.types) {
27360
+ implementsClause.push(type.expression.getText(sourceFile));
27361
+ }
27362
+ }
27363
+ }
27364
+ }
27365
+ const methods = [];
27366
+ const properties = [];
27367
+ for (const member of node.members) {
27368
+ if (getTS().isMethodDeclaration(member) || getTS().isConstructorDeclaration(member)) {
27369
+ const methodName = getTS().isConstructorDeclaration(member) ? "constructor" : member.name?.getText(sourceFile) || "<anonymous>";
27370
+ const { line: methodStart } = sourceFile.getLineAndCharacterOfPosition(member.getStart());
27371
+ const { line: methodEnd } = sourceFile.getLineAndCharacterOfPosition(member.getEnd());
27372
+ const modifiers = this.getModifiers(member);
27373
+ methods.push({
27374
+ name: methodName,
27375
+ startLine: methodStart + 1,
27376
+ endLine: methodEnd + 1,
27377
+ modifiers,
27378
+ isAbstract: modifiers.includes("abstract")
27379
+ });
27380
+ } else if (getTS().isPropertyDeclaration(member)) {
27381
+ const propName = member.name?.getText(sourceFile) || "<anonymous>";
27382
+ const { line } = sourceFile.getLineAndCharacterOfPosition(member.getStart());
27383
+ properties.push({
27384
+ name: propName,
27385
+ line: line + 1,
27386
+ type: member.type?.getText(sourceFile),
27387
+ modifiers: this.getModifiers(member)
27388
+ });
27389
+ }
27390
+ }
27391
+ const isAbstract = node.modifiers?.some(
27392
+ (mod) => mod.kind === getTS().SyntaxKind.AbstractKeyword
27393
+ ) || false;
27394
+ return {
27395
+ name,
27396
+ startLine: startLine + 1,
27397
+ endLine: endLine + 1,
27398
+ extendsClause,
27399
+ implementsClause,
27400
+ methods,
27401
+ properties,
27402
+ isAbstract
27403
+ };
27404
+ }
27405
+ extractFunctionInfo(node, sourceFile) {
27406
+ let name = "";
27407
+ if (getTS().isFunctionDeclaration(node) || getTS().isMethodDeclaration(node)) {
27408
+ name = node.name?.getText(sourceFile) || "";
27409
+ } else if (getTS().isFunctionExpression(node)) {
27410
+ name = node.name?.getText(sourceFile) || "";
27411
+ }
27412
+ if (!name && getTS().isVariableDeclaration(node.parent)) {
27413
+ name = node.parent.name.getText(sourceFile);
27414
+ }
27415
+ const { line: startLine } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
27416
+ const { line: endLine } = sourceFile.getLineAndCharacterOfPosition(node.getEnd());
27417
+ const modifiers = getTS().canHaveModifiers(node) ? getTS().getModifiers(node) : void 0;
27418
+ const isAsync = modifiers?.some((mod) => mod.kind === getTS().SyntaxKind.AsyncKeyword) || false;
27419
+ const isGenerator = !!((getTS().isFunctionDeclaration(node) || getTS().isFunctionExpression(node)) && node.asteriskToken);
27420
+ let body = "";
27421
+ if ("body" in node && node.body) {
27422
+ body = node.body.getText(sourceFile);
27423
+ }
27424
+ return {
27425
+ name,
27426
+ startLine: startLine + 1,
27427
+ endLine: endLine + 1,
27428
+ body,
27429
+ isAsync,
27430
+ isGenerator
27431
+ };
27432
+ }
27433
+ getModifiers(node) {
27434
+ const modifiers = [];
27435
+ const mods = getTS().canHaveModifiers(node) ? getTS().getModifiers(node) : void 0;
27436
+ if (!mods) return modifiers;
27437
+ for (const mod of mods) {
27438
+ switch (mod.kind) {
27439
+ case getTS().SyntaxKind.PublicKeyword:
27440
+ modifiers.push("public");
27441
+ break;
27442
+ case getTS().SyntaxKind.PrivateKeyword:
27443
+ modifiers.push("private");
27444
+ break;
27445
+ case getTS().SyntaxKind.ProtectedKeyword:
27446
+ modifiers.push("protected");
27447
+ break;
27448
+ case getTS().SyntaxKind.StaticKeyword:
27449
+ modifiers.push("static");
27450
+ break;
27451
+ case getTS().SyntaxKind.AsyncKeyword:
27452
+ modifiers.push("async");
27453
+ break;
27454
+ case getTS().SyntaxKind.AbstractKeyword:
27455
+ modifiers.push("abstract");
27456
+ break;
27457
+ case getTS().SyntaxKind.ReadonlyKeyword:
27458
+ modifiers.push("readonly");
27459
+ break;
27460
+ }
27461
+ }
27462
+ return modifiers;
27463
+ }
27464
+ getCallExpressionName(node) {
27465
+ const expression = node.expression;
27466
+ if (getTS().isIdentifier(expression)) {
27467
+ return expression.text;
27468
+ }
27469
+ if (getTS().isPropertyAccessExpression(expression)) {
27470
+ return expression.name.text;
27471
+ }
27472
+ return "";
27473
+ }
27474
+ isReturnValueCaptured(node) {
27475
+ const parent = node.parent;
27476
+ if (getTS().isVariableDeclaration(parent)) {
27477
+ return true;
27478
+ }
27479
+ if (getTS().isPropertyAssignment(parent)) {
27480
+ return true;
27481
+ }
27482
+ if (getTS().isBinaryExpression(parent) && parent.operatorToken.kind === getTS().SyntaxKind.EqualsToken) {
27483
+ return true;
27484
+ }
27485
+ return false;
27486
+ }
27487
+ getCapturedVariableName(node) {
27488
+ const parent = node.parent;
27489
+ if (getTS().isVariableDeclaration(parent)) {
27490
+ return parent.name.getText();
27491
+ }
27492
+ if (getTS().isBinaryExpression(parent) && parent.operatorToken.kind === getTS().SyntaxKind.EqualsToken) {
27493
+ return parent.left.getText();
27494
+ }
27495
+ return void 0;
27496
+ }
27497
+ /**
27498
+ * Check if a call expression has .unref() chained directly
27499
+ * e.g., setInterval(...).unref() or setInterval(...).unref?.()
27500
+ */
27501
+ hasChainedUnrefCall(node) {
27502
+ const parent = node.parent;
27503
+ if (getTS().isPropertyAccessExpression(parent)) {
27504
+ const propertyName = parent.name.text;
27505
+ if (propertyName === "unref") {
27506
+ const grandParent = parent.parent;
27507
+ if (getTS().isCallExpression(grandParent) && grandParent.expression === parent) {
27508
+ return true;
27509
+ }
27510
+ }
27511
+ }
27512
+ if (getTS().isCallChain && getTS().isCallChain(parent)) {
27513
+ const expression = parent.expression;
27514
+ if (getTS().isPropertyAccessExpression(expression) && expression.name.text === "unref") {
27515
+ return true;
27516
+ }
27517
+ }
27518
+ return false;
27519
+ }
27520
+ };
27521
+ var ALLOWLISTS = {
27522
+ /**
27523
+ * Patterns that indicate intentional Promise+setTimeout usage (not leaks)
27524
+ */
27525
+ promiseTimeout: {
27526
+ /** Function names that are intentional delays */
27527
+ // Note: 'wait' and 'timeout' removed as they are too generic
27528
+ // 'waitForCondition' or 'fetchWithTimeout' would be wrongly allowlisted
27529
+ functionNames: [
27530
+ "sleep",
27531
+ "delay",
27532
+ "pause",
27533
+ "debounce",
27534
+ "throttle",
27535
+ "rateLimitDelay",
27536
+ "backoff"
27537
+ ],
27538
+ /** File patterns to skip entirely */
27539
+ filePatterns: [
27540
+ /\.test\.[tj]sx?$/,
27541
+ /\.spec\.[tj]sx?$/,
27542
+ /\.bench\.[tj]sx?$/,
27543
+ /__tests__\//,
27544
+ /test\/fixtures\//
27545
+ ],
27546
+ /** Code patterns that are safe (simple delay utilities) */
27547
+ safePatterns: [
27548
+ // Simple sleep: return new Promise(resolve => setTimeout(resolve, ms))
27549
+ /return\s+new\s+Promise\s*\(\s*(?:resolve|r)\s*=>\s*setTimeout\s*\(\s*(?:resolve|r)\s*,\s*\w+\s*\)\s*\)/,
27550
+ // Arrow function sleep: const sleep = (ms) => new Promise(r => setTimeout(r, ms))
27551
+ /=>\s*new\s+Promise\s*\(\s*(?:resolve|r)\s*=>\s*setTimeout\s*\(\s*(?:resolve|r)\s*,\s*\w+\s*\)\s*\)/,
27552
+ // Inline await: await new Promise(r => setTimeout(r, 100))
27553
+ /await\s+new\s+Promise\s*\(\s*(?:resolve|r)\s*=>\s*setTimeout\s*\(\s*(?:resolve|r)\s*,\s*\d+\s*\)\s*\)/
27554
+ ]
27555
+ },
27556
+ /**
27557
+ * Patterns that indicate timer cleanup is handled elsewhere
27558
+ */
27559
+ timerLeak: {
27560
+ /** Classes with known lifecycle management */
27561
+ baseClasses: [
27562
+ "Disposable",
27563
+ "DisposableEventEmitter",
27564
+ "SafeEventEmitter",
27565
+ "Component",
27566
+ // React
27567
+ "BaseService"
27568
+ ],
27569
+ /** Methods that imply cleanup */
27570
+ cleanupMethods: ["destroy", "dispose", "cleanup", "close", "shutdown", "teardown", "componentWillUnmount"]
27571
+ },
27572
+ /**
27573
+ * Patterns that indicate destroy is handled by inheritance
27574
+ */
27575
+ missingDestroy: {
27576
+ /** Base classes that already have destroy() */
27577
+ baseClassesWithDestroy: [
27578
+ "DisposableEventEmitter",
27579
+ "Disposable",
27580
+ "SafeEventEmitter"
27581
+ ],
27582
+ /** Interfaces that imply destroy() exists */
27583
+ interfacesWithDestroy: [
27584
+ "IDisposable",
27585
+ "Destroyable",
27586
+ "IClosable"
27587
+ ]
27588
+ }
27589
+ };
27590
+ function createASTAnalyzer(cacheSize = 100) {
27591
+ return new ASTAnalyzer(cacheSize);
27592
+ }
27593
+
27594
+ // src/core/bugfix/bug-detector.ts
25128
27595
  var IGNORE_PATTERNS = {
25129
27596
  // Match: // ax-ignore or // ax-ignore timer_leak
25130
27597
  nextLine: /\/\/\s*ax-ignore(?:\s+(\w+))?\s*$/,
@@ -25134,25 +27601,30 @@ var IGNORE_PATTERNS = {
25134
27601
  blockEnd: /\/\/\s*ax-ignore-end\s*$/
25135
27602
  };
25136
27603
  var DEFAULT_DETECTION_RULES = [
25137
- // Timer leak: setInterval without .unref()
25138
- // v12.6.0: Increased withinLines from 5 to 50 to handle multi-line callbacks
25139
- // where .unref() is called after the closing brace (some callbacks are 35+ lines)
25140
- // NOTE: This is a workaround - proper fix would use AST-based detection
27604
+ // Timer leak: setInterval without cleanup
27605
+ // v12.8.0: Now uses AST-based detection with variable tracking and destroy() analysis
27606
+ // This eliminates false positives from timers that are properly cleaned up
25141
27607
  {
25142
27608
  id: "timer-leak-interval",
25143
27609
  type: "timer_leak",
25144
- name: "setInterval without unref",
25145
- description: "setInterval() without .unref() blocks process exit",
27610
+ name: "setInterval without cleanup",
27611
+ description: "setInterval() without clearInterval or .unref() may block process exit",
25146
27612
  pattern: "setInterval\\s*\\(",
25147
- negativePattern: "\\.unref\\s*\\(\\)",
25148
- withinLines: 50,
25149
- confidence: 0.9,
27613
+ // v12.8.0: negativePattern no longer used - AST handles cleanup detection
27614
+ negativePattern: void 0,
27615
+ withinLines: void 0,
27616
+ confidence: 0.92,
27617
+ // Higher confidence with AST-based detection
25150
27618
  severity: "high",
25151
27619
  autoFixable: true,
25152
27620
  fixTemplate: "add_unref",
25153
- fileExtensions: [".ts", ".js", ".mts", ".mjs"]
27621
+ fileExtensions: [".ts", ".js", ".mts", ".mjs"],
27622
+ // v12.8.0: Flag to use AST-based detection
27623
+ useAST: true
25154
27624
  },
25155
27625
  // Timer leak: setTimeout in promise without cleanup
27626
+ // v12.8.0: Now uses AST-based detection to identify safe sleep/delay patterns
27627
+ // This reduces false positives from simple utilities like sleep() or delay()
25156
27628
  {
25157
27629
  id: "timer-leak-timeout-promise",
25158
27630
  type: "promise_timeout_leak",
@@ -25161,28 +27633,36 @@ var DEFAULT_DETECTION_RULES = [
25161
27633
  pattern: "new\\s+Promise[^}]*setTimeout\\s*\\(",
25162
27634
  negativePattern: "finally|clearTimeout",
25163
27635
  withinLines: 20,
25164
- confidence: 0.7,
27636
+ confidence: 0.85,
27637
+ // Higher with AST-based detection
25165
27638
  severity: "medium",
25166
27639
  autoFixable: false,
25167
27640
  // Complex, needs manual review
25168
- fileExtensions: [".ts", ".js", ".mts", ".mjs"]
27641
+ fileExtensions: [".ts", ".js", ".mts", ".mjs"],
27642
+ // v12.8.0: Flag to use AST-based detection
27643
+ useAST: true
25169
27644
  },
25170
27645
  // Missing destroy: EventEmitter without destroy method
25171
- // v12.6.0: Increased withinLines from 100 to 800 to scan entire class
25172
- // Classes can be large - need to check the whole class for destroy() method
27646
+ // v12.8.0: Now uses AST-based detection for accurate class boundary analysis
27647
+ // The regex pattern is kept as a pre-filter, but actual detection is done via AST
27648
+ // This eliminates false positives from large classes where destroy() is far from class declaration
25173
27649
  {
25174
27650
  id: "missing-destroy-eventemitter",
25175
27651
  type: "missing_destroy",
25176
27652
  name: "EventEmitter without destroy",
25177
27653
  description: "Classes extending EventEmitter should have destroy() method",
25178
27654
  pattern: "class\\s+\\w+\\s+extends\\s+(?:EventEmitter|DisposableEventEmitter)",
25179
- negativePattern: "destroy\\s*\\(\\s*\\)",
25180
- withinLines: 800,
25181
- confidence: 0.85,
27655
+ // v12.8.0: negativePattern no longer used - AST handles method detection
27656
+ negativePattern: void 0,
27657
+ withinLines: void 0,
27658
+ confidence: 0.95,
27659
+ // Higher confidence with AST-based detection
25182
27660
  severity: "high",
25183
27661
  autoFixable: true,
25184
27662
  fixTemplate: "add_destroy_method",
25185
- fileExtensions: [".ts", ".js", ".mts", ".mjs"]
27663
+ fileExtensions: [".ts", ".js", ".mts", ".mjs"],
27664
+ // v12.8.0: Flag to use AST-based detection
27665
+ useAST: true
25186
27666
  },
25187
27667
  // Event leak: .on() without corresponding cleanup
25188
27668
  {
@@ -25218,9 +27698,12 @@ var DEFAULT_DETECTION_RULES = [
25218
27698
  var BugDetector = class {
25219
27699
  rules;
25220
27700
  config;
27701
+ astAnalyzer;
27702
+ astInitialized = false;
25221
27703
  constructor(config, customRules) {
25222
27704
  this.config = config;
25223
27705
  this.rules = customRules || DEFAULT_DETECTION_RULES;
27706
+ this.astAnalyzer = createASTAnalyzer();
25224
27707
  if (config.bugTypes && config.bugTypes.length > 0) {
25225
27708
  this.rules = this.rules.filter(
25226
27709
  (rule) => config.bugTypes.includes(rule.type)
@@ -25229,9 +27712,19 @@ var BugDetector = class {
25229
27712
  logger.debug("BugDetector initialized", {
25230
27713
  ruleCount: this.rules.length,
25231
27714
  bugTypes: config.bugTypes,
25232
- scope: config.scope
27715
+ scope: config.scope,
27716
+ astEnabled: true
25233
27717
  });
25234
27718
  }
27719
+ /**
27720
+ * Initialize AST analyzer (lazy loading to avoid bundling issues)
27721
+ */
27722
+ async initAST() {
27723
+ if (!this.astInitialized) {
27724
+ await this.astAnalyzer.init();
27725
+ this.astInitialized = true;
27726
+ }
27727
+ }
25235
27728
  /**
25236
27729
  * Parse ignore comments from file lines
25237
27730
  *
@@ -25363,8 +27856,13 @@ var BugDetector = class {
25363
27856
  continue;
25364
27857
  }
25365
27858
  }
25366
- const ruleFindings = this.applyRule(rule, content, lines, relativePath, ignoreState);
25367
- findings.push(...ruleFindings);
27859
+ if (rule.useAST) {
27860
+ const astFindings = await this.applyASTRule(rule, content, lines, filePath, relativePath, ignoreState);
27861
+ findings.push(...astFindings);
27862
+ } else {
27863
+ const ruleFindings = this.applyRule(rule, content, lines, relativePath, ignoreState);
27864
+ findings.push(...ruleFindings);
27865
+ }
25368
27866
  }
25369
27867
  return findings;
25370
27868
  }
@@ -25439,6 +27937,282 @@ var BugDetector = class {
25439
27937
  }
25440
27938
  return findings;
25441
27939
  }
27940
+ /**
27941
+ * Apply AST-based detection rule
27942
+ * v12.8.0: Uses TypeScript AST for accurate analysis
27943
+ */
27944
+ async applyASTRule(rule, content, lines, filePath, relativePath, ignoreState) {
27945
+ await this.initAST();
27946
+ try {
27947
+ switch (rule.type) {
27948
+ case "missing_destroy":
27949
+ return this.detectMissingDestroyAST(rule, content, lines, filePath, relativePath, ignoreState);
27950
+ case "promise_timeout_leak":
27951
+ return this.detectPromiseTimeoutLeakAST(rule, content, lines, filePath, relativePath, ignoreState);
27952
+ case "timer_leak":
27953
+ return this.detectTimerLeakAST(rule, content, lines, filePath, relativePath, ignoreState);
27954
+ default:
27955
+ logger.warn("AST detection not implemented for rule type", { type: rule.type });
27956
+ return this.applyRule(rule, content, lines, relativePath, ignoreState);
27957
+ }
27958
+ } catch (error) {
27959
+ logger.warn("AST rule application failed", {
27960
+ ruleId: rule.id,
27961
+ file: filePath,
27962
+ error: error.message
27963
+ });
27964
+ return this.applyRule(rule, content, lines, relativePath, ignoreState);
27965
+ }
27966
+ }
27967
+ /**
27968
+ * AST-based detection for missing_destroy bug type
27969
+ * v12.8.0: Accurate class boundary detection eliminates false positives
27970
+ */
27971
+ detectMissingDestroyAST(rule, content, lines, filePath, relativePath, ignoreState) {
27972
+ const findings = [];
27973
+ const sourceFile = this.astAnalyzer.parseFile(content, filePath);
27974
+ const eventEmitterPattern = /^(EventEmitter|DisposableEventEmitter|SafeEventEmitter)$/;
27975
+ const classes = this.astAnalyzer.findClassesExtending(sourceFile, eventEmitterPattern);
27976
+ for (const classInfo of classes) {
27977
+ if (this.shouldIgnore(classInfo.startLine, rule.type, ignoreState)) {
27978
+ logger.debug("Class ignored by comment", {
27979
+ file: relativePath,
27980
+ class: classInfo.name,
27981
+ line: classInfo.startLine
27982
+ });
27983
+ continue;
27984
+ }
27985
+ if (this.astAnalyzer.classHasDestroyMethod(classInfo)) {
27986
+ logger.debug("Class has destroy method - no bug", {
27987
+ file: relativePath,
27988
+ class: classInfo.name,
27989
+ methods: classInfo.methods.map((m) => m.name)
27990
+ });
27991
+ continue;
27992
+ }
27993
+ const extendsAllowlistedBase = classInfo.extendsClause.some(
27994
+ (base) => ALLOWLISTS.missingDestroy.baseClassesWithDestroy.includes(base)
27995
+ );
27996
+ if (extendsAllowlistedBase) {
27997
+ logger.debug("Class extends base with destroy - no bug", {
27998
+ file: relativePath,
27999
+ class: classInfo.name,
28000
+ extends: classInfo.extendsClause
28001
+ });
28002
+ continue;
28003
+ }
28004
+ const implementsDestroyInterface = classInfo.implementsClause.some(
28005
+ (iface) => ALLOWLISTS.missingDestroy.interfacesWithDestroy.includes(iface)
28006
+ );
28007
+ if (implementsDestroyInterface) {
28008
+ logger.debug("Class implements destroy interface - no bug", {
28009
+ file: relativePath,
28010
+ class: classInfo.name,
28011
+ implements: classInfo.implementsClause
28012
+ });
28013
+ continue;
28014
+ }
28015
+ if (classInfo.isAbstract) {
28016
+ logger.debug("Abstract class without destroy - flagging with lower confidence", {
28017
+ file: relativePath,
28018
+ class: classInfo.name
28019
+ });
28020
+ }
28021
+ const contextStart = Math.max(0, classInfo.startLine - 2);
28022
+ const contextEnd = Math.min(lines.length, classInfo.startLine + 5);
28023
+ const context = lines.slice(contextStart, contextEnd).join("\n");
28024
+ const finding = {
28025
+ id: randomUUID(),
28026
+ file: relativePath,
28027
+ lineStart: classInfo.startLine,
28028
+ lineEnd: Math.min(classInfo.startLine + 10, classInfo.endLine),
28029
+ type: rule.type,
28030
+ severity: rule.severity,
28031
+ message: `Class '${classInfo.name}' extends ${classInfo.extendsClause.join(", ")} but has no destroy() method. EventEmitter subclasses should implement destroy() to clean up listeners.`,
28032
+ context,
28033
+ fixStrategy: rule.autoFixable ? rule.fixTemplate : void 0,
28034
+ confidence: classInfo.isAbstract ? rule.confidence * 0.7 : rule.confidence,
28035
+ detectionMethod: "ast",
28036
+ metadata: {
28037
+ ruleId: rule.id,
28038
+ ruleName: rule.name,
28039
+ className: classInfo.name,
28040
+ extendsClause: classInfo.extendsClause,
28041
+ implementsClause: classInfo.implementsClause,
28042
+ isAbstract: classInfo.isAbstract,
28043
+ existingMethods: classInfo.methods.map((m) => m.name)
28044
+ },
28045
+ detectedAt: (/* @__PURE__ */ new Date()).toISOString()
28046
+ };
28047
+ findings.push(finding);
28048
+ logger.debug("Missing destroy bug detected (AST)", {
28049
+ file: relativePath,
28050
+ class: classInfo.name,
28051
+ line: classInfo.startLine,
28052
+ confidence: finding.confidence
28053
+ });
28054
+ }
28055
+ return findings;
28056
+ }
28057
+ /**
28058
+ * AST-based detection for promise_timeout_leak bug type
28059
+ * v12.8.0: Phase 3 - Identifies safe sleep/delay patterns to reduce false positives
28060
+ */
28061
+ detectPromiseTimeoutLeakAST(rule, content, lines, filePath, relativePath, ignoreState) {
28062
+ const findings = [];
28063
+ if (this.astAnalyzer.isTestFile(filePath)) {
28064
+ logger.debug("Skipping test file for promise_timeout_leak", { file: relativePath });
28065
+ return findings;
28066
+ }
28067
+ for (const pattern of ALLOWLISTS.promiseTimeout.filePatterns) {
28068
+ if (pattern.test(filePath)) {
28069
+ logger.debug("Skipping allowlisted file pattern", { file: relativePath, pattern: pattern.source });
28070
+ return findings;
28071
+ }
28072
+ }
28073
+ const sourceFile = this.astAnalyzer.parseFile(content, filePath);
28074
+ const promiseTimeouts = this.astAnalyzer.analyzePromiseTimeouts(sourceFile, content);
28075
+ for (const info of promiseTimeouts) {
28076
+ if (this.shouldIgnore(info.promiseLine, rule.type, ignoreState)) {
28077
+ logger.debug("Promise+setTimeout ignored by comment", {
28078
+ file: relativePath,
28079
+ line: info.promiseLine
28080
+ });
28081
+ continue;
28082
+ }
28083
+ if (info.isAllowlisted) {
28084
+ logger.debug("Promise+setTimeout is allowlisted", {
28085
+ file: relativePath,
28086
+ line: info.promiseLine,
28087
+ reason: info.allowlistReason
28088
+ });
28089
+ continue;
28090
+ }
28091
+ if (info.hasCleanup) {
28092
+ logger.debug("Promise+setTimeout has cleanup", {
28093
+ file: relativePath,
28094
+ line: info.promiseLine
28095
+ });
28096
+ continue;
28097
+ }
28098
+ const contextStart = Math.max(0, info.promiseLine - 2);
28099
+ const contextEnd = Math.min(lines.length, info.timeoutLine + 3);
28100
+ const context = lines.slice(contextStart, contextEnd).join("\n");
28101
+ let confidence = rule.confidence;
28102
+ if (info.timeoutIdCaptured) {
28103
+ confidence *= 0.8;
28104
+ }
28105
+ if (!info.enclosingFunction) {
28106
+ confidence *= 0.9;
28107
+ }
28108
+ const finding = {
28109
+ id: randomUUID(),
28110
+ file: relativePath,
28111
+ lineStart: info.promiseLine,
28112
+ lineEnd: info.timeoutLine,
28113
+ type: rule.type,
28114
+ severity: rule.severity,
28115
+ message: `Promise contains setTimeout without cleanup. If the Promise is rejected before the timeout fires, the timer will still run. Consider using try/finally with clearTimeout or Promise.race with AbortController.`,
28116
+ context,
28117
+ fixStrategy: void 0,
28118
+ // Complex, needs manual review
28119
+ confidence,
28120
+ detectionMethod: "ast",
28121
+ metadata: {
28122
+ ruleId: rule.id,
28123
+ ruleName: rule.name,
28124
+ promiseLine: info.promiseLine,
28125
+ timeoutLine: info.timeoutLine,
28126
+ enclosingFunction: info.enclosingFunction,
28127
+ timeoutIdCaptured: info.timeoutIdCaptured
28128
+ },
28129
+ detectedAt: (/* @__PURE__ */ new Date()).toISOString()
28130
+ };
28131
+ findings.push(finding);
28132
+ logger.debug("Promise timeout leak detected (AST)", {
28133
+ file: relativePath,
28134
+ line: info.promiseLine,
28135
+ confidence: finding.confidence
28136
+ });
28137
+ }
28138
+ return findings;
28139
+ }
28140
+ /**
28141
+ * AST-based detection for timer_leak bug type
28142
+ * v12.8.0: Phase 5 - Variable tracking and destroy() method analysis
28143
+ */
28144
+ detectTimerLeakAST(rule, content, lines, filePath, relativePath, ignoreState) {
28145
+ const findings = [];
28146
+ if (this.astAnalyzer.isTestFile(filePath)) {
28147
+ logger.debug("Skipping test file for timer_leak", { file: relativePath });
28148
+ return findings;
28149
+ }
28150
+ const sourceFile = this.astAnalyzer.parseFile(content, filePath);
28151
+ const timerLeaks = this.astAnalyzer.analyzeTimerLeaks(sourceFile, content);
28152
+ for (const info of timerLeaks) {
28153
+ if (this.shouldIgnore(info.line, rule.type, ignoreState)) {
28154
+ logger.debug("Timer leak ignored by comment", {
28155
+ file: relativePath,
28156
+ line: info.line
28157
+ });
28158
+ continue;
28159
+ }
28160
+ if (info.isFalsePositive) {
28161
+ logger.debug("Timer has proper cleanup - not a leak", {
28162
+ file: relativePath,
28163
+ line: info.line,
28164
+ reason: info.falsePositiveReason
28165
+ });
28166
+ continue;
28167
+ }
28168
+ const contextStart = Math.max(0, info.line - 2);
28169
+ const contextEnd = Math.min(lines.length, info.line + 5);
28170
+ const context = lines.slice(contextStart, contextEnd).join("\n");
28171
+ let confidence = rule.confidence;
28172
+ if (!info.isValueCaptured) {
28173
+ confidence = 0.98;
28174
+ }
28175
+ let message = `setInterval() `;
28176
+ if (!info.isValueCaptured) {
28177
+ message += `return value is not captured - timer cannot be cleared and will block process exit. `;
28178
+ } else {
28179
+ message += `(${info.capturedVariable}) has no cleanup. `;
28180
+ message += `No clearInterval() or .unref() found. `;
28181
+ }
28182
+ message += `Consider using .unref() for non-blocking timers or clearInterval() in a destroy() method.`;
28183
+ const finding = {
28184
+ id: randomUUID(),
28185
+ file: relativePath,
28186
+ lineStart: info.line,
28187
+ lineEnd: info.line + 3,
28188
+ type: rule.type,
28189
+ severity: rule.severity,
28190
+ message,
28191
+ context,
28192
+ fixStrategy: rule.autoFixable ? rule.fixTemplate : void 0,
28193
+ confidence,
28194
+ detectionMethod: "ast",
28195
+ metadata: {
28196
+ ruleId: rule.id,
28197
+ ruleName: rule.name,
28198
+ timerType: info.timerType,
28199
+ isValueCaptured: info.isValueCaptured,
28200
+ capturedVariable: info.capturedVariable,
28201
+ enclosingClass: info.enclosingClass,
28202
+ enclosingFunction: info.enclosingFunction
28203
+ },
28204
+ detectedAt: (/* @__PURE__ */ new Date()).toISOString()
28205
+ };
28206
+ findings.push(finding);
28207
+ logger.debug("Timer leak detected (AST)", {
28208
+ file: relativePath,
28209
+ line: info.line,
28210
+ confidence: finding.confidence,
28211
+ capturedVariable: info.capturedVariable
28212
+ });
28213
+ }
28214
+ return findings;
28215
+ }
25442
28216
  /**
25443
28217
  * Get all files to scan
25444
28218
  */
@@ -25995,8 +28769,7 @@ var BugFixer = class {
25995
28769
  for (let i = 0; i < lines.length; i++) {
25996
28770
  const currentLine = lines[i];
25997
28771
  if (!currentLine) continue;
25998
- const match = currentLine.match(classPattern);
25999
- if (match && match[1]) {
28772
+ if (classPattern.test(currentLine)) {
26000
28773
  classStartLine = i;
26001
28774
  break;
26002
28775
  }
@@ -26049,7 +28822,7 @@ var BugFixer = class {
26049
28822
  /**
26050
28823
  * Apply use DisposableEventEmitter fix
26051
28824
  */
26052
- applyUseDisposableEventEmitterFix(finding, originalContent, _lines) {
28825
+ applyUseDisposableEventEmitterFix(_finding, originalContent, _lines) {
26053
28826
  let fixedContent = originalContent.replace(
26054
28827
  /extends\s+EventEmitter\b/g,
26055
28828
  "extends DisposableEventEmitter"
@@ -26236,14 +29009,14 @@ var VerificationGate = class {
26236
29009
  * Run a shell command
26237
29010
  */
26238
29011
  async runCommand(command, name) {
26239
- return new Promise((resolve5) => {
29012
+ return new Promise((resolve6) => {
26240
29013
  const parts = command.split(" ");
26241
29014
  const cmd = parts[0];
26242
29015
  const args2 = parts.slice(1);
26243
29016
  const errors = [];
26244
29017
  let stderr = "";
26245
29018
  if (!cmd) {
26246
- resolve5({ success: false, errors: ["Empty command"] });
29019
+ resolve6({ success: false, errors: ["Empty command"] });
26247
29020
  return;
26248
29021
  }
26249
29022
  logger.debug(`Running ${name}`, { command });
@@ -26265,10 +29038,10 @@ var VerificationGate = class {
26265
29038
  proc.on("close", (code) => {
26266
29039
  clearTimeout(timeoutId);
26267
29040
  if (code === 0) {
26268
- resolve5({ success: true, errors: [] });
29041
+ resolve6({ success: true, errors: [] });
26269
29042
  } else {
26270
29043
  const errorLines = stderr.split("\n").filter((line) => line.includes("error") || line.includes("Error") || line.includes("FAIL")).slice(0, 10);
26271
- resolve5({
29044
+ resolve6({
26272
29045
  success: false,
26273
29046
  errors: errorLines.length > 0 ? errorLines : [`${name} failed with exit code ${code}`]
26274
29047
  });
@@ -26276,7 +29049,7 @@ var VerificationGate = class {
26276
29049
  });
26277
29050
  proc.on("error", (err) => {
26278
29051
  clearTimeout(timeoutId);
26279
- resolve5({
29052
+ resolve6({
26280
29053
  success: false,
26281
29054
  errors: [err.message]
26282
29055
  });
@@ -28891,6 +31664,18 @@ function shouldIgnoreLine6(lineNum, type, ignoreState) {
28891
31664
 
28892
31665
  // src/core/refactor/detectors/dead-code-detector.ts
28893
31666
  init_esm_shims();
31667
+ var astAnalyzer = null;
31668
+ var astInitialized = false;
31669
+ async function getASTAnalyzer() {
31670
+ if (!astAnalyzer) {
31671
+ astAnalyzer = createASTAnalyzer(50);
31672
+ }
31673
+ if (!astInitialized) {
31674
+ await astAnalyzer.init();
31675
+ astInitialized = true;
31676
+ }
31677
+ return astAnalyzer;
31678
+ }
28894
31679
  var DEAD_CODE_RULES = [
28895
31680
  {
28896
31681
  id: "unused-import",
@@ -28916,12 +31701,15 @@ var DEAD_CODE_RULES = [
28916
31701
  suggestion: "Remove unused variable or prefix with underscore",
28917
31702
  fileExtensions: [".ts", ".tsx", ".js", ".jsx"]
28918
31703
  },
31704
+ // v12.8.0: Changed to AST-based detection for better accuracy
31705
+ // AST analysis correctly handles switch/case statements and nested blocks
28919
31706
  {
28920
31707
  id: "unreachable-code",
28921
31708
  type: "dead_code",
28922
31709
  description: "Code after return/throw/break is unreachable",
28923
31710
  pattern: /(?:return|throw|break|continue)\s+[^;]*;\s*\n\s*[^}\s]/,
28924
- detector: "regex",
31711
+ detector: "ast",
31712
+ // Changed from 'regex' to 'ast'
28925
31713
  severity: "medium",
28926
31714
  confidence: 0.95,
28927
31715
  autoFixable: true,
@@ -28953,11 +31741,11 @@ var DEAD_CODE_RULES = [
28953
31741
  suggestion: "Implement function or add TODO comment"
28954
31742
  }
28955
31743
  ];
28956
- function detectDeadCode(filePath, content, lines, ignoreState, _config) {
31744
+ async function detectDeadCode(filePath, content, lines, ignoreState, _config) {
28957
31745
  const findings = [];
28958
31746
  findings.push(...detectUnusedImports(filePath, content, lines, ignoreState));
28959
31747
  findings.push(...detectUnusedVariables(filePath, content, lines, ignoreState));
28960
- findings.push(...detectUnreachableCode(filePath, content, lines, ignoreState));
31748
+ findings.push(...await detectUnreachableCode(filePath, content, lines, ignoreState));
28961
31749
  findings.push(...detectCommentedCode(filePath, content, lines, ignoreState));
28962
31750
  return findings;
28963
31751
  }
@@ -29068,7 +31856,42 @@ function detectUnusedVariables(filePath, content, lines, ignoreState) {
29068
31856
  }
29069
31857
  return findings;
29070
31858
  }
29071
- function detectUnreachableCode(filePath, content, lines, ignoreState) {
31859
+ async function detectUnreachableCode(filePath, content, lines, ignoreState) {
31860
+ const findings = [];
31861
+ try {
31862
+ const analyzer = await getASTAnalyzer();
31863
+ const sourceFile = analyzer.parseFile(content, filePath);
31864
+ const unreachableInfos = analyzer.analyzeUnreachableCode(sourceFile);
31865
+ for (const info of unreachableInfos) {
31866
+ if (info.isFalsePositive) {
31867
+ continue;
31868
+ }
31869
+ if (shouldIgnoreLine7(info.line, "dead_code", ignoreState)) {
31870
+ continue;
31871
+ }
31872
+ findings.push(
31873
+ createFinding(
31874
+ filePath,
31875
+ info.line,
31876
+ info.line,
31877
+ "dead_code",
31878
+ "medium",
31879
+ `Unreachable code after ${info.reason} statement`,
31880
+ info.code,
31881
+ "unreachable-code",
31882
+ 0.95,
31883
+ "static",
31884
+ "Remove unreachable code",
31885
+ { linesRemoved: 1 }
31886
+ )
31887
+ );
31888
+ }
31889
+ } catch {
31890
+ findings.push(...detectUnreachableCodeFallback(filePath, lines, ignoreState));
31891
+ }
31892
+ return findings;
31893
+ }
31894
+ function detectUnreachableCodeFallback(filePath, lines, ignoreState) {
29072
31895
  const findings = [];
29073
31896
  for (let i = 0; i < lines.length; i++) {
29074
31897
  const line = lines[i];
@@ -29093,7 +31916,8 @@ function detectUnreachableCode(filePath, content, lines, ignoreState) {
29093
31916
  "Unreachable code after return/throw/break",
29094
31917
  lines[j] || "",
29095
31918
  "unreachable-code",
29096
- 0.95,
31919
+ 0.85,
31920
+ // Lower confidence for fallback
29097
31921
  "static",
29098
31922
  "Remove unreachable code",
29099
31923
  { linesRemoved: 1 }
@@ -29583,7 +32407,7 @@ var RefactorDetector = class {
29583
32407
  findings.push(...detectConditionals(filePath, content, lines, ignoreState, this.config));
29584
32408
  break;
29585
32409
  case "dead_code":
29586
- findings.push(...detectDeadCode(filePath, content, lines, ignoreState, this.config));
32410
+ findings.push(...await detectDeadCode(filePath, content, lines, ignoreState, this.config));
29587
32411
  break;
29588
32412
  case "type_safety":
29589
32413
  findings.push(...detectTypeSafety(filePath, content, lines, ignoreState, this.config));
@@ -31916,7 +34740,7 @@ var NATIVE_MODULE_ERROR_PATTERNS = [
31916
34740
  "was compiled against a different Node.js version",
31917
34741
  "better_sqlite3.node"
31918
34742
  ];
31919
- var DEFAULT_CONFIG3 = {
34743
+ var DEFAULT_CONFIG4 = {
31920
34744
  dbPath: `${AX_PATHS.TASKS}/tasks.db`,
31921
34745
  maxPayloadBytes: 1024 * 1024,
31922
34746
  // 1MB
@@ -32082,7 +34906,7 @@ var TaskStore = class {
32082
34906
  stmtCountByStatus;
32083
34907
  stmtFindByHash;
32084
34908
  constructor(config = {}) {
32085
- this.config = { ...DEFAULT_CONFIG3, ...config };
34909
+ this.config = { ...DEFAULT_CONFIG4, ...config };
32086
34910
  this.db = DatabaseFactory.create(this.config.dbPath, {
32087
34911
  busyTimeout: this.config.busyTimeout,
32088
34912
  enableWal: true
@@ -32547,7 +35371,7 @@ var InMemoryTaskStore = class {
32547
35371
  config;
32548
35372
  closed = false;
32549
35373
  constructor(config = {}) {
32550
- this.config = { ...DEFAULT_CONFIG3, ...config };
35374
+ this.config = { ...DEFAULT_CONFIG4, ...config };
32551
35375
  logger.warn("Using in-memory task store fallback (SQLite unavailable)", {
32552
35376
  nodeVersion: process.version
32553
35377
  });
@@ -32789,7 +35613,7 @@ function createTaskStore(config) {
32789
35613
  // src/core/task-engine/engine.ts
32790
35614
  init_esm_shims();
32791
35615
  init_logger();
32792
- var DEFAULT_CONFIG4 = {
35616
+ var DEFAULT_CONFIG5 = {
32793
35617
  store: {},
32794
35618
  loopGuard: {},
32795
35619
  maxConcurrent: 50,
@@ -32820,10 +35644,10 @@ var TaskEngine = class extends EventEmitter {
32820
35644
  constructor(config = {}) {
32821
35645
  super();
32822
35646
  this.config = {
32823
- ...DEFAULT_CONFIG4,
35647
+ ...DEFAULT_CONFIG5,
32824
35648
  ...config,
32825
- store: { ...DEFAULT_CONFIG4.store, ...config.store },
32826
- loopGuard: { ...DEFAULT_CONFIG4.loopGuard, ...config.loopGuard }
35649
+ store: { ...DEFAULT_CONFIG5.store, ...config.store },
35650
+ loopGuard: { ...DEFAULT_CONFIG5.loopGuard, ...config.loopGuard }
32827
35651
  };
32828
35652
  this.loopGuard = createLoopGuard(this.config.loopGuard);
32829
35653
  this.store = createTaskStore(this.config.store);
@@ -33269,7 +36093,7 @@ var TaskEngine = class extends EventEmitter {
33269
36093
  }
33270
36094
  }
33271
36095
  sleep(ms, signal) {
33272
- return new Promise((resolve5, reject) => {
36096
+ return new Promise((resolve6, reject) => {
33273
36097
  if (signal?.aborted) {
33274
36098
  reject(new Error("Aborted"));
33275
36099
  return;
@@ -33280,7 +36104,7 @@ var TaskEngine = class extends EventEmitter {
33280
36104
  };
33281
36105
  const timeoutId = setTimeout(() => {
33282
36106
  signal?.removeEventListener("abort", abortHandler);
33283
- resolve5();
36107
+ resolve6();
33284
36108
  }, ms);
33285
36109
  signal?.addEventListener("abort", abortHandler, { once: true });
33286
36110
  });
@@ -33875,10 +36699,10 @@ var McpClient = class _McpClient extends EventEmitter {
33875
36699
  return;
33876
36700
  }
33877
36701
  if (this.state.status === "connecting") {
33878
- return new Promise((resolve5, reject) => {
36702
+ return new Promise((resolve6, reject) => {
33879
36703
  const onConnected = () => {
33880
36704
  this.off("error", onError);
33881
- resolve5();
36705
+ resolve6();
33882
36706
  };
33883
36707
  const onError = (err) => {
33884
36708
  this.off("connected", onConnected);
@@ -34053,14 +36877,14 @@ var McpClient = class _McpClient extends EventEmitter {
34053
36877
  method,
34054
36878
  params
34055
36879
  };
34056
- return new Promise((resolve5, reject) => {
36880
+ return new Promise((resolve6, reject) => {
34057
36881
  const timeoutMs = timeout ?? this.config.timeout;
34058
36882
  const timeoutHandle = setTimeout(() => {
34059
36883
  this.pendingRequests.delete(id);
34060
36884
  reject(new Error(`Request timed out after ${timeoutMs}ms: ${method}`));
34061
36885
  }, timeoutMs);
34062
36886
  this.pendingRequests.set(id, {
34063
- resolve: resolve5,
36887
+ resolve: resolve6,
34064
36888
  reject,
34065
36889
  timeout: timeoutHandle
34066
36890
  });
@@ -34263,7 +37087,7 @@ var ConnectionTimeoutError = class extends McpClientError {
34263
37087
 
34264
37088
  // src/providers/mcp/pool-manager.ts
34265
37089
  init_validation_limits();
34266
- var DEFAULT_CONFIG6 = {
37090
+ var DEFAULT_CONFIG7 = {
34267
37091
  maxConnectionsPerProvider: 2,
34268
37092
  idleTimeoutMs: TIMEOUTS.IDLE_CONNECTION,
34269
37093
  // 5 minutes
@@ -34284,7 +37108,7 @@ var McpClientPool = class extends EventEmitter {
34284
37108
  drainHandler;
34285
37109
  constructor(config = {}) {
34286
37110
  super();
34287
- this.config = { ...DEFAULT_CONFIG6, ...config };
37111
+ this.config = { ...DEFAULT_CONFIG7, ...config };
34288
37112
  this.startHealthChecks();
34289
37113
  this.startIdleCleanup();
34290
37114
  this.drainHandler = () => this.drain();
@@ -34556,15 +37380,15 @@ var McpClientPool = class extends EventEmitter {
34556
37380
  }
34557
37381
  }
34558
37382
  async waitForConnection(provider, pool) {
34559
- return new Promise((resolve5, reject) => {
37383
+ return new Promise((resolve6, reject) => {
34560
37384
  const timeout = setTimeout(() => {
34561
- const index = pool.waitQueue.findIndex((w) => w.resolve === resolve5);
37385
+ const index = pool.waitQueue.findIndex((w) => w.resolve === resolve6);
34562
37386
  if (index !== -1) {
34563
37387
  pool.waitQueue.splice(index, 1);
34564
37388
  }
34565
37389
  reject(new ConnectionTimeoutError(provider, this.config.acquireTimeoutMs));
34566
37390
  }, this.config.acquireTimeoutMs);
34567
- pool.waitQueue.push({ resolve: resolve5, reject, timeout });
37391
+ pool.waitQueue.push({ resolve: resolve6, reject, timeout });
34568
37392
  logger.debug("[MCP Pool] Waiting for connection", {
34569
37393
  provider,
34570
37394
  queuePosition: pool.waitQueue.length