minovative-mind-cli 2.6.3 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,7 +7,7 @@ and hope for the best.
7
7
 
8
8
  This CLI does the opposite — it uses a custom built agentic system called,
9
9
  **Precision-Context Verification (PCV)** engine to feed lightweight Flash models
10
- exactly the right context, execute code, verify the output compiles, and self-correct (if it even needs to), until
10
+ exactly the right context, execute code, verify compilation, execute Property-Based Testing (PBT) suites and diff-scoped mutation audits, and self-correct (if it even needs to), until
11
11
  the build/performance metrics are green.
12
12
 
13
13
  > The result: **Genuine Pro reasoning accuracy at Flash-level speed and efficiency.**
@@ -142,7 +142,7 @@ By prefixing file paths with `@alias/` (e.g., `@backend/src/api.ts` and `@fronte
142
142
 
143
143
  Minovative Mind CLI fundamentally supports **ALL programming languages** for chat, code generation, planning, and execution, as it relies on Gemini's vast training data.
144
144
 
145
- However, the PCV engine features deep, context-aware analysis across **12 major programming language families**. Our core advanced engines—**Smart Dependency Tracing**, **Performance Auditing** (across 9 language families), and **Ephemeral Analysis Scripts**—provide tailored support depending on the language's syntax and runtime model:
145
+ However, the PCV engine features deep, context-aware analysis across **12 major programming language families**. Our core advanced engines—**Smart Dependency Tracing**, **Property-Based & Mutation Verification**, **Performance Auditing** (across 9 language families), and **Ephemeral Analysis Scripts**—provide tailored support depending on the language's syntax and runtime model:
146
146
 
147
147
  | Language Family | Supported Extensions | Dependency Tracing | Performance Auditing | Ephemeral Analysis |
148
148
  | :--------------------------- | :------------------------------------------- | :----------------: | :------------------: | :----------------: |
@@ -165,7 +165,7 @@ _⚠️\*Support is limited or requires custom local system tooling/environment
165
165
 
166
166
  1. **Dependency Tracing:** Fully supported across language families. It uses lightning-fast static regex pattern matching to resolve local import structures and determine the "blast radius" of code changes without requiring local compilations.
167
167
  2. **Performance Auditing:** Executed natively across 9 major language families (JavaScript/TypeScript, Python, Go, Rust, PHP, C#, Java, C/C++, and Ruby). It uses zero-dependency, ultra-fast (<50ms) language-aware regex heuristics with comment/string stripping and line-preserving offset tracking to detect severe runtime anti-patterns (such as unbounded loops, synchronous I/O, chained array allocations, unnecessary allocations, and unclosed resources).
168
- 3. **Ephemeral Analysis Scripts:** Runs temporary files in sandbox directories to parse and extract local symbols. Also used for infrastructure probing (detecting available runtimes, checking ports, identifying project type), post-change validation (importing modified modules and asserting expected behavior), and lightweight ML-powered analysis (e.g., TF-IDF file ranking, anomaly detection, Big-O estimation). Defaults to Node.js as a safe baseline, but natively leverages host-installed runtimes (Python, Rust, Go compilers) and standard libraries whenever available in the workspace.
168
+ 3. **Ephemeral Analysis & Property-Based Probing:** Runs temporary files in sandbox directories to parse and extract local symbols, execute ephemeral Property-Based Testing (PBT) probes to discover and shrink counterexamples, perform infrastructure probing (detecting available runtimes, checking ports, identifying project type), post-change validation (importing modified modules and asserting expected behavior), and lightweight ML-powered analysis (e.g., TF-IDF file ranking, anomaly detection, Big-O estimation). Defaults to Node.js as a safe baseline, but natively leverages host-installed runtimes (Python, Rust, Go compilers) and standard libraries whenever available in the workspace.
169
169
 
170
170
  ---
171
171
 
@@ -1413,12 +1413,23 @@ const currentTasksByAgent = new Map();
1413
1413
  * @returns A promise resolving to a {@link ToolResult}.
1414
1414
  */
1415
1415
  export async function executeTool(workspaceRoot, toolName, args, abortSignal) {
1416
+ // Normalize common LLM tool alias hallucinations to registered tool names
1417
+ let normalizedToolName = toolName;
1418
+ if (normalizedToolName === 'search' || normalizedToolName === 'web_search') {
1419
+ normalizedToolName = (args.query || args.searchQuery || args.url) ? 'perform_web_search' : 'grep_search';
1420
+ }
1421
+ else if (normalizedToolName === 'grep' || normalizedToolName === 'search_codebase') {
1422
+ normalizedToolName = 'grep_search';
1423
+ }
1424
+ else if (normalizedToolName === 'finish_investigation') {
1425
+ normalizedToolName = 'finish_task';
1426
+ }
1416
1427
  // ─── Multi-Workspace Path Resolution ─────────────────────────────
1417
1428
  // Intercept @alias/ prefixed paths and swap workspaceRoot + relative path
1418
1429
  // before dispatching to the underlying tool functions (which remain unchanged).
1419
- const { effectiveRoot, resolvedArgs } = resolveWorkspaceArgs(workspaceRoot, toolName, args);
1430
+ const { effectiveRoot, resolvedArgs } = resolveWorkspaceArgs(workspaceRoot, normalizedToolName, args);
1420
1431
  let result;
1421
- switch (toolName) {
1432
+ switch (normalizedToolName) {
1422
1433
  case 'perform_web_search': {
1423
1434
  const { createWebSearchAgentSession } = await import('./ai.js');
1424
1435
  const webSession = createWebSearchAgentSession();
@@ -112,24 +112,14 @@ export declare function executeSingleTurn(workspaceRoot: string, userInput: stri
112
112
  } | void>;
113
113
  /**
114
114
  * Raw chat history entry count threshold to trigger chat history summarization.
115
- * 12 entries = 6 conversational user/model turns.
116
- */
117
- export declare const HISTORY_SUMMARIZATION_THRESHOLD = 12;
118
- /**
119
- * Checks and summarizes older chat history entries if the active session's history
120
- * exceeds HISTORY_SUMMARIZATION_THRESHOLD entries.
121
- *
122
- * Preserves the most recent 4 entries (2 turns) intact for conversational continuity,
123
- * replacing all preceding turns with a single summarized pair in the chat history.
124
- *
125
- * @param chat - The active ProxyChatSession instance.
126
- * @returns True if history was summarized and updated; false otherwise.
115
+ * 50 entries = 25 conversational user/model turns.
127
116
  */
117
+ export declare const HISTORY_SUMMARIZATION_THRESHOLD = 50;
128
118
  /**
129
119
  * Summarizes older chat history entries if the active session's history
130
120
  * exceeds {@link HISTORY_SUMMARIZATION_THRESHOLD} entries.
131
121
  *
132
- * Preserves the most recent 4 entries (2 turns) intact for conversational continuity,
122
+ * Preserves the most recent 20 entries (10 turns) intact for conversational continuity,
133
123
  * replacing all preceding turns with a single summarized pair in the chat history.
134
124
  *
135
125
  * @param chat - The active ProxyChatSession instance.
@@ -152,7 +152,7 @@ export async function startAgentLoop(workspaceRoot, version) {
152
152
  process.stdout.write('\n\n\n\x1b[3A');
153
153
  console.log(pc.dim('\nType your coding request below. Type "exit" or "quit" to leave.\n'));
154
154
  while (true) {
155
- if (chat.getRawHistory().length === 0 && chatSessionState.title !== '') {
155
+ if (chat.getFullHistory().length === 0 && chatSessionState.title !== '') {
156
156
  chatSessionState.id = crypto.randomUUID();
157
157
  chatSessionState.title = '';
158
158
  chatSessionState.totalTokens = 0;
@@ -466,19 +466,47 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
466
466
  const orchestrator = new Orchestrator(workspaceRoot, chatSessionState.id, inputHandler);
467
467
  const handledByOrchestrator = await orchestrator.runOrchestration(finalInput, dynamicSystemInstruction, ac.signal);
468
468
  if (typeof handledByOrchestrator === 'string') {
469
- // The orchestrator successfully decomposed and ran the task.
470
- // Inject the task context into the main proxy chat session so the agent remembers what happened
471
- // and so the history state gets saved properly on disk.
472
- chat.getRawHistory().push({
473
- role: 'user',
474
- parts: [{ text: userInput }],
475
- });
476
- chat.getRawHistory().push({
477
- role: 'model',
478
- parts: [{ text: handledByOrchestrator }],
479
- });
480
- // Print the summary text just like single-agent mode
481
- if (handledByOrchestrator !== '[Generation stopped.]') {
469
+ const isStopped = handledByOrchestrator.includes('Generation stopped') ||
470
+ handledByOrchestrator.includes('[Generation stopped');
471
+ if (isStopped) {
472
+ const currentChanges = changeLogger.getCurrentChangeSet()?.changes || [];
473
+ const modifiedFiles = currentChanges
474
+ .filter((c) => c.action === 'modify' || c.action === 'create' || c.action === 'delete')
475
+ .map((c) => `${c.action}: ${path.relative(workspaceRoot, c.filePath)}`);
476
+ let summaryMsg = '';
477
+ if (modifiedFiles.length > 0) {
478
+ summaryMsg = `Generation was stopped by the user before completion. Partial code changes made before stopping:\n- ${modifiedFiles.join('\n- ')}`;
479
+ p.log.warn(pc.yellow('\n⚠️ Generation stopped by user before completion.'));
480
+ p.log.info(pc.cyan('Partial code changes created/modified before stopping:'));
481
+ modifiedFiles.forEach((file) => p.log.message(` ${pc.dim('•')} ${file}`));
482
+ }
483
+ else {
484
+ summaryMsg = 'Generation was stopped by the user before completion. No code files were modified before stopping.';
485
+ p.log.warn(pc.yellow('\n⚠️ Generation stopped by user before completion (no files were modified).'));
486
+ }
487
+ chat.addTurn({
488
+ role: 'user',
489
+ parts: [{ text: `[USER INTERRUPT] User issued a "stop" command during execution: "${userInput}"` }],
490
+ }, {
491
+ role: 'model',
492
+ parts: [
493
+ {
494
+ text: `[GENERATION INTERRUPTED BY USER] I acknowledge that generation was manually stopped by the user before task completion.\n${summaryMsg}`,
495
+ },
496
+ ],
497
+ });
498
+ }
499
+ else {
500
+ // Inject the task context into the main proxy chat session so the agent remembers what happened
501
+ // and so the full history state gets saved properly on disk.
502
+ chat.addTurn({
503
+ role: 'user',
504
+ parts: [{ text: userInput }],
505
+ }, {
506
+ role: 'model',
507
+ parts: [{ text: handledByOrchestrator }],
508
+ });
509
+ // Print the summary text just like single-agent mode
482
510
  console.log(`\n${pc.blue('◆')} ${pc.bold('Minovative Mind')} ${pc.dim(`(Orchestrator)`)}\n`);
483
511
  const cleanText = handledByOrchestrator.replace(/\n([ \t]*\n){2,}/g, '\n\n');
484
512
  console.log(marked.parse(cleanText));
@@ -519,7 +547,7 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
519
547
  changeLogger.markComplete();
520
548
  changeLogger.commitChangeSet();
521
549
  // Auto-save chat history for the orchestrator turn
522
- const history = chat.getRawHistory();
550
+ const history = chat.getFullHistory();
523
551
  if (history.length > 0) {
524
552
  if (!chatSessionState.title) {
525
553
  chatSessionState.title = 'Generating title...';
@@ -628,8 +656,38 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
628
656
  const turnEndTime = Date.now();
629
657
  const turnDuration = ((turnEndTime - turnStartTime) / 1000).toFixed(1);
630
658
  p.log.info(`${pc.dim('Generated in')} ${pc.cyan(turnDuration + 's')}`);
631
- // If we reach this point without throwing or breaking early, and the generation wasn't stopped, the turn finished completely.
632
- if (finalText !== '[Generation stopped.]') {
659
+ // Check if generation was stopped by user
660
+ const isStopped = finalText.includes('Generation stopped') ||
661
+ finalText.includes('[Generation stopped');
662
+ if (isStopped) {
663
+ const currentChanges = changeLogger.getCurrentChangeSet()?.changes || [];
664
+ const modifiedFiles = currentChanges
665
+ .filter((c) => c.action === 'modify' || c.action === 'create' || c.action === 'delete')
666
+ .map((c) => `${c.action}: ${path.relative(workspaceRoot, c.filePath)}`);
667
+ let summaryMsg = '';
668
+ if (modifiedFiles.length > 0) {
669
+ summaryMsg = `Generation was stopped by the user before completion. Partial code changes made before stopping:\n- ${modifiedFiles.join('\n- ')}`;
670
+ p.log.warn(pc.yellow('\n⚠️ Generation stopped by user before completion.'));
671
+ p.log.info(pc.cyan('Partial code changes created/modified before stopping:'));
672
+ modifiedFiles.forEach((file) => p.log.message(` ${pc.dim('•')} ${file}`));
673
+ }
674
+ else {
675
+ summaryMsg = 'Generation was stopped by the user before completion. No code files were modified before stopping.';
676
+ p.log.warn(pc.yellow('\n⚠️ Generation stopped by user before completion (no files were modified).'));
677
+ }
678
+ chat.addTurn({
679
+ role: 'user',
680
+ parts: [{ text: `[USER INTERRUPT] User issued a "stop" command during execution: "${userInput}"` }],
681
+ }, {
682
+ role: 'model',
683
+ parts: [
684
+ {
685
+ text: `[GENERATION INTERRUPTED BY USER] I acknowledge that generation was manually stopped by the user before task completion.\n${summaryMsg}`,
686
+ },
687
+ ],
688
+ });
689
+ }
690
+ else {
633
691
  changeLogger.markComplete();
634
692
  }
635
693
  if (usage) {
@@ -647,7 +705,7 @@ export async function executeSingleTurn(workspaceRoot, userInput, chat, inputHan
647
705
  chatSessionState.totalCreditsUsed += usage.creditsUsed || 0;
648
706
  }
649
707
  // Auto-save chat history
650
- const history = chat.getRawHistory();
708
+ const history = chat.getFullHistory();
651
709
  if (history.length > 0) {
652
710
  if (!chatSessionState.title) {
653
711
  chatSessionState.title = 'Generating title...';
@@ -884,24 +942,14 @@ async function compressContextFiles(workspaceRoot, contextResult) {
884
942
  }
885
943
  /**
886
944
  * Raw chat history entry count threshold to trigger chat history summarization.
887
- * 12 entries = 6 conversational user/model turns.
888
- */
889
- export const HISTORY_SUMMARIZATION_THRESHOLD = 12;
890
- /**
891
- * Checks and summarizes older chat history entries if the active session's history
892
- * exceeds HISTORY_SUMMARIZATION_THRESHOLD entries.
893
- *
894
- * Preserves the most recent 4 entries (2 turns) intact for conversational continuity,
895
- * replacing all preceding turns with a single summarized pair in the chat history.
896
- *
897
- * @param chat - The active ProxyChatSession instance.
898
- * @returns True if history was summarized and updated; false otherwise.
945
+ * 50 entries = 25 conversational user/model turns.
899
946
  */
947
+ export const HISTORY_SUMMARIZATION_THRESHOLD = 50;
900
948
  /**
901
949
  * Summarizes older chat history entries if the active session's history
902
950
  * exceeds {@link HISTORY_SUMMARIZATION_THRESHOLD} entries.
903
951
  *
904
- * Preserves the most recent 4 entries (2 turns) intact for conversational continuity,
952
+ * Preserves the most recent 20 entries (10 turns) intact for conversational continuity,
905
953
  * replacing all preceding turns with a single summarized pair in the chat history.
906
954
  *
907
955
  * @param chat - The active ProxyChatSession instance.
@@ -913,8 +961,8 @@ export async function summarizeHistoryIfNeeded(chat) {
913
961
  return false;
914
962
  }
915
963
  try {
916
- // Keep the most recent 4 entries (2 full user/model turns) intact
917
- const recentCount = 4;
964
+ // Keep the most recent 20 entries (10 full user/model turns) intact
965
+ const recentCount = 20;
918
966
  const olderHistory = history.slice(0, history.length - recentCount);
919
967
  const recentHistory = history.slice(history.length - recentCount);
920
968
  debugLog(`Summarizing ${olderHistory.length} older chat history entries...`);
@@ -930,7 +978,7 @@ export async function summarizeHistoryIfNeeded(chat) {
930
978
  parts: [{ text: 'Understood. I have reviewed and absorbed the summary of our preceding conversation.' }],
931
979
  },
932
980
  ];
933
- chat.loadRawHistory([...summaryContent, ...recentHistory]);
981
+ chat.loadCompressedHistory([...summaryContent, ...recentHistory]);
934
982
  debugLog(`Chat history compressed successfully from ${history.length} to ${summaryContent.length + recentHistory.length} entries.`);
935
983
  return true;
936
984
  }
@@ -5,6 +5,7 @@ export declare function setGlobalActiveModel(model: string): void;
5
5
  export declare function getGlobalActiveModel(): string;
6
6
  export declare class ProxyChatSession {
7
7
  private history;
8
+ private fullHistory;
8
9
  private modelName;
9
10
  private systemInstruction;
10
11
  private tools;
@@ -17,7 +18,10 @@ export declare class ProxyChatSession {
17
18
  getModel(): string;
18
19
  clearHistory(): void;
19
20
  getRawHistory(): Content[];
21
+ getFullHistory(): Content[];
20
22
  loadRawHistory(history: Content[]): void;
23
+ loadCompressedHistory(history: Content[]): void;
24
+ addTurn(userContent: Content, modelContent: Content): void;
21
25
  /**
22
26
  * Retrieves the most recent conversation history as a formatted string.
23
27
  * Useful for passing conversation context to stateless background agents.
@@ -69,6 +69,7 @@ function truncatePartText(text) {
69
69
  }
70
70
  export class ProxyChatSession {
71
71
  history = [];
72
+ fullHistory = [];
72
73
  modelName;
73
74
  systemInstruction;
74
75
  tools;
@@ -95,25 +96,37 @@ export class ProxyChatSession {
95
96
  }
96
97
  clearHistory() {
97
98
  this.history = [];
99
+ this.fullHistory = [];
98
100
  }
99
101
  getRawHistory() {
100
102
  return this.history;
101
103
  }
104
+ getFullHistory() {
105
+ return this.fullHistory;
106
+ }
102
107
  loadRawHistory(history) {
108
+ this.history = [...history];
109
+ this.fullHistory = JSON.parse(JSON.stringify(history));
110
+ }
111
+ loadCompressedHistory(history) {
103
112
  this.history = history;
104
113
  }
114
+ addTurn(userContent, modelContent) {
115
+ this.history.push(userContent);
116
+ this.history.push(modelContent);
117
+ this.fullHistory.push(JSON.parse(JSON.stringify(userContent)));
118
+ this.fullHistory.push(JSON.parse(JSON.stringify(modelContent)));
119
+ }
105
120
  /**
106
121
  * Retrieves the most recent conversation history as a formatted string.
107
122
  * Useful for passing conversation context to stateless background agents.
108
123
  * @param turns Number of back-and-forth turns (user + model pair = 1 turn) to retrieve.
109
124
  */
110
125
  getRecentHistory(turns = 2) {
111
- if (this.history.length === 0)
126
+ if (this.fullHistory.length === 0 && this.history.length === 0)
112
127
  return '';
113
- // History is an array of Content { role: 'user' | 'model', parts: Part[] }
114
- // A single turn is generally 2 items (user, then model). Sometimes tools are interspersed.
115
- // We'll just grab the last N * 2 items.
116
- const recentItems = this.history.slice(-(turns * 2));
128
+ const sourceHistory = this.fullHistory.length > 0 ? this.fullHistory : this.history;
129
+ const recentItems = sourceHistory.slice(-(turns * 2));
117
130
  let formattedHistory = '';
118
131
  for (const item of recentItems) {
119
132
  const role = item.role === 'user' ? 'User' : 'Assistant';
@@ -134,6 +147,10 @@ export class ProxyChatSession {
134
147
  this.history.pop();
135
148
  this.history.pop();
136
149
  }
150
+ if (this.fullHistory.length >= 2) {
151
+ this.fullHistory.pop();
152
+ this.fullHistory.pop();
153
+ }
137
154
  }
138
155
  sessionId;
139
156
  workspaceRoot;
@@ -148,12 +165,15 @@ export class ProxyChatSession {
148
165
  */
149
166
  async pruneHistory() {
150
167
  if (this.history.length > MAX_HISTORY_ENTRIES) {
151
- // Always keep pairs aligned (user/model), so trim from the front
152
168
  const excess = this.history.length - MAX_HISTORY_ENTRIES;
153
- // Round up to the nearest even number to keep user/model pairs intact
154
169
  const trimCount = excess % 2 === 0 ? excess : excess + 1;
155
- const pruned = this.history.slice(0, trimCount);
156
170
  this.history = this.history.slice(trimCount);
171
+ }
172
+ if (this.fullHistory.length > MAX_HISTORY_ENTRIES) {
173
+ const excess = this.fullHistory.length - MAX_HISTORY_ENTRIES;
174
+ const trimCount = excess % 2 === 0 ? excess : excess + 1;
175
+ const pruned = this.fullHistory.slice(0, trimCount);
176
+ this.fullHistory = this.fullHistory.slice(trimCount);
157
177
  if (this.sessionId && this.workspaceRoot) {
158
178
  const archiveFile = `archives/archived_history_${this.sessionId}.json`;
159
179
  const existingArchive = readCache(this.workspaceRoot, archiveFile) || [];
@@ -199,10 +219,12 @@ export class ProxyChatSession {
199
219
  if (additionalText) {
200
220
  newParts.push({ text: truncatePartText(additionalText) });
201
221
  }
202
- this.history.push({
222
+ const userEntry = {
203
223
  role: 'user',
204
224
  parts: newParts,
205
- });
225
+ };
226
+ this.history.push(userEntry);
227
+ this.fullHistory.push(JSON.parse(JSON.stringify(userEntry)));
206
228
  // Prune old history before sending to keep payload bounded
207
229
  await this.pruneHistory();
208
230
  const effectiveGenerationConfig = { ...this.generationConfig };
@@ -286,10 +308,12 @@ export class ProxyChatSession {
286
308
  });
287
309
  }
288
310
  if (modelParts.length > 0) {
289
- this.history.push({
311
+ const modelEntry = {
290
312
  role: 'model',
291
313
  parts: modelParts,
292
- });
314
+ };
315
+ this.history.push(modelEntry);
316
+ this.fullHistory.push(JSON.parse(JSON.stringify(modelEntry)));
293
317
  // We must prune again if we just pushed new history
294
318
  await this.pruneHistory();
295
319
  }
@@ -83,6 +83,7 @@ export declare class MessageBus {
83
83
  private signals;
84
84
  private activityCursors;
85
85
  private signalCursors;
86
+ private persistQueue;
86
87
  private readonly persistPath;
87
88
  private readonly workspaceRoot;
88
89
  /** Maximum semantic signals any single agent can post */
@@ -35,6 +35,7 @@ export class MessageBus {
35
35
  signals = [];
36
36
  activityCursors = new Map();
37
37
  signalCursors = new Map();
38
+ persistQueue = Promise.resolve();
38
39
  persistPath;
39
40
  workspaceRoot;
40
41
  /** Maximum semantic signals any single agent can post */
@@ -172,8 +173,10 @@ export class MessageBus {
172
173
  activityCursors: Object.fromEntries(this.activityCursors),
173
174
  signalCursors: Object.fromEntries(this.signalCursors),
174
175
  };
175
- // Fire-and-forget — don't block the calling agent's tool loop
176
- atomicWriteFile(this.persistPath, JSON.stringify(snapshot)).catch((err) => {
176
+ // Fire-and-forget — don't block the calling agent's tool loop, but serialize writes sequentially
177
+ this.persistQueue = this.persistQueue
178
+ .then(() => atomicWriteFile(this.persistPath, JSON.stringify(snapshot)))
179
+ .catch((err) => {
177
180
  debugLog(`MessageBus: Failed to persist to disk: ${err}`);
178
181
  });
179
182
  }
@@ -5,11 +5,53 @@ export interface VerificationResult {
5
5
  errors: string[];
6
6
  aborted?: boolean;
7
7
  }
8
+ export interface Mutant {
9
+ id: string;
10
+ filePath: string;
11
+ line: number;
12
+ originalCode: string;
13
+ mutatedCode: string;
14
+ operator: string;
15
+ }
16
+ export interface MutationTestResult {
17
+ totalMutants: number;
18
+ killedMutants: number;
19
+ survivedMutants: number;
20
+ mutationScore: number;
21
+ mutants: Array<Mutant & {
22
+ killed: boolean;
23
+ output?: string;
24
+ }>;
25
+ }
8
26
  export declare function detectVerificationCommand(workspaceRoot: string): Promise<string | null>;
9
27
  export declare function runVerification(workspaceRoot: string, abortSignal?: AbortSignal): Promise<VerificationResult | null>;
10
28
  export declare function formatVerificationForModel(result: VerificationResult): string;
11
29
  export interface FullVerificationResult {
12
30
  errors: string | null;
13
31
  warnings: string | null;
32
+ mutationResult?: MutationTestResult | null;
14
33
  }
15
34
  export declare function verifyChangedFiles(workspaceRoot: string, filePaths: string[], abortSignal?: AbortSignal): Promise<FullVerificationResult | '[Verification Aborted]'>;
35
+ /**
36
+ * Generates mutants scoped specifically to modified lines of a source file.
37
+ */
38
+ export declare function generateDiffScopedMutants(filePath: string, content: string, modifiedLines?: number[]): Mutant[];
39
+ /**
40
+ * Runs diff-scoped mutation testing on modified files within the workspace.
41
+ * Mutates target lines in-memory/temp copy, executes workspace tests, and ensures mutants are killed.
42
+ */
43
+ export declare function runDiffScopedMutationTesting(workspaceRoot: string, filePaths: string[], modifiedLineMap?: Record<string, number[]>, abortSignal?: AbortSignal, options?: {
44
+ maxMutantsPerFile?: number;
45
+ timeoutMs?: number;
46
+ }): Promise<MutationTestResult>;
47
+ /**
48
+ * Detects whether property-based testing framework configuration or dependencies exist in workspace.
49
+ */
50
+ export declare function detectPropertyBasedTests(workspaceRoot: string): Promise<{
51
+ hasPbt: boolean;
52
+ framework?: string;
53
+ }>;
54
+ /**
55
+ * Executes workspace property-based tests if detected.
56
+ */
57
+ export declare function runPropertyBasedVerification(workspaceRoot: string, abortSignal?: AbortSignal): Promise<VerificationResult | null>;
@@ -299,3 +299,238 @@ export async function verifyChangedFiles(workspaceRoot, filePaths, abortSignal)
299
299
  warnings: terminalWarnings || null,
300
300
  };
301
301
  }
302
+ /**
303
+ * Common mutation operators for AST/line-level diff mutation testing.
304
+ */
305
+ const MUTATION_OPERATORS = [
306
+ {
307
+ name: 'EqualityReplacement',
308
+ pattern: /(===|!==|==|!=)/g,
309
+ replace: (m) => {
310
+ if (m === '===')
311
+ return '!==';
312
+ if (m === '!==')
313
+ return '===';
314
+ if (m === '==')
315
+ return '!=';
316
+ if (m === '!=')
317
+ return '==';
318
+ return m;
319
+ },
320
+ },
321
+ {
322
+ name: 'RelationalReplacement',
323
+ pattern: /(>=|<=|>|<)/g,
324
+ replace: (m) => {
325
+ if (m === '>=')
326
+ return '<';
327
+ if (m === '<=')
328
+ return '>';
329
+ if (m === '>')
330
+ return '<=';
331
+ if (m === '<')
332
+ return '>=';
333
+ return m;
334
+ },
335
+ },
336
+ {
337
+ name: 'ArithmeticReplacement',
338
+ pattern: /(\+|\-|\*|\/)/g,
339
+ replace: (m) => {
340
+ if (m === '+')
341
+ return '-';
342
+ if (m === '-')
343
+ return '+';
344
+ if (m === '*')
345
+ return '/';
346
+ if (m === '/')
347
+ return '*';
348
+ return m;
349
+ },
350
+ },
351
+ {
352
+ name: 'LogicalReplacement',
353
+ pattern: /(\&\&|\|\|)/g,
354
+ replace: (m) => (m === '&&' ? '||' : '&&'),
355
+ },
356
+ {
357
+ name: 'BooleanLiteralInversion',
358
+ pattern: /\b(true|false)\b/g,
359
+ replace: (m) => (m === 'true' ? 'false' : 'true'),
360
+ },
361
+ ];
362
+ /**
363
+ * Generates mutants scoped specifically to modified lines of a source file.
364
+ */
365
+ export function generateDiffScopedMutants(filePath, content, modifiedLines) {
366
+ const mutants = [];
367
+ const lines = content.split('\n');
368
+ const lineNumbers = modifiedLines && modifiedLines.length > 0
369
+ ? modifiedLines
370
+ : lines.map((_, i) => i + 1);
371
+ let idCounter = 1;
372
+ for (const lineNum of lineNumbers) {
373
+ if (lineNum < 1 || lineNum > lines.length)
374
+ continue;
375
+ const origLine = lines[lineNum - 1];
376
+ // Skip empty lines, comments, or imports/exports
377
+ const trimmed = origLine.trim();
378
+ if (!trimmed ||
379
+ trimmed.startsWith('//') ||
380
+ trimmed.startsWith('/*') ||
381
+ trimmed.startsWith('*') ||
382
+ trimmed.startsWith('import ') ||
383
+ trimmed.startsWith('export ') ||
384
+ trimmed.startsWith('#')) {
385
+ continue;
386
+ }
387
+ for (const op of MUTATION_OPERATORS) {
388
+ op.pattern.lastIndex = 0;
389
+ let match;
390
+ while ((match = op.pattern.exec(origLine)) !== null) {
391
+ const start = match.index;
392
+ const matchedText = match[0];
393
+ const replacement = op.replace(matchedText);
394
+ if (replacement === matchedText)
395
+ continue;
396
+ const mutatedLine = origLine.substring(0, start) + replacement + origLine.substring(start + matchedText.length);
397
+ mutants.push({
398
+ id: `mutant-${idCounter++}`,
399
+ filePath,
400
+ line: lineNum,
401
+ originalCode: origLine,
402
+ mutatedCode: mutatedLine,
403
+ operator: op.name,
404
+ });
405
+ }
406
+ }
407
+ }
408
+ return mutants;
409
+ }
410
+ /**
411
+ * Runs diff-scoped mutation testing on modified files within the workspace.
412
+ * Mutates target lines in-memory/temp copy, executes workspace tests, and ensures mutants are killed.
413
+ */
414
+ export async function runDiffScopedMutationTesting(workspaceRoot, filePaths, modifiedLineMap, abortSignal, options) {
415
+ const maxPerFile = options?.maxMutantsPerFile ?? 10;
416
+ const allMutants = [];
417
+ let totalKilled = 0;
418
+ for (const relPath of filePaths) {
419
+ if (abortSignal?.aborted)
420
+ break;
421
+ const fullPath = path.resolve(workspaceRoot, relPath);
422
+ let originalContent;
423
+ try {
424
+ originalContent = await fs.readFile(fullPath, 'utf-8');
425
+ }
426
+ catch {
427
+ continue;
428
+ }
429
+ const modifiedLines = modifiedLineMap ? modifiedLineMap[relPath] : undefined;
430
+ const mutants = generateDiffScopedMutants(relPath, originalContent, modifiedLines).slice(0, maxPerFile);
431
+ const lines = originalContent.split('\n');
432
+ for (const mutant of mutants) {
433
+ if (abortSignal?.aborted)
434
+ break;
435
+ // Create mutated file content
436
+ const mutatedLines = [...lines];
437
+ mutatedLines[mutant.line - 1] = mutant.mutatedCode;
438
+ const mutatedContent = mutatedLines.join('\n');
439
+ let killed = false;
440
+ let output = '';
441
+ try {
442
+ // Safely write mutated content with try...finally cleanup
443
+ await fs.writeFile(fullPath, mutatedContent, 'utf-8');
444
+ // Execute verification/test command
445
+ const testResult = await runVerification(workspaceRoot, abortSignal);
446
+ if (testResult && !testResult.success) {
447
+ killed = true;
448
+ output = testResult.errors.join('\n').substring(0, 500);
449
+ }
450
+ else if (!testResult) {
451
+ // No test runner detected; default to non-kill unless error occurs
452
+ killed = false;
453
+ }
454
+ }
455
+ catch (err) {
456
+ killed = true;
457
+ output = err.message || String(err);
458
+ }
459
+ finally {
460
+ // Restore original file unconditionally
461
+ try {
462
+ await fs.writeFile(fullPath, originalContent, 'utf-8');
463
+ }
464
+ catch (restoreErr) {
465
+ debugLog(`Failed to restore ${fullPath} after mutation testing: ${restoreErr}`);
466
+ }
467
+ }
468
+ if (killed)
469
+ totalKilled++;
470
+ allMutants.push({ ...mutant, killed, output });
471
+ }
472
+ }
473
+ const total = allMutants.length;
474
+ const score = total > 0 ? Math.round((totalKilled / total) * 100) : 100;
475
+ return {
476
+ totalMutants: total,
477
+ killedMutants: totalKilled,
478
+ survivedMutants: total - totalKilled,
479
+ mutationScore: score,
480
+ mutants: allMutants,
481
+ };
482
+ }
483
+ /**
484
+ * Detects whether property-based testing framework configuration or dependencies exist in workspace.
485
+ */
486
+ export async function detectPropertyBasedTests(workspaceRoot) {
487
+ // Check package.json for fast-check or jsverify
488
+ try {
489
+ const pkgStr = await fs.readFile(path.join(workspaceRoot, 'package.json'), 'utf-8');
490
+ if (pkgStr.includes('fast-check'))
491
+ return { hasPbt: true, framework: 'fast-check' };
492
+ if (pkgStr.includes('jsverify'))
493
+ return { hasPbt: true, framework: 'jsverify' };
494
+ }
495
+ catch { }
496
+ // Check Python environment for hypothesis
497
+ try {
498
+ const reqStr = await fs.readFile(path.join(workspaceRoot, 'requirements.txt'), 'utf-8');
499
+ if (reqStr.includes('hypothesis'))
500
+ return { hasPbt: true, framework: 'hypothesis' };
501
+ }
502
+ catch { }
503
+ try {
504
+ const pyprojStr = await fs.readFile(path.join(workspaceRoot, 'pyproject.toml'), 'utf-8');
505
+ if (pyprojStr.includes('hypothesis'))
506
+ return { hasPbt: true, framework: 'hypothesis' };
507
+ }
508
+ catch { }
509
+ // Check Cargo.toml for proptest / quickcheck
510
+ try {
511
+ const cargoStr = await fs.readFile(path.join(workspaceRoot, 'Cargo.toml'), 'utf-8');
512
+ if (cargoStr.includes('proptest'))
513
+ return { hasPbt: true, framework: 'proptest' };
514
+ if (cargoStr.includes('quickcheck'))
515
+ return { hasPbt: true, framework: 'quickcheck' };
516
+ }
517
+ catch { }
518
+ // Check go.mod for rapid
519
+ try {
520
+ const goModStr = await fs.readFile(path.join(workspaceRoot, 'go.mod'), 'utf-8');
521
+ if (goModStr.includes('pgregory.net/rapid'))
522
+ return { hasPbt: true, framework: 'rapid' };
523
+ }
524
+ catch { }
525
+ return { hasPbt: false };
526
+ }
527
+ /**
528
+ * Executes workspace property-based tests if detected.
529
+ */
530
+ export async function runPropertyBasedVerification(workspaceRoot, abortSignal) {
531
+ const pbtInfo = await detectPropertyBasedTests(workspaceRoot);
532
+ if (!pbtInfo.hasPbt)
533
+ return null;
534
+ debugLog(`Detected property-based testing framework: ${pbtInfo.framework}`);
535
+ return runVerification(workspaceRoot, abortSignal);
536
+ }
@@ -11,6 +11,18 @@ export interface EphemeralScriptOptions {
11
11
  /** AbortSignal to cancel execution. */
12
12
  abortSignal?: AbortSignal;
13
13
  }
14
+ export interface PropertyTestConfig {
15
+ numRuns?: number;
16
+ seed?: number;
17
+ shrinkTimeoutMs?: number;
18
+ }
19
+ export interface PropertyTestResult extends EphemeralScriptResult {
20
+ passed: boolean;
21
+ counterexample?: string;
22
+ shrunkInput?: string;
23
+ seed?: number;
24
+ numRunsCompleted?: number;
25
+ }
14
26
  /**
15
27
  * Normalizes user/AI provided language string to a standard runtime identifier.
16
28
  */
@@ -50,3 +62,11 @@ export declare function runEphemeralScript(workspaceRoot: string, language: stri
50
62
  * @param options - Execution options (timeoutMs, maxOutputChars, abortSignal).
51
63
  */
52
64
  export declare function runDebugScript(workspaceRoot: string, code: string, language?: string | EphemeralScriptOptions, options?: EphemeralScriptOptions): Promise<EphemeralScriptResult>;
65
+ /**
66
+ * Generates an ephemeral Property-Based Testing script template tailored to the target language and properties.
67
+ */
68
+ export declare function generatePBTScriptTemplate(language: string, targetFunctionOrFeature: string, properties: string[]): string;
69
+ /**
70
+ * Runs an ephemeral Property-Based Test script and extracts failure counterexamples and shrink results.
71
+ */
72
+ export declare function runPropertyBasedTest(workspaceRoot: string, code: string, language?: string | EphemeralScriptOptions, config?: PropertyTestConfig & EphemeralScriptOptions): Promise<PropertyTestResult>;
@@ -207,3 +207,120 @@ export async function runDebugScript(workspaceRoot, code, language = 'node', opt
207
207
  }
208
208
  return runEphemeralScript(workspaceRoot, lang, code, opts);
209
209
  }
210
+ /**
211
+ * Generates an ephemeral Property-Based Testing script template tailored to the target language and properties.
212
+ */
213
+ export function generatePBTScriptTemplate(language, targetFunctionOrFeature, properties) {
214
+ const normLang = normalizeLanguage(language);
215
+ if (normLang === 'python') {
216
+ return `import sys, random
217
+
218
+ # Target: ${targetFunctionOrFeature}
219
+ # Properties to verify:
220
+ ${properties.map((p) => `# - ${p}`).join('\n')}
221
+
222
+ def property_test():
223
+ runs = 100
224
+ for i in range(runs):
225
+ # Generate random inputs
226
+ val_int = random.randint(-10000, 10000)
227
+ val_str = "".join(random.choices("abcdefghijklmnopqrstuvwxyz0123456789", k=random.randint(0, 50)))
228
+
229
+ # Test property invariants
230
+ try:
231
+ # Assertion placeholder
232
+ assert isinstance(val_int, int)
233
+ except AssertionError as e:
234
+ print(f"[PBT FAILED] Seed/Iteration: {i}")
235
+ print(f"Counterexample: val_int={val_int}, val_str={val_str!r}")
236
+ sys.exit(1)
237
+
238
+ print(f"[PBT PASSED] Completed {runs} runs successfully.")
239
+
240
+ if __name__ == "__main__":
241
+ property_test()
242
+ `;
243
+ }
244
+ // Default Node / TypeScript / JS template
245
+ return `// Target: ${targetFunctionOrFeature}
246
+ // Properties to verify:
247
+ ${properties.map((p) => `// - ${p}`).join('\n')}
248
+
249
+ async function runPBT() {
250
+ const numRuns = 100;
251
+ let passed = 0;
252
+
253
+ for (let i = 0; i < numRuns; i++) {
254
+ // Generate pseudo-random inputs
255
+ const randInt = Math.floor(Math.random() * 20000) - 10000;
256
+ const randStr = Math.random().toString(36).substring(2);
257
+
258
+ try {
259
+ // Test property invariant
260
+ if (typeof randInt !== 'number' || isNaN(randInt)) {
261
+ throw new Error('Type invariant failed');
262
+ }
263
+ passed++;
264
+ } catch (err) {
265
+ console.log(\`[PBT FAILED] Iteration: \${i}\`);
266
+ console.log(\`Counterexample: randInt=\${randInt}, randStr="\${randStr}"\`);
267
+ console.log(\`Error: \${err.message}\`);
268
+ process.exit(1);
269
+ }
270
+ }
271
+
272
+ console.log(\`[PBT PASSED] Completed \${passed} runs successfully.\`);
273
+ }
274
+
275
+ runPBT();
276
+ `;
277
+ }
278
+ /**
279
+ * Runs an ephemeral Property-Based Test script and extracts failure counterexamples and shrink results.
280
+ */
281
+ export async function runPropertyBasedTest(workspaceRoot, code, language = 'node', config) {
282
+ let lang = 'node';
283
+ let opts;
284
+ if (typeof language === 'string') {
285
+ lang = language;
286
+ opts = config;
287
+ }
288
+ else if (typeof language === 'object' && language !== null) {
289
+ opts = language;
290
+ }
291
+ const scriptResult = await runEphemeralScript(workspaceRoot, lang, code, opts);
292
+ const combinedOutput = `${scriptResult.stdout}\n${scriptResult.stderr}`;
293
+ const isPassed = combinedOutput.includes('[PBT PASSED]') || (scriptResult.exitCode === 0 && !combinedOutput.includes('[PBT FAILED]'));
294
+ let counterexample;
295
+ let shrunkInput;
296
+ let seed;
297
+ let numRunsCompleted;
298
+ // Extract counterexample
299
+ const counterMatch = combinedOutput.match(/Counterexample:\s*([^\n]+)/i);
300
+ if (counterMatch) {
301
+ counterexample = counterMatch[1].trim();
302
+ }
303
+ // Extract shrunk input
304
+ const shrinkMatch = combinedOutput.match(/(?:Shrunk Input|Shrunk|Minimal):\s*([^\n]+)/i);
305
+ if (shrinkMatch) {
306
+ shrunkInput = shrinkMatch[1].trim();
307
+ }
308
+ // Extract seed
309
+ const seedMatch = combinedOutput.match(/Seed(?:|\/Iteration):\s*(\d+)/i);
310
+ if (seedMatch) {
311
+ seed = parseInt(seedMatch[1], 10);
312
+ }
313
+ // Extract completed runs
314
+ const runsMatch = combinedOutput.match(/Completed\s+(\d+)\s+runs/i);
315
+ if (runsMatch) {
316
+ numRunsCompleted = parseInt(runsMatch[1], 10);
317
+ }
318
+ return {
319
+ ...scriptResult,
320
+ passed: isPassed,
321
+ counterexample,
322
+ shrunkInput,
323
+ seed,
324
+ numRunsCompleted,
325
+ };
326
+ }
@@ -7,7 +7,8 @@ import path from 'node:path';
7
7
  */
8
8
  export async function atomicWriteFile(targetPath, data, encoding = 'utf-8') {
9
9
  const dir = path.dirname(targetPath);
10
- const tempPath = path.join(dir, `.${path.basename(targetPath)}.${Date.now()}.tmp`);
10
+ const randomSuffix = Math.random().toString(36).slice(2, 8);
11
+ const tempPath = path.join(dir, `.${path.basename(targetPath)}.${Date.now()}.${randomSuffix}.tmp`);
11
12
  try {
12
13
  // Ensure the directory exists
13
14
  await fs.mkdir(dir, { recursive: true });
@@ -65,5 +65,5 @@
65
65
  ]
66
66
  }
67
67
  },
68
- "version": "2.6.3"
68
+ "version": "2.7.0"
69
69
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "minovative-mind-cli",
3
3
  "description": "An automated AI agent powered by Vertex AI that helps you write software",
4
- "version": "2.6.3",
4
+ "version": "2.7.0",
5
5
  "author": "Daniel Ward",
6
6
  "bin": {
7
7
  "minovative-mind-cli": "bin/run.js"