minovative-mind-cli 2.13.3 → 2.13.4

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.
@@ -1,7 +1,9 @@
1
1
  import path from 'path';
2
2
  import { executeTool, getToolDeclarations as getBaseToolDeclarations } from '../agent-tools.js';
3
3
  import { MessageBus } from './messageBus.js';
4
+ import { computeReadCacheKey } from './readCache.js';
4
5
  import { resolveAndValidateMultiWorkspacePath } from '../../utils/pathSecurity.js';
6
+ import { recordFileRead } from '../../utils/fileReadGuard.js';
5
7
  import { debugLog } from '../../utils/logger.js';
6
8
  /**
7
9
  * Resolves a file path to its canonical absolute path for lock registry keying,
@@ -83,7 +85,7 @@ export function getScopedToolDeclarations() {
83
85
  /**
84
86
  * Executes a tool within the sub-agent execution boundary.
85
87
  * Handles concurrency locking for file-mutating operations, sends progress
86
- * notifications to the orchestrator, and logs activity to the message bus.
88
+ * notifications to the orchestrator, checks per-agent read caches, and logs activity to the message bus.
87
89
  *
88
90
  * @param name - Tool function name
89
91
  * @param args - Tool arguments object
@@ -94,8 +96,9 @@ export function getScopedToolDeclarations() {
94
96
  * @param onProgress - Callback to notify parent of sub-agent progress
95
97
  * @param options - Optional sub-path auto-focus and override configuration
96
98
  * @param taskScope - Optional task scope metadata (targetFiles, dependsOn)
99
+ * @param readCache - Optional per-agent in-memory file read cache
97
100
  */
