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.
Files changed (47) hide show
  1. package/README.md +40 -59
  2. package/dist/services/agent/slashCommands.js +144 -13
  3. package/dist/services/agent/toolLoop.js +11 -2
  4. package/dist/services/agent/types.d.ts +9 -2
  5. package/dist/services/agent-tools.d.ts +20 -1
  6. package/dist/services/agent-tools.js +272 -47
  7. package/dist/services/agent.js +43 -18
  8. package/dist/services/ai.d.ts +9 -0
  9. package/dist/services/ai.js +71 -21
  10. package/dist/services/chatHistoryService.d.ts +5 -0
  11. package/dist/services/contextAgent.d.ts +14 -6
  12. package/dist/services/contextAgent.js +36 -10
  13. package/dist/services/orchestration/investigationAgent.js +31 -7
  14. package/dist/services/orchestration/investigationCache.js +19 -9
  15. package/dist/services/orchestration/readCache.d.ts +1 -0
  16. package/dist/services/orchestration/readCache.js +5 -2
  17. package/dist/services/orchestration/scopedTools.js +37 -10
  18. package/dist/services/orchestration/subAgent.js +16 -2
  19. package/dist/services/proxyClient.d.ts +6 -0
  20. package/dist/services/proxyClient.js +24 -10
  21. package/dist/services/sessionSettings.d.ts +66 -0
  22. package/dist/services/sessionSettings.js +126 -0
  23. package/dist/services/userProfileService.d.ts +14 -0
  24. package/dist/services/userProfileService.js +105 -3
  25. package/dist/services/verificationService.js +24 -2
  26. package/dist/utils/analysisRunner.d.ts +120 -8
  27. package/dist/utils/analysisRunner.js +946 -125
  28. package/dist/utils/antiCheatingGuard.d.ts +21 -0
  29. package/dist/utils/antiCheatingGuard.js +554 -0
  30. package/dist/utils/contextPrompts.d.ts +39 -0
  31. package/dist/utils/contextPrompts.js +81 -9
  32. package/dist/utils/contextRanker.d.ts +216 -0
  33. package/dist/utils/contextRanker.js +603 -0
  34. package/dist/utils/dependencyTracer/modules/graph.d.ts +4 -1
  35. package/dist/utils/dependencyTracer/modules/graph.js +11 -0
  36. package/dist/utils/dependencyTracer/modules/types.d.ts +10 -0
  37. package/dist/utils/dependencyTracer.d.ts +40 -2
  38. package/dist/utils/dependencyTracer.js +95 -3
  39. package/dist/utils/fileReadCache.d.ts +58 -0
  40. package/dist/utils/fileReadCache.js +162 -0
  41. package/dist/utils/projectStorage.js +2 -1
  42. package/dist/utils/symbolExtractor.d.ts +12 -0
  43. package/dist/utils/symbolExtractor.js +111 -15
  44. package/dist/utils/systemPrompts.d.ts +4 -3
  45. package/dist/utils/systemPrompts.js +66 -8
  46. package/oclif.manifest.json +1 -1
  47. package/package.json +1 -1
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { changeLogger } from '../services/changeLogger.js';
7
7
  import { extractDeclarationsOutline } from './symbolExtractor.js';
8
+ import { rankFilesForBudget } from './contextRanker.js';
8
9
  /**
9
10
  * Standard character and token budgeting constants for context injections.
10
11
  */
@@ -15,6 +16,10 @@ export const CONTEXT_BUDGET_CONFIG = {
15
16
  DEFAULT_SCOPED_MAX_CHARS: 120_000,
16
17
  /** Estimated characters per token heuristic */
17
18
  ESTIMATED_CHARS_PER_TOKEN: 4,
19
+ /** Maximum fraction of budget allocated to Tier 1 full files in tiered context (70%) */
20
+ TIER1_BUDGET_RATIO: 0.7,
21
+ /** Maximum fraction of budget allocated to Tier 2 AST skeletons in tiered context (30%) */
22
+ TIER2_BUDGET_RATIO: 0.3,
18
23
  };
19
24
  /**
20
25
  * Estimates token count from a given text string.
@@ -88,7 +93,7 @@ ${context.webSearchSummary}
88
93
  * @param scoped Whether this content represents an AST-scoped outline.
89
94
  * @returns Formatted workspace_file block.
90
95
  */
