@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.
- package/README.md +74 -85
- package/dist/index.js +12796 -8906
- package/dist/mcp/index.js +2981 -157
- 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((
|
|
871
|
+
return new Promise((resolve6, reject) => {
|
|
872
872
|
const timeoutId = setTimeout(() => {
|
|
873
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
1051
|
-
finalTimeoutId = setTimeout(
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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
|
-
|
|
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.
|
|
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:
|
|
15972
|
+
const { mkdir: mkdir6 } = await import('fs/promises');
|
|
14895
15973
|
const destDir = dirname3(destPath);
|
|
14896
|
-
await
|
|
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:
|
|
16130
|
+
const { writeFile: writeFile8 } = await import('fs/promises');
|
|
15053
16131
|
const json = pretty ? JSON.stringify(exportData, null, 2) : JSON.stringify(exportData);
|
|
15054
|
-
await
|
|
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:
|
|
15104
|
-
const content = await
|
|
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
|
|
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
|
-
|
|
16782
|
-
|
|
16783
|
-
|
|
16784
|
-
|
|
16785
|
-
|
|
16786
|
-
|
|
16787
|
-
|
|
16788
|
-
|
|
16789
|
-
|
|
16790
|
-
|
|
16791
|
-
|
|
16792
|
-
|
|
16793
|
-
|
|
16794
|
-
|
|
16795
|
-
|
|
16796
|
-
|
|
16797
|
-
|
|
16798
|
-
|
|
16799
|
-
|
|
16800
|
-
|
|
16801
|
-
|
|
16802
|
-
|
|
16803
|
-
|
|
16804
|
-
|
|
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
|
-
|
|
16820
|
-
|
|
16821
|
-
|
|
16822
|
-
|
|
16823
|
-
|
|
16824
|
-
|
|
16825
|
-
|
|
16826
|
-
|
|
16827
|
-
|
|
16828
|
-
|
|
16829
|
-
|
|
16830
|
-
|
|
16831
|
-
|
|
16832
|
-
|
|
16833
|
-
|
|
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
|
-
|
|
16843
|
-
|
|
16844
|
-
|
|
16845
|
-
|
|
16846
|
-
|
|
16847
|
-
|
|
16848
|
-
|
|
16849
|
-
|
|
16850
|
-
|
|
16851
|
-
|
|
16852
|
-
|
|
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.
|
|
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 += "##
|
|
16954
|
-
for (const mod of context.index.modules.slice(0,
|
|
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 += "##
|
|
16962
|
-
for (const [name, script] of Object.entries(context.commands).slice(0,
|
|
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
|
|
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 += "
|
|
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
|
|
17165
|
-
if (
|
|
17166
|
-
logger.debug("Memory search query
|
|
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
|
-
|
|
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:
|
|
17175
|
-
// Use
|
|
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:
|
|
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((
|
|
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
|
-
|
|
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
|
|
25138
|
-
// v12.
|
|
25139
|
-
//
|
|
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
|
|
25145
|
-
description: "setInterval() without .unref()
|
|
27610
|
+
name: "setInterval without cleanup",
|
|
27611
|
+
description: "setInterval() without clearInterval or .unref() may block process exit",
|
|
25146
27612
|
pattern: "setInterval\\s*\\(",
|
|
25147
|
-
|
|
25148
|
-
|
|
25149
|
-
|
|
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.
|
|
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.
|
|
25172
|
-
//
|
|
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
|
-
|
|
25180
|
-
|
|
25181
|
-
|
|
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
|
-
|
|
25367
|
-
|
|
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
|
-
|
|
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(
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: "
|
|
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.
|
|
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
|
|
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 = { ...
|
|
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 = { ...
|
|
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
|
|
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
|
-
...
|
|
35647
|
+
...DEFAULT_CONFIG5,
|
|
32824
35648
|
...config,
|
|
32825
|
-
store: { ...
|
|
32826
|
-
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((
|
|
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
|
-
|
|
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((
|
|
36702
|
+
return new Promise((resolve6, reject) => {
|
|
33879
36703
|
const onConnected = () => {
|
|
33880
36704
|
this.off("error", onError);
|
|
33881
|
-
|
|
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((
|
|
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:
|
|
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
|
|
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 = { ...
|
|
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((
|
|
37383
|
+
return new Promise((resolve6, reject) => {
|
|
34560
37384
|
const timeout = setTimeout(() => {
|
|
34561
|
-
const index = pool.waitQueue.findIndex((w) => w.resolve ===
|
|
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:
|
|
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
|