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.
- package/dist/services/ai.d.ts +6 -2
- package/dist/services/ai.js +18 -7
- package/dist/services/contextAgent.js +32 -22
- package/dist/services/orchestration/investigationAgent.js +40 -6
- package/dist/services/orchestration/readCache.d.ts +34 -12
- package/dist/services/orchestration/readCache.js +75 -17
- package/dist/services/orchestration/scopedTools.d.ts +4 -2
- package/dist/services/orchestration/scopedTools.js +105 -50
- package/dist/services/orchestration/subAgent.d.ts +1 -0
- package/dist/services/orchestration/subAgent.js +24 -2
- package/dist/utils/localSyntaxValidator.d.ts +29 -2
- package/dist/utils/localSyntaxValidator.js +143 -78
- package/dist/utils/systemPrompts.d.ts +1 -1
- package/dist/utils/systemPrompts.js +6 -3
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
|
@@ -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
|
-
//
|
|
153
|
-
let
|
|
154
|
-
let
|
|
155
|
-
if (name === '
|
|
156
|
-
|
|
157
|
-
if (
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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 '/'
|
|
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
|
|
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
|
|
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
|
|
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/
|
|
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 ||
|
|
336
|
+
if (!last || BRACKET_PAIRS[last] !== char) {
|
|
272
337
|
return {
|
|
273
338
|
isValid: false,
|
|
274
339
|
error: "Unbalanced character '" + char + "' at position " + i,
|