91
- function formatWorkspaceFileBlock(filePath, contentText, scoped = false) {
96
+ export function formatWorkspaceFileBlock(filePath, contentText, scoped = false) {
92
97
  let aliasAttr = '';
93
98
  if (filePath.startsWith('@')) {
94
99
  const slashIndex = filePath.indexOf('/');
@@ -140,12 +145,17 @@ function assembleContextWithFiles(baseContext, relevantFiles, maxChars, useScope
140
145
  const safetyAllowance = 1; // for newline before closing tag
141
146
  for (let i = 0; i < fileEntries.length; i++) {
142
147
  const [filePath, contentObj] = fileEntries[i];
148
+ // Check if explicitly omitted via Tier 3
149
+ if (contentObj.tier === 3) {
150
+ continue;
151
+ }
152
+ let isScoped = useScopedOutline || Boolean(contentObj.scoped) || contentObj.tier === 2;
143
153
  let contentText = contentObj.text;
144
- if (useScopedOutline) {
145
- contentText = extractDeclarationsOutline(contentText, filePath) || contentText;
154
+ if (isScoped) {
155
+ contentText = contentObj.outline || extractDeclarationsOutline(contentText, filePath) || contentText;
146
156
  }
147
157
  if (!hasBudget) {
148
- injection += formatWorkspaceFileBlock(filePath, contentText, useScopedOutline);
158
+ injection += formatWorkspaceFileBlock(filePath, contentText, isScoped);
149
159
  continue;
150
160
  }
151
161
  const remainingFilesCount = fileEntries.length - i - 1;
@@ -153,14 +163,14 @@ function assembleContextWithFiles(baseContext, relevantFiles, maxChars, useScope
153
163
  ? `<!-- ${remainingFilesCount} additional relevant file(s) omitted to stay within token budget -->\n`.length
154
164
  : 0;
155
165
  const remainingBudgetForThisFile = maxChars - injection.length - closingTag.length - omissionNoticeEstimate - safetyAllowance;
156
- const fullBlock = formatWorkspaceFileBlock(filePath, contentText, useScopedOutline);
166
+ const fullBlock = formatWorkspaceFileBlock(filePath, contentText, isScoped);
157
167
  if (fullBlock.length <= remainingBudgetForThisFile) {
158
168
  injection += fullBlock;
159
169
  continue;
160
170
  }
161
171
  // Token-efficiency optimization: If full file does not fit and wasn't scoped, attempt AST outline first
162
- if (!useScopedOutline) {
163
- const outlinedText = extractDeclarationsOutline(contentText, filePath);
172
+ if (!isScoped) {
173
+ const outlinedText = contentObj.outline || extractDeclarationsOutline(contentText, filePath);
164
174
  if (outlinedText) {
165
175
  const outlinedBlock = formatWorkspaceFileBlock(filePath, outlinedText, true);
166
176
  if (outlinedBlock.length <= remainingBudgetForThisFile) {
@@ -170,13 +180,13 @@ function assembleContextWithFiles(baseContext, relevantFiles, maxChars, useScope
170
180
  }
171
181
  }
172
182
  // Try partial/truncated block
173
- const emptyBlock = formatWorkspaceFileBlock(filePath, '', useScopedOutline);
183
+ const emptyBlock = formatWorkspaceFileBlock(filePath, '', isScoped);
174
184
  const wrapperOverhead = emptyBlock.length;
175
185
  const availableContentChars = remainingBudgetForThisFile - wrapperOverhead - truncationNotice.length;
176
186
  let includedTruncated = false;
177
187
  if (availableContentChars > 50) {
178
188
  const truncatedContent = contentText.slice(0, availableContentChars) + truncationNotice;
179
- injection += formatWorkspaceFileBlock(filePath, truncatedContent, useScopedOutline);
189
+ injection += formatWorkspaceFileBlock(filePath, truncatedContent, isScoped);
180
190
  includedTruncated = true;
181
191
  }
182
192
  const remainingFiles = fileEntries.length - (includedTruncated ? i + 1 : i);
@@ -227,6 +237,68 @@ export function buildScopedContextInjection(context, maxChars) {
227
237
  const baseContext = buildBaseContext(context);
228
238
  return assembleContextWithFiles(baseContext, context.relevantFiles, maxChars, true);
229
239
  }
240
+ /**
241
+ * Constructs a tiered, asymmetric XML/Markdown-like project context string.
242
+ *
243
+ * Automatically partitions files into:
244
+ * - Tier 1: Primary edit targets & high-relevance files with 100% full raw source code.
245
+ * - Tier 2: Peripheral hubs & dependent modules with compact AST declaration skeletons (scoped="outline").
246
+ * - Tier 3: Budget overflow files (omitted with token budget omission notice).
247
+ *
248
+ * @param context The collected context data from ContextAgent.
249
+ * @param options Tiered context options or maximum character budget.
250
+ * @returns A formatted string containing project profile, structure, investigation summaries, and tiered file blocks.
251
+ */
252
+ export function buildTieredContextInjection(context, options) {
253
+ const maxChars = typeof options === 'number'
254
+ ? options
255
+ : typeof options?.maxChars === 'number'
256
+ ? options.maxChars
257
+ : CONTEXT_BUDGET_CONFIG.DEFAULT_CONTEXT_MAX_CHARS;
258
+ const baseContext = buildBaseContext(context);
259
+ // If pre-computed tierPartition exists on context or options, use it directly
260
+ const tierPartition = typeof options === 'object' && options?.tierPartition ? options.tierPartition : context.tierPartition;
261
+ if (tierPartition) {
262
+ const fileMap = new Map();
263
+ for (const f of tierPartition.tier1 || []) {
264
+ fileMap.set(f.filePath, { text: f.content, tier: 1, scoped: false });
265
+ }
266
+ for (const f of tierPartition.tier2 || []) {
267
+ fileMap.set(f.filePath, { text: f.outline || f.content, outline: f.outline, tier: 2, scoped: true });
268
+ }
269
+ return assembleContextWithFiles(baseContext, fileMap, maxChars, false);
270
+ }
271
+ // If a prompt is provided in options, dynamically rank and partition using contextRanker
272
+ const prompt = typeof options === 'object' ? options?.prompt : undefined;
273
+ if (prompt && context.relevantFiles && context.relevantFiles.size > 0) {
274
+ try {
275
+ const candidates = Array.from(context.relevantFiles.entries()).map(([filePath, contentObj]) => ({
276
+ filePath,
277
+ content: contentObj.text,
278
+ isExplicitTarget: Boolean(contentObj.tier === 1),
279
+ outline: contentObj.outline,
280
+ }));
281
+ const partition = rankFilesForBudget(candidates, prompt, {
282
+ charBudget: maxChars,
283
+ graph: typeof options === 'object' ? options?.graph : undefined,
284
+ });
285
+ const fileMap = new Map();
286
+ for (const f of partition.tier1) {
287
+ const orig = context.relevantFiles.get(f.filePath);
288
+ fileMap.set(f.filePath, { text: f.content, inlineData: orig?.inlineData, tier: 1, scoped: false });
289
+ }
290
+ for (const f of partition.tier2) {
291
+ const orig = context.relevantFiles.get(f.filePath);
292
+ fileMap.set(f.filePath, { text: f.outline || f.content, outline: f.outline, inlineData: orig?.inlineData, tier: 2, scoped: true });
293
+ }
294
+ return assembleContextWithFiles(baseContext, fileMap, maxChars, false);
295
+ }
296
+ catch {
297
+ // Fallback to standard assembly if ranking fails
298
+ }
299
+ }
300
+ return assembleContextWithFiles(baseContext, context.relevantFiles, maxChars, false);
301
+ }
230
302
  /**
231
303
  * Formats an array of resolved context mention blocks into a single structured
232
304
  * XML `<context_mentions>` block adhering to character/token budget constraints.
@@ -0,0 +1,216 @@
1
+ /**
2
+ * @file src/utils/contextRanker.ts
3
+ * @description Classical BM25 / TF-IDF information retrieval engine with dependency graph
4
+ * centrality scoring and deterministic 3-tier token budgeting.
5
+ *
6
+ * Partitions candidate workspace files into:
7
+ * - Tier 1: Full raw source code for high-relevance edit targets and primary files.
8
+ * - Tier 2: Compact AST declaration skeletons for peripheral hubs and dependent modules.
9
+ * - Tier 3: Omitted overflow files to strictly respect token budget boundaries.
10
+ */
11
+ import { type DependencyGraph, type CentralityMetrics } from './dependencyTracer.js';
12
+ export type ContextTier = 1 | 2 | 3;
13
+ export interface FileCandidate {
14
+ /** Relative workspace file path or alias (e.g. "src/services/auth.ts") */
15
+ filePath: string;
16
+ /** Raw full text content of the file */
17
+ content: string;
18
+ /** If true, this file is an explicitly targeted or user-mentioned file */
19
+ isExplicitTarget?: boolean;
20
+ /** Optional pre-computed AST declarations outline skeleton */
21
+ outline?: string;
22
+ /** Optional metadata associated with the file candidate */
23
+ metadata?: Record<string, unknown>;
24
+ }
25
+ export interface RankedFile {
26
+ /** File path */
27
+ filePath: string;
28
+ /** Full raw file content */
29
+ content: string;
30
+ /** AST declarations outline skeleton */
31
+ outline: string;
32
+ /** Assigned context tier (1 = full, 2 = outline skeleton, 3 = omitted) */
33
+ tier: ContextTier;
34
+ /** Final composite relevance score (0.0 to 1.0+, higher is more relevant) */
35
+ score: number;
36
+ /** BM25 / TF-IDF query match score (normalized 0.0 to 1.0) */
37
+ bm25Score: number;
38
+ /** Dependency graph hub centrality score (normalized 0.0 to 1.0) */
39
+ centralityScore: number;
40
+ /** File path keyword match score (0.0 to 1.0) */
41
+ pathMatchScore: number;
42
+ /** Estimated token cost for this file based on its assigned tier */
43
+ estimatedTokens: number;
44
+ /** Estimated character cost for this file based on its assigned tier */
45
+ estimatedChars: number;
46
+ /** Whether the file was flagged as an explicit target */
47
+ isExplicitTarget?: boolean;
48
+ /** Metadata preserved from candidate */
49
+ metadata?: Record<string, unknown>;
50
+ }
51
+ export interface ContextRankerWeights {
52
+ /** Weight for BM25 content and query matching (default: 0.60) */
53
+ bm25?: number;
54
+ /** Weight for dependency graph centrality / hub score (default: 0.25) */
55
+ centrality?: number;
56
+ /** Weight for file path keyword match (default: 0.15) */
57
+ pathMatch?: number;
58
+ }
59
+ export interface RankFilesOptions {
60
+ /** Dependency graph to derive structural centrality from */
61
+ graph?: DependencyGraph;
62
+ /** Pre-calculated centrality metrics map (optional alternative to graph) */
63
+ centralityMetrics?: Map<string, CentralityMetrics>;
64
+ /** Total token budget available (default derived from CONTEXT_BUDGET_CONFIG: 60,000 tokens) */
65
+ tokenBudget?: number;
66
+ /** Total character budget (if provided, overrides tokenBudget * charsPerToken) */
67
+ charBudget?: number;
68
+ /** Estimated characters per token ratio (default: 4) */
69
+ charsPerToken?: number;
70
+ /** Maximum fraction of budget allocated to Tier 1 full files (default: 0.70) */
71
+ tier1BudgetRatio?: number;
72
+ /** Maximum fraction of budget allocated to Tier 2 AST skeletons (default: 0.30) */
73
+ tier2BudgetRatio?: number;
74
+ /** Minimum composite score required to be considered for Tier 1 / Tier 2 (default: 0.0) */
75
+ minScoreThreshold?: number;
76
+ /** Custom scoring weights */
77
+ weights?: ContextRankerWeights;
78
+ /** BM25 parameter k1 (term frequency saturation, default: 1.2) */
79
+ bm25K1?: number;
80
+ /** BM25 parameter b (document length normalization, default: 0.75) */
81
+ bm25B?: number;
82
+ }
83
+ export interface TierPartitionResult {
84
+ /** Tier 1: Primary edit targets & high-relevance files with 100% full content */
85
+ tier1: RankedFile[];
86
+ /** Tier 2: Peripheral hubs & dependent modules with compact AST skeletons */
87
+ tier2: RankedFile[];
88
+ /** Tier 3: Omitted files that exceed budget constraints */
89
+ tier3: RankedFile[];
90
+ /** All evaluated candidate files sorted descending by composite score */
91
+ rankedFiles: RankedFile[];
92
+ /** Total estimated tokens consumed by Tier 1 + Tier 2 files */
93
+ totalTokensUsed: number;
94
+ /** Total characters consumed by Tier 1 + Tier 2 files */
95
+ totalCharsUsed: number;
96
+ /** Budget statistics */
97
+ stats: {
98
+ totalCandidates: number;
99
+ tier1Count: number;
100
+ tier2Count: number;
101
+ tier3Count: number;
102
+ tokenBudget: number;
103
+ tokensUsed: number;
104
+ charBudget: number;
105
+ charsUsed: number;
106
+ budgetUtilization: number;
107
+ };
108
+ }
109
+ /**
110
+ * Tokenizes code and text into normalized keywords.
111
+ * Handles camelCase, PascalCase, snake_case, kebab-case, and dot/slash delimiters.
112
+ *
113
+ * @param text The input string to tokenize
114
+ * @param filterStopWords Whether to filter common English stop words (default: true)
115
+ * @returns Array of lowercase alphanumeric tokens
116
+ */
117
+ export declare function tokenize(text: string, filterStopWords?: boolean): string[];
118
+ /**
119
+ * Extracts unique search terms from a user prompt or query.
120
+ */
121
+ export declare function extractQueryTerms(prompt: string): string[];
122
+ export interface BM25Document {
123
+ id: string;
124
+ tokens: string[];
125
+ termFreqs: Map<string, number>;
126
+ length: number;
127
+ }
128
+ export interface BM25Options {
129
+ k1?: number;
130
+ b?: number;
131
+ }
132
+ /**
133
+ * In-memory BM25 index and scoring implementation.
134
+ * Delivers deterministic <5ms search ranking across candidate files.
135
+ */
136
+ export declare class BM25Engine {
137
+ private avgDocLength;
138
+ private b;
139
+ private docFrequency;
140
+ private documents;
141
+ private k1;
142
+ private totalDocs;
143
+ constructor(options?: BM25Options);
144
+ /**
145
+ * Adds a document to the BM25 index.
146
+ * Path tokens can be repeated to apply a structural path boost.
147
+ */
148
+ addDocument(id: string, content: string, filePath: string, pathBoostFactor?: number): void;
149
+ /**
150
+ * Finalizes document statistics (average document length).
151
+ */
152
+ build(): void;
153
+ /**
154
+ * Scores all indexed documents against query terms using standard BM25 formula with Robertson-Spärck Jones IDF.
155
+ *
156
+ * @param queryTerms Array of query terms
157
+ * @returns Map of document ID to raw BM25 score
158
+ */
159
+ score(queryTerms: string[]): Map<string, number>;
160
+ }
161
+ /**
162
+ * Computes BM25 scores for a collection of file candidates.
163
+ *
164
+ * @param candidates List of file candidates
165
+ * @param queryTerms Query terms from prompt
166
+ * @param options BM25 parameters
167
+ * @returns Map of filePath to normalized BM25 score in [0.0, 1.0]
168
+ */
169
+ export declare function computeBM25Scores(candidates: FileCandidate[], queryTerms: string[], options?: BM25Options): Map<string, {
170
+ rawScore: number;
171
+ normalizedScore: number;
172
+ }>;
173
+ /**
174
+ * Computes TF-IDF scores for a collection of candidates as a lightweight alternative.
175
+ */
176
+ export declare function computeTfIdfScores(candidates: FileCandidate[], queryTerms: string[]): Map<string, {
177
+ rawScore: number;
178
+ normalizedScore: number;
179
+ }>;
180
+ /**
181
+ * Calculates a relevance score for a file path based on matching query terms against
182
+ * the file's basename, directory segments, and file extension.
183
+ *
184
+ * @param filePath File path
185
+ * @param queryTerms Extracted query terms
186
+ * @returns Path match score between 0.0 and 1.0
187
+ */
188
+ export declare function calculatePathMatchScore(filePath: string, queryTerms: string[]): number;
189
+ /**
190
+ * Extracts and normalizes centrality metrics for a list of candidate files.
191
+ *
192
+ * @param candidates List of candidate files
193
+ * @param graph Optional dependency graph
194
+ * @param customCentrality Optional pre-computed centrality map
195
+ * @returns Map of filePath to normalized centrality score in [0.0, 1.0]
196
+ */
197
+ export declare function computeCandidateCentralityScores(candidates: FileCandidate[], graph?: DependencyGraph, customCentrality?: Map<string, CentralityMetrics>): Map<string, {
198
+ metrics: CentralityMetrics;
199
+ normalizedScore: number;
200
+ }>;
201
+ /**
202
+ * Estimates token count from string length using a characters-per-token heuristic.
203
+ */
204
+ export declare function estimateTokens(text: string, charsPerToken?: number): number;
205
+ /**
206
+ * Core Context Ranker:
207
+ * Ranks candidate files via hybrid BM25 + Graph Centrality + Path Match scoring,
208
+ * then partitions them into Tier 1 (full source), Tier 2 (AST outline skeleton),
209
+ * and Tier 3 (omitted) within strict token budgets.
210
+ *
211
+ * @param files Array of candidate file objects or map of filePath -> content
212
+ * @param prompt User prompt or instruction
213
+ * @param options Ranking and budgeting options
214
+ * @returns Partitioned 3-Tier result with complete metadata and statistics
215
+ */
216
+ export declare function rankFilesForBudget(files: FileCandidate[] | Map<string, string> | Record<string, string>, prompt: string, options?: RankFilesOptions): TierPartitionResult;