flowseeker 0.1.7 → 0.1.8
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/.env.example +7 -0
- package/CHANGELOG.md +131 -108
- package/README.md +288 -221
- package/dist/cli/flowCommand.js +175 -0
- package/dist/cli/main.js +1794 -0
- package/dist/cli/mcpServer.js +7 -1
- package/dist/cli/runEvaluation.js +178 -2
- package/dist/config/defaultConfig.js +11 -1
- package/dist/config/env.js +118 -0
- package/dist/config/loadConfig.js +18 -1
- package/dist/config/loadConfigFromPath.js +3 -1
- package/dist/extension.js +23 -0
- package/dist/gateway/embeddingProviders.js +852 -0
- package/dist/index/cacheStore.js +43 -0
- package/dist/index/configRouteDiscoveryProbe.js +288 -0
- package/dist/index/embeddingIndex.js +193 -0
- package/dist/index/graphIndex.js +460 -0
- package/dist/index/indexWatcher.js +86 -0
- package/dist/index/structuredExtractor.js +303 -12
- package/dist/index/treeSitterExtractor.js +264 -0
- package/dist/index/vectorStore.js +41 -0
- package/dist/index/workspaceIndex.js +591 -26
- package/dist/mcp/mcpTools.js +51 -0
- package/dist/pipeline/contextPack.js +3 -3
- package/dist/pipeline/deterministicReranker.js +358 -0
- package/dist/pipeline/evaluationMetrics.js +14 -2
- package/dist/pipeline/fileGroups.js +7 -1
- package/dist/pipeline/fileScanner.js +93 -11
- package/dist/pipeline/llmReranker.js +151 -0
- package/dist/pipeline/nodeScan.js +91 -12
- package/dist/pipeline/ranker.js +875 -16
- package/dist/pipeline/retrievalFusion.js +41 -0
- package/dist/pipeline/runHeadless.js +35 -0
- package/dist/pipeline/solvePacket.js +549 -42
- package/dist/pipeline/subsystem.js +21 -0
- package/docs/demo-screenshot-checklist.md +86 -0
- package/docs/marketplace-copy.md +92 -0
- package/docs/mcp-onboarding.md +191 -0
- package/package.json +633 -561
|
@@ -36,15 +36,24 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.loadOrBuildWorkspaceIndex = loadOrBuildWorkspaceIndex;
|
|
37
37
|
exports.selectIndexCandidates = selectIndexCandidates;
|
|
38
38
|
exports.clearNodeWorkspaceIndex = clearNodeWorkspaceIndex;
|
|
39
|
+
const crypto = __importStar(require("crypto"));
|
|
39
40
|
const fs = __importStar(require("fs/promises"));
|
|
40
41
|
const path = __importStar(require("path"));
|
|
41
42
|
const dependencyExtractor_1 = require("./dependencyExtractor");
|
|
42
43
|
const fileDiscovery_1 = require("./fileDiscovery");
|
|
44
|
+
const embeddingIndex_1 = require("./embeddingIndex");
|
|
45
|
+
const graphIndex_1 = require("./graphIndex");
|
|
43
46
|
const structuredExtractor_1 = require("./structuredExtractor");
|
|
47
|
+
const treeSitterExtractor_1 = require("./treeSitterExtractor");
|
|
48
|
+
const retrievalFusion_1 = require("../pipeline/retrievalFusion");
|
|
44
49
|
const text_1 = require("../utils/text");
|
|
45
50
|
const fileScanner_1 = require("../pipeline/fileScanner");
|
|
46
51
|
const subsystem_1 = require("../pipeline/subsystem");
|
|
47
|
-
const indexVersion =
|
|
52
|
+
const indexVersion = 7;
|
|
53
|
+
const regexExtractorVersion = "structured-regex-mvp-1";
|
|
54
|
+
const treeSitterExtractorVersion = "tree-sitter-mvp-1";
|
|
55
|
+
const chunkerVersion = "symbol-range-mvp-1";
|
|
56
|
+
const graphVersion = "indexed-edges-mvp-1";
|
|
48
57
|
const tokenLimitPerFile = 2500;
|
|
49
58
|
async function loadOrBuildWorkspaceIndex(rootPath, config, options = {}) {
|
|
50
59
|
const startedAt = options.startedAt ?? Date.now();
|
|
@@ -54,6 +63,8 @@ async function loadOrBuildWorkspaceIndex(rootPath, config, options = {}) {
|
|
|
54
63
|
const currentFingerprint = fingerprintConfig(config);
|
|
55
64
|
const reusablePriorIndex = priorIndex?.configFingerprint === currentFingerprint ? priorIndex : undefined;
|
|
56
65
|
const priorByPath = new Map((reusablePriorIndex?.files ?? []).map((file) => [file.relativePath, file]));
|
|
66
|
+
const discoveredPathSet = new Set(discovery.files.map((file) => file.relativePath));
|
|
67
|
+
const deletedFiles = (reusablePriorIndex?.files ?? []).filter((file) => !discoveredPathSet.has(file.relativePath)).length;
|
|
57
68
|
const files = [];
|
|
58
69
|
let reusedFiles = 0;
|
|
59
70
|
let updatedFiles = 0;
|
|
@@ -104,29 +115,58 @@ async function loadOrBuildWorkspaceIndex(rootPath, config, options = {}) {
|
|
|
104
115
|
configFingerprint: currentFingerprint,
|
|
105
116
|
files
|
|
106
117
|
};
|
|
107
|
-
|
|
118
|
+
if (!options.readOnly) {
|
|
119
|
+
await writeIndex(resolvedRoot, index);
|
|
120
|
+
}
|
|
108
121
|
return {
|
|
109
122
|
index,
|
|
110
123
|
discoveredFiles: discovery.files.length,
|
|
111
124
|
reusedFiles,
|
|
112
125
|
updatedFiles,
|
|
126
|
+
deletedFiles,
|
|
113
127
|
indexedBytes: files.reduce((sum, file) => sum + file.size, 0),
|
|
114
128
|
indexedTokens: files.reduce((sum, file) => sum + (file.tokenCount ?? Math.ceil(file.size / 4)), 0),
|
|
115
129
|
stopReason
|
|
116
130
|
};
|
|
117
131
|
}
|
|
118
|
-
function selectIndexCandidates(index, profile, config) {
|
|
132
|
+
async function selectIndexCandidates(index, profile, config) {
|
|
119
133
|
const terms = (0, fileScanner_1.getWeightedTerms)(profile);
|
|
120
134
|
if (terms.length === 0) {
|
|
121
|
-
return [];
|
|
135
|
+
return { files: [], semanticStatus: "disabled", deepCandidateDiscoveryCandidates: 0, deepCandidateDiscoveryAdded: 0, deepCandidateDiscoveryAvoidBlocked: 0, semanticEmbeddedFiles: 0, semanticCacheHits: 0, semanticCacheMisses: 0, semanticHitCount: 0, semanticAddedFileCount: 0, semanticSupportedFileCount: 0 };
|
|
122
136
|
}
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
137
|
+
const baseHits = buildIndexChannelHits(index.files, terms, profile);
|
|
138
|
+
const semanticResult = await (0, embeddingIndex_1.searchSemanticFiles)(index.rootPath, index, profile.normalizedTask || profile.rawTask, config);
|
|
139
|
+
// Only add semantic hits for files NOT already discovered by other channels.
|
|
140
|
+
// This prevents semantic from modifying the ranking of lexically-discovered files.
|
|
141
|
+
const discoveredFiles = new Set(baseHits.map((h) => h.relativePath));
|
|
142
|
+
// Track semantic contributions + evidence for supported lexical files
|
|
143
|
+
let semanticAddedFileCount = 0;
|
|
144
|
+
let semanticSupportedFileCount = 0;
|
|
145
|
+
const semanticSupportByPath = new Map();
|
|
146
|
+
const semanticDiscoveryByPath = new Map();
|
|
147
|
+
semanticResult.hits.forEach((hit, rank) => {
|
|
148
|
+
if (discoveredFiles.has(hit.relativePath)) {
|
|
149
|
+
semanticSupportedFileCount++;
|
|
150
|
+
semanticSupportByPath.set(hit.relativePath, { score: hit.score, rank: rank + 1 });
|
|
151
|
+
return; // supported existing file but don't modify ranking
|
|
152
|
+
}
|
|
153
|
+
semanticAddedFileCount++;
|
|
154
|
+
semanticDiscoveryByPath.set(hit.relativePath, { score: hit.score, rank: rank + 1 });
|
|
155
|
+
baseHits.push({
|
|
156
|
+
channel: "semantic_vector",
|
|
157
|
+
relativePath: hit.relativePath,
|
|
158
|
+
score: hit.score,
|
|
159
|
+
rank: rank + 1,
|
|
160
|
+
reason: `semantic vector similarity ${hit.score.toFixed(3)} rank=${rank + 1} discovered by semantic`
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
const fused = (0, retrievalFusion_1.fuseRetrievalChannelHits)(baseHits);
|
|
164
|
+
const scored = fused
|
|
165
|
+
.map((candidate) => ({
|
|
166
|
+
candidate,
|
|
167
|
+
file: index.files.find((file) => file.relativePath === candidate.relativePath)
|
|
127
168
|
}))
|
|
128
|
-
.filter((item) => item.
|
|
129
|
-
.sort((left, right) => right.score - left.score);
|
|
169
|
+
.filter((item) => Boolean(item.file));
|
|
130
170
|
const selected = new Map();
|
|
131
171
|
const maxCandidates = config.index.maxIndexedCandidateFiles;
|
|
132
172
|
const maxSeeds = Math.min(config.index.maxSeedCandidateFiles, maxCandidates);
|
|
@@ -134,6 +174,8 @@ function selectIndexCandidates(index, profile, config) {
|
|
|
134
174
|
for (const seed of seeds) {
|
|
135
175
|
selected.set(seed.relativePath, seed);
|
|
136
176
|
}
|
|
177
|
+
const candidateTrace = new Map(scored.map((item) => [item.file.relativePath, item.candidate]));
|
|
178
|
+
const graphReasons = new Map();
|
|
137
179
|
if (config.index.enableDependencyExpansion && selected.size < maxCandidates) {
|
|
138
180
|
const maxExpansion = Math.min(config.index.maxDependencyExpansionFiles, maxCandidates - selected.size);
|
|
139
181
|
let expanded = 0;
|
|
@@ -147,16 +189,164 @@ function selectIndexCandidates(index, profile, config) {
|
|
|
147
189
|
}
|
|
148
190
|
}
|
|
149
191
|
}
|
|
192
|
+
if (config.index.enableDependencyExpansion && selected.size < maxCandidates) {
|
|
193
|
+
const graphBudget = Math.min(25, maxCandidates - selected.size);
|
|
194
|
+
for (const related of (0, graphIndex_1.expandIndexedGraph)(index, seeds.map((seed) => seed.relativePath), { maxAddedFiles: graphBudget })) {
|
|
195
|
+
if (selected.size >= maxCandidates) {
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
selected.set(related.file.relativePath, related.file);
|
|
199
|
+
graphReasons.set(related.file.relativePath, [...(graphReasons.get(related.file.relativePath) ?? []), related.reason]);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// Attach semantic support evidence to lexically-discovered files (evidence-only, no ranking change)
|
|
203
|
+
for (const [relPath, semInfo] of semanticSupportByPath) {
|
|
204
|
+
if (selected.has(relPath)) {
|
|
205
|
+
const reasons = graphReasons.get(relPath) ?? [];
|
|
206
|
+
reasons.push("semantic_vector: similarity " + semInfo.score.toFixed(3) + " rank=" + semInfo.rank + " supported existing candidate");
|
|
207
|
+
graphReasons.set(relPath, reasons);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// Attach semantic discovered evidence to selected files
|
|
211
|
+
for (const [relPath, semInfo] of semanticDiscoveryByPath) {
|
|
212
|
+
if (selected.has(relPath)) {
|
|
213
|
+
const reasons = graphReasons.get(relPath) ?? [];
|
|
214
|
+
reasons.push("semantic_vector: similarity " + semInfo.score.toFixed(3) + " rank=" + semInfo.rank + " discovered by semantic");
|
|
215
|
+
graphReasons.set(relPath, reasons);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// Semantic candidate expansion: adds new files from semantic search that
|
|
219
|
+
// lexical channels missed. Used with fusion isolation (semantic hits only
|
|
220
|
+
// for undiscovered files) to avoid modifying lexical ranking of known files.
|
|
221
|
+
if (semanticResult.hits.length > 0 && selected.size < maxCandidates &&
|
|
222
|
+
semanticResult.providerName !== "deterministicTest") {
|
|
223
|
+
const semanticBudget = Math.min(40, maxCandidates - selected.size);
|
|
224
|
+
let added = 0;
|
|
225
|
+
for (const hit of semanticResult.hits) {
|
|
226
|
+
if (added >= semanticBudget || selected.size >= maxCandidates)
|
|
227
|
+
break;
|
|
228
|
+
const file = index.files.find((f) => f.relativePath === hit.relativePath);
|
|
229
|
+
if (!file || selected.has(file.relativePath))
|
|
230
|
+
continue;
|
|
231
|
+
if (isExpansionAvoidPath(file.relativePath))
|
|
232
|
+
continue;
|
|
233
|
+
selected.set(file.relativePath, file);
|
|
234
|
+
added++;
|
|
235
|
+
graphReasons.set(file.relativePath, [...(graphReasons.get(file.relativePath) ?? []), `semantic vector similarity ${hit.score.toFixed(3)} rank=${added} discovered by semantic`]);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
// Deep candidate discovery: scan indexed files NOT already selected
|
|
239
|
+
// for files with 3+ independent deterministic evidence signals.
|
|
240
|
+
// Always evaluates for telemetry. Adds at most 3 (or replaces if pool full).
|
|
241
|
+
let deepCandidateDiscoveryCandidates = 0;
|
|
242
|
+
let deepCandidateDiscoveryAdded = 0;
|
|
243
|
+
let deepCandidateDiscoveryAvoidBlocked = 0;
|
|
244
|
+
// Always evaluate for telemetry, regardless of pool capacity
|
|
245
|
+
const allCandidates = discoverDeepCandidates(index.files, selected, profile, terms, 999);
|
|
246
|
+
deepCandidateDiscoveryCandidates = allCandidates.length;
|
|
247
|
+
if (allCandidates.length > 0) {
|
|
248
|
+
if (selected.size < maxCandidates) {
|
|
249
|
+
// Pool has space: add directly
|
|
250
|
+
const deepBudget = Math.min(3, maxCandidates - selected.size);
|
|
251
|
+
const addedCandidates = allCandidates.slice(0, deepBudget);
|
|
252
|
+
deepCandidateDiscoveryAdded = addedCandidates.length;
|
|
253
|
+
for (const dc of addedCandidates) {
|
|
254
|
+
selected.set(dc.file.relativePath, dc.file);
|
|
255
|
+
graphReasons.set(dc.file.relativePath, [...(graphReasons.get(dc.file.relativePath) ?? []), dc.reason]);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
// Pool is full: replace up to 3 lowest-priority non-required below top7
|
|
260
|
+
const requiredSlots = new Set(profile.blueprint.requiredSlots);
|
|
261
|
+
const selectedArr = Array.from(selected.entries()); // [path, file][]
|
|
262
|
+
const maxReplace = Math.min(3, allCandidates.length);
|
|
263
|
+
// Build replacement targets: indices >= 7 where the file is not a sole required-slot occupant
|
|
264
|
+
const slotOccupants = new Map();
|
|
265
|
+
for (const [, file] of selectedArr) {
|
|
266
|
+
const role = classifyDeepPathRole(file.relativePath);
|
|
267
|
+
if (role)
|
|
268
|
+
slotOccupants.set(role, (slotOccupants.get(role) ?? 0) + 1);
|
|
269
|
+
}
|
|
270
|
+
// Find replaceable indices (7+, non-sole-required, lowest priority = highest index)
|
|
271
|
+
const replaceTargets = [];
|
|
272
|
+
for (let i = selectedArr.length - 1; i >= 7 && replaceTargets.length < maxReplace; i--) {
|
|
273
|
+
const [, file] = selectedArr[i];
|
|
274
|
+
const role = classifyDeepPathRole(file.relativePath);
|
|
275
|
+
const isSoleRequired = role && requiredSlots.has(role) && (slotOccupants.get(role) ?? 0) <= 1;
|
|
276
|
+
if (!isSoleRequired) {
|
|
277
|
+
replaceTargets.push(i);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
let added = 0;
|
|
281
|
+
for (const dc of allCandidates) {
|
|
282
|
+
if (added >= maxReplace || added >= replaceTargets.length)
|
|
283
|
+
break;
|
|
284
|
+
const targetIdx = replaceTargets[added];
|
|
285
|
+
const [oldPath] = selectedArr[targetIdx];
|
|
286
|
+
selected.delete(oldPath);
|
|
287
|
+
selected.set(dc.file.relativePath, dc.file);
|
|
288
|
+
graphReasons.set(dc.file.relativePath, [...(graphReasons.get(dc.file.relativePath) ?? []), dc.reason + " (replaced pool-full slot)"]);
|
|
289
|
+
added++;
|
|
290
|
+
}
|
|
291
|
+
deepCandidateDiscoveryAdded = added;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
150
294
|
for (const item of scored) {
|
|
151
295
|
if (selected.size >= maxCandidates) {
|
|
152
296
|
break;
|
|
153
297
|
}
|
|
154
298
|
selected.set(item.file.relativePath, item.file);
|
|
155
299
|
}
|
|
156
|
-
return
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
300
|
+
return {
|
|
301
|
+
files: Array.from(selected.values()).map((item) => {
|
|
302
|
+
const trace = candidateTrace.get(item.relativePath);
|
|
303
|
+
const graphTrace = graphReasons.get(item.relativePath) ?? [];
|
|
304
|
+
return {
|
|
305
|
+
absolutePath: path.join(index.rootPath, item.relativePath),
|
|
306
|
+
relativePath: item.relativePath,
|
|
307
|
+
channelHits: trace?.channelHits,
|
|
308
|
+
retrievalTrace: trace?.reasons,
|
|
309
|
+
graphReasons: graphTrace
|
|
310
|
+
};
|
|
311
|
+
}),
|
|
312
|
+
semanticStatus: semanticResult.status,
|
|
313
|
+
semanticErrorType: semanticResult.errorType,
|
|
314
|
+
semanticErrorMessage: semanticResult.errorMessage,
|
|
315
|
+
semanticEmbeddedFiles: semanticResult.embeddedFiles,
|
|
316
|
+
semanticCacheHits: semanticResult.cacheHits,
|
|
317
|
+
semanticCacheMisses: semanticResult.cacheMisses,
|
|
318
|
+
semanticHitCount: semanticResult.hits.length,
|
|
319
|
+
semanticAddedFileCount,
|
|
320
|
+
semanticSupportedFileCount,
|
|
321
|
+
deepCandidateDiscoveryCandidates,
|
|
322
|
+
deepCandidateDiscoveryAdded,
|
|
323
|
+
deepCandidateDiscoveryAvoidBlocked
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
function isExpansionAvoidPath(normalizedPath) {
|
|
327
|
+
const p = normalizedPath.replace(/\\/g, "/").toLowerCase();
|
|
328
|
+
// Helper/util/console/command files
|
|
329
|
+
if (/(^|\/)(helpers?|utils?|console|commands?|scripts?|tasks?)(\/|$)/.test(p))
|
|
330
|
+
return true;
|
|
331
|
+
// Admin/dashboard surfaces
|
|
332
|
+
if (/(^|\/)(admin|dashboard)(\/|$)/.test(p))
|
|
333
|
+
return true;
|
|
334
|
+
// Lock files
|
|
335
|
+
if (/(package-lock|yarn\.lock|pnpm-lock|composer\.lock|Gemfile\.lock|poetry\.lock|cargo\.lock)$/.test(p))
|
|
336
|
+
return true;
|
|
337
|
+
// Templates/views
|
|
338
|
+
if (/(^|\/)(views?|templates?|resources\/views)(\/|$)|\.(blade\.php|twig|erb|hbs|ejs)$/.test(p))
|
|
339
|
+
return true;
|
|
340
|
+
// Generated/snapshot
|
|
341
|
+
if (/(^|\/)(generated|__generated__|gen|codegen|snapshots?)(\/|$)|\.snap$|\.generated\./.test(p))
|
|
342
|
+
return true;
|
|
343
|
+
// Static assets
|
|
344
|
+
if (/(^|\/)(public|assets?|static|images?|fonts?)(\/|$)|\.(css|scss|less|svg|png|jpe?g|gif|ico|woff2?)$/.test(p))
|
|
345
|
+
return true;
|
|
346
|
+
// Notification/mail files unless specific pattern match
|
|
347
|
+
if (/(^|\/)(notifications?|mail|email)(\/|$)/.test(p))
|
|
348
|
+
return true;
|
|
349
|
+
return false;
|
|
160
350
|
}
|
|
161
351
|
function findDependencyRelatedFiles(files, seeds, maxResults) {
|
|
162
352
|
const lookup = buildDependencyLookup(files);
|
|
@@ -291,8 +481,11 @@ async function indexFile(discovered, prior, config) {
|
|
|
291
481
|
if (stat.size > config.project.maxFileSizeKb * 1024) {
|
|
292
482
|
return undefined;
|
|
293
483
|
}
|
|
294
|
-
|
|
295
|
-
|
|
484
|
+
const language = (0, structuredExtractor_1.detectLanguage)(discovered.relativePath);
|
|
485
|
+
const useTreeSitter = config.index.enableTreeSitter && language != null && (0, treeSitterExtractor_1.supportsTreeSitterLanguage)(language);
|
|
486
|
+
const effectiveExtractorVersion = useTreeSitter ? treeSitterExtractorVersion : regexExtractorVersion;
|
|
487
|
+
if (prior && prior.size === stat.size && prior.mtimeMs === stat.mtimeMs && prior.extractorVersion === effectiveExtractorVersion && prior.chunkerVersion === chunkerVersion && prior.graphVersion === graphVersion) {
|
|
488
|
+
return { file: { ...prior, parserKind: prior.parserKind || (useTreeSitter ? "tree-sitter" : "structured-regex"), parserVersion: prior.parserVersion || effectiveExtractorVersion, symbolCount: prior.symbolCount ?? prior.symbols.length, graphEdgeCount: prior.graphEdgeCount ?? prior.edges.length, indexedAt: prior.indexedAt || new Date().toISOString() }, reused: true };
|
|
296
489
|
}
|
|
297
490
|
let content;
|
|
298
491
|
try {
|
|
@@ -304,10 +497,24 @@ async function indexFile(discovered, prior, config) {
|
|
|
304
497
|
if (content.includes("\u0000")) {
|
|
305
498
|
return undefined;
|
|
306
499
|
}
|
|
307
|
-
const
|
|
308
|
-
|
|
500
|
+
const contentHash = hashText(content);
|
|
501
|
+
if (prior && prior.contentHash === contentHash && prior.extractorVersion === effectiveExtractorVersion && prior.chunkerVersion === chunkerVersion && prior.graphVersion === graphVersion) {
|
|
502
|
+
return {
|
|
503
|
+
file: {
|
|
504
|
+
...prior,
|
|
505
|
+
size: stat.size,
|
|
506
|
+
mtimeMs: stat.mtimeMs
|
|
507
|
+
},
|
|
508
|
+
reused: true
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
const structuredNodes = config.index.enableTreeSitter ? extractStructuredNodesForIndex(discovered.relativePath, content) : [];
|
|
512
|
+
const symbols = extractSymbols(structuredNodes, discovered.relativePath);
|
|
309
513
|
const calls = extractCalls(structuredNodes);
|
|
310
514
|
const subsystemTerms = (0, subsystem_1.inferSubsystemTerms)(discovered.relativePath, symbols.map((symbol) => symbol.name));
|
|
515
|
+
const imports = (0, dependencyExtractor_1.extractImports)(content).map((item) => item.source).slice(0, 50);
|
|
516
|
+
const chunks = buildIndexedChunks(discovered.relativePath, contentHash, structuredNodes);
|
|
517
|
+
const edges = buildIndexedEdges(discovered.relativePath, imports, symbols, calls);
|
|
311
518
|
return {
|
|
312
519
|
reused: false,
|
|
313
520
|
file: {
|
|
@@ -317,14 +524,116 @@ async function indexFile(discovered, prior, config) {
|
|
|
317
524
|
language: (0, text_1.languageFromPath)(discovered.relativePath),
|
|
318
525
|
tokenCount: (0, text_1.fastTokenEstimate)(`${discovered.relativePath}\n${content}`),
|
|
319
526
|
tokens: tokenizeForIndex(`${discovered.relativePath}\n${content}\n${symbolText(symbols)}`),
|
|
320
|
-
imports
|
|
527
|
+
imports,
|
|
321
528
|
symbols,
|
|
322
529
|
calls,
|
|
530
|
+
contentHash,
|
|
531
|
+
extractorVersion: effectiveExtractorVersion,
|
|
532
|
+
chunkerVersion,
|
|
533
|
+
graphVersion,
|
|
534
|
+
chunks,
|
|
535
|
+
edges,
|
|
323
536
|
subsystem: subsystemTerms[0],
|
|
324
|
-
subsystemTerms
|
|
537
|
+
subsystemTerms,
|
|
538
|
+
parserKind: useTreeSitter ? "tree-sitter" : "structured-regex",
|
|
539
|
+
parserVersion: effectiveExtractorVersion,
|
|
540
|
+
symbolCount: symbols.length,
|
|
541
|
+
graphEdgeCount: edges.length,
|
|
542
|
+
indexedAt: new Date().toISOString()
|
|
325
543
|
}
|
|
326
544
|
};
|
|
327
545
|
}
|
|
546
|
+
function buildIndexChannelHits(files, terms, profile) {
|
|
547
|
+
const hits = [];
|
|
548
|
+
const channelBuckets = {
|
|
549
|
+
exact_literal: [],
|
|
550
|
+
path: [],
|
|
551
|
+
lexical_content: [],
|
|
552
|
+
symbol: [],
|
|
553
|
+
semantic_vector: [],
|
|
554
|
+
dependency: [],
|
|
555
|
+
graph: [],
|
|
556
|
+
editor_context: []
|
|
557
|
+
};
|
|
558
|
+
for (const file of files) {
|
|
559
|
+
const scores = scoreIndexedFileChannels(file, terms, profile);
|
|
560
|
+
for (const [channel, score] of Object.entries(scores)) {
|
|
561
|
+
if (score <= 0) {
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
channelBuckets[channel].push({
|
|
565
|
+
channel,
|
|
566
|
+
relativePath: file.relativePath,
|
|
567
|
+
score,
|
|
568
|
+
rank: 0,
|
|
569
|
+
reason: `${channel} score ${score.toFixed(1)}`
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
for (const bucket of Object.values(channelBuckets)) {
|
|
574
|
+
bucket.sort((left, right) => right.score - left.score);
|
|
575
|
+
bucket.forEach((hit, index) => {
|
|
576
|
+
hits.push({ ...hit, rank: index + 1 });
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
return hits;
|
|
580
|
+
}
|
|
581
|
+
function scoreIndexedFileChannels(file, terms, profile) {
|
|
582
|
+
const normalizedPath = (0, text_1.normalizeText)(file.relativePath);
|
|
583
|
+
const compactPath = normalizedPath.replace(/[^a-z0-9]+/g, "");
|
|
584
|
+
const normalizedBaseName = (0, text_1.normalizeText)(fileBaseName(file.relativePath));
|
|
585
|
+
const compactBaseName = normalizedBaseName.replace(/[^a-z0-9]+/g, "");
|
|
586
|
+
const tokenSet = new Set(file.tokens);
|
|
587
|
+
const imports = file.imports.map(text_1.normalizeText).join("\n");
|
|
588
|
+
const symbolNames = file.symbols.map((symbol) => (0, text_1.normalizeText)(symbol.name));
|
|
589
|
+
const callNames = (file.calls ?? []).map(text_1.normalizeText);
|
|
590
|
+
const subsystemTerms = file.subsystemTerms ?? [];
|
|
591
|
+
const taskSubsystems = (0, subsystem_1.inferTaskSubsystemTerms)(profile);
|
|
592
|
+
const scores = {};
|
|
593
|
+
const add = (channel, value) => {
|
|
594
|
+
scores[channel] = (scores[channel] ?? 0) + value;
|
|
595
|
+
};
|
|
596
|
+
for (const term of terms) {
|
|
597
|
+
const normalizedTerm = (0, text_1.normalizeText)(term.term);
|
|
598
|
+
const compactTerm = normalizedTerm.replace(/[^a-z0-9]+/g, "");
|
|
599
|
+
if (normalizedPath.includes(normalizedTerm)) {
|
|
600
|
+
add("path", 6 * term.weight);
|
|
601
|
+
}
|
|
602
|
+
if (compactTerm.length >= 5 && compactBaseName.includes(compactTerm)) {
|
|
603
|
+
add("exact_literal", 18 * term.weight);
|
|
604
|
+
}
|
|
605
|
+
if (compactTerm.length >= 5 && compactPath.includes(compactTerm)) {
|
|
606
|
+
add("path", 8 * term.weight);
|
|
607
|
+
}
|
|
608
|
+
if (tokenSet.has(normalizedTerm)) {
|
|
609
|
+
add("lexical_content", 5 * term.weight);
|
|
610
|
+
}
|
|
611
|
+
if (imports.includes(normalizedTerm)) {
|
|
612
|
+
add("dependency", 2 * term.weight);
|
|
613
|
+
}
|
|
614
|
+
if (symbolNames.some((symbol) => symbol === normalizedTerm || symbol.includes(normalizedTerm))) {
|
|
615
|
+
add("symbol", 5 * term.weight);
|
|
616
|
+
}
|
|
617
|
+
if (callNames.some((call) => call === normalizedTerm || call.includes(normalizedTerm))) {
|
|
618
|
+
add("dependency", 3 * term.weight);
|
|
619
|
+
}
|
|
620
|
+
if (subsystemTerms.some((subsystem) => subsystem === normalizedTerm || subsystem.includes(normalizedTerm))) {
|
|
621
|
+
add("path", 4 * term.weight);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
const subsystemMatches = subsystemTerms.filter((subsystem) => taskSubsystems.some((taskSubsystem) => subsystem === taskSubsystem || subsystem.includes(taskSubsystem) || taskSubsystem.includes(subsystem)));
|
|
625
|
+
if (subsystemMatches.length > 0) {
|
|
626
|
+
add("path", 4 + Math.min(10, subsystemMatches.length * 4));
|
|
627
|
+
}
|
|
628
|
+
if (wantsSourceCode(profile) && /(^|\/)src\//.test(file.relativePath.replace(/\\/g, "/"))) {
|
|
629
|
+
add("path", 14);
|
|
630
|
+
}
|
|
631
|
+
const pathModifier = pathQualityModifier(file.relativePath);
|
|
632
|
+
if (pathModifier !== 0) {
|
|
633
|
+
add("path", pathModifier);
|
|
634
|
+
}
|
|
635
|
+
return scores;
|
|
636
|
+
}
|
|
328
637
|
function scoreIndexedFile(file, terms, profile) {
|
|
329
638
|
const normalizedPath = (0, text_1.normalizeText)(file.relativePath);
|
|
330
639
|
const compactPath = normalizedPath.replace(/[^a-z0-9]+/g, "");
|
|
@@ -410,6 +719,9 @@ function pathQualityModifier(relativePath) {
|
|
|
410
719
|
if (/(package-lock\.json|pnpm-lock\.yaml|yarn\.lock|composer\.lock|poetry\.lock|cargo\.lock)$/.test(normalizedPath)) {
|
|
411
720
|
modifier -= 40;
|
|
412
721
|
}
|
|
722
|
+
if (/(^|\/)(helpers?|utils?|console|commands?|scripts?|tasks?)(\/|$)/.test(normalizedPath) && !/(controller|handler|service|repository|model|route|router|worker|job|listener|command|resolver|middleware|policy|request|resource)/.test(normalizedPath)) {
|
|
723
|
+
modifier -= 28;
|
|
724
|
+
}
|
|
413
725
|
return modifier;
|
|
414
726
|
}
|
|
415
727
|
function tokenizeForIndex(text) {
|
|
@@ -439,15 +751,83 @@ function extractStructuredNodesForIndex(relativePath, content) {
|
|
|
439
751
|
}
|
|
440
752
|
return (0, structuredExtractor_1.extractStructuredNodes)(content.slice(0, 200000), language);
|
|
441
753
|
}
|
|
442
|
-
function
|
|
754
|
+
function buildIndexedChunks(relativePath, contentHash, nodes) {
|
|
755
|
+
const symbolChunks = nodes
|
|
756
|
+
.filter((node) => node.kind === "declaration" && node.name)
|
|
757
|
+
.slice(0, 120)
|
|
758
|
+
.map((node) => ({
|
|
759
|
+
id: `${relativePath}:${node.range.startLine}-${node.range.endLine}:${node.name}`,
|
|
760
|
+
relativePath,
|
|
761
|
+
range: node.range,
|
|
762
|
+
kind: "symbol",
|
|
763
|
+
symbolFqn: node.fqn ?? node.name,
|
|
764
|
+
textHash: contentHash
|
|
765
|
+
}));
|
|
766
|
+
if (symbolChunks.length > 0) {
|
|
767
|
+
return symbolChunks;
|
|
768
|
+
}
|
|
769
|
+
return [{
|
|
770
|
+
id: `${relativePath}:file`,
|
|
771
|
+
relativePath,
|
|
772
|
+
range: { startLine: 1, endLine: 1 },
|
|
773
|
+
kind: inferChunkKind(relativePath),
|
|
774
|
+
textHash: contentHash
|
|
775
|
+
}];
|
|
776
|
+
}
|
|
777
|
+
function inferChunkKind(relativePath) {
|
|
778
|
+
const normalized = relativePath.replace(/\\/g, "/").toLowerCase();
|
|
779
|
+
if (/\.(md|mdx|txt|rst)$/.test(normalized)) {
|
|
780
|
+
return "doc";
|
|
781
|
+
}
|
|
782
|
+
if (/(^|\/)(config|configs?|settings?)(\/|$)|\.(json|ya?ml|toml|ini|env)$/.test(normalized)) {
|
|
783
|
+
return "config";
|
|
784
|
+
}
|
|
785
|
+
return "file_region";
|
|
786
|
+
}
|
|
787
|
+
function buildIndexedEdges(relativePath, imports, symbols, calls) {
|
|
788
|
+
const edges = [];
|
|
789
|
+
for (const importSource of imports.slice(0, 80)) {
|
|
790
|
+
edges.push({
|
|
791
|
+
id: `${relativePath}:import:${importSource}`,
|
|
792
|
+
type: "import",
|
|
793
|
+
from: relativePath,
|
|
794
|
+
to: importSource,
|
|
795
|
+
fromPath: relativePath,
|
|
796
|
+
toPath: importSource,
|
|
797
|
+
weight: 0.75,
|
|
798
|
+
reason: `imports ${importSource}`
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
for (const call of calls.slice(0, 120)) {
|
|
802
|
+
edges.push({
|
|
803
|
+
id: `${relativePath}:call:${call}`,
|
|
804
|
+
type: "callee",
|
|
805
|
+
from: symbols[0]?.fqn ?? relativePath,
|
|
806
|
+
to: call,
|
|
807
|
+
fromPath: relativePath,
|
|
808
|
+
toPath: "",
|
|
809
|
+
weight: 0.65,
|
|
810
|
+
reason: `calls ${call}`
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
return edges;
|
|
814
|
+
}
|
|
815
|
+
function hashText(text) {
|
|
816
|
+
return crypto.createHash("sha256").update(text).digest("hex");
|
|
817
|
+
}
|
|
818
|
+
function extractSymbols(nodes, relativePath = "") {
|
|
443
819
|
return nodes
|
|
444
820
|
.filter((node) => node.kind === "declaration" && node.name)
|
|
445
821
|
.slice(0, 80)
|
|
446
822
|
.map((node) => ({
|
|
823
|
+
id: `${relativePath}:${node.fqn ?? node.name}:${node.range.startLine}`,
|
|
447
824
|
name: node.name ?? "",
|
|
448
|
-
|
|
825
|
+
fqn: node.fqn ?? node.name ?? "",
|
|
826
|
+
kind: node.symbolKind ?? "declaration",
|
|
449
827
|
line: node.range.startLine,
|
|
450
|
-
|
|
828
|
+
endLine: node.range.endLine,
|
|
829
|
+
exported: node.parent === "exported",
|
|
830
|
+
parentFqn: node.parentFqn
|
|
451
831
|
}));
|
|
452
832
|
}
|
|
453
833
|
function extractCalls(nodes) {
|
|
@@ -486,22 +866,38 @@ function yieldToHost() {
|
|
|
486
866
|
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
487
867
|
}
|
|
488
868
|
async function readIndex(rootPath) {
|
|
869
|
+
const filePath = cachePath(rootPath);
|
|
489
870
|
try {
|
|
490
|
-
const text = await fs.readFile(
|
|
871
|
+
const text = await fs.readFile(filePath, "utf8");
|
|
491
872
|
const parsed = JSON.parse(text);
|
|
492
873
|
if (parsed.version !== indexVersion || parsed.rootPath !== path.resolve(rootPath)) {
|
|
493
874
|
return undefined;
|
|
494
875
|
}
|
|
495
876
|
return parsed;
|
|
496
877
|
}
|
|
497
|
-
catch {
|
|
878
|
+
catch (error) {
|
|
879
|
+
await moveCorruptIndexAside(filePath, error);
|
|
498
880
|
return undefined;
|
|
499
881
|
}
|
|
500
882
|
}
|
|
501
883
|
async function writeIndex(rootPath, index) {
|
|
502
884
|
const filePath = cachePath(rootPath);
|
|
503
885
|
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
504
|
-
|
|
886
|
+
const tempPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
887
|
+
await fs.writeFile(tempPath, JSON.stringify(index), "utf8");
|
|
888
|
+
await fs.rename(tempPath, filePath);
|
|
889
|
+
}
|
|
890
|
+
async function moveCorruptIndexAside(filePath, error) {
|
|
891
|
+
const code = typeof error === "object" && error && "code" in error ? String(error.code) : "";
|
|
892
|
+
if (code === "ENOENT") {
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
try {
|
|
896
|
+
await fs.rename(filePath, `${filePath}.corrupt-${Date.now()}`);
|
|
897
|
+
}
|
|
898
|
+
catch {
|
|
899
|
+
// Cache can be rebuilt if corrupt/missing.
|
|
900
|
+
}
|
|
505
901
|
}
|
|
506
902
|
function cachePath(rootPath) {
|
|
507
903
|
return path.join(path.resolve(rootPath), ".flowseeker", "cache", "workspace-index.json");
|
|
@@ -515,4 +911,173 @@ function fingerprintConfig(config) {
|
|
|
515
911
|
indexVersion
|
|
516
912
|
});
|
|
517
913
|
}
|
|
914
|
+
function discoverDeepCandidates(files, selected, profile, terms, budget) {
|
|
915
|
+
if (budget <= 0)
|
|
916
|
+
return [];
|
|
917
|
+
const results = [];
|
|
918
|
+
const taskTokens = extractDeepTaskTokens(profile);
|
|
919
|
+
const taskSubsystems = (0, subsystem_1.inferTaskSubsystemTerms)(profile);
|
|
920
|
+
const seeds = [...selected.values()].slice(0, 3);
|
|
921
|
+
const seedDirs = new Set(seeds.map(s => deepDirname(s.relativePath)));
|
|
922
|
+
for (const file of files) {
|
|
923
|
+
if (results.length >= budget)
|
|
924
|
+
break;
|
|
925
|
+
if (selected.has(file.relativePath))
|
|
926
|
+
continue;
|
|
927
|
+
if (isDeepAvoidPath(file.relativePath))
|
|
928
|
+
continue;
|
|
929
|
+
const signals = computeDeepSignals(file, taskTokens, taskSubsystems, seedDirs, profile);
|
|
930
|
+
if (signals.length < 3)
|
|
931
|
+
continue;
|
|
932
|
+
const basename = deepBasename(file.relativePath);
|
|
933
|
+
const reason = `deep_discovery: ${basename} (${signals.length} signals: ${signals.join(", ")})`;
|
|
934
|
+
results.push({ file, reason, signals });
|
|
935
|
+
}
|
|
936
|
+
return results;
|
|
937
|
+
}
|
|
938
|
+
function extractDeepTaskTokens(profile) {
|
|
939
|
+
const tokens = new Set();
|
|
940
|
+
const allTerms = [...profile.keywords, ...profile.literalTerms, ...profile.concepts, ...profile.actions, ...profile.entities];
|
|
941
|
+
for (const term of allTerms) {
|
|
942
|
+
const n = (0, text_1.normalizeText)(term);
|
|
943
|
+
if (n.length >= 3)
|
|
944
|
+
tokens.add(n);
|
|
945
|
+
// Also split camelCase and snake_case
|
|
946
|
+
for (const part of splitCamelSnake(n)) {
|
|
947
|
+
if (part.length >= 3)
|
|
948
|
+
tokens.add(part);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
return tokens;
|
|
952
|
+
}
|
|
953
|
+
function computeDeepSignals(file, taskTokens, taskSubsystems, seedDirs, profile) {
|
|
954
|
+
const signals = [];
|
|
955
|
+
const normalizedPath = (0, text_1.normalizeText)(file.relativePath);
|
|
956
|
+
const compactPath = normalizedPath.replace(/[^a-z0-9]+/g, "");
|
|
957
|
+
const normalizedBase = (0, text_1.normalizeText)(deepBasename(file.relativePath));
|
|
958
|
+
const compactBase = normalizedBase.replace(/[^a-z0-9]+/g, "");
|
|
959
|
+
const requiredSlots = new Set(profile.blueprint.requiredSlots);
|
|
960
|
+
// Signal 1: Path token overlap (task term of length >= 4 appears in file path)
|
|
961
|
+
let pathOverlapCount = 0;
|
|
962
|
+
for (const token of taskTokens) {
|
|
963
|
+
if (token.length >= 4 && compactPath.includes(token)) {
|
|
964
|
+
pathOverlapCount++;
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
if (pathOverlapCount >= 1)
|
|
968
|
+
signals.push("path_token");
|
|
969
|
+
// Signal 2: Basename overlap (task term in basename, camelCase/snake_split)
|
|
970
|
+
let baseOverlapCount = 0;
|
|
971
|
+
for (const token of taskTokens) {
|
|
972
|
+
if (token.length >= 4 && compactBase.includes(token)) {
|
|
973
|
+
baseOverlapCount++;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
if (baseOverlapCount >= 1)
|
|
977
|
+
signals.push("basename_token");
|
|
978
|
+
// Signal 3: Symbol overlap (file symbols match task terms)
|
|
979
|
+
const symbolNames = file.symbols.map(s => (0, text_1.normalizeText)(s.name));
|
|
980
|
+
const symbolOverlap = symbolNames.filter(sym => [...taskTokens].some(t => t.length >= 3 && (sym === t || sym.includes(t))));
|
|
981
|
+
if (symbolOverlap.length >= 1)
|
|
982
|
+
signals.push("symbol_match");
|
|
983
|
+
// Signal 4: Import overlap (file imports match task terms/entities)
|
|
984
|
+
const importNorms = file.imports.map(text_1.normalizeText);
|
|
985
|
+
const importOverlap = importNorms.filter(imp => [...taskTokens].some(t => t.length >= 3 && imp.includes(t)));
|
|
986
|
+
if (importOverlap.length >= 1)
|
|
987
|
+
signals.push("import_match");
|
|
988
|
+
// Signal 5: Subsystem match (file subsystem matches task-inferred subsystem)
|
|
989
|
+
const fileSubsysTerms = file.subsystemTerms ?? [];
|
|
990
|
+
const subsystemOverlap = fileSubsysTerms.filter(st => taskSubsystems.some(ts => st === ts || st.includes(ts) || ts.includes(st)));
|
|
991
|
+
if (subsystemOverlap.length >= 1 || (file.subsystem && taskSubsystems.includes((0, text_1.normalizeText)(file.subsystem)))) {
|
|
992
|
+
signals.push("subsystem_match");
|
|
993
|
+
}
|
|
994
|
+
// Signal 6: Slot/role match (path role classification matches required slot)
|
|
995
|
+
const pathRole = classifyDeepPathRole(file.relativePath);
|
|
996
|
+
if (pathRole && requiredSlots.has(pathRole)) {
|
|
997
|
+
signals.push("slot_match");
|
|
998
|
+
}
|
|
999
|
+
// Signal 7: Same directory as a seed file
|
|
1000
|
+
const fileDir = deepDirname(file.relativePath);
|
|
1001
|
+
if (seedDirs.has(fileDir)) {
|
|
1002
|
+
signals.push("same_directory");
|
|
1003
|
+
}
|
|
1004
|
+
// Signal 8: Token content overlap (file tokens match task terms)
|
|
1005
|
+
const tokenSet = new Set(file.tokens.map(text_1.normalizeText));
|
|
1006
|
+
const tokenOverlap = [...taskTokens].filter(t => t.length >= 3 && tokenSet.has(t));
|
|
1007
|
+
if (tokenOverlap.length >= 2)
|
|
1008
|
+
signals.push("token_content");
|
|
1009
|
+
return signals;
|
|
1010
|
+
}
|
|
1011
|
+
function classifyDeepPathRole(relativePath) {
|
|
1012
|
+
const p = relativePath.replace(/\\/g, "/").toLowerCase();
|
|
1013
|
+
if (/(^|\/)(routes?|router)(\/|$)|urlpatterns|urls\.py$|web\.php$|api\.php$|\+page\.|\+server\.|page\.(tsx|jsx|svelte)$/.test(p))
|
|
1014
|
+
return "entry";
|
|
1015
|
+
if (/(^|\/)(controllers?|handlers?)(\/|$)/.test(p))
|
|
1016
|
+
return "handler";
|
|
1017
|
+
if (/(^|\/)(services?|usecases?|domain)(\/|$)/.test(p))
|
|
1018
|
+
return "core_logic";
|
|
1019
|
+
if (/(^|\/)(models?|entities?)(\/|$)|schema\.(ts|js|prisma)$/.test(p))
|
|
1020
|
+
return "data";
|
|
1021
|
+
if (/(^|\/)(migrations?|schema)(\/|$)|\.sql$|\.prisma$/.test(p))
|
|
1022
|
+
return "data";
|
|
1023
|
+
if (/(^|\/)(config|configs?|settings?)(\/|$)|\.(env|toml|ya?ml|ini|json|properties)$/.test(p))
|
|
1024
|
+
return "infrastructure";
|
|
1025
|
+
if (/(^|\/)(jobs?|workers?)(\/|$)/.test(p))
|
|
1026
|
+
return "side_effect";
|
|
1027
|
+
if (/(^|\/)(listeners?|events?)(\/|$)/.test(p))
|
|
1028
|
+
return "side_effect";
|
|
1029
|
+
if (/(^|\/)(notifications?|mail|email)(\/|$)/.test(p))
|
|
1030
|
+
return "side_effect";
|
|
1031
|
+
if (/(^|\/)(components?|views?|templates?|pages?)(\/|$)|\.(svelte|vue|tsx|jsx|blade\.php|twig)$/.test(p))
|
|
1032
|
+
return "ui";
|
|
1033
|
+
if (/(^|\/)(policies?|permissions?|guards?|middleware|auth)(\/|$)/.test(p))
|
|
1034
|
+
return "permission";
|
|
1035
|
+
if (/(^|\/)(tests?|specs?|__tests__)(\/|$)|\.(test|spec)\./.test(p))
|
|
1036
|
+
return "tests";
|
|
1037
|
+
if (/(^|\/)(validators?|validation)(\/|$)/.test(p))
|
|
1038
|
+
return "validation";
|
|
1039
|
+
return null;
|
|
1040
|
+
}
|
|
1041
|
+
function isDeepAvoidPath(normalizedPath) {
|
|
1042
|
+
const p = normalizedPath.replace(/\\/g, "/").toLowerCase();
|
|
1043
|
+
// Helpers/utils (unless they also match a strong signal)
|
|
1044
|
+
if (/(^|\/)(helpers?|utils?)(\/|$)/.test(p) && !/(controller|handler|service|repository|model|route|router)\b/.test(p))
|
|
1045
|
+
return true;
|
|
1046
|
+
// Lock files
|
|
1047
|
+
if (/(package-lock|yarn\.lock|pnpm-lock|composer\.lock|Gemfile\.lock|poetry\.lock|cargo\.lock)$/.test(p))
|
|
1048
|
+
return true;
|
|
1049
|
+
// Generated/snapshot
|
|
1050
|
+
if (/(^|\/)(generated|__generated__|gen|codegen|snapshots?)(\/|$)|\.snap$|\.generated\./.test(p))
|
|
1051
|
+
return true;
|
|
1052
|
+
// Static assets
|
|
1053
|
+
if (/(^|\/)(public|assets?|static|images?|fonts?)(\/|$)|\.(css|scss|less|svg|png|jpe?g|gif|ico|woff2?)$/.test(p))
|
|
1054
|
+
return true;
|
|
1055
|
+
// Admin/dashboard (unless task targets it)
|
|
1056
|
+
if (/(^|\/)(admin|dashboard)(\/|$)/.test(p))
|
|
1057
|
+
return true;
|
|
1058
|
+
// Templates/views
|
|
1059
|
+
if (/(^|\/)(views?|templates?|resources\/views)(\/|$)|\.(blade\.php|twig|erb|hbs|ejs)$/.test(p))
|
|
1060
|
+
return true;
|
|
1061
|
+
// Console/commands
|
|
1062
|
+
if (/(^|\/)(console|commands?|scheduler|schedulers?|cli)(\/|$)/.test(p))
|
|
1063
|
+
return true;
|
|
1064
|
+
return false;
|
|
1065
|
+
}
|
|
1066
|
+
function deepDirname(filePath) {
|
|
1067
|
+
return filePath.replace(/\\/g, "/").toLowerCase().split("/").slice(0, -1).join("/");
|
|
1068
|
+
}
|
|
1069
|
+
function deepBasename(filePath) {
|
|
1070
|
+
return filePath.replace(/\\/g, "/").split("/").pop()?.replace(/\.[^.]+$/, "") || filePath;
|
|
1071
|
+
}
|
|
1072
|
+
function splitCamelSnake(s) {
|
|
1073
|
+
const parts = [];
|
|
1074
|
+
for (const seg of s.toLowerCase().split(/[_\-]/)) {
|
|
1075
|
+
const camelParts = seg.replace(/([a-z])([A-Z])/g, "$1 $2").split(/[\s.]+/);
|
|
1076
|
+
for (const p of camelParts) {
|
|
1077
|
+
if (p.length >= 3)
|
|
1078
|
+
parts.push(p);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
return [...new Set(parts)];
|
|
1082
|
+
}
|
|
518
1083
|
//# sourceMappingURL=workspaceIndex.js.map
|