98
- export async function executeScopedTool(name, args, workspaceRoot, agentId, bus, locks, onProgress, options, taskScope) {
101
+ export async function executeScopedTool(name, args, workspaceRoot, agentId, bus, locks, onProgress, options, taskScope, readCache) {
99
102
  // Update heartbeat so the orchestrator knows we are making progress
100
103
  onProgress();
101
104
  const timestamp = Date.now();
@@ -148,48 +151,108 @@ export async function executeScopedTool(name, args, workspaceRoot, agentId, bus,
148
151
  semanticSignals: MessageBus.formatSignals(unread.signals),
149
152
  };
150
153
  }
151
- // ─── Standard Tool Execution with Locking ────────────────────────
152
- // Handle lock acquisition for write-oriented tools
153
- let lockedFile = null;
154
- let diffContext = null;
155
- if (name === 'write_file' || name === 'modify_file' || name === 'delete_file') {
156
- const rawPath = args.filePath;
157
- if (rawPath) {
158
- lockedFile = resolveCanonicalLockPath(workspaceRoot, rawPath, options);
159
- debugLog(`Agent "${agentId}" requesting lock for "${lockedFile}" (raw: "${rawPath}", tool: ${name})`);
160
- onProgress(`waiting for lock on ${path.basename(lockedFile)}...`);
161
- const lockRes = await locks.acquire(lockedFile, agentId);
162
- onProgress(`acquired lock, executing...`);
163
- diffContext = lockRes.previousDiff;
164
- if (lockRes.forceReleased) {
165
- debugLog(`Agent "${agentId}" got forced lock on "${lockedFile}" (previous owner stalled)`);
166
- }
154
+ // ─── Standard Tool Execution with Locking & Caching ──────────────
155
+ // Check read cache for read_file operations
156
+ let cacheHit = false;
157
+ let cacheKey = '';
158
+ if (name === 'read_file' && readCache && args?.filePath) {
159
+ cacheKey = computeReadCacheKey(args.filePath, args.startLine, args.endLine, args.targetElements);
160
+ if (readCache.has(cacheKey)) {
161
+ const cached = readCache.get(cacheKey);
162
+ recordFileRead(args.filePath);
163
+ result = {
164
+ output: cached.text,
165
+ ...(cached.inlineData ? { inlineData: cached.inlineData } : {}),
166
+ };
167
+ cacheHit = true;
167
168
  }
168
169
  }
169
- else if (name === 'rename_file') {
170
- const rawPath = args.sourcePath;
171
- if (rawPath) {
172
- lockedFile = resolveCanonicalLockPath(workspaceRoot, rawPath, options);
173
- debugLog(`Agent "${agentId}" requesting lock for "${lockedFile}" (raw: "${rawPath}", tool: ${name})`);
174
- onProgress(`waiting for lock on ${path.basename(lockedFile)}...`);
175
- const lockRes = await locks.acquire(lockedFile, agentId);
176
- onProgress(`acquired lock, executing...`);
177
- diffContext = lockRes.previousDiff;
178
- if (lockRes.forceReleased) {
179
- debugLog(`Agent "${agentId}" got forced lock on "${lockedFile}" (previous owner stalled)`);
170
+ if (!cacheHit) {
171
+ // Handle lock acquisition for write-oriented tools
172
+ let lockedFile = null;
173
+ let diffContext = null;
174
+ if (name === 'write_file' || name === 'modify_file' || name === 'delete_file') {
175
+ const rawPath = args.filePath;
176
+ if (rawPath) {
177
+ lockedFile = resolveCanonicalLockPath(workspaceRoot, rawPath, options);
178
+ debugLog(`Agent "${agentId}" requesting lock for "${lockedFile}" (raw: "${rawPath}", tool: ${name})`);
179
+ onProgress(`waiting for lock on ${path.basename(lockedFile)}...`);
180
+ const lockRes = await locks.acquire(lockedFile, agentId);
181
+ onProgress(`acquired lock, executing...`);
182
+ diffContext = lockRes.previousDiff;
183
+ if (lockRes.forceReleased) {
184
+ debugLog(`Agent "${agentId}" got forced lock on "${lockedFile}" (previous owner stalled)`);
185
+ }
180
186
  }
181
187
  }
182
- }
183
- // Execute the underlying standard tool
184
- try {
185
- result = await executeTool(workspaceRoot, name, args);
186
- // If we got a file lock and had context from a previous writer, we should
187
- // inject that diff info back into the agent's tool return payload so it
188
- // knows someone else changed the file while it was queued.
189
- if (diffContext && typeof result === 'object' && result !== null) {
190
- result._orchestrationWarning = `NOTICE: Another agent modified this file while you were waiting. Diff context: ${diffContext}`;
188
+ else if (name === 'rename_file') {
189
+ const rawPath = args.sourcePath;
190
+ if (rawPath) {
191
+ lockedFile = resolveCanonicalLockPath(workspaceRoot, rawPath, options);
192
+ debugLog(`Agent "${agentId}" requesting lock for "${lockedFile}" (raw: "${rawPath}", tool: ${name})`);
193
+ onProgress(`waiting for lock on ${path.basename(lockedFile)}...`);
194
+ const lockRes = await locks.acquire(lockedFile, agentId);
195
+ onProgress(`acquired lock, executing...`);
196
+ diffContext = lockRes.previousDiff;
197
+ if (lockRes.forceReleased) {
198
+ debugLog(`Agent "${agentId}" got forced lock on "${lockedFile}" (previous owner stalled)`);
199
+ }
200
+ }
201
+ }
202
+ // Execute the underlying standard tool
203
+ try {
204
+ result = await executeTool(workspaceRoot, name, args);
205
+ // If read_file succeeded, store in readCache
206
+ if (name === 'read_file' &&
207
+ readCache &&
208
+ cacheKey &&
209
+ result &&
210
+ !result.error &&
211
+ typeof result.output === 'string') {
212
+ readCache.set(cacheKey, {
213
+ text: result.output,
214
+ inlineData: result.inlineData,
215
+ });
216
+ }
217
+ // If a file was modified/written/deleted/renamed, invalidate readCache entries for affected file(s)
218
+ if (readCache) {
219
+ if (name === 'write_file' || name === 'modify_file' || name === 'delete_file') {
220
+ if (args?.filePath) {
221
+ readCache.invalidate(args.filePath);
222
+ }
223
+ }
224
+ else if (name === 'rename_file') {
225
+ if (args?.sourcePath)
226
+ readCache.invalidate(args.sourcePath);
227
+ if (args?.targetPath)
228
+ readCache.invalidate(args.targetPath);
229
+ }
230
+ }
231
+ // If we got a file lock and had context from a previous writer, we should
232
+ // inject that diff info back into the agent's tool return payload so it
233
+ // knows someone else changed the file while it was queued.
234
+ if (diffContext && typeof result === 'object' && result !== null) {
235
+ result._orchestrationWarning = `NOTICE: Another agent modified this file while you were waiting. Diff context: ${diffContext}`;
236
+ }
237
+ // In-band relevance-gated urgent signals (warnings, targeted requests, or upstream completions)
238
+ if (typeof result === 'object' && result !== null && typeof bus?.peekUrgentSignals === 'function') {
239
+ const urgentSignals = bus.peekUrgentSignals(agentId, taskScope);
240
+ if (urgentSignals && urgentSignals.length > 0) {
241
+ result._orchestrationNotice = MessageBus.formatSignals(urgentSignals);
242
+ }
243
+ }
244
+ }
245
+ finally {
246
+ // Release the lock if we grabbed one
247
+ if (lockedFile) {
248
+ // We do a simple release. The actual diff context extraction happens at the orchestrator
249
+ // layer during verification, but we release immediately to unblock others.
250
+ locks.release(lockedFile, agentId, `Action: ${name} completed`);
251
+ }
191
252
  }
192
- // In-band relevance-gated urgent signals (warnings, targeted requests, or upstream completions)
253
+ }
254
+ else {
255
+ // For cache hit, also check urgent signals from bus if needed
193
256
  if (typeof result === 'object' && result !== null && typeof bus?.peekUrgentSignals === 'function') {
194
257
  const urgentSignals = bus.peekUrgentSignals(agentId, taskScope);
195
258
  if (urgentSignals && urgentSignals.length > 0) {
@@ -197,14 +260,6 @@ export async function executeScopedTool(name, args, workspaceRoot, agentId, bus,
197
260
  }
198
261
  }
199
262
  }
200
- finally {
201
- // Release the lock if we grabbed one
202
- if (lockedFile) {
203
- // We do a simple release. The actual diff context extraction happens at the orchestrator
204
- // layer during verification, but we release immediately to unblock others.
205
- locks.release(lockedFile, agentId, `Action: ${name} completed`);
206
- }
207
- }
208
263
  // ─── Formatting Log Outputs ──────────────────────────────────────
209
264
  if (name === 'write_file' || name === 'modify_file') {
210
265
  targetDesc = args.filePath;
@@ -213,9 +268,9 @@ export async function executeScopedTool(name, args, workspaceRoot, agentId, bus,
213
268
  }
214
269
  else if (name === 'read_file') {
215
270
  targetDesc = args.filePath;
216
- actionDesc = 'Read';
217
- const len = typeof result === 'string' ? result.length : (result.fileContents?.length || 0);
218
- resultSummary = `${len} chars`;
271
+ actionDesc = cacheHit ? 'Read (cached)' : 'Read';
272
+ const len = typeof result === 'string' ? result.length : (result.fileContents?.length || (result.output ? result.output.length : 0));
273
+ resultSummary = `${len} chars${cacheHit ? ' (cached)' : ''}`;
219
274
  }
220
275
  else if (name === 'run_command') {
221
276
  targetDesc = args.command;
@@ -62,6 +62,7 @@ export declare class SubAgentRunner {
62
62
  private inputTokens;
63
63
  private lastHeartbeat;
64
64
  private outputTokens;
65
+ private readonly readCache;
65
66
  /**
66
67
  * Updates the heartbeat. Passed to `executeScopedTool` to ensure the agent
67
68
  * isn't marked as stalled while performing long-running commands.
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { ProxyChatSession, getGlobalActiveModel } from '../ai.js';
8
8
  import { GEMINI_MODELS, MAX_OUTPUT_TOKENS } from '../../utils/config.js';
9
+ import { ReadCache } from './readCache.js';
9
10
  import { executeScopedTool, getScopedToolDeclarations } from './scopedTools.js';
10
11
  import { debugLog } from '../../utils/logger.js';
11
12
  import { runWithAgentId } from '../../utils/asyncContext.js';
@@ -140,6 +141,7 @@ export class SubAgentRunner {
140
141
  inputTokens = 0;
141
142
  lastHeartbeat = Date.now();
142
143
  outputTokens = 0;
144
+ readCache = new ReadCache();
143
145
  /**
144
146
  * Updates the heartbeat. Passed to `executeScopedTool` to ensure the agent
145
147
  * isn't marked as stalled while performing long-running commands.
@@ -169,6 +171,10 @@ export class SubAgentRunner {
169
171
  temperature: 0.3, // Lower temperature for more focused execution
170
172
  topP: 0.95,
171
173
  topK: 40,
174
+ }, {
175
+ functionCallingConfig: {
176
+ mode: 'ANY',
177
+ },
172
178
  });
173
179
  }
174
180
  /**
@@ -249,15 +255,30 @@ export class SubAgentRunner {
249
255
  // Tool Loop
250
256
  const MAX_TURNS = Infinity;
251
257
  let turns = 0;
258
+ let textRetryCount = 0;
259
+ const MAX_TEXT_RETRIES = 2;
260
+ let hasExecutedAnyTool = false;
252
261
  while (turns < MAX_TURNS && !crashed && !signal.aborted) {
253
262
  this.pingHeartbeat();
254
263
  const calls = turnResult.response.functionCalls();
255
- // If the model stopped calling tools, it is done
264
+ // If the model stopped calling tools, check if it executed any tools or exited prematurely on Turn 1
256
265
  if (!calls || calls.length === 0) {
266
+ if (!hasExecutedAnyTool && textRetryCount < MAX_TEXT_RETRIES) {
267
+ textRetryCount++;
268
+ debugLog(`SubAgent [${this.taskId}]: Model returned text without executing tools. Rejecting and enforcing tool execution (attempt ${textRetryCount}/${MAX_TEXT_RETRIES}).`);
269
+ const forcePrompt = `[SYSTEM DIRECTIVE (CRITICAL)]: You are an autonomous execution sub-agent. You have returned text without executing any tools or making code changes. You MUST call tools (e.g. 'read_file', 'modify_file', 'write_file', 'grep_search', 'run_command', or 'run_debug_script') to implement your assigned task objective: "${this.intent}". Do NOT just describe your plan in text—execute the changes using your tools now.`;
270
+ this.pingHeartbeat();
271
+ turnResult = await this.chat.sendMessage(forcePrompt, undefined, signal);
272
+ this.updateUsage(turnResult);
273
+ continue;
274
+ }
257
275
  success = true;
258
276
  finalSummary = turnResult.response.text() || 'Task completed without text summary.';
259
277
  break;
260
278
  }
279
+ hasExecutedAnyTool = true;
280
+ // Once tools have been called, switch to AUTO mode so the agent can provide a text summary on completion
281
+ this.chat.setToolConfig(undefined);
261
282
  const toolResponses = [];
262
283
  // Execute all requested tools sequentially (to avoid parallel lock contention from the same agent)
263
284
  for (const call of calls) {
@@ -273,7 +294,7 @@ export class SubAgentRunner {
273
294
  debugLog(`SubAgent [${this.taskId}]: Executing tool ${call.name}`);
274
295
  let responseData;
275
296
  try {
276
- responseData = await executeScopedTool(call.name, call.args, this.workspaceRoot, this.taskId, this.bus, this.locks, this.pingHeartbeat, undefined, this.taskScope);
297
+ responseData = await executeScopedTool(call.name, call.args, this.workspaceRoot, this.taskId, this.bus, this.locks, this.pingHeartbeat, undefined, this.taskScope, this.readCache);
277
298
  }
278
299
  catch (err) {
279
300
  responseData = { error: err.message || String(err) };
@@ -365,6 +386,7 @@ export class SubAgentRunner {
365
386
  ` - Step 3 (Verify): Use 'run_debug_script', 'run_fuzz_probe', 'check_heap_delta', 'check_behavioral_drift', or 'run_command' to test your changes if necessary.\n` +
366
387
  ` - Step 4 (Conclude): Once your specific objective is fully met, stop calling tools and return a text summary of your changes.\n` +
367
388
  `4. Coordinate: Use 'read_messages' to check for updates from other agents. Use 'post_message' if you discover breaking changes affecting others.\n` +
389
+ `5. IMMEDIATE TOOL ACTION (CRITICAL): On your very first turn, you MUST call tools to inspect or modify code. Do NOT output a conversational text plan or description on Turn 1—call the required tools directly.\n` +
368
390
  `</critical_guidelines>`);
369
391
  }
370
392
  /**
@@ -1,9 +1,36 @@
1
1
  /**
2
- * Performs fast, local syntax validation using bracket-matching state machine and native parsers.
3
- * Acts as a first-pass filter before AI-based validation.
2
+ * Represents the structured result of a local syntax validation check.
4
3
  */
5
4
  export interface ValidationResult {
5
+ /**
6
+ * Indicates whether the validated source code is syntactically valid without detected defects,
7
+ * unclosed delimiters, or AI-generated truncation markers.
8
+ */
6
9
  isValid: boolean;
10
+ /**
11
+ * A detailed, human-readable error description explaining why syntax validation failed,
12
+ * frequently including the character offset or delimiter mismatch information.
13
+ */
7
14
  error?: string;
8
15
  }
16
+ /**
17
+ * Performs fast, deterministic local syntax validation on source code using native parsers,
18
+ * AI truncation heuristics, and a single-pass lexical bracket-matching state machine.
19
+ *
20
+ * ### Validation Lifecycle:
21
+ * 1. **Native JSON Verification**: Validates `.json` files via `JSON.parse` with exact error reporting.
22
+ * 2. **AI Truncation Detection**: Flags incomplete edits containing ellipsis placeholder comments via {@link isTruncated}.
23
+ * 3. **Dialect Classification**: Categorizes files into C-style, Hash-style, or JS/TS dialects, allowing unrecognized formats to pass through.
24
+ * 4. **Single-Pass Lexical Scanning ($O(N)$)**:
25
+ * - Ignores single-line (`//`, `#`) and multi-line (`/* ... *\/`) comments.
26
+ * - Skips string literals (`'...'`, `"..."`) while handling escape sequences (`\`).
27
+ * - Tracks JS/TS template literals (`` `...` ``) and nested interpolation expressions (`${...}`).
28
+ * - Disambiguates JS/TS regular expressions (`/.../flags`) from division operators (`/`) using token lookback.
29
+ * - Maintains a delimiter stack for `{}`, `[]`, `()`, and `${}` with positional error reporting on mismatches.
30
+ * 5. **Terminal State Verification**: Confirms all brackets, comments, strings, template literals, and regexes are properly closed.
31
+ *
32
+ * @param filePath - The file path used to identify file extension and language-specific syntax rules.
33
+ * @param content - The raw text content of the file to validate.
34
+ * @returns A {@link ValidationResult} object indicating whether validation passed or detailing the failure reason.
35
+ */
9
36
  export declare function localValidate(filePath: string, content: string): ValidationResult;
@@ -1,6 +1,15 @@
1
1
  import path from 'path';
2
2
  /**
3
- * Checks if file content contains placeholder comments indicating AI code truncation.
3
+ * Inspects source file content for comment markers that indicate AI code generation truncation
4
+ * (e.g., `// ... existing code ...`, `/* ... rest of implementation ... *\/`, or `// ... remaining ...`).
5
+ *
6
+ * Large language models often insert placeholder comments to abbreviate long files when generating edits.
7
+ * Applying truncated content directly to disk would destroy existing code; this heuristic flags such
8
+ * content so it can be rejected or regenerated prior to mutation.
9
+ *
10
+ * @internal
11
+ * @param content - The raw source file content string to inspect.
12
+ * @returns `true` if any line matches known truncation placeholder patterns; `false` otherwise.
4
13
  */
5
14
  function isTruncated(content) {
6
15
  const lines = content.split('\n');
@@ -16,6 +25,122 @@ function isTruncated(content) {
16
25
  }
17
26
  return false;
18
27
  }
28
+ /**
29
+ * File extensions that follow C-style syntax conventions, including `//` single-line comments,
30
+ * `/* ... *\/` block comments, and standard `{`, `[`, `(` bracket nesting rules.
31
+ */
32
+ const C_STYLE_EXTS = new Set([
33
+ '.ts',
34
+ '.js',
35
+ '.tsx',
36
+ '.jsx',
37
+ '.css',
38
+ '.html',
39
+ '.rs',
40
+ '.go',
41
+ '.java',
42
+ '.cpp',
43
+ '.c',
44
+ '.h',
45
+ '.cs',
46
+ '.php',
47
+ '.swift',
48
+ ]);
49
+ /**
50
+ * File extensions for scripting and configuration languages that utilize `#` for single-line comments.
51
+ */
52
+ const HASH_STYLE_EXTS = new Set(['.py', '.rb', '.sh', '.yaml', '.yml']);
53
+ /**
54
+ * File extensions for JavaScript and TypeScript dialects requiring specialized parsing
55
+ * for template literals (`` `...` ``) and regular expression literals (`/.../`).
56
+ */
57
+ const JS_TS_EXTS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs']);
58
+ /**
59
+ * JavaScript/TypeScript keywords that can immediately precede an expression context.
60
+ * When a forward slash `/` follows one of these keywords, it is parsed as a regular expression literal
61
+ * rather than an arithmetic division operator.
62
+ */
63
+ const EXPR_KEYWORDS = new Set([
64
+ 'return',
65
+ 'case',
66
+ 'typeof',
67
+ 'void',
68
+ 'delete',
69
+ 'yield',
70
+ 'await',
71
+ 'throw',
72
+ 'instanceof',
73
+ 'in',
74
+ 'of',
75
+ 'new',
76
+ 'else',
77
+ 'do',
78
+ 'tok',
79
+ ]);
80
+ /**
81
+ * Punctuation characters and operators that denote an expression context in JavaScript/TypeScript.
82
+ * When a forward slash `/` follows one of these tokens, it initiates a regular expression literal.
83
+ */
84
+ const EXPR_PUNCT = new Set([
85
+ '(',
86
+ '[',
87
+ '{',
88
+ ';',
89
+ ',',
90
+ '=',
91
+ '+',
92
+ '-',
93
+ '*',
94
+ '%',
95
+ '&',
96
+ '|',
97
+ '^',
98
+ '!',
99
+ '~',
100
+ '?',
101
+ ':',
102
+ '<',
103
+ '/',
104
+ '=>',
105
+ '${',
106
+ ]);
107
+ /**
108
+ * Regular expression matching word characters valid in JavaScript/TypeScript identifiers.
109
+ */
110
+ const wordCharRegex = new RegExp('[a-zA-Z0-9_$]');
111
+ /**
112
+ * Regular expression matching alphabetic characters representing regex flags (e.g., `g`, `i`, `m`, `s`, `u`, `y`, `v`).
113
+ */
114
+ const alphaRegex = new RegExp('[a-zA-Z]');
115
+ /**
116
+ * Mapping of opening delimiter tokens to their expected closing delimiter counterparts.
117
+ */
118
+ const BRACKET_PAIRS = {
119
+ '{': '}',
120
+ '[': ']',
121
+ '(': ')',
122
+ '${': '}',
123
+ };
124
+ /**
125
+ * Performs fast, deterministic local syntax validation on source code using native parsers,
126
+ * AI truncation heuristics, and a single-pass lexical bracket-matching state machine.
127
+ *
128
+ * ### Validation Lifecycle:
129
+ * 1. **Native JSON Verification**: Validates `.json` files via `JSON.parse` with exact error reporting.
130
+ * 2. **AI Truncation Detection**: Flags incomplete edits containing ellipsis placeholder comments via {@link isTruncated}.
131
+ * 3. **Dialect Classification**: Categorizes files into C-style, Hash-style, or JS/TS dialects, allowing unrecognized formats to pass through.
132
+ * 4. **Single-Pass Lexical Scanning ($O(N)$)**:
133
+ * - Ignores single-line (`//`, `#`) and multi-line (`/* ... *\/`) comments.
134
+ * - Skips string literals (`'...'`, `"..."`) while handling escape sequences (`\`).
135
+ * - Tracks JS/TS template literals (`` `...` ``) and nested interpolation expressions (`${...}`).
136
+ * - Disambiguates JS/TS regular expressions (`/.../flags`) from division operators (`/`) using token lookback.
137
+ * - Maintains a delimiter stack for `{}`, `[]`, `()`, and `${}` with positional error reporting on mismatches.
138
+ * 5. **Terminal State Verification**: Confirms all brackets, comments, strings, template literals, and regexes are properly closed.
139
+ *
140
+ * @param filePath - The file path used to identify file extension and language-specific syntax rules.
141
+ * @param content - The raw text content of the file to validate.
142
+ * @returns A {@link ValidationResult} object indicating whether validation passed or detailing the failure reason.
143
+ */
19
144
  export function localValidate(filePath, content) {
20
145
  const ext = path.extname(filePath).toLowerCase();
21
146
  // 1. Check JSON files using native JSON parser
@@ -31,44 +156,23 @@ export function localValidate(filePath, content) {
31
156
  };
32
157
  }
33
158
  }
34
- // 2. Check for truncation markers
159
+ // 2. Check for truncation markers (AI placeholder comments)
35
160
  if (isTruncated(content)) {
36
161
  return {
37
162
  isValid: false,
38
163
  error: 'File appears to be truncated (contains placeholder comments)',
39
164
  };
40
165
  }
41
- const C_STYLE_EXTS = [
42
- '.ts',
43
- '.js',
44
- '.tsx',
45
- '.jsx',
46
- '.css',
47
- '.html',
48
- '.rs',
49
- '.go',
50
- '.java',
51
- '.cpp',
52
- '.c',
53
- '.h',
54
- '.cs',
55
- '.php',
56
- '.swift',
57
- ];
58
- const HASH_STYLE_EXTS = ['.py', '.rb', '.sh', '.yaml', '.yml'];
59
- const isJsTs = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'].includes(ext);
60
- const isCStyle = C_STYLE_EXTS.includes(ext);
61
- const isHashStyle = HASH_STYLE_EXTS.includes(ext);
166
+ const isJsTs = JS_TS_EXTS.has(ext);
167
+ const isCStyle = C_STYLE_EXTS.has(ext);
168
+ const isHashStyle = HASH_STYLE_EXTS.has(ext);
169
+ // Pass-through for unrecognized or un-parsed file formats (e.g., plain text, markdown)
62
170
  if (!isCStyle && !isHashStyle && !isJsTs) {
63
171
  return { isValid: true };
64
172
  }
173
+ // Delimiter tracking stack storing expected open delimiters ('{', '[', '(', '${')
65
174
  const stack = [];
66
- const pairs = {
67
- '{': '}',
68
- '[': ']',
69
- '(': ')',
70
- '${': '}',
71
- };
175
+ // Lexical scanner state flags
72
176
  let inSingleComment = false;
73
177
  let inMultiComment = false;
74
178
  let inString = false;
@@ -76,51 +180,9 @@ export function localValidate(filePath, content) {
76
180
  let inTemplate = false;
77
181
  let inRegex = false;
78
182
  let inRegexCharClass = false;
79
- // Track tokens to determine if '/' can start a regex literal
183
+ // Track preceding tokens to determine if '/' represents a regex literal or division operator
80
184
  let lastToken = '';
81
185
  let currentWord = '';
82
- const EXPR_KEYWORDS = new Set([
83
- 'return',
84
- 'case',
85
- 'typeof',
86
- 'void',
87
- 'delete',
88
- 'yield',
89
- 'await',
90
- 'throw',
91
- 'instanceof',
92
- 'in',
93
- 'of',
94
- 'new',
95
- 'else',
96
- 'do',
97
- 'tok',
98
- ]);
99
- const EXPR_PUNCT = new Set([
100
- '(',
101
- '[',
102
- '{',
103
- ';',
104
- ',',
105
- '=',
106
- '+',
107
- '-',
108
- '*',
109
- '%',
110
- '&',
111
- '|',
112
- '^',
113
- '!',
114
- '~',
115
- '?',
116
- ':',
117
- '<',
118
- '/',
119
- '=>',
120
- '${',
121
- ]);
122
- const wordCharRegex = new RegExp('[a-zA-Z0-9_$]');
123
- const alphaRegex = new RegExp('[a-zA-Z]');
124
186
  for (let i = 0; i < content.length; i++) {
125
187
  const char = content[i];
126
188
  const nextChar = content[i + 1];
@@ -142,7 +204,7 @@ export function localValidate(filePath, content) {
142
204
  // --- 3. STRING LITERAL ('...' or "...") ---
143
205
  if (inString) {
144
206
  if (char === '\\') {
145
- i++; // skip escaped char
207
+ i++; // skip escaped character
146
208
  continue;
147
209
  }
148
210
  if (char === stringChar) {
@@ -154,11 +216,11 @@ export function localValidate(filePath, content) {
154
216
  // --- 4. TEMPLATE LITERAL (`...`) ---
155
217
  if (inTemplate) {
156
218
  if (char === '\\') {
157
- i++; // skip escaped char
219
+ i++; // skip escaped character
158
220
  continue;
159
221
  }
160
222
  if (char === '$' && nextChar === '{') {
161
- // Enter template expression ${...}
223
+ // Enter template interpolation expression ${...}
162
224
  stack.push('${');
163
225
  inTemplate = false;
164
226
  i++; // skip '{'
@@ -174,20 +236,22 @@ export function localValidate(filePath, content) {
174
236
  // --- 5. REGEX LITERAL (/.../) ---
175
237
  if (inRegex) {
176
238
  if (char === '\\') {
177
- i++; // skip escaped char
239
+ i++; // skip escaped regex character
178
240
  continue;
179
241
  }
180
242
  if (char === '[') {
243
+ // Enter regex character class [...] where '/' does not terminate the regex
181
244
  inRegexCharClass = true;
182
245
  continue;
183
246
  }
184
247
  if (char === ']') {
248
+ // Exit regex character class
185
249
  inRegexCharClass = false;
186
250
  continue;
187
251
  }
188
252
  if (char === '/' && !inRegexCharClass) {
189
253
  inRegex = false;
190
- // Skip regex flags (e.g. /foo/gim)
254
+ // Skip trailing regex flags (e.g. /foo/gimsu)
191
255
  while (i + 1 < content.length && alphaRegex.test(content[i + 1])) {
192
256
  i++;
193
257
  }
@@ -260,6 +324,7 @@ export function localValidate(filePath, content) {
260
324
  }
261
325
  // --- 7. BRACKET MATCHING ---
262
326
  if (['{', '[', '('].includes(char)) {
327
+ // For hash-style languages (Python/YAML), skip curly braces to avoid false positives with dictionary/block formatting
263
328
  if (isHashStyle && char === '{')
264
329
  continue;
265
330
  stack.push(char);
@@ -268,7 +333,7 @@ export function localValidate(filePath, content) {
268
333
  if (isHashStyle && char === '}')
269
334
  continue;
270
335
  const last = stack.pop();
271
- if (!last || pairs[last] !== char) {
336
+ if (!last || BRACKET_PAIRS[last] !== char) {
272
337
  return {
273
338
  isValid: false,
274
339
  error: "Unbalanced character '" + char + "' at position " + i,