scai 0.1.175 → 0.1.177
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/agents/MainAgent.js +68 -77
- package/dist/agents/fileCheckStep.js +13 -5
- package/dist/agents/guardPolicy.js +43 -0
- package/dist/commands/FindCmd.js +3 -3
- package/dist/commands/SummaryCmd.js +5 -4
- package/dist/db/fileIndex.js +25 -72
- package/dist/fileRules/ignoredPaths.js +17 -0
- package/dist/pipeline/modules/fileSearchModule.js +26 -6
- package/dist/pipeline/modules/gatherInfoModule.js +13 -8
- package/dist/utils/compileSearchQuery.js +138 -0
- package/dist/utils/resolveTargetsToFiles.js +5 -0
- package/package.json +1 -1
package/dist/agents/MainAgent.js
CHANGED
|
@@ -28,6 +28,8 @@ import { structuralPreloadStep } from "./structuralPreloadStep.js";
|
|
|
28
28
|
import { extractFileReferences } from "../utils/extractFileReferences.js";
|
|
29
29
|
import { PREFILTER_STOP_WORDS } from "../fileRules/stopWords.js";
|
|
30
30
|
import { MAX_WELL_KNOWN_REPO_FILES, WELL_KNOWN_REPO_FILE_BASENAMES } from "../fileRules/wellKnownRepoFiles.js";
|
|
31
|
+
import { isPathIgnoredByFolderGlobs } from "../fileRules/ignoredPaths.js";
|
|
32
|
+
import { canExecutePhase, canExecuteRoute, canExecuteScope } from "./guardPolicy.js";
|
|
31
33
|
import chalk from "chalk";
|
|
32
34
|
import path from "path";
|
|
33
35
|
import fs from "fs";
|
|
@@ -172,12 +174,12 @@ export class MainAgent {
|
|
|
172
174
|
break;
|
|
173
175
|
}
|
|
174
176
|
// ---------------- INFORMATION ACQUISITION ----------------
|
|
175
|
-
const canRouteSearchExpansion = this.
|
|
177
|
+
const canRouteSearchExpansion = canExecuteRoute(this.context, "search-expand");
|
|
176
178
|
if (!canRouteSearchExpansion) {
|
|
177
179
|
this.logLine("PLAN", "infoPlanGen", undefined, "skipped (routing disallows search expansion)", { highlight: false });
|
|
178
180
|
}
|
|
179
|
-
else if (this.
|
|
180
|
-
this.
|
|
181
|
+
else if (canExecutePhase(this.context, "planning") &&
|
|
182
|
+
canExecuteScope(this.context, "planning")) {
|
|
181
183
|
const t = this.startTimer();
|
|
182
184
|
await infoPlanGenStep.run(this.context);
|
|
183
185
|
const infoPlan = this.context.analysis?.planSuggestion?.plan ?? { steps: [] };
|
|
@@ -208,7 +210,7 @@ export class MainAgent {
|
|
|
208
210
|
*/
|
|
209
211
|
async runResearch() {
|
|
210
212
|
var _a, _b;
|
|
211
|
-
if (!this.
|
|
213
|
+
if (!canExecuteRoute(this.context, "research")) {
|
|
212
214
|
this.logLine("RESEARCH", "taskStepSeed", undefined, "skipped (route disallows research)");
|
|
213
215
|
return;
|
|
214
216
|
}
|
|
@@ -298,7 +300,7 @@ export class MainAgent {
|
|
|
298
300
|
var _a, _b;
|
|
299
301
|
if (!this.context.task)
|
|
300
302
|
return;
|
|
301
|
-
if (!this.
|
|
303
|
+
if (!canExecutePhase(this.context, "planning") || !canExecuteScope(this.context, "planning"))
|
|
302
304
|
return;
|
|
303
305
|
(_a = this.context).analysis || (_a.analysis = {});
|
|
304
306
|
(_b = this.context.task).taskSteps || (_b.taskSteps = []);
|
|
@@ -478,6 +480,9 @@ export class MainAgent {
|
|
|
478
480
|
// Step-level iterations
|
|
479
481
|
// ---------------------------
|
|
480
482
|
const stepAction = await this.runStepIterations(taskStep);
|
|
483
|
+
if (stepAction === "expand-scope") {
|
|
484
|
+
this.applyScopeExpansionRouteHint();
|
|
485
|
+
}
|
|
481
486
|
this.finishTaskStep(taskStep, stepCount, stepAction);
|
|
482
487
|
}
|
|
483
488
|
this.logLine("TASK", "Max task step limit reached — stopping work loop", undefined, undefined, { highlight: false });
|
|
@@ -517,6 +522,8 @@ export class MainAgent {
|
|
|
517
522
|
return "request-feedback";
|
|
518
523
|
if (nextAction === "redo-step")
|
|
519
524
|
continue;
|
|
525
|
+
if (nextAction === "expand-scope")
|
|
526
|
+
return "expand-scope";
|
|
520
527
|
}
|
|
521
528
|
return "continue";
|
|
522
529
|
}
|
|
@@ -532,7 +539,7 @@ export class MainAgent {
|
|
|
532
539
|
await this.executeResearchTaskStep(taskStep);
|
|
533
540
|
return;
|
|
534
541
|
}
|
|
535
|
-
if (this.
|
|
542
|
+
if (canExecutePhase(this.context, "analysis") && canExecuteScope(this.context, "analysis")) {
|
|
536
543
|
const tAnalysis = this.startTimer();
|
|
537
544
|
await analysisPlanGenStep.run(this.context);
|
|
538
545
|
this.logLine("PLAN", "analysisPlanGen", tAnalysis(), undefined, { highlight: false });
|
|
@@ -545,7 +552,9 @@ export class MainAgent {
|
|
|
545
552
|
if (this.context.analysis)
|
|
546
553
|
this.context.analysis.planSuggestion = undefined;
|
|
547
554
|
}
|
|
548
|
-
if (
|
|
555
|
+
if (canExecutePhase(this.context, "transform") &&
|
|
556
|
+
canExecuteScope(this.context, "transform") &&
|
|
557
|
+
canExecuteRoute(this.context, "transform")) {
|
|
549
558
|
const tTransform = this.startTimer();
|
|
550
559
|
await transformPlanGenStep.run(this.context);
|
|
551
560
|
this.logLine("PLAN", "transformPlanGen", tTransform(), undefined, { highlight: false });
|
|
@@ -558,7 +567,7 @@ export class MainAgent {
|
|
|
558
567
|
}
|
|
559
568
|
if (this.context.analysis)
|
|
560
569
|
this.context.analysis.planSuggestion = undefined;
|
|
561
|
-
if (this.
|
|
570
|
+
if (canExecutePhase(this.context, "write") && canExecuteScope(this.context, "write")) {
|
|
562
571
|
const tWrite = this.startTimer();
|
|
563
572
|
await writeFileStep.run({ query: this.query, context: this.context });
|
|
564
573
|
this.logLine("WRITE", "writeFileStep", tWrite());
|
|
@@ -1165,6 +1174,8 @@ export class MainAgent {
|
|
|
1165
1174
|
const existsOrResearch = (filePath) => {
|
|
1166
1175
|
if (filePath.startsWith("__research__/"))
|
|
1167
1176
|
return true;
|
|
1177
|
+
if (isPathIgnoredByFolderGlobs(filePath))
|
|
1178
|
+
return false;
|
|
1168
1179
|
return fs.existsSync(filePath);
|
|
1169
1180
|
};
|
|
1170
1181
|
let removedRelated = 0;
|
|
@@ -1218,7 +1229,7 @@ export class MainAgent {
|
|
|
1218
1229
|
*/
|
|
1219
1230
|
getTaskStepBudget() {
|
|
1220
1231
|
const scope = this.context.analysis?.scopeType ?? "repo-wide";
|
|
1221
|
-
if (this.
|
|
1232
|
+
if (canExecuteRoute(this.context, "research"))
|
|
1222
1233
|
return 10;
|
|
1223
1234
|
if (scope === "multi-file")
|
|
1224
1235
|
return 7;
|
|
@@ -1231,7 +1242,7 @@ export class MainAgent {
|
|
|
1231
1242
|
* Example: require at least two analyzed files plus one understanding signal.
|
|
1232
1243
|
*/
|
|
1233
1244
|
isResearchGateSatisfied() {
|
|
1234
|
-
if (!this.
|
|
1245
|
+
if (!canExecuteRoute(this.context, "research"))
|
|
1235
1246
|
return true;
|
|
1236
1247
|
const scope = this.context.analysis?.scopeType ?? "repo-wide";
|
|
1237
1248
|
const researchPlanCount = this.context.task?.taskSteps?.filter(s => typeof s.action === "string" && s.action.startsWith("research-")).length ?? 0;
|
|
@@ -1302,29 +1313,45 @@ export class MainAgent {
|
|
|
1302
1313
|
this.context.task.status = status;
|
|
1303
1314
|
this.persistTaskDataForRun();
|
|
1304
1315
|
}
|
|
1316
|
+
applyScopeExpansionRouteHint() {
|
|
1317
|
+
var _a;
|
|
1318
|
+
(_a = this.context).analysis || (_a.analysis = {});
|
|
1319
|
+
const route = this.context.analysis.routingDecision;
|
|
1320
|
+
if (!route)
|
|
1321
|
+
return;
|
|
1322
|
+
route.decision = "needs-info";
|
|
1323
|
+
route.allowSearch = true;
|
|
1324
|
+
route.scopeLocked = false;
|
|
1325
|
+
route.rationale = `${route.rationale}; stepReasoning=expand-scope`;
|
|
1326
|
+
this.logLine("TASK", "Route updated from step reasoning", undefined, "expand-scope requested; re-enabled search expansion");
|
|
1327
|
+
}
|
|
1305
1328
|
startTaskStep(taskStep, stepCount) {
|
|
1306
1329
|
this.ensureWorkingFilesLoaded([taskStep.filePath], "Current task step");
|
|
1330
|
+
const db = getDbForRepo();
|
|
1331
|
+
this.ensureTaskIdentityForPersistence(db);
|
|
1307
1332
|
this.context.task.currentStep = taskStep;
|
|
1308
1333
|
taskStep.taskId = this.taskId;
|
|
1309
1334
|
taskStep.stepIndex = stepCount;
|
|
1310
1335
|
taskStep.status = "pending";
|
|
1311
|
-
persistTaskStepInsert(taskStep,
|
|
1336
|
+
persistTaskStepInsert(taskStep, db);
|
|
1312
1337
|
const displayPath = this.formatTaskStepDisplayPath(taskStep.filePath);
|
|
1313
1338
|
this.logLine("NEW STEP", `Processing taskStep ${stepCount}`, undefined, displayPath, { highlight: true });
|
|
1314
1339
|
taskStep.startTime = Date.now();
|
|
1315
|
-
persistTaskStepStart(taskStep,
|
|
1340
|
+
persistTaskStepStart(taskStep, db);
|
|
1316
1341
|
}
|
|
1317
1342
|
finishTaskStep(taskStep, stepCount, stepAction) {
|
|
1343
|
+
const db = getDbForRepo();
|
|
1344
|
+
this.ensureTaskIdentityForPersistence(db);
|
|
1318
1345
|
const displayPath = this.formatTaskStepDisplayPath(taskStep.filePath);
|
|
1319
1346
|
taskStep.endTime = Date.now();
|
|
1320
1347
|
if (stepAction === "complete") {
|
|
1321
1348
|
taskStep.status = "completed";
|
|
1322
|
-
persistTaskStepCompletion(taskStep,
|
|
1349
|
+
persistTaskStepCompletion(taskStep, db);
|
|
1323
1350
|
this.logLine("STEP-DONE", `Completed taskStep ${stepCount}`, undefined, displayPath, { highlight: false });
|
|
1324
1351
|
return;
|
|
1325
1352
|
}
|
|
1326
1353
|
taskStep.status = "pending";
|
|
1327
|
-
persistTaskStepCompletion(taskStep,
|
|
1354
|
+
persistTaskStepCompletion(taskStep, db);
|
|
1328
1355
|
this.logLine("STEP", `Pending taskStep ${stepCount}`, undefined, displayPath);
|
|
1329
1356
|
}
|
|
1330
1357
|
/**
|
|
@@ -1336,69 +1363,6 @@ export class MainAgent {
|
|
|
1336
1363
|
? filePath.replace("__research__/", "research/")
|
|
1337
1364
|
: filePath;
|
|
1338
1365
|
}
|
|
1339
|
-
/* ───────────── execution gates ───────────── */
|
|
1340
|
-
/**
|
|
1341
|
-
* Gate model:
|
|
1342
|
-
* 1) Phase + scope gates decide coarse permissions (what broad work is allowed).
|
|
1343
|
-
* 2) Route gate decides finer sub-decisions within those allowed areas (what to do next).
|
|
1344
|
-
*/
|
|
1345
|
-
/**
|
|
1346
|
-
* Gate 1: Is this kind of work allowed at all?
|
|
1347
|
-
* Plain meaning: checks capability rules (e.g. read-only vs file-writing).
|
|
1348
|
-
* Example: for docs-only mode, analysis/planning are blocked, and writes are limited.
|
|
1349
|
-
*/
|
|
1350
|
-
canExecutePhase(phase) {
|
|
1351
|
-
const constraints = this.context.executionControl?.constraints;
|
|
1352
|
-
const docsOnly = constraints?.docsOnly ?? false;
|
|
1353
|
-
let allowed = false;
|
|
1354
|
-
switch (phase) {
|
|
1355
|
-
case "analysis":
|
|
1356
|
-
case "planning":
|
|
1357
|
-
allowed = !docsOnly;
|
|
1358
|
-
break;
|
|
1359
|
-
case "transform":
|
|
1360
|
-
case "write":
|
|
1361
|
-
allowed = constraints?.allowFileWrites ?? false;
|
|
1362
|
-
break;
|
|
1363
|
-
}
|
|
1364
|
-
return allowed;
|
|
1365
|
-
}
|
|
1366
|
-
/* ───────────── scope gates ───────────── */
|
|
1367
|
-
/**
|
|
1368
|
-
* Gate 2: Is this work allowed for the current scope size?
|
|
1369
|
-
* Plain meaning: checks scope rules (none/single/multi/repo-wide).
|
|
1370
|
-
* Example: if scope is "analysis", only analysis/planning run and transform/write are blocked.
|
|
1371
|
-
*/
|
|
1372
|
-
canExecuteScope(phase) {
|
|
1373
|
-
const scope = this.context.analysis?.scopeType ?? "repo-wide";
|
|
1374
|
-
let allowed = false;
|
|
1375
|
-
switch (scope) {
|
|
1376
|
-
case "none":
|
|
1377
|
-
allowed = phase === "analysis" || phase === "planning";
|
|
1378
|
-
break;
|
|
1379
|
-
default:
|
|
1380
|
-
allowed = true;
|
|
1381
|
-
}
|
|
1382
|
-
return allowed;
|
|
1383
|
-
}
|
|
1384
|
-
/**
|
|
1385
|
-
* Gate 3: Does this request path want this action right now?
|
|
1386
|
-
* Plain meaning: checks route-specific intent from routingDecision.
|
|
1387
|
-
* Example: search expansion is skipped when routing says allowSearch=false.
|
|
1388
|
-
*/
|
|
1389
|
-
canExecuteRoute(action) {
|
|
1390
|
-
const routing = this.context.analysis?.routingDecision;
|
|
1391
|
-
switch (action) {
|
|
1392
|
-
case "search-expand":
|
|
1393
|
-
return routing?.allowSearch ?? true;
|
|
1394
|
-
case "transform":
|
|
1395
|
-
return routing?.allowTransform ?? true;
|
|
1396
|
-
case "research":
|
|
1397
|
-
return routing?.allowResearch ?? false;
|
|
1398
|
-
default:
|
|
1399
|
-
return true;
|
|
1400
|
-
}
|
|
1401
|
-
}
|
|
1402
1366
|
/* ----------------------------------- */
|
|
1403
1367
|
/* ------------- helpers ------------- */
|
|
1404
1368
|
/* ----------------------------------- */
|
|
@@ -1479,6 +1443,33 @@ export class MainAgent {
|
|
|
1479
1443
|
}
|
|
1480
1444
|
}
|
|
1481
1445
|
}
|
|
1446
|
+
/**
|
|
1447
|
+
* Ensures the current task id exists in the active DB before step persistence.
|
|
1448
|
+
* Example: if repo/db context switched, re-create task row and rebind step taskIds.
|
|
1449
|
+
*/
|
|
1450
|
+
ensureTaskIdentityForPersistence(db) {
|
|
1451
|
+
var _a;
|
|
1452
|
+
const activeTaskId = this.taskId ?? this.context.task?.id;
|
|
1453
|
+
if (!activeTaskId) {
|
|
1454
|
+
this.taskId = bootTaskForRepo(this.context, db, this.logLine.bind(this));
|
|
1455
|
+
this.context.task.id = this.taskId;
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
const row = db.prepare("SELECT id FROM tasks WHERE id = ?").get(activeTaskId);
|
|
1459
|
+
if (row?.id) {
|
|
1460
|
+
this.taskId = activeTaskId;
|
|
1461
|
+
this.context.task.id = activeTaskId;
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
const reboundTaskId = bootTaskForRepo(this.context, db, this.logLine.bind(this));
|
|
1465
|
+
this.taskId = reboundTaskId;
|
|
1466
|
+
this.context.task.id = reboundTaskId;
|
|
1467
|
+
(_a = this.context.task).taskSteps || (_a.taskSteps = []);
|
|
1468
|
+
for (const step of this.context.task.taskSteps) {
|
|
1469
|
+
step.taskId = reboundTaskId;
|
|
1470
|
+
}
|
|
1471
|
+
this.logLine("TASK", "taskId rebound", undefined, `previous=${activeTaskId}, rebound=${reboundTaskId}`);
|
|
1472
|
+
}
|
|
1482
1473
|
}
|
|
1483
1474
|
function scoreCandidateFiles(filePaths, relatedFileScores, retrievalQuery) {
|
|
1484
1475
|
const explicitRefs = extractFileReferences(retrievalQuery, { lowercase: true });
|
|
@@ -3,6 +3,7 @@ import { generate } from "../lib/generate.js";
|
|
|
3
3
|
import { logInputOutput } from "../utils/promptLogHelper.js";
|
|
4
4
|
import { cleanupModule } from "../pipeline/modules/cleanupModule.js";
|
|
5
5
|
import path from "path";
|
|
6
|
+
import { isPathIgnoredByFolderGlobs } from "../fileRules/ignoredPaths.js";
|
|
6
7
|
export async function fileCheckStep(context) {
|
|
7
8
|
var _a;
|
|
8
9
|
context.analysis ?? (context.analysis = {});
|
|
@@ -10,9 +11,7 @@ export async function fileCheckStep(context) {
|
|
|
10
11
|
const intent = context.analysis.intent;
|
|
11
12
|
const planSuggestion = context.analysis.planSuggestion?.text ?? "";
|
|
12
13
|
// Step 1: gather known files from initContext only
|
|
13
|
-
const knownFiles = new Set([
|
|
14
|
-
...(context.initContext?.relatedFiles ?? []),
|
|
15
|
-
]);
|
|
14
|
+
const knownFiles = new Set((context.initContext?.relatedFiles ?? []).filter(filePath => !isPathIgnoredByFolderGlobs(filePath)));
|
|
16
15
|
// Step 2: extract file names from normalizedQuery or planSuggestion
|
|
17
16
|
const extractedFiles = extractFilesFromAnalysis(context.analysis);
|
|
18
17
|
// Step 3: populate focus with safe defaults
|
|
@@ -138,16 +137,25 @@ Task:
|
|
|
138
137
|
// Merge parsed output safely
|
|
139
138
|
if (Array.isArray(parsed.selectedFiles)) {
|
|
140
139
|
const existing = new Set(context.analysis.focus.selectedFiles);
|
|
140
|
+
const safeSelected = parsed.selectedFiles
|
|
141
|
+
.filter((f) => typeof f === "string" && knownFiles.has(f) && !isPathIgnoredByFolderGlobs(f));
|
|
141
142
|
context.analysis.focus.selectedFiles = [
|
|
142
143
|
...context.analysis.focus.selectedFiles,
|
|
143
|
-
...
|
|
144
|
+
...safeSelected.filter((f) => !existing.has(f))
|
|
144
145
|
];
|
|
145
146
|
}
|
|
146
147
|
if (Array.isArray(parsed.candidateFiles)) {
|
|
147
148
|
const existing = new Set(context.analysis.focus.candidateFiles);
|
|
149
|
+
const safeCandidates = parsed.candidateFiles
|
|
150
|
+
.filter((f) => typeof f === "string")
|
|
151
|
+
.filter((f) => {
|
|
152
|
+
if (knownFiles.has(f))
|
|
153
|
+
return !isPathIgnoredByFolderGlobs(f);
|
|
154
|
+
return /^[\w\-./]+\.(js|ts|jsx|tsx|py|java|go|rs|cpp|c|cs|md)$/i.test(f);
|
|
155
|
+
});
|
|
148
156
|
context.analysis.focus.candidateFiles = [
|
|
149
157
|
...context.analysis.focus.candidateFiles,
|
|
150
|
-
...
|
|
158
|
+
...safeCandidates.filter((f) => !existing.has(f))
|
|
151
159
|
];
|
|
152
160
|
}
|
|
153
161
|
if (typeof parsed.rationale === "string") {
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gate 1: capability constraints (execution control).
|
|
3
|
+
*/
|
|
4
|
+
export function canExecutePhase(context, phase) {
|
|
5
|
+
const constraints = context.executionControl?.constraints;
|
|
6
|
+
const docsOnly = constraints?.docsOnly ?? false;
|
|
7
|
+
switch (phase) {
|
|
8
|
+
case "analysis":
|
|
9
|
+
case "planning":
|
|
10
|
+
return !docsOnly;
|
|
11
|
+
case "transform":
|
|
12
|
+
case "write":
|
|
13
|
+
return constraints?.allowFileWrites ?? false;
|
|
14
|
+
default:
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Gate 2: scope constraints (none/single/multi/repo-wide).
|
|
20
|
+
*/
|
|
21
|
+
export function canExecuteScope(context, phase) {
|
|
22
|
+
const scope = context.analysis?.scopeType ?? "repo-wide";
|
|
23
|
+
if (scope === "none") {
|
|
24
|
+
return phase === "analysis" || phase === "planning";
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Gate 3: route constraints (dynamic routing decisions).
|
|
30
|
+
*/
|
|
31
|
+
export function canExecuteRoute(context, action) {
|
|
32
|
+
const routing = context.analysis?.routingDecision;
|
|
33
|
+
switch (action) {
|
|
34
|
+
case "search-expand":
|
|
35
|
+
return routing?.allowSearch ?? true;
|
|
36
|
+
case "transform":
|
|
37
|
+
return routing?.allowTransform ?? true;
|
|
38
|
+
case "research":
|
|
39
|
+
return routing?.allowResearch ?? false;
|
|
40
|
+
default:
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
}
|
package/dist/commands/FindCmd.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { queryFiles } from '../db/fileIndex.js';
|
|
2
|
-
import {
|
|
2
|
+
import { compileSearchQuery } from '../utils/compileSearchQuery.js';
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import os from 'os';
|
|
5
5
|
export async function runFindCommand(query) {
|
|
@@ -8,8 +8,8 @@ export async function runFindCommand(query) {
|
|
|
8
8
|
return;
|
|
9
9
|
}
|
|
10
10
|
console.log(`\n🔍 Searching for: "${query}"\n`);
|
|
11
|
-
const
|
|
12
|
-
const results = queryFiles(
|
|
11
|
+
const compiled = compileSearchQuery({ query, mode: "fts" });
|
|
12
|
+
const results = compiled.fts.expression ? queryFiles(compiled.fts.expression) : [];
|
|
13
13
|
if (results.length === 0) {
|
|
14
14
|
console.log('⚠️ No matching files found.');
|
|
15
15
|
return;
|
|
@@ -4,7 +4,7 @@ import readline from 'readline';
|
|
|
4
4
|
import { queryFiles } from '../db/fileIndex.js';
|
|
5
5
|
import { summaryModule } from '../pipeline/modules/summaryModule.js';
|
|
6
6
|
import { detectFileType } from '../fileRules/detectFileType.js';
|
|
7
|
-
import {
|
|
7
|
+
import { compileSearchQuery } from '../utils/compileSearchQuery.js';
|
|
8
8
|
import { styleText } from '../utils/outputFormatter.js';
|
|
9
9
|
import { indexFile } from '../daemon/runIndexingBatch.js';
|
|
10
10
|
export async function summarizeFile(filepath) {
|
|
@@ -12,8 +12,8 @@ export async function summarizeFile(filepath) {
|
|
|
12
12
|
let filePathResolved;
|
|
13
13
|
// 📁 Resolve file path from index or local disk
|
|
14
14
|
if (filepath) {
|
|
15
|
-
const
|
|
16
|
-
const matches = queryFiles(
|
|
15
|
+
const compiled = compileSearchQuery({ query: filepath, mode: "fts" });
|
|
16
|
+
const matches = compiled.fts.expression ? queryFiles(compiled.fts.expression) : [];
|
|
17
17
|
if (matches.length > 0) {
|
|
18
18
|
const topMatch = matches[0];
|
|
19
19
|
filePathResolved = path.resolve(process.cwd(), topMatch.path);
|
|
@@ -34,7 +34,8 @@ export async function summarizeFile(filepath) {
|
|
|
34
34
|
}
|
|
35
35
|
// 📄 Load file content (from path or stdin)
|
|
36
36
|
if (filePathResolved) {
|
|
37
|
-
const
|
|
37
|
+
const compiled = compileSearchQuery({ query: filePathResolved, mode: "fts" });
|
|
38
|
+
const matches = compiled.fts.expression ? queryFiles(compiled.fts.expression) : [];
|
|
38
39
|
const match = matches.find(row => path.resolve(row.path) === filePathResolved);
|
|
39
40
|
if (match?.summary) {
|
|
40
41
|
console.log(`🧠 Cached summary for ${filepath}:\n`);
|
package/dist/db/fileIndex.js
CHANGED
|
@@ -15,9 +15,8 @@ import * as sqlTemplates from '../db/sqlTemplates.js';
|
|
|
15
15
|
import { RELATED_FILES_LIMIT } from '../constants.js';
|
|
16
16
|
import { generate } from '../lib/generate.js';
|
|
17
17
|
import { logInputOutput } from '../utils/promptLogHelper.js';
|
|
18
|
-
import {
|
|
18
|
+
import { compileSearchQuery } from '../utils/compileSearchQuery.js';
|
|
19
19
|
import { extractTaggedContent } from '../utils/parseTaggedContent.js';
|
|
20
|
-
import { extractFileReferences } from '../utils/extractFileReferences.js';
|
|
21
20
|
const QUERY_OPERATOR_TOKENS = new Set(["or", "and", "not", "near"]);
|
|
22
21
|
const GENERIC_FTS_TERMS = new Set([
|
|
23
22
|
"file",
|
|
@@ -659,12 +658,14 @@ export async function plannerSearchFiles(originalQuery, query, topK = 5) {
|
|
|
659
658
|
const db = getDbForRepo();
|
|
660
659
|
const seen = new Map();
|
|
661
660
|
const usedQueries = [];
|
|
662
|
-
const
|
|
661
|
+
const compiledPrimary = compileSearchQuery({ query, mode: "fts" });
|
|
662
|
+
const safeQuery = compiledPrimary.fts.expression;
|
|
663
663
|
if (safeQuery)
|
|
664
664
|
usedQueries.push(safeQuery);
|
|
665
|
-
const primaryResults =
|
|
666
|
-
.prepare(sqlTemplates.searchFilesTemplate)
|
|
667
|
-
|
|
665
|
+
const primaryResults = safeQuery
|
|
666
|
+
? db.prepare(sqlTemplates.searchFilesTemplate)
|
|
667
|
+
.all(safeQuery, RELATED_FILES_LIMIT)
|
|
668
|
+
: [];
|
|
668
669
|
primaryResults.forEach(r => seen.set(r.id, r));
|
|
669
670
|
logInputOutput("plannerSearchFiles primary FTS", "input", {
|
|
670
671
|
safeQuery,
|
|
@@ -673,12 +674,12 @@ export async function plannerSearchFiles(originalQuery, query, topK = 5) {
|
|
|
673
674
|
if (primaryResults.length === 0) {
|
|
674
675
|
const stmt = db.prepare(sqlTemplates.searchFilesTemplate);
|
|
675
676
|
const llmPrimaryQuery = await generatePrimaryFtsQuery(originalQuery);
|
|
676
|
-
const llmFallbackQueries = await generateFallbackFtsQueries(originalQuery, llmPrimaryQuery || safeQuery);
|
|
677
|
+
const llmFallbackQueries = await generateFallbackFtsQueries(originalQuery, llmPrimaryQuery || safeQuery || originalQuery);
|
|
677
678
|
const candidateQueries = [];
|
|
678
679
|
const pushCandidate = (q) => {
|
|
679
680
|
if (!q)
|
|
680
681
|
return;
|
|
681
|
-
const sanitized =
|
|
682
|
+
const sanitized = compileSearchQuery({ query: q, mode: "fts" }).fts.expression;
|
|
682
683
|
if (!sanitized)
|
|
683
684
|
return;
|
|
684
685
|
if (sanitized === safeQuery)
|
|
@@ -764,8 +765,12 @@ function buildQueryExpansionTerms(originalQuery, queries) {
|
|
|
764
765
|
* - output: "\"fileIndex.ts\" OR semanticsearchfiles* OR output*"
|
|
765
766
|
*/
|
|
766
767
|
function enforceFtsQueryPolicy(userQuery, candidateQuery, maxTerms, intent = {}) {
|
|
767
|
-
const
|
|
768
|
-
|
|
768
|
+
const candidateTerms = compileSearchQuery({
|
|
769
|
+
query: candidateQuery,
|
|
770
|
+
intent,
|
|
771
|
+
mode: "fts",
|
|
772
|
+
maxTerms,
|
|
773
|
+
}).fts.terms;
|
|
769
774
|
const prioritizedAnchors = buildAnchorTerms(userQuery, intent);
|
|
770
775
|
const hasAnchors = prioritizedAnchors.length > 0;
|
|
771
776
|
const filtered = candidateTerms.filter(term => {
|
|
@@ -777,25 +782,17 @@ function enforceFtsQueryPolicy(userQuery, candidateQuery, maxTerms, intent = {})
|
|
|
777
782
|
return true;
|
|
778
783
|
});
|
|
779
784
|
const merged = dedupeTerms([...prioritizedAnchors, ...filtered]);
|
|
780
|
-
return merged
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
* Example: "\"fileIndex.ts\" OR semanticsearchfiles* OR output*" ->
|
|
785
|
-
* ["\"fileIndex.ts\"", "semanticsearchfiles*", "output*"]
|
|
786
|
-
*/
|
|
787
|
-
function splitOrTerms(query) {
|
|
788
|
-
return query
|
|
789
|
-
.split(/\s+OR\s+/i)
|
|
790
|
-
.map(part => part.trim())
|
|
791
|
-
.filter(Boolean);
|
|
785
|
+
return merged
|
|
786
|
+
.slice(0, maxTerms)
|
|
787
|
+
.map(term => `${normalizeFtsTerm(term)}*`)
|
|
788
|
+
.join(" OR ");
|
|
792
789
|
}
|
|
793
790
|
/**
|
|
794
791
|
* Normalizes an FTS term for stable comparisons by removing quotes/wildcards.
|
|
795
792
|
* Example: "\"fileIndex.ts\"" -> "fileindex.ts", "Module*" -> "module"
|
|
796
793
|
*/
|
|
797
794
|
function normalizeFtsTerm(term) {
|
|
798
|
-
return term.replace(/[
|
|
795
|
+
return term.toLowerCase().replace(/[^a-z0-9_]/g, "").trim();
|
|
799
796
|
}
|
|
800
797
|
/**
|
|
801
798
|
* Dedupes terms using normalized keys while preserving first-seen order.
|
|
@@ -820,54 +817,10 @@ function dedupeTerms(terms) {
|
|
|
820
817
|
* - output includes: ["\"fileIndex.ts\"", "semanticsearchfiles*"]
|
|
821
818
|
*/
|
|
822
819
|
function buildAnchorTerms(userQuery, intent = {}) {
|
|
823
|
-
const
|
|
824
|
-
const
|
|
825
|
-
const
|
|
826
|
-
|
|
827
|
-
: [];
|
|
828
|
-
const targetSymbols = Array.isArray(intent.targetSymbols)
|
|
829
|
-
? dedupeNormalizedStrings(intent.targetSymbols)
|
|
830
|
-
: [];
|
|
831
|
-
for (const fileRef of targetFiles) {
|
|
832
|
-
for (const matchedFile of extractFileReferences(fileRef)) {
|
|
833
|
-
const filename = path.basename(matchedFile);
|
|
834
|
-
if (filename)
|
|
835
|
-
anchorTerms.push(`"${filename}"`);
|
|
836
|
-
}
|
|
837
|
-
}
|
|
838
|
-
for (const fileRef of explicitFiles) {
|
|
839
|
-
const filename = path.basename(fileRef);
|
|
840
|
-
if (filename)
|
|
841
|
-
anchorTerms.push(`"${filename}"`);
|
|
842
|
-
}
|
|
843
|
-
for (const symbol of targetSymbols) {
|
|
844
|
-
for (const form of buildSymbolSearchForms(symbol)) {
|
|
845
|
-
const normalized = normalizeFtsTerm(form);
|
|
846
|
-
if (!normalized)
|
|
847
|
-
continue;
|
|
848
|
-
if (QUERY_OPERATOR_TOKENS.has(normalized))
|
|
849
|
-
continue;
|
|
850
|
-
if (GENERIC_FTS_TERMS.has(normalized))
|
|
851
|
-
continue;
|
|
852
|
-
anchorTerms.push(`${normalized}*`);
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
const symbolMatches = userQuery.match(/[A-Za-z_][A-Za-z0-9_]*/g) ?? [];
|
|
856
|
-
for (const token of symbolMatches) {
|
|
857
|
-
const isLikelySymbol = /[A-Z]/.test(token) ||
|
|
858
|
-
token.includes("_") ||
|
|
859
|
-
token.endsWith("Step") ||
|
|
860
|
-
token.endsWith("Module");
|
|
861
|
-
if (!isLikelySymbol)
|
|
862
|
-
continue;
|
|
863
|
-
const normalized = token.toLowerCase();
|
|
864
|
-
if (QUERY_OPERATOR_TOKENS.has(normalized))
|
|
865
|
-
continue;
|
|
866
|
-
if (GENERIC_FTS_TERMS.has(normalized))
|
|
867
|
-
continue;
|
|
868
|
-
anchorTerms.push(`${normalized}*`);
|
|
869
|
-
}
|
|
870
|
-
return dedupeTerms(anchorTerms);
|
|
820
|
+
const compiled = compileSearchQuery({ query: userQuery, intent, mode: "fts" });
|
|
821
|
+
const fileTerms = compiled.anchors.files.flatMap(fileRef => compileSearchQuery({ query: path.basename(fileRef), mode: "fts", maxTerms: 8 }).fts.terms);
|
|
822
|
+
const symbolTerms = compiled.anchors.symbols.flatMap(symbol => compileSearchQuery({ query: symbol, mode: "fts", maxTerms: 8 }).fts.terms);
|
|
823
|
+
return dedupeTerms([...fileTerms, ...symbolTerms]);
|
|
871
824
|
}
|
|
872
825
|
function dedupeNormalizedStrings(tokens) {
|
|
873
826
|
// Lightweight normalization only (trim + slash normalization + case-insensitive dedupe).
|
|
@@ -909,7 +862,7 @@ Question:
|
|
|
909
862
|
const response = await generate({ content: prompt, query: "" });
|
|
910
863
|
const rawText = String(response.data ?? "");
|
|
911
864
|
const { content } = extractTaggedContent(rawText, "FILE_CONTENT");
|
|
912
|
-
return
|
|
865
|
+
return compileSearchQuery({ query: content, mode: "fts" }).fts.expression;
|
|
913
866
|
}
|
|
914
867
|
catch {
|
|
915
868
|
return null;
|
|
@@ -43,3 +43,20 @@ export const IGNORED_FOLDER_GLOBS = [
|
|
|
43
43
|
'**/debug.log',
|
|
44
44
|
'**/Dockerfile',
|
|
45
45
|
];
|
|
46
|
+
export const IGNORED_DIR_NAMES = Array.from(new Set(IGNORED_FOLDER_GLOBS
|
|
47
|
+
.map((pattern) => {
|
|
48
|
+
const match = pattern.match(/^\*\*\/([^*\/]+)\/\*\*$/);
|
|
49
|
+
return match?.[1];
|
|
50
|
+
})
|
|
51
|
+
.filter((name) => !!name)));
|
|
52
|
+
/**
|
|
53
|
+
* Coarse path guard used by runtime search/selection fallback paths.
|
|
54
|
+
* Matches the same ignore-glob intent used during indexing.
|
|
55
|
+
*/
|
|
56
|
+
export function isPathIgnoredByFolderGlobs(filePath) {
|
|
57
|
+
const normalizedPath = String(filePath ?? "").replace(/\\/g, "/");
|
|
58
|
+
return IGNORED_FOLDER_GLOBS.some((pattern) => {
|
|
59
|
+
const cleanPattern = pattern.replace(/^\*\*\/?/, "").replace(/\/\*\*$/, "");
|
|
60
|
+
return cleanPattern.length > 0 && normalizedPath.includes(cleanPattern);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
// File: src/modules/fileSearchModule.ts
|
|
2
|
-
import {
|
|
2
|
+
import { execFileSync } from "child_process";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import fs from "fs";
|
|
5
5
|
import { plannerSearchFiles } from "../../db/fileIndex.js";
|
|
6
6
|
import { Config } from "../../config.js";
|
|
7
7
|
import { getDbForRepo } from "../../db/client.js";
|
|
8
8
|
import { IGNORED_EXTENSIONS } from "../../fileRules/ignoredExtensions.js";
|
|
9
|
+
import { IGNORED_DIR_NAMES, isPathIgnoredByFolderGlobs } from "../../fileRules/ignoredPaths.js";
|
|
9
10
|
import { logInputOutput } from "../../utils/promptLogHelper.js";
|
|
11
|
+
import { compileSearchQuery } from "../../utils/compileSearchQuery.js";
|
|
10
12
|
async function fetchSummariesForPaths(paths) {
|
|
11
13
|
if (paths.length === 0)
|
|
12
14
|
return {};
|
|
@@ -63,14 +65,32 @@ export const fileSearchModule = {
|
|
|
63
65
|
// -------------------------------------------------
|
|
64
66
|
if (results.length === 0) {
|
|
65
67
|
try {
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const
|
|
68
|
+
const excludeExtArgs = IGNORED_EXTENSIONS.map(ext => `--exclude=*${ext}`);
|
|
69
|
+
const excludeDirArgs = IGNORED_DIR_NAMES.map(dir => `--exclude-dir=${dir}`);
|
|
70
|
+
const compiledFallback = compileSearchQuery({ query: subQuery, mode: "auto" });
|
|
71
|
+
const rgExcludeExtArgs = excludeExtArgs.map(arg => {
|
|
72
|
+
const value = arg.replace("--exclude=", "");
|
|
73
|
+
return `!${value}`;
|
|
74
|
+
});
|
|
75
|
+
const rgExcludeDirArgs = excludeDirArgs.map(arg => {
|
|
76
|
+
const value = arg.replace("--exclude-dir=", "");
|
|
77
|
+
return `!${value}/**`;
|
|
78
|
+
});
|
|
79
|
+
const rgArgs = [
|
|
80
|
+
"--files-with-matches",
|
|
81
|
+
"--no-messages",
|
|
82
|
+
...rgExcludeDirArgs.flatMap(pattern => ["-g", pattern]),
|
|
83
|
+
...rgExcludeExtArgs.flatMap(pattern => ["-g", pattern]),
|
|
84
|
+
...(compiledFallback.regex.enabled ? [] : ["-F"]),
|
|
85
|
+
subQuery,
|
|
86
|
+
repoRoot,
|
|
87
|
+
];
|
|
88
|
+
const stdout = execFileSync("rg", rgArgs, { encoding: "utf8" });
|
|
70
89
|
results = stdout
|
|
71
90
|
.split("\n")
|
|
72
91
|
.filter(Boolean)
|
|
73
|
-
.map(f => ({ path: f }))
|
|
92
|
+
.map(f => ({ path: f }))
|
|
93
|
+
.filter(record => !isPathIgnoredByFolderGlobs(record.path));
|
|
74
94
|
}
|
|
75
95
|
catch (err) {
|
|
76
96
|
if (err?.status !== 1) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/pipeline/modules/gatherInfoModule.ts
|
|
2
2
|
import { getDbForRepo } from "../../db/client.js";
|
|
3
3
|
import chalk from "chalk";
|
|
4
|
-
import {
|
|
4
|
+
import { compileSearchQuery } from "../../utils/compileSearchQuery.js";
|
|
5
5
|
import { logInputOutput } from "../../utils/promptLogHelper.js"; // ✅ import logger
|
|
6
6
|
/** Escape % and _ for LIKE queries */
|
|
7
7
|
function sanitizeForLike(input) {
|
|
@@ -32,13 +32,15 @@ export const gatherInfoModule = {
|
|
|
32
32
|
logInputOutput("gatherInfo", "output", stripEmbeddings(emptyOutput)); // ✅
|
|
33
33
|
return emptyOutput;
|
|
34
34
|
}
|
|
35
|
-
const
|
|
35
|
+
const compiled = compileSearchQuery({ query, mode: "fts" });
|
|
36
|
+
const sanitizedFts = compiled.fts.expression;
|
|
36
37
|
const likeQuery = `%${sanitizeForLike(query)}%`;
|
|
37
38
|
// 🩹 Handle legacy DBs that might not have `functions_extracted`
|
|
38
39
|
let files = [];
|
|
39
40
|
try {
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
if (sanitizedFts) {
|
|
42
|
+
files = db
|
|
43
|
+
.prepare(`
|
|
42
44
|
SELECT f.id, f.path, f.type, f.summary, f.embedding,
|
|
43
45
|
f.last_modified, f.indexed_at,
|
|
44
46
|
COALESCE(f.functions_extracted, 0) AS functions_extracted,
|
|
@@ -49,12 +51,14 @@ export const gatherInfoModule = {
|
|
|
49
51
|
ORDER BY f.path ASC
|
|
50
52
|
LIMIT ?;
|
|
51
53
|
`)
|
|
52
|
-
|
|
54
|
+
.all(maxFiles);
|
|
55
|
+
}
|
|
53
56
|
}
|
|
54
57
|
catch (err) {
|
|
55
58
|
console.warn(chalk.yellow("⚠️ 'functions_extracted' column missing in files table, running fallback query..."));
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
if (sanitizedFts) {
|
|
60
|
+
files = db
|
|
61
|
+
.prepare(`
|
|
58
62
|
SELECT f.id, f.path, f.type, f.summary, f.embedding,
|
|
59
63
|
f.last_modified, f.indexed_at, f.processing_status
|
|
60
64
|
FROM files f
|
|
@@ -63,7 +67,8 @@ export const gatherInfoModule = {
|
|
|
63
67
|
ORDER BY f.path ASC
|
|
64
68
|
LIMIT ?;
|
|
65
69
|
`)
|
|
66
|
-
|
|
70
|
+
.all(maxFiles);
|
|
71
|
+
}
|
|
67
72
|
}
|
|
68
73
|
const functions = db
|
|
69
74
|
.prepare(`
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { STOP_WORDS } from "../fileRules/stopWords.js";
|
|
3
|
+
import { extractFileReferences } from "./extractFileReferences.js";
|
|
4
|
+
const QUERY_OPERATOR_TOKENS = new Set(["or", "and", "not", "near"]);
|
|
5
|
+
const GENERIC_FTS_TERMS = new Set([
|
|
6
|
+
"file",
|
|
7
|
+
"files",
|
|
8
|
+
"code",
|
|
9
|
+
"source",
|
|
10
|
+
"repository",
|
|
11
|
+
"result",
|
|
12
|
+
"results",
|
|
13
|
+
"output",
|
|
14
|
+
"entry",
|
|
15
|
+
"database",
|
|
16
|
+
"configuration",
|
|
17
|
+
]);
|
|
18
|
+
const SYMBOL_HINT_SUFFIXES = ["step", "module", "service", "controller", "handler", "manager"];
|
|
19
|
+
export function compileSearchQuery(args) {
|
|
20
|
+
const rawQuery = String(args.query ?? "").trim();
|
|
21
|
+
const intent = args.intent ?? {};
|
|
22
|
+
const mode = args.mode ?? "auto";
|
|
23
|
+
const maxTerms = Math.max(1, args.maxTerms ?? 12);
|
|
24
|
+
const fileRefs = dedupeNormalizedStrings([
|
|
25
|
+
...extractFileReferences(rawQuery),
|
|
26
|
+
...collectIntentFileRefs(intent),
|
|
27
|
+
]);
|
|
28
|
+
const symbolRefs = dedupeNormalizedStrings([
|
|
29
|
+
...collectIntentSymbols(intent),
|
|
30
|
+
...extractLikelySymbols(rawQuery),
|
|
31
|
+
]);
|
|
32
|
+
const anchorFileTerms = fileRefs.flatMap(ref => toSearchTerms(path.basename(ref)));
|
|
33
|
+
const anchorSymbolTerms = symbolRefs.flatMap(ref => toSearchTerms(ref));
|
|
34
|
+
const baseTerms = dedupeNormalizedStrings([
|
|
35
|
+
...toSearchTerms(rawQuery),
|
|
36
|
+
...anchorFileTerms,
|
|
37
|
+
...anchorSymbolTerms,
|
|
38
|
+
]);
|
|
39
|
+
const droppedTerms = [];
|
|
40
|
+
const filteredTerms = baseTerms.filter(term => {
|
|
41
|
+
if (term.length < 2) {
|
|
42
|
+
droppedTerms.push(term);
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
if (STOP_WORDS.has(term) || QUERY_OPERATOR_TOKENS.has(term)) {
|
|
46
|
+
droppedTerms.push(term);
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
});
|
|
51
|
+
const hasAnchors = anchorFileTerms.length > 0 || anchorSymbolTerms.length > 0;
|
|
52
|
+
const ftsTerms = dedupeNormalizedStrings(filteredTerms.filter(term => !(hasAnchors && GENERIC_FTS_TERMS.has(term)))).slice(0, maxTerms);
|
|
53
|
+
const literalTerms = dedupeNormalizedStrings([
|
|
54
|
+
rawQuery,
|
|
55
|
+
...fileRefs,
|
|
56
|
+
...symbolRefs,
|
|
57
|
+
...filteredTerms,
|
|
58
|
+
]).filter(Boolean);
|
|
59
|
+
const regexEnabled = mode === "regex" || (mode === "auto" && looksLikeRegex(rawQuery));
|
|
60
|
+
return {
|
|
61
|
+
rawQuery,
|
|
62
|
+
mode,
|
|
63
|
+
anchors: {
|
|
64
|
+
files: fileRefs,
|
|
65
|
+
symbols: symbolRefs,
|
|
66
|
+
},
|
|
67
|
+
fts: {
|
|
68
|
+
terms: ftsTerms,
|
|
69
|
+
expression: ftsTerms.length > 0 ? ftsTerms.map(term => `${term}*`).join(" OR ") : null,
|
|
70
|
+
},
|
|
71
|
+
literal: {
|
|
72
|
+
terms: literalTerms,
|
|
73
|
+
},
|
|
74
|
+
regex: {
|
|
75
|
+
terms: regexEnabled ? [rawQuery].filter(Boolean) : [],
|
|
76
|
+
enabled: regexEnabled,
|
|
77
|
+
},
|
|
78
|
+
diagnostics: {
|
|
79
|
+
droppedTerms,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
function collectIntentFileRefs(intent) {
|
|
84
|
+
if (!Array.isArray(intent.targetFiles))
|
|
85
|
+
return [];
|
|
86
|
+
return intent.targetFiles
|
|
87
|
+
.map(entry => String(entry ?? "").trim())
|
|
88
|
+
.filter(Boolean);
|
|
89
|
+
}
|
|
90
|
+
function collectIntentSymbols(intent) {
|
|
91
|
+
if (!Array.isArray(intent.targetSymbols))
|
|
92
|
+
return [];
|
|
93
|
+
return intent.targetSymbols
|
|
94
|
+
.map(entry => String(entry ?? "").trim())
|
|
95
|
+
.filter(Boolean);
|
|
96
|
+
}
|
|
97
|
+
function dedupeNormalizedStrings(tokens) {
|
|
98
|
+
const out = [];
|
|
99
|
+
const seen = new Set();
|
|
100
|
+
for (const token of tokens) {
|
|
101
|
+
if (typeof token !== "string")
|
|
102
|
+
continue;
|
|
103
|
+
const normalized = token.trim().replace(/\\/g, "/");
|
|
104
|
+
if (!normalized)
|
|
105
|
+
continue;
|
|
106
|
+
const key = normalized.toLowerCase();
|
|
107
|
+
if (seen.has(key))
|
|
108
|
+
continue;
|
|
109
|
+
seen.add(key);
|
|
110
|
+
out.push(normalized);
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
function extractLikelySymbols(input) {
|
|
115
|
+
const matches = String(input ?? "").match(/[A-Za-z_][A-Za-z0-9_]{2,}/g) ?? [];
|
|
116
|
+
const out = new Set();
|
|
117
|
+
for (const token of matches) {
|
|
118
|
+
const lowered = token.toLowerCase();
|
|
119
|
+
const looksLikeSymbol = /[A-Z]/.test(token) ||
|
|
120
|
+
token.includes("_") ||
|
|
121
|
+
SYMBOL_HINT_SUFFIXES.some(suffix => lowered.endsWith(suffix));
|
|
122
|
+
if (!looksLikeSymbol)
|
|
123
|
+
continue;
|
|
124
|
+
out.add(token);
|
|
125
|
+
}
|
|
126
|
+
return Array.from(out);
|
|
127
|
+
}
|
|
128
|
+
function toSearchTerms(input) {
|
|
129
|
+
const matches = String(input ?? "")
|
|
130
|
+
.toLowerCase()
|
|
131
|
+
.match(/[a-z0-9_]{2,}/g) ?? [];
|
|
132
|
+
return Array.from(new Set(matches));
|
|
133
|
+
}
|
|
134
|
+
function looksLikeRegex(input) {
|
|
135
|
+
if (!input)
|
|
136
|
+
return false;
|
|
137
|
+
return /[\[\]{}()+?|\\.^$]/.test(input);
|
|
138
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import fs from "fs/promises";
|
|
2
2
|
import path from "path";
|
|
3
|
+
import { isPathIgnoredByFolderGlobs } from "../fileRules/ignoredPaths.js";
|
|
3
4
|
export async function resolveTargetsToFiles(targets, exts) {
|
|
4
5
|
const files = [];
|
|
5
6
|
for (const target of targets) {
|
|
7
|
+
if (isPathIgnoredByFolderGlobs(target))
|
|
8
|
+
continue;
|
|
6
9
|
const stat = await fs.stat(target);
|
|
7
10
|
if (stat.isDirectory()) {
|
|
8
11
|
files.push(...await collectFilesRecursive(target, exts));
|
|
@@ -18,6 +21,8 @@ async function collectFilesRecursive(dir, exts) {
|
|
|
18
21
|
const files = [];
|
|
19
22
|
for (const entry of entries) {
|
|
20
23
|
const fullPath = path.join(dir, entry.name);
|
|
24
|
+
if (isPathIgnoredByFolderGlobs(fullPath))
|
|
25
|
+
continue;
|
|
21
26
|
if (entry.isDirectory()) {
|
|
22
27
|
files.push(...await collectFilesRecursive(fullPath, exts));
|
|
23
28
|
}
|