minovative-mind-cli 2.14.1 → 2.14.3
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 +40 -59
- package/dist/services/agent/slashCommands.js +144 -13
- package/dist/services/agent/toolLoop.js +11 -2
- package/dist/services/agent/types.d.ts +9 -2
- package/dist/services/agent-tools.d.ts +20 -1
- package/dist/services/agent-tools.js +272 -47
- package/dist/services/agent.js +43 -18
- package/dist/services/ai.d.ts +9 -0
- package/dist/services/ai.js +71 -21
- package/dist/services/chatHistoryService.d.ts +5 -0
- package/dist/services/contextAgent.d.ts +14 -6
- package/dist/services/contextAgent.js +36 -10
- package/dist/services/orchestration/investigationAgent.js +31 -7
- package/dist/services/orchestration/investigationCache.js +19 -9
- package/dist/services/orchestration/readCache.d.ts +1 -0
- package/dist/services/orchestration/readCache.js +5 -2
- package/dist/services/orchestration/scopedTools.js +37 -10
- package/dist/services/orchestration/subAgent.js +16 -2
- package/dist/services/proxyClient.d.ts +6 -0
- package/dist/services/proxyClient.js +24 -10
- package/dist/services/sessionSettings.d.ts +66 -0
- package/dist/services/sessionSettings.js +126 -0
- package/dist/services/userProfileService.d.ts +14 -0
- package/dist/services/userProfileService.js +105 -3
- package/dist/services/verificationService.js +24 -2
- package/dist/utils/analysisRunner.d.ts +120 -8
- package/dist/utils/analysisRunner.js +946 -125
- package/dist/utils/antiCheatingGuard.d.ts +21 -0
- package/dist/utils/antiCheatingGuard.js +554 -0
- package/dist/utils/contextPrompts.d.ts +39 -0
- package/dist/utils/contextPrompts.js +81 -9
- package/dist/utils/contextRanker.d.ts +216 -0
- package/dist/utils/contextRanker.js +603 -0
- package/dist/utils/dependencyTracer/modules/graph.d.ts +4 -1
- package/dist/utils/dependencyTracer/modules/graph.js +11 -0
- package/dist/utils/dependencyTracer/modules/types.d.ts +10 -0
- package/dist/utils/dependencyTracer.d.ts +40 -2
- package/dist/utils/dependencyTracer.js +95 -3
- package/dist/utils/fileReadCache.d.ts +58 -0
- package/dist/utils/fileReadCache.js +162 -0
- package/dist/utils/projectStorage.js +2 -1
- package/dist/utils/symbolExtractor.d.ts +12 -0
- package/dist/utils/symbolExtractor.js +111 -15
- package/dist/utils/systemPrompts.d.ts +4 -3
- package/dist/utils/systemPrompts.js +66 -8
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
|
@@ -1,9 +1,28 @@
|
|
|
1
|
+
export declare function invalidateDependencyGraph(workspaceRoot?: string): void;
|
|
2
|
+
export declare function clearDependencyGraphCache(): void;
|
|
3
|
+
export declare function getDependencyGraphCacheStats(): Readonly<{
|
|
4
|
+
hits: number;
|
|
5
|
+
misses: number;
|
|
6
|
+
invalidations: number;
|
|
7
|
+
cachedWorkspaces: number;
|
|
8
|
+
}>;
|
|
9
|
+
export declare function resetDependencyGraphCacheStats(): void;
|
|
1
10
|
export interface DependencyNode {
|
|
2
11
|
/** Files this file directly imports (forward/downstream) */
|
|
3
12
|
imports: Set<string>;
|
|
4
13
|
/** Files that directly import this file (reverse/upstream) */
|
|
5
14
|
importedBy: Set<string>;
|
|
6
15
|
}
|
|
16
|
+
export interface CentralityMetrics {
|
|
17
|
+
/** Number of files that directly import this file (hub dependency) */
|
|
18
|
+
inDegree: number;
|
|
19
|
+
/** Number of files this file directly imports */
|
|
20
|
+
outDegree: number;
|
|
21
|
+
/** Total direct connections (inDegree + outDegree) */
|
|
22
|
+
totalDegree: number;
|
|
23
|
+
/** Centrality score (weighted: inDegree * 1.5 + outDegree * 0.5) */
|
|
24
|
+
score: number;
|
|
25
|
+
}
|
|
7
26
|
export interface DependencyGraph {
|
|
8
27
|
/** What files does `filePath` directly import? */
|
|
9
28
|
getImports(filePath: string): string[];
|
|
@@ -13,6 +32,10 @@ export interface DependencyGraph {
|
|
|
13
32
|
getReverseDependencyTree(filePath: string, maxDepth?: number): string[];
|
|
14
33
|
/** Transitive: all files reachable via import chains, up to maxDepth */
|
|
15
34
|
getForwardDependencyTree(filePath: string, maxDepth?: number): string[];
|
|
35
|
+
/** Centrality metrics for a specific file */
|
|
36
|
+
getCentrality(filePath: string): CentralityMetrics;
|
|
37
|
+
/** Centrality metrics across all indexed files in the graph */
|
|
38
|
+
getAllCentrality(): Map<string, CentralityMetrics>;
|
|
16
39
|
/** The full raw graph for inspection */
|
|
17
40
|
readonly nodes: ReadonlyMap<string, DependencyNode>;
|
|
18
41
|
}
|
|
@@ -21,9 +44,13 @@ export interface DependencyGraph {
|
|
|
21
44
|
*
|
|
22
45
|
* Performance: This is pure regex + filesystem walking — no AST parsing.
|
|
23
46
|
* For a typical project (<5,000 source files), this completes in <1s.
|
|
24
|
-
* The graph is
|
|
47
|
+
* The graph is cached in-memory per workspaceRoot and automatically reused across
|
|
48
|
+
* subsequent queries until invalidated by file modifications.
|
|
49
|
+
*
|
|
50
|
+
* @param workspaceRoot - Root directory of the target project workspace
|
|
51
|
+
* @param forceRebuild - If true, bypasses the in-memory cache and forces a full rescan
|
|
25
52
|
*/
|
|
26
|
-
export declare function buildDependencyGraph(workspaceRoot: string): Promise<DependencyGraph>;
|
|
53
|
+
export declare function buildDependencyGraph(workspaceRoot: string, forceRebuild?: boolean): Promise<DependencyGraph>;
|
|
27
54
|
export interface FindDependenciesResult {
|
|
28
55
|
filePath: string;
|
|
29
56
|
forwardDeps: string[];
|
|
@@ -42,6 +69,17 @@ export declare function findDependencies(workspaceRoot: string, filePath: string
|
|
|
42
69
|
* suitable for feeding back to the LLM.
|
|
43
70
|
*/
|
|
44
71
|
export declare function formatDependencyResult(result: FindDependenciesResult): string;
|
|
72
|
+
/**
|
|
73
|
+
* Calculates graph centrality metrics for all nodes in a dependency graph.
|
|
74
|
+
*/
|
|
75
|
+
export declare function computeGraphCentrality(nodes: ReadonlyMap<string, DependencyNode> | Map<string, DependencyNode>): Map<string, CentralityMetrics>;
|
|
76
|
+
/**
|
|
77
|
+
* Returns top dependency hub files sorted descending by centrality score.
|
|
78
|
+
*/
|
|
79
|
+
export declare function getHubFiles(graph: DependencyGraph, limit?: number): Array<{
|
|
80
|
+
filePath: string;
|
|
81
|
+
metrics: CentralityMetrics;
|
|
82
|
+
}>;
|
|
45
83
|
/**
|
|
46
84
|
* Resets the tsconfig alias cache (useful between workspace changes).
|
|
47
85
|
*/
|
|
@@ -1,6 +1,47 @@
|
|
|
1
1
|
import { promises as fs } from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { EXCLUDED_EXTENSIONS } from './excludedExtensions.js';
|
|
4
|
+
import { debugLog } from './logger.js';
|
|
5
|
+
// ─── Dependency Graph Cache ──────────────────────────────────────────
|
|
6
|
+
const dependencyGraphCache = new Map();
|
|
7
|
+
const dependencyGraphCacheStats = {
|
|
8
|
+
hits: 0,
|
|
9
|
+
misses: 0,
|
|
10
|
+
invalidations: 0,
|
|
11
|
+
};
|
|
12
|
+
function normalizeWorkspaceKey(workspaceRoot) {
|
|
13
|
+
const resolved = path.resolve(workspaceRoot).replace(/\\/g, '/');
|
|
14
|
+
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
15
|
+
}
|
|
16
|
+
export function invalidateDependencyGraph(workspaceRoot) {
|
|
17
|
+
if (workspaceRoot) {
|
|
18
|
+
const key = normalizeWorkspaceKey(workspaceRoot);
|
|
19
|
+
if (dependencyGraphCache.delete(key)) {
|
|
20
|
+
dependencyGraphCacheStats.invalidations++;
|
|
21
|
+
debugLog(`[DependencyTracer] Invalidated graph cache for workspace: ${workspaceRoot}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
const count = dependencyGraphCache.size;
|
|
26
|
+
dependencyGraphCache.clear();
|
|
27
|
+
dependencyGraphCacheStats.invalidations += count;
|
|
28
|
+
debugLog(`[DependencyTracer] Cleared graph cache for all workspaces (${count})`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function clearDependencyGraphCache() {
|
|
32
|
+
invalidateDependencyGraph();
|
|
33
|
+
}
|
|
34
|
+
export function getDependencyGraphCacheStats() {
|
|
35
|
+
return {
|
|
36
|
+
...dependencyGraphCacheStats,
|
|
37
|
+
cachedWorkspaces: dependencyGraphCache.size,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export function resetDependencyGraphCacheStats() {
|
|
41
|
+
dependencyGraphCacheStats.hits = 0;
|
|
42
|
+
dependencyGraphCacheStats.misses = 0;
|
|
43
|
+
dependencyGraphCacheStats.invalidations = 0;
|
|
44
|
+
}
|
|
4
45
|
// ─── Language Profile Registry ───────────────────────────────────────
|
|
5
46
|
//
|
|
6
47
|
// Each profile defines regex patterns that extract import specifiers for
|
|
@@ -503,9 +544,24 @@ async function walkWorkspace(workspaceRoot) {
|
|
|
503
544
|
*
|
|
504
545
|
* Performance: This is pure regex + filesystem walking — no AST parsing.
|
|
505
546
|
* For a typical project (<5,000 source files), this completes in <1s.
|
|
506
|
-
* The graph is
|
|
547
|
+
* The graph is cached in-memory per workspaceRoot and automatically reused across
|
|
548
|
+
* subsequent queries until invalidated by file modifications.
|
|
549
|
+
*
|
|
550
|
+
* @param workspaceRoot - Root directory of the target project workspace
|
|
551
|
+
* @param forceRebuild - If true, bypasses the in-memory cache and forces a full rescan
|
|
507
552
|
*/
|
|
508
|
-
export async function buildDependencyGraph(workspaceRoot) {
|
|
553
|
+
export async function buildDependencyGraph(workspaceRoot, forceRebuild = false) {
|
|
554
|
+
const key = normalizeWorkspaceKey(workspaceRoot);
|
|
555
|
+
if (!forceRebuild) {
|
|
556
|
+
const cached = dependencyGraphCache.get(key);
|
|
557
|
+
if (cached) {
|
|
558
|
+
dependencyGraphCacheStats.hits++;
|
|
559
|
+
debugLog(`[DependencyTracer] Graph cache HIT for ${workspaceRoot} (${cached.graph.nodes.size} nodes)`);
|
|
560
|
+
return cached.graph;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
dependencyGraphCacheStats.misses++;
|
|
564
|
+
debugLog(`[DependencyTracer] Graph cache MISS, building graph for ${workspaceRoot}`);
|
|
509
565
|
const nodes = new Map();
|
|
510
566
|
function getOrCreate(filePath) {
|
|
511
567
|
let node = nodes.get(filePath);
|
|
@@ -540,7 +596,7 @@ export async function buildDependencyGraph(workspaceRoot) {
|
|
|
540
596
|
}
|
|
541
597
|
}));
|
|
542
598
|
// 3. Return the graph with query methods
|
|
543
|
-
|
|
599
|
+
const graph = {
|
|
544
600
|
nodes,
|
|
545
601
|
getImports(filePath) {
|
|
546
602
|
return Array.from(nodes.get(filePath)?.imports || []);
|
|
@@ -554,7 +610,20 @@ export async function buildDependencyGraph(workspaceRoot) {
|
|
|
554
610
|
getForwardDependencyTree(filePath, maxDepth = 3) {
|
|
555
611
|
return bfsTraverse(nodes, filePath, 'imports', maxDepth);
|
|
556
612
|
},
|
|
613
|
+
getCentrality(filePath) {
|
|
614
|
+
const node = nodes.get(filePath);
|
|
615
|
+
const inDegree = node?.importedBy.size ?? 0;
|
|
616
|
+
const outDegree = node?.imports.size ?? 0;
|
|
617
|
+
const totalDegree = inDegree + outDegree;
|
|
618
|
+
const score = inDegree * 1.5 + outDegree * 0.5;
|
|
619
|
+
return { inDegree, outDegree, totalDegree, score };
|
|
620
|
+
},
|
|
621
|
+
getAllCentrality() {
|
|
622
|
+
return computeGraphCentrality(nodes);
|
|
623
|
+
},
|
|
557
624
|
};
|
|
625
|
+
dependencyGraphCache.set(key, { graph, timestamp: Date.now() });
|
|
626
|
+
return graph;
|
|
558
627
|
}
|
|
559
628
|
/**
|
|
560
629
|
* BFS traversal of the dependency graph in a given direction.
|
|
@@ -652,6 +721,29 @@ export function formatDependencyResult(result) {
|
|
|
652
721
|
lines.push(`Total impact radius: ${totalImpact} file(s)`);
|
|
653
722
|
return lines.join('\n');
|
|
654
723
|
}
|
|
724
|
+
/**
|
|
725
|
+
* Calculates graph centrality metrics for all nodes in a dependency graph.
|
|
726
|
+
*/
|
|
727
|
+
export function computeGraphCentrality(nodes) {
|
|
728
|
+
const result = new Map();
|
|
729
|
+
for (const [file, node] of nodes.entries()) {
|
|
730
|
+
const inDegree = node.importedBy.size;
|
|
731
|
+
const outDegree = node.imports.size;
|
|
732
|
+
const totalDegree = inDegree + outDegree;
|
|
733
|
+
const score = inDegree * 1.5 + outDegree * 0.5;
|
|
734
|
+
result.set(file, { inDegree, outDegree, totalDegree, score });
|
|
735
|
+
}
|
|
736
|
+
return result;
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Returns top dependency hub files sorted descending by centrality score.
|
|
740
|
+
*/
|
|
741
|
+
export function getHubFiles(graph, limit) {
|
|
742
|
+
const allCentrality = graph.getAllCentrality();
|
|
743
|
+
const entries = Array.from(allCentrality.entries()).map(([filePath, metrics]) => ({ filePath, metrics }));
|
|
744
|
+
entries.sort((a, b) => b.metrics.score - a.metrics.score || b.metrics.inDegree - a.metrics.inDegree);
|
|
745
|
+
return typeof limit === 'number' && limit > 0 ? entries.slice(0, limit) : entries;
|
|
746
|
+
}
|
|
655
747
|
/**
|
|
656
748
|
* Resets the tsconfig alias cache (useful between workspace changes).
|
|
657
749
|
*/
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export interface CachedFileEntry {
|
|
2
|
+
content: string;
|
|
3
|
+
mtimeMs: number;
|
|
4
|
+
size: number;
|
|
5
|
+
lastAccessed: number;
|
|
6
|
+
byteLength: number;
|
|
7
|
+
}
|
|
8
|
+
export interface FileReadCacheStats {
|
|
9
|
+
hits: number;
|
|
10
|
+
misses: number;
|
|
11
|
+
invalidations: number;
|
|
12
|
+
currentEntries: number;
|
|
13
|
+
currentBytes: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Normalizes a file path to create a deterministic, cross-platform cache key.
|
|
17
|
+
* Resolves relative segments, standardizes to forward slashes, and on Windows
|
|
18
|
+
* lowercases the path to guarantee case-insensitive parity.
|
|
19
|
+
*
|
|
20
|
+
* @param filePath - The absolute or relative file path to normalize.
|
|
21
|
+
* @returns Standardized cache key string.
|
|
22
|
+
*/
|
|
23
|
+
export declare function normalizeCacheKey(filePath: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* Retrieves file content with in-turn caching and strict mtime validation.
|
|
26
|
+
*
|
|
27
|
+
* Behavior:
|
|
28
|
+
* 1. Checks `fs.stat(absPath)`.
|
|
29
|
+
* 2. If file exceeds 1MB, reads from disk directly without polluting memory cache.
|
|
30
|
+
* 3. If cached with identical `mtimeMs` and `size`, returns the cached string ($O(1)$ disk avoidance).
|
|
31
|
+
* 4. If modified or uncached, reads from disk, stores in cache with LRU eviction, and returns.
|
|
32
|
+
*
|
|
33
|
+
* @param absPath - Absolute path to the file on disk.
|
|
34
|
+
* @returns The string content of the file.
|
|
35
|
+
*/
|
|
36
|
+
export declare function getCachedFileContent(absPath: string): Promise<string>;
|
|
37
|
+
/**
|
|
38
|
+
* Invalidates the cached entry for a specific file path.
|
|
39
|
+
* Must be invoked whenever `modify_file`, `write_file`, `delete_file`, or `rename_file`
|
|
40
|
+
* modifies the target on disk.
|
|
41
|
+
*
|
|
42
|
+
* @param filePath - The file path that was modified.
|
|
43
|
+
*/
|
|
44
|
+
export declare function invalidateFileReadCache(filePath: string): void;
|
|
45
|
+
/**
|
|
46
|
+
* Completely clears the in-memory file read cache.
|
|
47
|
+
* Must be invoked when arbitrary shell commands run (e.g. `npm run build`, `git checkout`),
|
|
48
|
+
* or when `/revert` restores previous file trees.
|
|
49
|
+
*/
|
|
50
|
+
export declare function clearFileReadCache(): void;
|
|
51
|
+
/**
|
|
52
|
+
* Returns diagnostic statistics for the file read cache.
|
|
53
|
+
*/
|
|
54
|
+
export declare function getFileReadCacheStats(): Readonly<FileReadCacheStats>;
|
|
55
|
+
/**
|
|
56
|
+
* Resets the telemetry counters (useful for unit tests).
|
|
57
|
+
*/
|
|
58
|
+
export declare function resetFileReadCacheStats(): void;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { debugLog } from './logger.js';
|
|
4
|
+
/**
|
|
5
|
+
* Maximum number of distinct files to keep in the in-memory read cache.
|
|
6
|
+
*/
|
|
7
|
+
const MAX_CACHED_FILES = 200;
|
|
8
|
+
/**
|
|
9
|
+
* Maximum aggregate memory size (in bytes) allowed for cached file contents (15MB).
|
|
10
|
+
*/
|
|
11
|
+
const MAX_TOTAL_BYTES = 15 * 1024 * 1024;
|
|
12
|
+
/**
|
|
13
|
+
* Single file size ceiling (1MB). Files larger than this bypass the in-memory
|
|
14
|
+
* cache to prevent memory bloat from huge generated assets.
|
|
15
|
+
*/
|
|
16
|
+
const MAX_SINGLE_FILE_BYTES = 1 * 1024 * 1024;
|
|
17
|
+
const cache = new Map();
|
|
18
|
+
let currentTotalBytes = 0;
|
|
19
|
+
const stats = {
|
|
20
|
+
hits: 0,
|
|
21
|
+
misses: 0,
|
|
22
|
+
invalidations: 0,
|
|
23
|
+
currentEntries: 0,
|
|
24
|
+
currentBytes: 0,
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Normalizes a file path to create a deterministic, cross-platform cache key.
|
|
28
|
+
* Resolves relative segments, standardizes to forward slashes, and on Windows
|
|
29
|
+
* lowercases the path to guarantee case-insensitive parity.
|
|
30
|
+
*
|
|
31
|
+
* @param filePath - The absolute or relative file path to normalize.
|
|
32
|
+
* @returns Standardized cache key string.
|
|
33
|
+
*/
|
|
34
|
+
export function normalizeCacheKey(filePath) {
|
|
35
|
+
const resolved = path.resolve(filePath).replace(/\\/g, '/');
|
|
36
|
+
return process.platform === 'win32' ? resolved.toLowerCase() : resolved;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Evicts the least recently accessed cache entries until the cache satisfies
|
|
40
|
+
* both the max file count and max byte size constraints.
|
|
41
|
+
*/
|
|
42
|
+
function evictOldestEntries(extraBytesNeeded = 0) {
|
|
43
|
+
while ((cache.size >= MAX_CACHED_FILES || currentTotalBytes + extraBytesNeeded > MAX_TOTAL_BYTES) && cache.size > 0) {
|
|
44
|
+
let oldestKey = null;
|
|
45
|
+
let oldestTime = Infinity;
|
|
46
|
+
for (const [key, entry] of cache.entries()) {
|
|
47
|
+
if (entry.lastAccessed < oldestTime) {
|
|
48
|
+
oldestTime = entry.lastAccessed;
|
|
49
|
+
oldestKey = key;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (!oldestKey)
|
|
53
|
+
break;
|
|
54
|
+
const evicted = cache.get(oldestKey);
|
|
55
|
+
if (evicted) {
|
|
56
|
+
currentTotalBytes -= evicted.byteLength;
|
|
57
|
+
}
|
|
58
|
+
cache.delete(oldestKey);
|
|
59
|
+
debugLog(`[FileReadCache] Evicted LRU entry: ${oldestKey}`);
|
|
60
|
+
}
|
|
61
|
+
stats.currentEntries = cache.size;
|
|
62
|
+
stats.currentBytes = currentTotalBytes;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Retrieves file content with in-turn caching and strict mtime validation.
|
|
66
|
+
*
|
|
67
|
+
* Behavior:
|
|
68
|
+
* 1. Checks `fs.stat(absPath)`.
|
|
69
|
+
* 2. If file exceeds 1MB, reads from disk directly without polluting memory cache.
|
|
70
|
+
* 3. If cached with identical `mtimeMs` and `size`, returns the cached string ($O(1)$ disk avoidance).
|
|
71
|
+
* 4. If modified or uncached, reads from disk, stores in cache with LRU eviction, and returns.
|
|
72
|
+
*
|
|
73
|
+
* @param absPath - Absolute path to the file on disk.
|
|
74
|
+
* @returns The string content of the file.
|
|
75
|
+
*/
|
|
76
|
+
export async function getCachedFileContent(absPath) {
|
|
77
|
+
const stat = await fs.stat(absPath);
|
|
78
|
+
if (stat.size > MAX_SINGLE_FILE_BYTES) {
|
|
79
|
+
debugLog(`[FileReadCache] Bypassing cache for large file (${stat.size} bytes > 1MB): ${absPath}`);
|
|
80
|
+
stats.misses++;
|
|
81
|
+
return fs.readFile(absPath, 'utf-8');
|
|
82
|
+
}
|
|
83
|
+
const key = normalizeCacheKey(absPath);
|
|
84
|
+
const existing = cache.get(key);
|
|
85
|
+
if (existing && existing.mtimeMs === stat.mtimeMs && existing.size === stat.size) {
|
|
86
|
+
existing.lastAccessed = Date.now();
|
|
87
|
+
stats.hits++;
|
|
88
|
+
debugLog(`[FileReadCache] HIT (${stat.size} bytes): ${absPath}`);
|
|
89
|
+
return existing.content;
|
|
90
|
+
}
|
|
91
|
+
stats.misses++;
|
|
92
|
+
debugLog(`[FileReadCache] MISS (reading disk): ${absPath}`);
|
|
93
|
+
const content = await fs.readFile(absPath, 'utf-8');
|
|
94
|
+
const byteLength = Buffer.byteLength(content, 'utf8');
|
|
95
|
+
if (existing) {
|
|
96
|
+
currentTotalBytes -= existing.byteLength;
|
|
97
|
+
}
|
|
98
|
+
evictOldestEntries(byteLength);
|
|
99
|
+
cache.set(key, {
|
|
100
|
+
content,
|
|
101
|
+
mtimeMs: stat.mtimeMs,
|
|
102
|
+
size: stat.size,
|
|
103
|
+
lastAccessed: Date.now(),
|
|
104
|
+
byteLength,
|
|
105
|
+
});
|
|
106
|
+
currentTotalBytes += byteLength;
|
|
107
|
+
stats.currentEntries = cache.size;
|
|
108
|
+
stats.currentBytes = currentTotalBytes;
|
|
109
|
+
return content;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Invalidates the cached entry for a specific file path.
|
|
113
|
+
* Must be invoked whenever `modify_file`, `write_file`, `delete_file`, or `rename_file`
|
|
114
|
+
* modifies the target on disk.
|
|
115
|
+
*
|
|
116
|
+
* @param filePath - The file path that was modified.
|
|
117
|
+
*/
|
|
118
|
+
export function invalidateFileReadCache(filePath) {
|
|
119
|
+
const key = normalizeCacheKey(filePath);
|
|
120
|
+
const existing = cache.get(key);
|
|
121
|
+
if (existing) {
|
|
122
|
+
currentTotalBytes -= existing.byteLength;
|
|
123
|
+
cache.delete(key);
|
|
124
|
+
stats.invalidations++;
|
|
125
|
+
stats.currentEntries = cache.size;
|
|
126
|
+
stats.currentBytes = currentTotalBytes;
|
|
127
|
+
debugLog(`[FileReadCache] INVALIDATED entry: ${filePath}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Completely clears the in-memory file read cache.
|
|
132
|
+
* Must be invoked when arbitrary shell commands run (e.g. `npm run build`, `git checkout`),
|
|
133
|
+
* or when `/revert` restores previous file trees.
|
|
134
|
+
*/
|
|
135
|
+
export function clearFileReadCache() {
|
|
136
|
+
const count = cache.size;
|
|
137
|
+
cache.clear();
|
|
138
|
+
currentTotalBytes = 0;
|
|
139
|
+
stats.currentEntries = 0;
|
|
140
|
+
stats.currentBytes = 0;
|
|
141
|
+
debugLog(`[FileReadCache] CLEARED ${count} entries from cache`);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Returns diagnostic statistics for the file read cache.
|
|
145
|
+
*/
|
|
146
|
+
export function getFileReadCacheStats() {
|
|
147
|
+
return {
|
|
148
|
+
...stats,
|
|
149
|
+
currentEntries: cache.size,
|
|
150
|
+
currentBytes: currentTotalBytes,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Resets the telemetry counters (useful for unit tests).
|
|
155
|
+
*/
|
|
156
|
+
export function resetFileReadCacheStats() {
|
|
157
|
+
stats.hits = 0;
|
|
158
|
+
stats.misses = 0;
|
|
159
|
+
stats.invalidations = 0;
|
|
160
|
+
stats.currentEntries = cache.size;
|
|
161
|
+
stats.currentBytes = currentTotalBytes;
|
|
162
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import * as pc from 'picocolors';
|
|
4
|
-
import { buildDependencyGraph } from './dependencyTracer.js';
|
|
4
|
+
import { buildDependencyGraph, invalidateDependencyGraph } from './dependencyTracer.js';
|
|
5
5
|
/**
|
|
6
6
|
* Gets the path to the project's .minovativemind storage directory.
|
|
7
7
|
*/
|
|
@@ -236,6 +236,7 @@ export async function invalidateCacheForDependents(workspaceRoot, changedFiles)
|
|
|
236
236
|
if (updated) {
|
|
237
237
|
writeCache(workspaceRoot, 'context_cache.json', cachedContext);
|
|
238
238
|
}
|
|
239
|
+
invalidateDependencyGraph(workspaceRoot);
|
|
239
240
|
try {
|
|
240
241
|
const { invalidateFilesFromInvestigationCache } = await import('../services/orchestration/investigationCache.js');
|
|
241
242
|
await invalidateFilesFromInvestigationCache(workspaceRoot, changedFiles);
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
export interface SymbolExtractorCacheStats {
|
|
2
|
+
outlineHits: number;
|
|
3
|
+
outlineMisses: number;
|
|
4
|
+
symbolsHits: number;
|
|
5
|
+
symbolsMisses: number;
|
|
6
|
+
indexHits: number;
|
|
7
|
+
indexMisses: number;
|
|
8
|
+
size: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function clearSymbolExtractorCache(): void;
|
|
11
|
+
export declare function getSymbolExtractorCacheStats(): Readonly<SymbolExtractorCacheStats>;
|
|
12
|
+
export declare function resetSymbolExtractorCacheStats(): void;
|
|
1
13
|
/**
|
|
2
14
|
* Supported classification kinds for extracted AST symbols and definition chunks.
|
|
3
15
|
*/
|
|
@@ -1,4 +1,57 @@
|
|
|
1
1
|
import * as path from 'node:path';
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
// ─── AST Symbol Cache ────────────────────────────────────────────────
|
|
4
|
+
const MAX_SYMBOL_CACHE_ENTRIES = 1000;
|
|
5
|
+
const symbolCache = new Map();
|
|
6
|
+
const symbolCacheStats = {
|
|
7
|
+
outlineHits: 0,
|
|
8
|
+
outlineMisses: 0,
|
|
9
|
+
symbolsHits: 0,
|
|
10
|
+
symbolsMisses: 0,
|
|
11
|
+
indexHits: 0,
|
|
12
|
+
indexMisses: 0,
|
|
13
|
+
};
|
|
14
|
+
function hashContent(content) {
|
|
15
|
+
return crypto.createHash('sha256').update(content).digest('hex').substring(0, 16);
|
|
16
|
+
}
|
|
17
|
+
function getFromSymbolCache(key) {
|
|
18
|
+
if (!symbolCache.has(key))
|
|
19
|
+
return undefined;
|
|
20
|
+
const value = symbolCache.get(key);
|
|
21
|
+
// Refresh LRU order
|
|
22
|
+
symbolCache.delete(key);
|
|
23
|
+
symbolCache.set(key, value);
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
function setToSymbolCache(key, value) {
|
|
27
|
+
if (symbolCache.has(key)) {
|
|
28
|
+
symbolCache.delete(key);
|
|
29
|
+
}
|
|
30
|
+
else if (symbolCache.size >= MAX_SYMBOL_CACHE_ENTRIES) {
|
|
31
|
+
const oldestKey = symbolCache.keys().next().value;
|
|
32
|
+
if (oldestKey !== undefined) {
|
|
33
|
+
symbolCache.delete(oldestKey);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
symbolCache.set(key, value);
|
|
37
|
+
}
|
|
38
|
+
export function clearSymbolExtractorCache() {
|
|
39
|
+
symbolCache.clear();
|
|
40
|
+
}
|
|
41
|
+
export function getSymbolExtractorCacheStats() {
|
|
42
|
+
return {
|
|
43
|
+
...symbolCacheStats,
|
|
44
|
+
size: symbolCache.size,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function resetSymbolExtractorCacheStats() {
|
|
48
|
+
symbolCacheStats.outlineHits = 0;
|
|
49
|
+
symbolCacheStats.outlineMisses = 0;
|
|
50
|
+
symbolCacheStats.symbolsHits = 0;
|
|
51
|
+
symbolCacheStats.symbolsMisses = 0;
|
|
52
|
+
symbolCacheStats.indexHits = 0;
|
|
53
|
+
symbolCacheStats.indexMisses = 0;
|
|
54
|
+
}
|
|
2
55
|
/**
|
|
3
56
|
* Estimates token consumption for a given string using a standard ~3.8 characters per token heuristic.
|
|
4
57
|
*
|
|
@@ -204,8 +257,14 @@ function findPrecedingContext(lines, declarationIndex, ext) {
|
|
|
204
257
|
const isDecorator = prevLine.startsWith('@');
|
|
205
258
|
const isPythonComment = (ext === '.py' || ext === '.pyi') && prevLine.startsWith('#');
|
|
206
259
|
const isLineComment = prevLine.startsWith('//');
|
|
207
|
-
const isRustDocOrAttr =
|
|
208
|
-
if (isDecorator ||
|
|
260
|
+
const isRustDocOrAttr = ext === '.rs' && (prevLine.startsWith('///') || prevLine.startsWith('//!') || prevLine.startsWith('#['));
|
|
261
|
+
if (isDecorator ||
|
|
262
|
+
isPythonComment ||
|
|
263
|
+
isLineComment ||
|
|
264
|
+
isJsDocEnd ||
|
|
265
|
+
isJsDocLine ||
|
|
266
|
+
isJsDocStart ||
|
|
267
|
+
isRustDocOrAttr) {
|
|
209
268
|
start--;
|
|
210
269
|
if (isJsDocStart) {
|
|
211
270
|
insideJsDoc = false;
|
|
@@ -227,7 +286,8 @@ function findPrecedingContext(lines, declarationIndex, ext) {
|
|
|
227
286
|
* @returns True if the line contains valid code characters outside comments/strings
|
|
228
287
|
*/
|
|
229
288
|
function scanLineValidity(line, state) {
|
|
230
|
-
let j = 0
|
|
289
|
+
let j = 0;
|
|
290
|
+
let lineHasValidCode = false;
|
|
231
291
|
while (j < line.length) {
|
|
232
292
|
if (!state.inMultiLineComment && state.inMultiLineString) {
|
|
233
293
|
if (line[j] === '\\') {
|
|
@@ -357,7 +417,11 @@ function findBlockEnd(lines, i, ext) {
|
|
|
357
417
|
processLineChars(lines[j], parseState);
|
|
358
418
|
if (parseState.foundOpen && parseState.braces === 0 && parseState.brackets === 0 && parseState.parens === 0)
|
|
359
419
|
break;
|
|
360
|
-
if (!parseState.foundOpen &&
|
|
420
|
+
if (!parseState.foundOpen &&
|
|
421
|
+
j > i &&
|
|
422
|
+
parseState.braces === 0 &&
|
|
423
|
+
parseState.brackets === 0 &&
|
|
424
|
+
parseState.parens === 0) {
|
|
361
425
|
const prevLine = lines[j - 1].trim();
|
|
362
426
|
if (!prevLine.endsWith(',') && !prevLine.endsWith('.')) {
|
|
363
427
|
endIndex = j - 1;
|
|
@@ -421,7 +485,8 @@ export function extractSymbolMetadata(content, filePath, targetElements) {
|
|
|
421
485
|
for (let i = 0; i < lines.length; i++) {
|
|
422
486
|
const startsInMulti = scanState.inMultiLineComment || scanState.inMultiLineString !== null;
|
|
423
487
|
const lineHasValidCode = scanLineValidity(lines[i], scanState);
|
|
424
|
-
if (startsInMulti ||
|
|
488
|
+
if (startsInMulti ||
|
|
489
|
+
(!lineHasValidCode && (scanState.inMultiLineComment || scanState.inMultiLineString !== null))) {
|
|
425
490
|
validLines[i] = false;
|
|
426
491
|
}
|
|
427
492
|
}
|
|
@@ -447,7 +512,6 @@ export function extractSymbolMetadata(content, filePath, targetElements) {
|
|
|
447
512
|
// 1. If target specified a parent (e.g. Class.method)
|
|
448
513
|
if (symTarget.parentName) {
|
|
449
514
|
if (symTarget.regex.test(line)) {
|
|
450
|
-
const parentStartIndex = findPrecedingContext(lines, i, ext);
|
|
451
515
|
const parentEndIndex = findBlockEnd(lines, i, ext);
|
|
452
516
|
// Scan inside the parent body for the member
|
|
453
517
|
for (let m = i + 1; m <= parentEndIndex; m++) {
|
|
@@ -513,6 +577,13 @@ export function extractSymbolMetadata(content, filePath, targetElements) {
|
|
|
513
577
|
export function extractSymbolIndex(content, filePath) {
|
|
514
578
|
if (!content || !content.trim())
|
|
515
579
|
return [];
|
|
580
|
+
const key = `index:${filePath}:${hashContent(content)}`;
|
|
581
|
+
const cached = getFromSymbolCache(key);
|
|
582
|
+
if (cached !== undefined) {
|
|
583
|
+
symbolCacheStats.indexHits++;
|
|
584
|
+
return cached;
|
|
585
|
+
}
|
|
586
|
+
symbolCacheStats.indexMisses++;
|
|
516
587
|
const ext = path.extname(filePath).toLowerCase();
|
|
517
588
|
const lines = content.split('\n');
|
|
518
589
|
const symbols = [];
|
|
@@ -826,6 +897,7 @@ export function extractSymbolIndex(content, filePath) {
|
|
|
826
897
|
}
|
|
827
898
|
i++;
|
|
828
899
|
}
|
|
900
|
+
setToSymbolCache(key, symbols);
|
|
829
901
|
return symbols;
|
|
830
902
|
}
|
|
831
903
|
/**
|
|
@@ -895,12 +967,19 @@ export function chunkDefinitions(content, filePath, options) {
|
|
|
895
967
|
export function extractSymbols(content, filePath, targetElements, options) {
|
|
896
968
|
if (!targetElements || targetElements.length === 0)
|
|
897
969
|
return content;
|
|
898
|
-
const
|
|
970
|
+
const sortedTargets = targetElements.slice().sort().join(',');
|
|
971
|
+
const key = `symbols:${filePath}:${hashContent(content)}:${sortedTargets}:${options?.maxLinesPerSymbol ?? ''}`;
|
|
972
|
+
const cached = getFromSymbolCache(key);
|
|
973
|
+
if (cached !== undefined) {
|
|
974
|
+
symbolCacheStats.symbolsHits++;
|
|
975
|
+
return cached;
|
|
976
|
+
}
|
|
977
|
+
symbolCacheStats.symbolsMisses++;
|
|
899
978
|
const lines = content.split('\n');
|
|
900
979
|
const linesToKeep = new Set();
|
|
901
980
|
const metadata = extractSymbolMetadata(content, filePath, targetElements);
|
|
902
981
|
for (const meta of metadata) {
|
|
903
|
-
|
|
982
|
+
const start = meta.startLine;
|
|
904
983
|
let end = meta.endLine;
|
|
905
984
|
if (options?.maxLinesPerSymbol && end - start + 1 > options.maxLinesPerSymbol) {
|
|
906
985
|
end = start + options.maxLinesPerSymbol - 1;
|
|
@@ -919,7 +998,9 @@ export function extractSymbols(content, filePath, targetElements, options) {
|
|
|
919
998
|
output.push(lines[lineNum]);
|
|
920
999
|
previousLineNum = lineNum;
|
|
921
1000
|
}
|
|
922
|
-
|
|
1001
|
+
const result = output.join('\n');
|
|
1002
|
+
setToSymbolCache(key, result);
|
|
1003
|
+
return result;
|
|
923
1004
|
}
|
|
924
1005
|
/**
|
|
925
1006
|
* Extracts a compact declarations outline for TypeScript/JavaScript source files.
|
|
@@ -1312,7 +1393,7 @@ function extractPythonOutline(lines) {
|
|
|
1312
1393
|
if (cTrim.startsWith('"""') || cTrim.startsWith("'''")) {
|
|
1313
1394
|
const delim = cTrim.startsWith('"""') ? '"""' : "'''";
|
|
1314
1395
|
output.push(cLine);
|
|
1315
|
-
if (cTrim.length > 3 && cTrim.endsWith(delim) && cTrim.
|
|
1396
|
+
if (cTrim.length > 3 && cTrim.endsWith(delim) && cTrim.includes(delim, 3)) {
|
|
1316
1397
|
// single line docstring
|
|
1317
1398
|
}
|
|
1318
1399
|
else {
|
|
@@ -1844,8 +1925,16 @@ function extractRustOutline(lines) {
|
|
|
1844
1925
|
export function extractDeclarationsOutline(content, filePath) {
|
|
1845
1926
|
if (!content || !content.trim())
|
|
1846
1927
|
return '';
|
|
1928
|
+
const key = `outline:${filePath}:${hashContent(content)}`;
|
|
1929
|
+
const cached = getFromSymbolCache(key);
|
|
1930
|
+
if (cached !== undefined) {
|
|
1931
|
+
symbolCacheStats.outlineHits++;
|
|
1932
|
+
return cached;
|
|
1933
|
+
}
|
|
1934
|
+
symbolCacheStats.outlineMisses++;
|
|
1847
1935
|
const ext = path.extname(filePath).toLowerCase();
|
|
1848
1936
|
const lines = content.split('\n');
|
|
1937
|
+
let result = '';
|
|
1849
1938
|
switch (ext) {
|
|
1850
1939
|
case '.ts':
|
|
1851
1940
|
case '.tsx':
|
|
@@ -1855,15 +1944,22 @@ export function extractDeclarationsOutline(content, filePath) {
|
|
|
1855
1944
|
case '.cjs':
|
|
1856
1945
|
case '.mts':
|
|
1857
1946
|
case '.cts':
|
|
1858
|
-
|
|
1947
|
+
result = extractTsJsOutline(lines);
|
|
1948
|
+
break;
|
|
1859
1949
|
case '.py':
|
|
1860
1950
|
case '.pyi':
|
|
1861
|
-
|
|
1951
|
+
result = extractPythonOutline(lines);
|
|
1952
|
+
break;
|
|
1862
1953
|
case '.go':
|
|
1863
|
-
|
|
1954
|
+
result = extractGoOutline(lines);
|
|
1955
|
+
break;
|
|
1864
1956
|
case '.rs':
|
|
1865
|
-
|
|
1957
|
+
result = extractRustOutline(lines);
|
|
1958
|
+
break;
|
|
1866
1959
|
default:
|
|
1867
|
-
|
|
1960
|
+
result = content.trim();
|
|
1961
|
+
break;
|
|
1868
1962
|
}
|
|
1963
|
+
setToSymbolCache(key, result);
|
|
1964
|
+
return result;
|
|
1869
1965
|
}
|