@neurcode-ai/cli 0.9.22 → 0.9.24
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/commands/ask.d.ts.map +1 -1
- package/dist/commands/ask.js +959 -38
- package/dist/commands/ask.js.map +1 -1
- package/package.json +1 -1
package/dist/commands/ask.js
CHANGED
|
@@ -25,7 +25,12 @@ catch {
|
|
|
25
25
|
white: (str) => str,
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
-
const MAX_SCAN_FILES =
|
|
28
|
+
const MAX_SCAN_FILES = (() => {
|
|
29
|
+
const raw = Number(process.env.NEURCODE_ASK_MAX_SCAN_FILES || '900');
|
|
30
|
+
if (!Number.isFinite(raw))
|
|
31
|
+
return 900;
|
|
32
|
+
return Math.max(120, Math.min(Math.trunc(raw), 2000));
|
|
33
|
+
})();
|
|
29
34
|
const MAX_FILE_BYTES = 512 * 1024;
|
|
30
35
|
const MAX_RAW_CITATIONS = 120;
|
|
31
36
|
const PRIMARY_SOURCE_EXTENSIONS = new Set([
|
|
@@ -45,7 +50,28 @@ const STOP_WORDS = new Set([
|
|
|
45
50
|
const LOW_SIGNAL_TERMS = new Set([
|
|
46
51
|
'used', 'use', 'using', 'mentioned', 'mention', 'where', 'tell', 'read', 'check', 'find', 'search',
|
|
47
52
|
'workflow', 'repo', 'repository', 'codebase', 'anywhere', 'can', 'type', 'types', 'list', 'show', 'like',
|
|
48
|
-
'neurcode', 'cli',
|
|
53
|
+
'neurcode', 'cli', 'ask', 'file', 'files', 'path', 'filepath', 'header', 'added', 'add', 'request', 'requests',
|
|
54
|
+
'flag', 'flags', 'option', 'options',
|
|
55
|
+
'defined', 'define', 'implemented', 'implement', 'called', 'call', 'computed', 'compute', 'resolved', 'resolve',
|
|
56
|
+
'lookup', 'lookups', 'decide', 'decides',
|
|
57
|
+
]);
|
|
58
|
+
const GENERIC_OUTPUT_TERMS = new Set(['file', 'files', 'path', 'filepath']);
|
|
59
|
+
const REPO_SCOPE_TERMS = [
|
|
60
|
+
'repo', 'repository', 'codebase', 'neurcode', 'cli', 'command', 'commands', 'ship', 'plan', 'ask', 'verify',
|
|
61
|
+
'apply', 'watch', 'config', 'login', 'logout', 'whoami', 'doctor', 'module', 'file', 'files', 'path',
|
|
62
|
+
'middleware', 'api', 'service', 'services', 'branch', 'commit', 'cache', 'brain', 'org', 'organization',
|
|
63
|
+
'tenant', 'tenancy', 'x-org-id', 'readme', 'docs',
|
|
64
|
+
];
|
|
65
|
+
const EXTERNAL_WORLD_TERMS = [
|
|
66
|
+
'capital', 'exchange rate', 'stock price', 'weather', 'temperature', 'forecast', 'news', 'election',
|
|
67
|
+
'president', 'prime minister', 'population', 'gdp', 'sports score', 'match result', 'bitcoin price',
|
|
68
|
+
'usd', 'inr', 'eur', 'jpy', 'currency',
|
|
69
|
+
];
|
|
70
|
+
const REALTIME_WORLD_TERMS = ['right now', 'currently', 'today', 'tomorrow', 'yesterday', 'live', 'real time'];
|
|
71
|
+
const CLI_COMMAND_NAMES = new Set([
|
|
72
|
+
'check', 'refactor', 'security', 'brain', 'login', 'logout', 'init', 'doctor',
|
|
73
|
+
'whoami', 'config', 'map', 'ask', 'plan', 'ship', 'apply', 'allow', 'watch',
|
|
74
|
+
'session', 'verify', 'prompt', 'revert',
|
|
49
75
|
]);
|
|
50
76
|
function scanFiles(dir, maxFiles = MAX_SCAN_FILES) {
|
|
51
77
|
const files = [];
|
|
@@ -110,12 +136,19 @@ function escapeRegExp(input) {
|
|
|
110
136
|
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
111
137
|
}
|
|
112
138
|
function normalizeTerm(raw) {
|
|
113
|
-
|
|
139
|
+
const normalized = raw
|
|
114
140
|
.toLowerCase()
|
|
115
141
|
.replace(/['"`]/g, '')
|
|
116
142
|
.replace(/[^a-z0-9_\-\s]/g, ' ')
|
|
117
143
|
.replace(/\s+/g, ' ')
|
|
118
144
|
.trim();
|
|
145
|
+
if (!normalized)
|
|
146
|
+
return '';
|
|
147
|
+
return normalized
|
|
148
|
+
.split(' ')
|
|
149
|
+
.map((token) => token.replace(/^[-_]+|[-_]+$/g, ''))
|
|
150
|
+
.filter(Boolean)
|
|
151
|
+
.join(' ');
|
|
119
152
|
}
|
|
120
153
|
function extractQuotedTerms(question) {
|
|
121
154
|
const seen = new Set();
|
|
@@ -162,6 +195,25 @@ function extractComparisonTerms(question) {
|
|
|
162
195
|
return identities.slice(0, 2);
|
|
163
196
|
return [];
|
|
164
197
|
}
|
|
198
|
+
function extractPhraseTerms(question, maxTerms = 6) {
|
|
199
|
+
const tokens = (0, plan_cache_1.normalizeIntent)(question)
|
|
200
|
+
.replace(/[^a-z0-9_\-\s]/g, ' ')
|
|
201
|
+
.split(/\s+/)
|
|
202
|
+
.map((token) => token.trim())
|
|
203
|
+
.filter((token) => token.length >= 3 && !STOP_WORDS.has(token) && !LOW_SIGNAL_TERMS.has(token));
|
|
204
|
+
const phrases = [];
|
|
205
|
+
const seen = new Set();
|
|
206
|
+
for (let i = 0; i < tokens.length - 1; i++) {
|
|
207
|
+
const phrase = `${tokens[i]} ${tokens[i + 1]}`;
|
|
208
|
+
if (seen.has(phrase))
|
|
209
|
+
continue;
|
|
210
|
+
seen.add(phrase);
|
|
211
|
+
phrases.push(phrase);
|
|
212
|
+
if (phrases.length >= maxTerms)
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
return phrases;
|
|
216
|
+
}
|
|
165
217
|
function buildTermMatchers(term, weight) {
|
|
166
218
|
const normalized = normalizeTerm(term);
|
|
167
219
|
if (!normalized)
|
|
@@ -178,6 +230,10 @@ function buildTermMatchers(term, weight) {
|
|
|
178
230
|
if (tokens.length === 1) {
|
|
179
231
|
const token = escapeRegExp(tokens[0]);
|
|
180
232
|
push(normalized, `\\b${token}\\b`);
|
|
233
|
+
if (/^[a-z0-9_]+$/.test(tokens[0]) && tokens[0].length >= 5) {
|
|
234
|
+
push(normalized, `\\b${token}[a-z0-9_]*\\b`);
|
|
235
|
+
push(normalized, `\\b[a-z0-9_]*${token}[a-z0-9_]*\\b`);
|
|
236
|
+
}
|
|
181
237
|
return out;
|
|
182
238
|
}
|
|
183
239
|
const tokenChain = tokens.map((t) => escapeRegExp(t)).join('[\\s_-]*');
|
|
@@ -212,6 +268,20 @@ function expandSearchTerms(terms) {
|
|
|
212
268
|
if (term.endsWith('ed') && term.length > 4) {
|
|
213
269
|
expanded.add(term.slice(0, -2));
|
|
214
270
|
}
|
|
271
|
+
if (term.includes('-')) {
|
|
272
|
+
expanded.add(term.replace(/-/g, ' '));
|
|
273
|
+
expanded.add(term.replace(/-/g, ''));
|
|
274
|
+
}
|
|
275
|
+
if (term.endsWith('cache')) {
|
|
276
|
+
expanded.add(`${term}d`);
|
|
277
|
+
}
|
|
278
|
+
if (term.endsWith('cached')) {
|
|
279
|
+
expanded.add(term.slice(0, -1)); // cached -> cache
|
|
280
|
+
}
|
|
281
|
+
if (term.includes('cache') && term.includes('-')) {
|
|
282
|
+
expanded.add(term.replace(/cache\b/, 'cached'));
|
|
283
|
+
expanded.add(term.replace(/cached\b/, 'cache'));
|
|
284
|
+
}
|
|
215
285
|
}
|
|
216
286
|
return [...expanded];
|
|
217
287
|
}
|
|
@@ -229,9 +299,11 @@ function buildMatchers(question) {
|
|
|
229
299
|
};
|
|
230
300
|
}
|
|
231
301
|
const quoted = extractQuotedTerms(question);
|
|
302
|
+
const identityTerms = extractIdentityTerms(question);
|
|
303
|
+
const phraseTerms = extractPhraseTerms(question);
|
|
232
304
|
const keywords = tokenizeQuestion(question).slice(0, 8);
|
|
233
305
|
const quotedSet = new Set(quoted.map((term) => normalizeTerm(term)));
|
|
234
|
-
const baseTerms = [...new Set([...quoted, ...keywords].map(normalizeTerm).filter(Boolean))];
|
|
306
|
+
const baseTerms = [...new Set([...quoted, ...phraseTerms, ...identityTerms, ...keywords].map(normalizeTerm).filter(Boolean))];
|
|
235
307
|
const filteredTerms = baseTerms.filter((term) => quotedSet.has(term) || !LOW_SIGNAL_TERMS.has(term));
|
|
236
308
|
const terms = expandSearchTerms(filteredTerms.length > 0 ? filteredTerms : baseTerms).filter(Boolean);
|
|
237
309
|
const matchers = terms.flatMap((term) => buildTermMatchers(term, quoted.includes(term) ? 0.9 : 0.55));
|
|
@@ -271,6 +343,10 @@ function derivePathHints(question) {
|
|
|
271
343
|
hints.push('services/api/src/routes/');
|
|
272
344
|
hints.push('packages/cli/src/');
|
|
273
345
|
}
|
|
346
|
+
if (/\brequest|requests|header|inject|injected\b/.test(normalized)) {
|
|
347
|
+
hints.push('packages/cli/src/api-client.ts');
|
|
348
|
+
hints.push('services/api/src/middleware/');
|
|
349
|
+
}
|
|
274
350
|
if (/(github action|\bci\b)/.test(normalized)) {
|
|
275
351
|
hints.push('.github/workflows/');
|
|
276
352
|
hints.push('packages/action/');
|
|
@@ -278,6 +354,149 @@ function derivePathHints(question) {
|
|
|
278
354
|
}
|
|
279
355
|
return [...new Set(hints)];
|
|
280
356
|
}
|
|
357
|
+
function extractCodeLikeIdentifiers(question) {
|
|
358
|
+
const quoted = extractQuotedTerms(question);
|
|
359
|
+
const tokens = question.match(/[A-Za-z_][A-Za-z0-9_]{2,}/g) || [];
|
|
360
|
+
const seen = new Set();
|
|
361
|
+
const output = [];
|
|
362
|
+
for (const raw of [...quoted, ...tokens]) {
|
|
363
|
+
const normalized = raw.trim();
|
|
364
|
+
if (!normalized)
|
|
365
|
+
continue;
|
|
366
|
+
const key = normalized.toLowerCase();
|
|
367
|
+
if (STOP_WORDS.has(key) || LOW_SIGNAL_TERMS.has(key))
|
|
368
|
+
continue;
|
|
369
|
+
if (seen.has(key))
|
|
370
|
+
continue;
|
|
371
|
+
seen.add(key);
|
|
372
|
+
output.push(normalized);
|
|
373
|
+
}
|
|
374
|
+
return output.slice(0, 8);
|
|
375
|
+
}
|
|
376
|
+
function deriveQuerySignals(question, normalizedQuestion, terms) {
|
|
377
|
+
const asksLocation = /\b(where|which file|in which file|filepath|file path|location)\b/.test(normalizedQuestion);
|
|
378
|
+
const asksHow = /\b(how|flow|trace|walk me through|explain)\b/.test(normalizedQuestion);
|
|
379
|
+
const asksCommandSurface = /\b(command|commands|subcommand|subcommands|flag|flags|option|options)\b/.test(normalizedQuestion);
|
|
380
|
+
const asksSchema = /\b(field|fields|key|keys|schema|interface|input|output|parameter|parameters)\b/.test(normalizedQuestion);
|
|
381
|
+
const asksList = /\b(list|show|which|available)\b/.test(normalizedQuestion) &&
|
|
382
|
+
/\b(command|commands|subcommand|subcommands|files|fields|features)\b/.test(normalizedQuestion);
|
|
383
|
+
const asksDefinition = /\b(defined|implemented|called|computed|resolved)\b/.test(normalizedQuestion);
|
|
384
|
+
const identifiers = extractCodeLikeIdentifiers(question);
|
|
385
|
+
const highSignalTerms = terms.filter((term) => !LOW_SIGNAL_TERMS.has(term) && term.length >= 3);
|
|
386
|
+
return {
|
|
387
|
+
asksLocation,
|
|
388
|
+
asksHow,
|
|
389
|
+
asksList,
|
|
390
|
+
asksDefinition,
|
|
391
|
+
asksCommandSurface,
|
|
392
|
+
asksSchema,
|
|
393
|
+
identifiers,
|
|
394
|
+
highSignalTerms,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
function extractAnchorTerms(question) {
|
|
398
|
+
const quoted = extractQuotedTerms(question);
|
|
399
|
+
const special = (question.match(/[A-Za-z0-9_-]{4,}/g) || []).filter((token) => /[_-]/.test(token) || /[A-Z]/.test(token));
|
|
400
|
+
const normalized = [...new Set([...quoted, ...special].map(normalizeTerm).filter(Boolean))]
|
|
401
|
+
.filter((term) => !STOP_WORDS.has(term) && !LOW_SIGNAL_TERMS.has(term));
|
|
402
|
+
return expandSearchTerms(normalized).slice(0, 12);
|
|
403
|
+
}
|
|
404
|
+
function looksLikeImportLine(rawLine) {
|
|
405
|
+
const trimmed = rawLine.trim();
|
|
406
|
+
return /^import\s+/.test(trimmed) || /^export\s+\{/.test(trimmed);
|
|
407
|
+
}
|
|
408
|
+
function countQuestionTermHits(normalizedQuestion, terms) {
|
|
409
|
+
let hits = 0;
|
|
410
|
+
const seen = new Set();
|
|
411
|
+
for (const term of terms) {
|
|
412
|
+
const normalizedTerm = term.toLowerCase().trim();
|
|
413
|
+
if (!normalizedTerm || seen.has(normalizedTerm))
|
|
414
|
+
continue;
|
|
415
|
+
seen.add(normalizedTerm);
|
|
416
|
+
if (normalizedTerm.includes(' ')) {
|
|
417
|
+
if (!normalizedQuestion.includes(normalizedTerm))
|
|
418
|
+
continue;
|
|
419
|
+
hits += 1;
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
const boundaryPattern = new RegExp(`(?:^|[^a-z0-9])${escapeRegExp(normalizedTerm)}(?:$|[^a-z0-9])`);
|
|
423
|
+
if (boundaryPattern.test(normalizedQuestion)) {
|
|
424
|
+
hits += 1;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return hits;
|
|
428
|
+
}
|
|
429
|
+
function assessQuestionScope(normalizedQuestion) {
|
|
430
|
+
const repoHits = countQuestionTermHits(normalizedQuestion, REPO_SCOPE_TERMS);
|
|
431
|
+
const externalHits = countQuestionTermHits(normalizedQuestion, EXTERNAL_WORLD_TERMS);
|
|
432
|
+
const realtimeHits = countQuestionTermHits(normalizedQuestion, REALTIME_WORLD_TERMS);
|
|
433
|
+
const currencyPairPattern = /\b[a-z]{3}\s*(?:to|\/)\s*[a-z]{3}\b/;
|
|
434
|
+
const hasCurrencyPair = currencyPairPattern.test(normalizedQuestion);
|
|
435
|
+
const hasWorldEntity = /\b(france|india|usa|united states|china|germany|europe|asia|africa)\b/.test(normalizedQuestion);
|
|
436
|
+
const looksExternal = hasCurrencyPair ||
|
|
437
|
+
externalHits > 0 ||
|
|
438
|
+
(hasWorldEntity && /\bcapital|population|gdp|president|prime minister\b/.test(normalizedQuestion)) ||
|
|
439
|
+
(realtimeHits > 0 && /\bexchange rate|weather|stock|price|news|capital|population\b/.test(normalizedQuestion));
|
|
440
|
+
if (looksExternal && repoHits === 0) {
|
|
441
|
+
const reasons = ['This question appears to require external world knowledge rather than repository evidence.'];
|
|
442
|
+
if (hasCurrencyPair) {
|
|
443
|
+
reasons.push('Detected a currency/exchange-rate query, which is outside repo-grounded scope.');
|
|
444
|
+
}
|
|
445
|
+
if (realtimeHits > 0) {
|
|
446
|
+
reasons.push('Detected real-time wording (for example "right now"/"currently"), which is not answerable from local files.');
|
|
447
|
+
}
|
|
448
|
+
return { isOutOfScope: true, reasons };
|
|
449
|
+
}
|
|
450
|
+
return { isOutOfScope: false, reasons: [] };
|
|
451
|
+
}
|
|
452
|
+
function buildOutOfScopeAnswerPayload(question, normalizedQuestion, reasons) {
|
|
453
|
+
const truthReasons = [
|
|
454
|
+
'Neurcode Ask is repository-grounded and does not use web/external knowledge.',
|
|
455
|
+
...reasons,
|
|
456
|
+
];
|
|
457
|
+
return {
|
|
458
|
+
question,
|
|
459
|
+
questionNormalized: normalizedQuestion,
|
|
460
|
+
mode: 'search',
|
|
461
|
+
answer: [
|
|
462
|
+
'I can only answer from this repository.',
|
|
463
|
+
'This question looks external/realtime, so I cannot answer it from repo evidence.',
|
|
464
|
+
'Try a repo-scoped variant, for example: `Where is org id injected in CLI requests?`',
|
|
465
|
+
].join('\n'),
|
|
466
|
+
findings: [
|
|
467
|
+
'Scope guard triggered: external/non-repo query.',
|
|
468
|
+
'No repo files were scanned to avoid returning misleading answers.',
|
|
469
|
+
],
|
|
470
|
+
confidence: 'low',
|
|
471
|
+
truth: {
|
|
472
|
+
status: 'insufficient',
|
|
473
|
+
score: 0.05,
|
|
474
|
+
reasons: truthReasons,
|
|
475
|
+
sourceCitations: 0,
|
|
476
|
+
sourceFiles: 0,
|
|
477
|
+
minCitationsRequired: 2,
|
|
478
|
+
minFilesRequired: 1,
|
|
479
|
+
},
|
|
480
|
+
citations: [],
|
|
481
|
+
generatedAt: new Date().toISOString(),
|
|
482
|
+
stats: {
|
|
483
|
+
scannedFiles: 0,
|
|
484
|
+
matchedFiles: 0,
|
|
485
|
+
matchedLines: 0,
|
|
486
|
+
brainCandidates: 0,
|
|
487
|
+
},
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
function tryBuildDeterministicAnswer(_cwd, question, normalizedQuestion) {
|
|
491
|
+
const scope = assessQuestionScope(normalizedQuestion);
|
|
492
|
+
if (scope.isOutOfScope) {
|
|
493
|
+
return {
|
|
494
|
+
payload: buildOutOfScopeAnswerPayload(question, normalizedQuestion, scope.reasons),
|
|
495
|
+
reason: 'out_of_scope',
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
281
500
|
function normalizeSnippet(line) {
|
|
282
501
|
return line
|
|
283
502
|
.replace(/\t/g, ' ')
|
|
@@ -438,6 +657,34 @@ function extractNeurcodeCommandsFromCitations(citations) {
|
|
|
438
657
|
}
|
|
439
658
|
return [...commandSet].slice(0, 20);
|
|
440
659
|
}
|
|
660
|
+
function extractCommandFocus(normalizedQuestion) {
|
|
661
|
+
const toKnownCommand = (candidate) => {
|
|
662
|
+
if (!candidate)
|
|
663
|
+
return null;
|
|
664
|
+
const normalized = candidate.toLowerCase().trim();
|
|
665
|
+
return CLI_COMMAND_NAMES.has(normalized) ? normalized : null;
|
|
666
|
+
};
|
|
667
|
+
const direct = normalizedQuestion.match(/\bneurcode\s+([a-z][a-z0-9-]*)\b/);
|
|
668
|
+
const directCommand = toKnownCommand(direct?.[1] || null);
|
|
669
|
+
if (directCommand)
|
|
670
|
+
return directCommand;
|
|
671
|
+
const scoped = normalizedQuestion.match(/\b(?:under|for|within|inside|in)\s+(?:neurcode\s+)?([a-z][a-z0-9-]*)\b/);
|
|
672
|
+
const scopedCommand = toKnownCommand(scoped?.[1] || null);
|
|
673
|
+
if (scopedCommand)
|
|
674
|
+
return scopedCommand;
|
|
675
|
+
const commandPattern = normalizedQuestion.match(/\b([a-z][a-z0-9-]*)\s+command\b/);
|
|
676
|
+
const commandPhrase = toKnownCommand(commandPattern?.[1] || null);
|
|
677
|
+
if (commandPhrase)
|
|
678
|
+
return commandPhrase;
|
|
679
|
+
const mentionedCommands = [...CLI_COMMAND_NAMES].filter((command) => new RegExp(`\\b${escapeRegExp(command)}\\b`, 'i').test(normalizedQuestion));
|
|
680
|
+
if (mentionedCommands.length !== 1)
|
|
681
|
+
return null;
|
|
682
|
+
const hasImplementationSignals = /\b(where|how|defined|implemented|called|computed|resolved|lookup|cache|flag|option|verdict|pass|fail|schema|field|fields|key|keys)\b/.test(normalizedQuestion);
|
|
683
|
+
if (hasImplementationSignals) {
|
|
684
|
+
return mentionedCommands[0];
|
|
685
|
+
}
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
441
688
|
function extractInstallCommandsFromCitations(citations) {
|
|
442
689
|
const installSet = new Set();
|
|
443
690
|
for (const citation of citations) {
|
|
@@ -460,6 +707,162 @@ function formatInsightSnippet(snippet) {
|
|
|
460
707
|
.trim()
|
|
461
708
|
.slice(0, 180);
|
|
462
709
|
}
|
|
710
|
+
function countTermHitsInText(text, terms) {
|
|
711
|
+
if (!text || terms.length === 0)
|
|
712
|
+
return 0;
|
|
713
|
+
let hits = 0;
|
|
714
|
+
const isCompactText = !/\s/.test(text);
|
|
715
|
+
const compactText = isCompactText ? text.replace(/[^a-z0-9]/g, '') : '';
|
|
716
|
+
for (const term of terms) {
|
|
717
|
+
const normalized = term.toLowerCase().trim();
|
|
718
|
+
if (!normalized)
|
|
719
|
+
continue;
|
|
720
|
+
if (normalized.includes(' ')) {
|
|
721
|
+
if (text.includes(normalized))
|
|
722
|
+
hits += 1;
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
const pattern = new RegExp(`(?:^|[^a-z0-9])${escapeRegExp(normalized)}(?:$|[^a-z0-9])`, 'i');
|
|
726
|
+
if (pattern.test(text)) {
|
|
727
|
+
hits += 1;
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
if (isCompactText) {
|
|
731
|
+
const compactTerm = normalized.replace(/[^a-z0-9]/g, '');
|
|
732
|
+
if (compactTerm.length >= 3 && compactText.includes(compactTerm)) {
|
|
733
|
+
hits += 1;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
return hits;
|
|
738
|
+
}
|
|
739
|
+
function findInterfaceFieldBlock(lines, startLine) {
|
|
740
|
+
const startIndex = Math.max(0, startLine - 1);
|
|
741
|
+
const lookbackStart = Math.max(0, startIndex - 14);
|
|
742
|
+
const lookaheadEnd = Math.min(lines.length - 1, startIndex + 10);
|
|
743
|
+
let headerIndex = -1;
|
|
744
|
+
let interfaceName = '';
|
|
745
|
+
for (let i = startIndex; i >= lookbackStart; i--) {
|
|
746
|
+
const match = lines[i].match(/^\s*(?:export\s+)?interface\s+([A-Za-z_][A-Za-z0-9_]*)\b/);
|
|
747
|
+
if (!match?.[1])
|
|
748
|
+
continue;
|
|
749
|
+
headerIndex = i;
|
|
750
|
+
interfaceName = match[1];
|
|
751
|
+
break;
|
|
752
|
+
}
|
|
753
|
+
if (headerIndex < 0) {
|
|
754
|
+
for (let i = startIndex; i <= lookaheadEnd; i++) {
|
|
755
|
+
const match = lines[i].match(/^\s*(?:export\s+)?interface\s+([A-Za-z_][A-Za-z0-9_]*)\b/);
|
|
756
|
+
if (!match?.[1])
|
|
757
|
+
continue;
|
|
758
|
+
headerIndex = i;
|
|
759
|
+
interfaceName = match[1];
|
|
760
|
+
break;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
if (headerIndex < 0 || !interfaceName)
|
|
764
|
+
return null;
|
|
765
|
+
let opened = false;
|
|
766
|
+
let depth = 0;
|
|
767
|
+
const fields = [];
|
|
768
|
+
const seen = new Set();
|
|
769
|
+
for (let i = headerIndex; i < Math.min(lines.length, headerIndex + 120); i++) {
|
|
770
|
+
const line = lines[i];
|
|
771
|
+
if (!opened) {
|
|
772
|
+
if (line.includes('{')) {
|
|
773
|
+
opened = true;
|
|
774
|
+
depth = 1;
|
|
775
|
+
}
|
|
776
|
+
continue;
|
|
777
|
+
}
|
|
778
|
+
const fieldMatch = line.match(/^\s*([A-Za-z_][A-Za-z0-9_?]*)\s*:/);
|
|
779
|
+
if (fieldMatch?.[1]) {
|
|
780
|
+
const field = fieldMatch[1].replace(/\?$/, '');
|
|
781
|
+
if (!seen.has(field)) {
|
|
782
|
+
seen.add(field);
|
|
783
|
+
fields.push(field);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
for (const ch of line) {
|
|
787
|
+
if (ch === '{')
|
|
788
|
+
depth += 1;
|
|
789
|
+
if (ch === '}')
|
|
790
|
+
depth -= 1;
|
|
791
|
+
}
|
|
792
|
+
if (depth <= 0)
|
|
793
|
+
break;
|
|
794
|
+
}
|
|
795
|
+
if (fields.length === 0)
|
|
796
|
+
return null;
|
|
797
|
+
return { interfaceName, fields };
|
|
798
|
+
}
|
|
799
|
+
function splitIdentifierTokens(value) {
|
|
800
|
+
if (!value)
|
|
801
|
+
return [];
|
|
802
|
+
const tokens = value
|
|
803
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
804
|
+
.replace(/[_\-\s]+/g, ' ')
|
|
805
|
+
.toLowerCase()
|
|
806
|
+
.split(/\s+/)
|
|
807
|
+
.map((token) => token.trim())
|
|
808
|
+
.filter((token) => token.length >= 2);
|
|
809
|
+
return [...new Set(tokens)];
|
|
810
|
+
}
|
|
811
|
+
function buildSchemaFieldSummaries(citations, lineCache, terms) {
|
|
812
|
+
const scored = [];
|
|
813
|
+
const seen = new Set();
|
|
814
|
+
const highSignalTerms = terms.filter((term) => !LOW_SIGNAL_TERMS.has(term));
|
|
815
|
+
const queryTermSet = new Set(highSignalTerms.map((term) => term.toLowerCase()));
|
|
816
|
+
for (const citation of citations) {
|
|
817
|
+
const lines = lineCache.get(citation.path);
|
|
818
|
+
if (!lines || lines.length === 0)
|
|
819
|
+
continue;
|
|
820
|
+
const parsed = findInterfaceFieldBlock(lines, citation.line);
|
|
821
|
+
if (!parsed)
|
|
822
|
+
continue;
|
|
823
|
+
const interfaceKey = `${citation.path}:${parsed.interfaceName}`;
|
|
824
|
+
if (seen.has(interfaceKey))
|
|
825
|
+
continue;
|
|
826
|
+
const ifaceLower = parsed.interfaceName.toLowerCase();
|
|
827
|
+
const ifaceTokens = splitIdentifierTokens(parsed.interfaceName);
|
|
828
|
+
const ifaceText = `${ifaceLower} ${ifaceTokens.join(' ')}`.trim();
|
|
829
|
+
const fieldText = parsed.fields.join(' ').toLowerCase();
|
|
830
|
+
const nameHits = countTermHitsInText(ifaceText, highSignalTerms);
|
|
831
|
+
const fieldHits = countTermHitsInText(fieldText, highSignalTerms);
|
|
832
|
+
let relevanceScore = nameHits * 2 + fieldHits;
|
|
833
|
+
if (queryTermSet.has('plan') && ifaceText.includes('plan'))
|
|
834
|
+
relevanceScore += 0.8;
|
|
835
|
+
if (queryTermSet.has('cache') && ifaceText.includes('cache'))
|
|
836
|
+
relevanceScore += 0.8;
|
|
837
|
+
if (queryTermSet.has('key') && ifaceText.includes('key'))
|
|
838
|
+
relevanceScore += 1.2;
|
|
839
|
+
if ((queryTermSet.has('field') || queryTermSet.has('fields')) && parsed.fields.length > 0) {
|
|
840
|
+
relevanceScore += Math.min(1, parsed.fields.length / 8);
|
|
841
|
+
}
|
|
842
|
+
if (highSignalTerms.length > 0 && relevanceScore === 0) {
|
|
843
|
+
continue;
|
|
844
|
+
}
|
|
845
|
+
seen.add(interfaceKey);
|
|
846
|
+
scored.push({
|
|
847
|
+
score: relevanceScore,
|
|
848
|
+
summary: `${parsed.interfaceName}: ${parsed.fields.slice(0, 12).join(', ')}`,
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
const ranked = scored.sort((a, b) => b.score - a.score);
|
|
852
|
+
if (ranked.length === 0)
|
|
853
|
+
return [];
|
|
854
|
+
const topScore = ranked[0].score;
|
|
855
|
+
let minScore = topScore >= 2 ? topScore * 0.6 : 0;
|
|
856
|
+
const strictExactFieldRequest = queryTermSet.has('exact') &&
|
|
857
|
+
(queryTermSet.has('field') || queryTermSet.has('fields'));
|
|
858
|
+
if (strictExactFieldRequest) {
|
|
859
|
+
minScore = Math.max(minScore, topScore - 0.5);
|
|
860
|
+
}
|
|
861
|
+
return ranked
|
|
862
|
+
.filter((item) => item.score >= minScore)
|
|
863
|
+
.slice(0, 6)
|
|
864
|
+
.map((item) => item.summary);
|
|
865
|
+
}
|
|
463
866
|
function extractInsightLines(citations, limit) {
|
|
464
867
|
const out = [];
|
|
465
868
|
const seen = new Set();
|
|
@@ -484,6 +887,35 @@ function extractInsightLines(citations, limit) {
|
|
|
484
887
|
}
|
|
485
888
|
return out;
|
|
486
889
|
}
|
|
890
|
+
function isCommandCatalogIntent(normalizedQuestion) {
|
|
891
|
+
const mentionsCommandSurface = /\b(cli|cmd|cmds|command|commands|subcommand|subcommands)\b/.test(normalizedQuestion);
|
|
892
|
+
if (!mentionsCommandSurface)
|
|
893
|
+
return false;
|
|
894
|
+
const listIntent = /\blist\b/.test(normalizedQuestion) ||
|
|
895
|
+
/\bshow\b/.test(normalizedQuestion) ||
|
|
896
|
+
/\bavailable\b/.test(normalizedQuestion) ||
|
|
897
|
+
/\ball commands?\b/.test(normalizedQuestion) ||
|
|
898
|
+
/\bwhich commands\b/.test(normalizedQuestion) ||
|
|
899
|
+
/\bwhat commands\b/.test(normalizedQuestion) ||
|
|
900
|
+
/\bwhat can i (type|run)\b/.test(normalizedQuestion) ||
|
|
901
|
+
/\bcan i type\b/.test(normalizedQuestion) ||
|
|
902
|
+
/\bcmds\b/.test(normalizedQuestion);
|
|
903
|
+
if (!listIntent)
|
|
904
|
+
return false;
|
|
905
|
+
const specificIntent = /\bwhere\b/.test(normalizedQuestion) ||
|
|
906
|
+
/\bhow\b/.test(normalizedQuestion) ||
|
|
907
|
+
/\bwhy\b/.test(normalizedQuestion) ||
|
|
908
|
+
/\bwhen\b/.test(normalizedQuestion) ||
|
|
909
|
+
/\bwhich file\b/.test(normalizedQuestion) ||
|
|
910
|
+
/\bin which file\b/.test(normalizedQuestion) ||
|
|
911
|
+
/\bfilepath\b/.test(normalizedQuestion) ||
|
|
912
|
+
/\bfile path\b/.test(normalizedQuestion) ||
|
|
913
|
+
/\binject(?:ed)?\b/.test(normalizedQuestion) ||
|
|
914
|
+
/\bhandle(?:s|d)?\b/.test(normalizedQuestion) ||
|
|
915
|
+
/\bused?\b/.test(normalizedQuestion) ||
|
|
916
|
+
/\bflow\b/.test(normalizedQuestion);
|
|
917
|
+
return !specificIntent;
|
|
918
|
+
}
|
|
487
919
|
function isPrimarySourcePath(filePath) {
|
|
488
920
|
const normalized = filePath.trim().replace(/\\/g, '/').toLowerCase();
|
|
489
921
|
if (!normalized)
|
|
@@ -606,16 +1038,113 @@ function buildAnswer(mode, question, terms, citations, stats, termCounts, perFil
|
|
|
606
1038
|
const normalizedQuestion = (0, plan_cache_1.normalizeIntent)(question);
|
|
607
1039
|
const findings = [];
|
|
608
1040
|
let answer = '';
|
|
609
|
-
const
|
|
1041
|
+
const asksCommandCatalog = isCommandCatalogIntent(normalizedQuestion);
|
|
610
1042
|
const asksInstall = /\b(install|upgrade|update|latest)\b/.test(normalizedQuestion);
|
|
611
1043
|
const asksFeatures = /\b(feature|features|capability|capabilities|offers?)\b/.test(normalizedQuestion);
|
|
1044
|
+
const asksSchema = /\b(field|fields|key|keys|schema|interface|input|output|parameter|parameters)\b/.test(normalizedQuestion);
|
|
612
1045
|
const asksTenancy = /\b(tenant|tenancy|single|multi|organization)\b/.test(normalizedQuestion);
|
|
1046
|
+
const asksLocation = /\b(where|which file|in which file|filepath|file path|location|defined|implemented|called|computed|resolved)\b/.test(normalizedQuestion);
|
|
1047
|
+
const asksHow = /\bhow\b/.test(normalizedQuestion);
|
|
1048
|
+
const asksDecision = /\b(decide|decision|verdict|pass|fail)\b/.test(normalizedQuestion);
|
|
1049
|
+
const asksCommandSurface = /\b(command|commands|subcommand|subcommands|flag|flags|option|options)\b/.test(normalizedQuestion);
|
|
1050
|
+
const asksSingleCommandLookup = /\b(what|which)\s+command\b/.test(normalizedQuestion) &&
|
|
1051
|
+
!/\b(commands|subcommands)\b/.test(normalizedQuestion);
|
|
1052
|
+
const asksOrgRequestInjection = /\b(inject|injected|header|request|requests)\b/.test(normalizedQuestion) &&
|
|
1053
|
+
/\b(org|organization)\b/.test(normalizedQuestion);
|
|
1054
|
+
const extractedCommandMatches = extractNeurcodeCommandsFromCitations(citations);
|
|
1055
|
+
const knownCommands = context.knownCommands || [];
|
|
1056
|
+
const knownCommandSet = new Set(knownCommands);
|
|
1057
|
+
const knownRoots = new Set(knownCommands.map((command) => command.split(/\s+/).slice(0, 2).join(' ')));
|
|
1058
|
+
const filteredExtractedMatches = knownCommands.length > 0
|
|
1059
|
+
? extractedCommandMatches.filter((command) => {
|
|
1060
|
+
if (knownCommandSet.has(command))
|
|
1061
|
+
return true;
|
|
1062
|
+
const root = command.split(/\s+/).slice(0, 2).join(' ');
|
|
1063
|
+
return knownRoots.has(root);
|
|
1064
|
+
})
|
|
1065
|
+
: extractedCommandMatches;
|
|
613
1066
|
const commandMatches = [
|
|
614
|
-
...
|
|
615
|
-
...
|
|
1067
|
+
...knownCommands,
|
|
1068
|
+
...filteredExtractedMatches,
|
|
616
1069
|
].filter((value, index, arr) => arr.indexOf(value) === index);
|
|
1070
|
+
const commandFocus = extractCommandFocus(normalizedQuestion);
|
|
617
1071
|
const installMatches = extractInstallCommandsFromCitations(citations);
|
|
618
1072
|
const insightLines = extractInsightLines(citations, 5);
|
|
1073
|
+
const highSignalAnswerTerms = terms.filter((term) => !LOW_SIGNAL_TERMS.has(term) && term.length >= 3).slice(0, 10);
|
|
1074
|
+
const anchorTerms = extractAnchorTerms(question);
|
|
1075
|
+
const queriedFlags = (question.match(/--[a-z0-9-]+/gi) || []).map((flag) => flag.toLowerCase());
|
|
1076
|
+
const pathAnchorTerms = (0, plan_cache_1.normalizeIntent)(question)
|
|
1077
|
+
.replace(/[^a-z0-9_\-\s]/g, ' ')
|
|
1078
|
+
.split(/\s+/)
|
|
1079
|
+
.map((token) => token.trim())
|
|
1080
|
+
.filter((token) => token.length >= 3 &&
|
|
1081
|
+
!STOP_WORDS.has(token) &&
|
|
1082
|
+
![
|
|
1083
|
+
'where', 'which', 'file', 'files', 'path', 'paths', 'location',
|
|
1084
|
+
'defined', 'implemented', 'called', 'computed', 'resolved',
|
|
1085
|
+
].includes(token))
|
|
1086
|
+
.slice(0, 10);
|
|
1087
|
+
const citationRelevanceScore = (citation) => {
|
|
1088
|
+
const snippetLower = (citation.snippet || '').toLowerCase();
|
|
1089
|
+
let score = countTermHitsInText(snippetLower, highSignalAnswerTerms);
|
|
1090
|
+
score += countTermHitsInText(citation.path.toLowerCase(), highSignalAnswerTerms) * 0.5;
|
|
1091
|
+
const pathAnchorHits = countTermHitsInText(citation.path.toLowerCase(), pathAnchorTerms);
|
|
1092
|
+
score += pathAnchorHits * 0.9;
|
|
1093
|
+
if (asksLocation && pathAnchorTerms.length > 0 && pathAnchorHits === 0) {
|
|
1094
|
+
score -= 0.35;
|
|
1095
|
+
}
|
|
1096
|
+
const anchorHits = countTermHitsInText(snippetLower, anchorTerms);
|
|
1097
|
+
score += anchorHits * 1.4;
|
|
1098
|
+
if ((asksLocation || asksHow) && anchorTerms.length > 0 && anchorHits === 0) {
|
|
1099
|
+
score -= 0.45;
|
|
1100
|
+
}
|
|
1101
|
+
if (queriedFlags.length > 0) {
|
|
1102
|
+
const flagHits = queriedFlags.filter((flag) => snippetLower.includes(flag)).length;
|
|
1103
|
+
score += flagHits * 2;
|
|
1104
|
+
if (asksLocation && flagHits === 0)
|
|
1105
|
+
score -= 0.4;
|
|
1106
|
+
if (/\.option\(/i.test(snippetLower) && flagHits > 0)
|
|
1107
|
+
score += 2.5;
|
|
1108
|
+
}
|
|
1109
|
+
if (asksCommandSurface) {
|
|
1110
|
+
if (/\.command\(|\bcommand\(/i.test(snippetLower))
|
|
1111
|
+
score += 2;
|
|
1112
|
+
if (/\.option\(|--[a-z0-9-]+/i.test(snippetLower))
|
|
1113
|
+
score += 1.4;
|
|
1114
|
+
if (citation.path.includes('/commands/') || citation.path.endsWith('/index.ts'))
|
|
1115
|
+
score += 0.6;
|
|
1116
|
+
}
|
|
1117
|
+
if (commandFocus) {
|
|
1118
|
+
if (citation.path.includes(`/commands/${commandFocus}.`)) {
|
|
1119
|
+
score += 2.1;
|
|
1120
|
+
}
|
|
1121
|
+
else if ((asksLocation || asksHow) && citation.path.includes('/commands/')) {
|
|
1122
|
+
score -= 1.0;
|
|
1123
|
+
}
|
|
1124
|
+
if (citation.path.endsWith('/index.ts') &&
|
|
1125
|
+
new RegExp(`\\.command\\('${escapeRegExp(commandFocus)}'\\)`, 'i').test(citation.snippet)) {
|
|
1126
|
+
score += 1.4;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
if ((asksLocation || asksHow) && looksLikeImportLine(citation.snippet)) {
|
|
1130
|
+
score -= 0.8;
|
|
1131
|
+
}
|
|
1132
|
+
if ((asksLocation || asksHow) && /^\s*[?:]?\s*['"`].+['"`][,;]?\s*$/i.test(citation.snippet)) {
|
|
1133
|
+
score -= 0.9;
|
|
1134
|
+
}
|
|
1135
|
+
if (asksLocation && /console\.log\(/i.test(citation.snippet)) {
|
|
1136
|
+
score -= 1.0;
|
|
1137
|
+
}
|
|
1138
|
+
if (asksDecision) {
|
|
1139
|
+
if (/\b(verdict|exitcode|policydecision)\b/i.test(snippetLower) || /\bif\s*\(|\?\s*'[^']+'\s*:\s*'[^']+'/i.test(citation.snippet)) {
|
|
1140
|
+
score += 1.1;
|
|
1141
|
+
}
|
|
1142
|
+
if (/(log|roi|estimat|time saved|badge|banner)/i.test(snippetLower)) {
|
|
1143
|
+
score -= 0.9;
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
return score;
|
|
1147
|
+
};
|
|
619
1148
|
const multiSignals = citations.filter((citation) => /\bmulti[- ]tenant|x-org-id|organization[_ -]?id|org-scoped\b/i.test(citation.snippet)).length;
|
|
620
1149
|
const singleSignals = citations.filter((citation) => /\bsingle[- ]tenant|single-user\b/i.test(citation.snippet)).length;
|
|
621
1150
|
if (mode === 'comparison' && terms.length >= 2) {
|
|
@@ -660,13 +1189,159 @@ function buildAnswer(mode, question, terms, citations, stats, termCounts, perFil
|
|
|
660
1189
|
const bullets = (context.featureBullets || []).slice(0, 6).map((line) => ` • ${line}`);
|
|
661
1190
|
answer = ['Here are the main platform features I could verify from the repo:', ...bullets].join('\n');
|
|
662
1191
|
}
|
|
663
|
-
else if (
|
|
664
|
-
const
|
|
1192
|
+
else if (asksSchema && citations.length > 0) {
|
|
1193
|
+
const lineCache = context.lineCache || new Map();
|
|
1194
|
+
const schemaFieldSummaries = buildSchemaFieldSummaries(citations, lineCache, terms);
|
|
1195
|
+
if (schemaFieldSummaries.length > 0) {
|
|
1196
|
+
const bullets = schemaFieldSummaries.map((line) => ` • ${line}`);
|
|
1197
|
+
answer = ['From the matched code, these fields/signatures are relevant:', ...bullets].join('\n');
|
|
1198
|
+
}
|
|
1199
|
+
else {
|
|
1200
|
+
const schemaLines = citations
|
|
1201
|
+
.map((citation) => formatInsightSnippet(citation.snippet))
|
|
1202
|
+
.filter((line) => /^export\s+interface\b/i.test(line) ||
|
|
1203
|
+
/^export\s+type\b/i.test(line) ||
|
|
1204
|
+
(/^[A-Za-z_][A-Za-z0-9_?]*\s*:\s*[^=,]+;?$/.test(line) &&
|
|
1205
|
+
!line.includes('{') &&
|
|
1206
|
+
!line.includes('=>')));
|
|
1207
|
+
const selected = [...new Set(schemaLines)].slice(0, 8);
|
|
1208
|
+
if (selected.length > 0) {
|
|
1209
|
+
const bullets = selected.map((line) => ` • ${line}`);
|
|
1210
|
+
answer = ['From the matched code, these fields/signatures are relevant:', ...bullets].join('\n');
|
|
1211
|
+
}
|
|
1212
|
+
else {
|
|
1213
|
+
answer = 'I found cache/schema-related code, but not enough direct field declarations in the top evidence.';
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
else if (asksSingleCommandLookup && commandMatches.length > 0) {
|
|
1218
|
+
const commandQueryTerms = [...new Set(tokenizeQuestion(question).flatMap((term) => {
|
|
1219
|
+
const out = [term.toLowerCase()];
|
|
1220
|
+
if (term.endsWith('s') && term.length > 4)
|
|
1221
|
+
out.push(term.slice(0, -1).toLowerCase());
|
|
1222
|
+
if (term.endsWith('ing') && term.length > 5)
|
|
1223
|
+
out.push(term.slice(0, -3).toLowerCase());
|
|
1224
|
+
if (term.endsWith('ed') && term.length > 4)
|
|
1225
|
+
out.push(term.slice(0, -2).toLowerCase());
|
|
1226
|
+
return out;
|
|
1227
|
+
}))];
|
|
1228
|
+
const scored = commandMatches
|
|
1229
|
+
.map((command) => {
|
|
1230
|
+
const normalized = command.toLowerCase();
|
|
1231
|
+
let score = countTermHitsInText(normalized, commandQueryTerms);
|
|
1232
|
+
if (/\bend(s|ed|ing)?\b/.test(normalizedQuestion) && /\bend\b/.test(normalized))
|
|
1233
|
+
score += 0.8;
|
|
1234
|
+
if (commandFocus &&
|
|
1235
|
+
(normalized === `neurcode ${commandFocus}` || normalized.startsWith(`neurcode ${commandFocus} `))) {
|
|
1236
|
+
score += 1.2;
|
|
1237
|
+
}
|
|
1238
|
+
return { command, score };
|
|
1239
|
+
})
|
|
1240
|
+
.sort((a, b) => b.score - a.score);
|
|
1241
|
+
const selected = (scored.filter((item) => item.score > 0).slice(0, 3).map((item) => item.command));
|
|
1242
|
+
const top = selected.length > 0 ? selected : [scored[0].command];
|
|
1243
|
+
answer = top.length === 1
|
|
1244
|
+
? `Use \`${top[0]}\`.`
|
|
1245
|
+
: ['Most relevant commands:', ...top.map((command) => ` • \`${command}\``)].join('\n');
|
|
1246
|
+
}
|
|
1247
|
+
else if (asksCommandCatalog && commandMatches.length > 0) {
|
|
1248
|
+
const scopedCommands = commandFocus
|
|
1249
|
+
? commandMatches.filter((command) => command === `neurcode ${commandFocus}` || command.startsWith(`neurcode ${commandFocus} `))
|
|
1250
|
+
: commandMatches;
|
|
1251
|
+
const normalizedCommands = (scopedCommands.length > 0 ? scopedCommands : commandMatches)
|
|
665
1252
|
.filter((command) => /^neurcode\s+[a-z]/.test(command))
|
|
666
1253
|
.slice(0, 22);
|
|
667
1254
|
const commandBullets = normalizedCommands.map((command) => ` • \`${command}\``);
|
|
668
1255
|
answer = ['Here are the CLI commands I could verify from the repo:', ...commandBullets].join('\n');
|
|
669
1256
|
}
|
|
1257
|
+
else if (asksLocation && citations.length > 0) {
|
|
1258
|
+
const focusedCitations = citations.filter((citation) => {
|
|
1259
|
+
const term = (citation.term || '').toLowerCase();
|
|
1260
|
+
return term.length === 0 || !GENERIC_OUTPUT_TERMS.has(term);
|
|
1261
|
+
});
|
|
1262
|
+
const locationPool = focusedCitations.length > 0 ? [...focusedCitations] : [...citations];
|
|
1263
|
+
locationPool.sort((a, b) => citationRelevanceScore(b) - citationRelevanceScore(a));
|
|
1264
|
+
if (asksOrgRequestInjection) {
|
|
1265
|
+
const score = (citation) => {
|
|
1266
|
+
const snippet = citation.snippet.toLowerCase();
|
|
1267
|
+
let value = 0;
|
|
1268
|
+
if (snippet.includes('x-org-id'))
|
|
1269
|
+
value += 4;
|
|
1270
|
+
if (snippet.includes('auto-inject') || snippet.includes('inject'))
|
|
1271
|
+
value += 2;
|
|
1272
|
+
if (snippet.includes('headers[') || snippet.includes('header'))
|
|
1273
|
+
value += 2;
|
|
1274
|
+
if (snippet.includes('request'))
|
|
1275
|
+
value += 1;
|
|
1276
|
+
if (citation.path.includes('api-client.ts'))
|
|
1277
|
+
value += 2;
|
|
1278
|
+
return value;
|
|
1279
|
+
};
|
|
1280
|
+
locationPool.sort((a, b) => score(b) - score(a));
|
|
1281
|
+
}
|
|
1282
|
+
const prefersImplementationLines = /\b(defined|implemented|called|computed|resolved|lookup)\b/.test(normalizedQuestion);
|
|
1283
|
+
const queriedFlags = (question.match(/--[a-z0-9-]+/gi) || []).map((flag) => flag.toLowerCase());
|
|
1284
|
+
const declarationLike = prefersImplementationLines
|
|
1285
|
+
? locationPool.filter((citation) => /\b(?:export\s+)?(?:function|const|let|var|class|interface|type)\b/i.test(citation.snippet))
|
|
1286
|
+
: [];
|
|
1287
|
+
const orderedLocationPool = declarationLike.length >= 2
|
|
1288
|
+
? [...declarationLike, ...locationPool.filter((citation) => !declarationLike.includes(citation))]
|
|
1289
|
+
: locationPool;
|
|
1290
|
+
const commandFocusedLocationPool = commandFocus
|
|
1291
|
+
? [...orderedLocationPool].sort((a, b) => {
|
|
1292
|
+
const score = (citation) => {
|
|
1293
|
+
let value = 0;
|
|
1294
|
+
if (citation.path.includes(`/commands/${commandFocus}.`))
|
|
1295
|
+
value += 3;
|
|
1296
|
+
if (citation.path.endsWith('/index.ts') &&
|
|
1297
|
+
new RegExp(`\\.command\\('${escapeRegExp(commandFocus)}'\\)`, 'i').test(citation.snippet)) {
|
|
1298
|
+
value += 2;
|
|
1299
|
+
}
|
|
1300
|
+
if (citation.path.includes('/commands/') && !citation.path.includes(`/commands/${commandFocus}.`)) {
|
|
1301
|
+
value -= 0.5;
|
|
1302
|
+
}
|
|
1303
|
+
if (queriedFlags.length > 0) {
|
|
1304
|
+
const snippetLower = citation.snippet.toLowerCase();
|
|
1305
|
+
const matchedFlagCount = queriedFlags.filter((flag) => snippetLower.includes(flag)).length;
|
|
1306
|
+
value += matchedFlagCount * 2;
|
|
1307
|
+
if (/\.option\(/i.test(citation.snippet) && matchedFlagCount > 0)
|
|
1308
|
+
value += 2;
|
|
1309
|
+
}
|
|
1310
|
+
return value;
|
|
1311
|
+
};
|
|
1312
|
+
return score(b) - score(a);
|
|
1313
|
+
})
|
|
1314
|
+
: orderedLocationPool;
|
|
1315
|
+
const locations = commandFocusedLocationPool
|
|
1316
|
+
.slice(0, 5)
|
|
1317
|
+
.map((citation) => ` • ${citation.path}:${citation.line} — ${formatInsightSnippet(citation.snippet)}`);
|
|
1318
|
+
answer = ['I found the relevant references here:', ...locations].join('\n');
|
|
1319
|
+
}
|
|
1320
|
+
else if (asksHow && insightLines.length > 0) {
|
|
1321
|
+
const focusedInsights = [...citations]
|
|
1322
|
+
.sort((a, b) => citationRelevanceScore(b) - citationRelevanceScore(a))
|
|
1323
|
+
.filter((citation) => citationRelevanceScore(citation) >= 1)
|
|
1324
|
+
.map((citation) => formatInsightSnippet(citation.snippet))
|
|
1325
|
+
.filter((line) => !/^const\s+[A-Za-z_$][\w$]*\s*=\s*\/.+\/[a-z]*\.test\(/i.test(line))
|
|
1326
|
+
.filter((line) => !/^[A-Za-z_$][\w$]*,\s*$/.test(line))
|
|
1327
|
+
.filter((line) => !/^['"`].+['"`],?\s*$/.test(line));
|
|
1328
|
+
const decisionFilteredInsights = asksDecision
|
|
1329
|
+
? focusedInsights.filter((line) => !/(log|roi|estimat|time saved|badge|banner|record[a-z]*event|telemetry|metric)/i.test(line))
|
|
1330
|
+
: focusedInsights;
|
|
1331
|
+
const decisionCoreInsights = asksDecision
|
|
1332
|
+
? decisionFilteredInsights.filter((line) => /\b(verdict|policydecision|exitcode|pass|fail|warn)\b/i.test(line) ||
|
|
1333
|
+
/\?\s*'[^']+'\s*:\s*'[^']+'/.test(line))
|
|
1334
|
+
: [];
|
|
1335
|
+
const selectedInsights = decisionCoreInsights.length > 0
|
|
1336
|
+
? [...new Set(decisionCoreInsights)].slice(0, 5)
|
|
1337
|
+
: decisionFilteredInsights.length > 0
|
|
1338
|
+
? [...new Set(decisionFilteredInsights)].slice(0, 5)
|
|
1339
|
+
: focusedInsights.length > 0
|
|
1340
|
+
? [...new Set(focusedInsights)].slice(0, 5)
|
|
1341
|
+
: insightLines;
|
|
1342
|
+
const bullets = selectedInsights.map((line) => ` • ${line}`);
|
|
1343
|
+
answer = ['From the matched code, this is how it works:', ...bullets].join('\n');
|
|
1344
|
+
}
|
|
670
1345
|
else if (asksTenancy && (multiSignals > 0 || singleSignals > 0)) {
|
|
671
1346
|
if (multiSignals >= singleSignals + 2) {
|
|
672
1347
|
answer = 'This codebase is multi-tenant, with organization-scoped flows (for example org IDs / `x-org-id` context).';
|
|
@@ -687,7 +1362,7 @@ function buildAnswer(mode, question, terms, citations, stats, termCounts, perFil
|
|
|
687
1362
|
else {
|
|
688
1363
|
answer = `I found grounded evidence in ${stats.matchedFiles} file(s), but it is mostly low-level implementation detail.`;
|
|
689
1364
|
}
|
|
690
|
-
if (
|
|
1365
|
+
if (asksCommandCatalog && commandMatches.length === 0 && citations.length > 0) {
|
|
691
1366
|
findings.push('Command-style question detected, but no command declarations were found in the matched lines.');
|
|
692
1367
|
}
|
|
693
1368
|
if (asksInstall && installMatches.length === 0 && !context.cliPackageName) {
|
|
@@ -798,6 +1473,25 @@ async function askCommand(question, options = {}) {
|
|
|
798
1473
|
const maxCitations = Math.max(3, Math.min(options.maxCitations || 12, 30));
|
|
799
1474
|
const shouldUseCache = options.cache !== false && process.env.NEURCODE_ASK_NO_CACHE !== '1';
|
|
800
1475
|
const normalizedQuestion = (0, plan_cache_1.normalizeIntent)(question);
|
|
1476
|
+
const deterministic = tryBuildDeterministicAnswer(cwd, question, normalizedQuestion);
|
|
1477
|
+
if (deterministic) {
|
|
1478
|
+
if (!options.json) {
|
|
1479
|
+
console.log(chalk.dim(`🧠 Asking repo context in ${cwd}...`));
|
|
1480
|
+
}
|
|
1481
|
+
emitAskResult(deterministic.payload, {
|
|
1482
|
+
json: options.json,
|
|
1483
|
+
maxCitations,
|
|
1484
|
+
fromPlan: options.fromPlan,
|
|
1485
|
+
verbose: options.verbose,
|
|
1486
|
+
});
|
|
1487
|
+
if (orgId && projectId) {
|
|
1488
|
+
(0, brain_context_1.recordBrainProgressEvent)(cwd, scope, {
|
|
1489
|
+
type: 'ask',
|
|
1490
|
+
note: `mode=deterministic;reason=${deterministic.reason};truth=${deterministic.payload.truth.status};score=${deterministic.payload.truth.score.toFixed(2)}`,
|
|
1491
|
+
});
|
|
1492
|
+
}
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
801
1495
|
if (process.stdout.isTTY && !process.env.CI) {
|
|
802
1496
|
(0, neurcode_context_1.ensureDefaultLocalContextFile)(cwd);
|
|
803
1497
|
}
|
|
@@ -907,6 +1601,7 @@ async function askCommand(question, options = {}) {
|
|
|
907
1601
|
const { mode, terms, matchers } = buildMatchers(question);
|
|
908
1602
|
const pathHints = derivePathHints(question);
|
|
909
1603
|
const mentionsAskCommand = /\bask\b/.test(normalizedQuestion);
|
|
1604
|
+
const asksQuestionAnsweringInternals = /\b(question|questions|answer|answers|cache|brain|citation|grounded)\b/.test(normalizedQuestion);
|
|
910
1605
|
if (matchers.length === 0) {
|
|
911
1606
|
console.error(chalk.red('❌ Could not derive useful search terms from the question.'));
|
|
912
1607
|
process.exit(1);
|
|
@@ -919,6 +1614,8 @@ async function askCommand(question, options = {}) {
|
|
|
919
1614
|
}
|
|
920
1615
|
addAnchorCandidates(fileTree, candidateSet, pathPriority, normalizedQuestion);
|
|
921
1616
|
const tokenHints = tokenizeQuestion(question);
|
|
1617
|
+
const codeFocusedQuestion = /\b(how|where|which file|defined|implemented|called|computed|resolved|function|class|api|command|flow|trace|why|field|fields|key|keys|schema|interface|parameter|parameters)\b/.test(normalizedQuestion) &&
|
|
1618
|
+
!/\b(feature|features|install|setup|readme|docs|documentation)\b/.test(normalizedQuestion);
|
|
922
1619
|
if (candidateSet.size < 80) {
|
|
923
1620
|
for (const filePath of fileTree) {
|
|
924
1621
|
const normalizedPath = filePath.toLowerCase();
|
|
@@ -933,7 +1630,12 @@ async function askCommand(question, options = {}) {
|
|
|
933
1630
|
if (normalizedPath.startsWith('scripts/')) {
|
|
934
1631
|
score -= 0.1;
|
|
935
1632
|
}
|
|
936
|
-
if (
|
|
1633
|
+
if (codeFocusedQuestion && (filePath === 'README.md' || normalizedPath.startsWith('docs/'))) {
|
|
1634
|
+
score -= 0.35;
|
|
1635
|
+
}
|
|
1636
|
+
if (!mentionsAskCommand &&
|
|
1637
|
+
!asksQuestionAnsweringInternals &&
|
|
1638
|
+
(filePath.endsWith('/commands/ask.ts') || filePath.endsWith('/utils/ask-cache.ts'))) {
|
|
937
1639
|
score -= 0.45;
|
|
938
1640
|
}
|
|
939
1641
|
if (pathHints.some((hint) => normalizedPath.startsWith(hint))) {
|
|
@@ -968,11 +1670,15 @@ async function askCommand(question, options = {}) {
|
|
|
968
1670
|
? prioritized.filter((filePath) => !pathHints.some((hint) => filePath.startsWith(hint)))
|
|
969
1671
|
: prioritized;
|
|
970
1672
|
const candidates = [...hinted, ...nonHinted].slice(0, MAX_SCAN_FILES);
|
|
971
|
-
const
|
|
1673
|
+
const querySignals = deriveQuerySignals(question, normalizedQuestion, terms);
|
|
1674
|
+
const commandFocusQuery = extractCommandFocus(normalizedQuestion);
|
|
1675
|
+
const anchorTerms = extractAnchorTerms(question);
|
|
1676
|
+
const hasTemporalIntent = /\b(before|after|during|when)\b/.test(normalizedQuestion);
|
|
1677
|
+
const highSignalSet = new Set(querySignals.highSignalTerms);
|
|
1678
|
+
const rawFileMatches = new Map();
|
|
1679
|
+
const termFileHits = new Map();
|
|
972
1680
|
let scannedFiles = 0;
|
|
973
1681
|
for (const filePath of candidates) {
|
|
974
|
-
if (citations.length >= MAX_RAW_CITATIONS)
|
|
975
|
-
break;
|
|
976
1682
|
const fullPath = (0, path_1.join)(cwd, filePath);
|
|
977
1683
|
let content = '';
|
|
978
1684
|
try {
|
|
@@ -986,34 +1692,165 @@ async function askCommand(question, options = {}) {
|
|
|
986
1692
|
}
|
|
987
1693
|
scannedFiles++;
|
|
988
1694
|
const lines = content.split(/\r?\n/);
|
|
1695
|
+
const lineTerms = new Map();
|
|
1696
|
+
const matchedTerms = new Set();
|
|
989
1697
|
for (let idx = 0; idx < lines.length; idx++) {
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
const line = lines[idx];
|
|
993
|
-
if (!line || line.trim().length === 0)
|
|
1698
|
+
const rawLine = lines[idx];
|
|
1699
|
+
if (!rawLine || rawLine.trim().length === 0)
|
|
994
1700
|
continue;
|
|
995
1701
|
for (const matcher of matchers) {
|
|
996
|
-
if (!matcher.regex.test(
|
|
997
|
-
continue;
|
|
998
|
-
const snippet = normalizeSnippet(line);
|
|
999
|
-
if (!snippet)
|
|
1702
|
+
if (!matcher.regex.test(rawLine))
|
|
1000
1703
|
continue;
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1704
|
+
if (!lineTerms.has(idx))
|
|
1705
|
+
lineTerms.set(idx, new Set());
|
|
1706
|
+
lineTerms.get(idx).add(matcher.label);
|
|
1707
|
+
matchedTerms.add(matcher.label);
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
if (lineTerms.size === 0)
|
|
1711
|
+
continue;
|
|
1712
|
+
rawFileMatches.set(filePath, {
|
|
1713
|
+
lines,
|
|
1714
|
+
lineTerms,
|
|
1715
|
+
pathScore: pathPriority.get(filePath) || 0,
|
|
1716
|
+
hintBoost: pathHints.some((hint) => filePath.startsWith(hint)) ? 0.25 : 0,
|
|
1717
|
+
});
|
|
1718
|
+
for (const term of matchedTerms) {
|
|
1719
|
+
termFileHits.set(term, (termFileHits.get(term) || 0) + 1);
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
const matchedFileTotal = Math.max(rawFileMatches.size, 1);
|
|
1723
|
+
const termWeight = new Map();
|
|
1724
|
+
for (const term of terms) {
|
|
1725
|
+
const docFreq = termFileHits.get(term) || 0;
|
|
1726
|
+
const idfWeight = Math.log((matchedFileTotal + 1) / (docFreq + 1)) + 0.25;
|
|
1727
|
+
const highSignalBoost = highSignalSet.has(term) ? 0.35 : 0;
|
|
1728
|
+
termWeight.set(term, Math.max(0.08, idfWeight + highSignalBoost));
|
|
1729
|
+
}
|
|
1730
|
+
const citations = [];
|
|
1731
|
+
for (const [filePath, fileData] of rawFileMatches.entries()) {
|
|
1732
|
+
const { lines, lineTerms, pathScore, hintBoost } = fileData;
|
|
1733
|
+
for (const [lineIdx, directTerms] of lineTerms.entries()) {
|
|
1734
|
+
if (citations.length >= MAX_RAW_CITATIONS)
|
|
1735
|
+
break;
|
|
1736
|
+
const rawLine = lines[lineIdx] || '';
|
|
1737
|
+
const snippet = normalizeSnippet(rawLine);
|
|
1738
|
+
if (!snippet)
|
|
1739
|
+
continue;
|
|
1740
|
+
const windowTerms = new Set();
|
|
1741
|
+
const contextRadius = querySignals.asksLocation || querySignals.asksDefinition ? 6 : 2;
|
|
1742
|
+
for (let i = Math.max(0, lineIdx - contextRadius); i <= Math.min(lines.length - 1, lineIdx + contextRadius); i++) {
|
|
1743
|
+
const termsAtLine = lineTerms.get(i);
|
|
1744
|
+
if (!termsAtLine)
|
|
1012
1745
|
continue;
|
|
1746
|
+
for (const term of termsAtLine)
|
|
1747
|
+
windowTerms.add(term);
|
|
1748
|
+
}
|
|
1749
|
+
const highSignalWindow = [...windowTerms].filter((term) => highSignalSet.has(term));
|
|
1750
|
+
let score = pathScore + hintBoost;
|
|
1751
|
+
for (const term of windowTerms) {
|
|
1752
|
+
score += termWeight.get(term) || 0.1;
|
|
1753
|
+
}
|
|
1754
|
+
score += highSignalWindow.length * 0.7;
|
|
1755
|
+
score += windowTerms.size * 0.12;
|
|
1756
|
+
if (querySignals.highSignalTerms.length > 0 && highSignalWindow.length === 0) {
|
|
1757
|
+
score -= 0.65;
|
|
1758
|
+
}
|
|
1759
|
+
if ((querySignals.asksLocation || querySignals.asksDefinition) && querySignals.highSignalTerms.length >= 2 && highSignalWindow.length < 2) {
|
|
1760
|
+
score -= 0.9;
|
|
1761
|
+
}
|
|
1762
|
+
const anchorHits = countTermHitsInText(rawLine.toLowerCase(), anchorTerms);
|
|
1763
|
+
score += anchorHits * 1.2;
|
|
1764
|
+
if ((querySignals.asksLocation || querySignals.asksDefinition) && anchorTerms.length > 0 && anchorHits === 0) {
|
|
1765
|
+
score -= 0.6;
|
|
1766
|
+
}
|
|
1767
|
+
if (querySignals.asksLocation || querySignals.asksDefinition) {
|
|
1768
|
+
if (looksLikeImportLine(rawLine))
|
|
1769
|
+
score -= 0.75;
|
|
1770
|
+
if (/\b(?:export\s+)?(?:function|const|let|var|class|interface|type)\b/.test(rawLine) &&
|
|
1771
|
+
countTermHitsInText(rawLine.toLowerCase(), querySignals.highSignalTerms) >= 1) {
|
|
1772
|
+
score += 0.9;
|
|
1013
1773
|
}
|
|
1014
|
-
|
|
1015
|
-
|
|
1774
|
+
if (querySignals.asksDefinition && /console\.log\(/.test(rawLine)) {
|
|
1775
|
+
score -= 0.6;
|
|
1776
|
+
}
|
|
1777
|
+
for (const identifier of querySignals.identifiers) {
|
|
1778
|
+
const escaped = escapeRegExp(identifier);
|
|
1779
|
+
if (new RegExp(`\\b${escaped}\\s*\\(`, 'i').test(rawLine)) {
|
|
1780
|
+
score += 0.95;
|
|
1781
|
+
}
|
|
1782
|
+
if (new RegExp(`\\b(?:function|const|let|var|class|interface|type)\\s+${escaped}\\b`, 'i').test(rawLine)) {
|
|
1783
|
+
score += 0.8;
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
if (hasTemporalIntent) {
|
|
1787
|
+
if (/\b[A-Za-z_][A-Za-z0-9_]*\s*\(/.test(rawLine))
|
|
1788
|
+
score += 0.75;
|
|
1789
|
+
if (/^\s*export\s+(interface|type)\b/.test(rawLine))
|
|
1790
|
+
score -= 0.9;
|
|
1791
|
+
if (/^\s*const\s+[A-Z0-9_]+\s*=/.test(rawLine))
|
|
1792
|
+
score -= 0.7;
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
if (querySignals.asksHow && /\b(if|else|return|await|for|while|switch)\b/.test(rawLine)) {
|
|
1796
|
+
score += 0.2;
|
|
1797
|
+
}
|
|
1798
|
+
if (querySignals.asksHow) {
|
|
1799
|
+
const trimmed = rawLine.trim();
|
|
1800
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('*'))
|
|
1801
|
+
score -= 0.45;
|
|
1802
|
+
if (/\.description\(/.test(rawLine))
|
|
1803
|
+
score -= 0.4;
|
|
1804
|
+
}
|
|
1805
|
+
if (querySignals.asksSchema) {
|
|
1806
|
+
if (/^\s*export\s+interface\s+/i.test(rawLine) || /^\s*export\s+type\s+/i.test(rawLine))
|
|
1807
|
+
score += 1.1;
|
|
1808
|
+
if (/^\s*[A-Za-z_][A-Za-z0-9_?]*\s*:\s*/.test(rawLine))
|
|
1809
|
+
score += 0.8;
|
|
1810
|
+
if (filePath.endsWith('.md'))
|
|
1811
|
+
score -= 0.5;
|
|
1812
|
+
}
|
|
1813
|
+
if (rawLine.length > 180 && rawLine.includes(',') && /['"`].*['"`].*['"`]/.test(rawLine)) {
|
|
1814
|
+
score -= 0.35;
|
|
1815
|
+
}
|
|
1816
|
+
if ((querySignals.asksLocation || querySignals.asksDefinition) && /Usage:\s*neurcode/i.test(rawLine)) {
|
|
1817
|
+
score -= 0.6;
|
|
1818
|
+
}
|
|
1819
|
+
if ((querySignals.asksLocation || querySignals.asksDefinition) && /^\s*type:\s*['"`][a-z0-9_-]+['"`],?\s*$/i.test(rawLine)) {
|
|
1820
|
+
score -= 0.7;
|
|
1821
|
+
}
|
|
1822
|
+
if (querySignals.asksCommandSurface) {
|
|
1823
|
+
if (/\.command\(|\bcommand\(/i.test(rawLine))
|
|
1824
|
+
score += 1.2;
|
|
1825
|
+
if (/\.option\(|--[a-z0-9-]+/i.test(rawLine))
|
|
1826
|
+
score += 0.8;
|
|
1827
|
+
if (filePath.endsWith('/index.ts') || filePath.includes('/commands/'))
|
|
1828
|
+
score += 0.35;
|
|
1016
1829
|
}
|
|
1830
|
+
if (commandFocusQuery) {
|
|
1831
|
+
if (filePath.includes(`/commands/${commandFocusQuery}.`)) {
|
|
1832
|
+
score += 1.4;
|
|
1833
|
+
}
|
|
1834
|
+
else if ((querySignals.asksHow || querySignals.asksLocation) && filePath.includes('/commands/')) {
|
|
1835
|
+
score -= 1.2;
|
|
1836
|
+
}
|
|
1837
|
+
if (filePath.endsWith('/index.ts') &&
|
|
1838
|
+
new RegExp(`\\.command\\('${escapeRegExp(commandFocusQuery)}'\\)`, 'i').test(rawLine)) {
|
|
1839
|
+
score += 1.1;
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
const dominantTerm = [...directTerms].sort((a, b) => (termWeight.get(b) || 0) - (termWeight.get(a) || 0))[0] ||
|
|
1843
|
+
[...windowTerms].sort((a, b) => (termWeight.get(b) || 0) - (termWeight.get(a) || 0))[0] ||
|
|
1844
|
+
'';
|
|
1845
|
+
if (score <= 0)
|
|
1846
|
+
continue;
|
|
1847
|
+
citations.push({
|
|
1848
|
+
path: filePath,
|
|
1849
|
+
line: lineIdx + 1,
|
|
1850
|
+
snippet,
|
|
1851
|
+
term: dominantTerm,
|
|
1852
|
+
weight: score,
|
|
1853
|
+
});
|
|
1017
1854
|
}
|
|
1018
1855
|
}
|
|
1019
1856
|
citations.sort((a, b) => b.weight - a.weight);
|
|
@@ -1027,21 +1864,100 @@ async function askCommand(question, options = {}) {
|
|
|
1027
1864
|
}
|
|
1028
1865
|
}
|
|
1029
1866
|
const selectedForOutput = [];
|
|
1867
|
+
const selectedFileCounts = new Map();
|
|
1868
|
+
const pushSelected = (citation) => {
|
|
1869
|
+
if (selectedForOutput.length >= maxCitations)
|
|
1870
|
+
return false;
|
|
1871
|
+
if (selectedForOutput.some((existing) => existing.path === citation.path && existing.line === citation.line && existing.term === citation.term)) {
|
|
1872
|
+
return false;
|
|
1873
|
+
}
|
|
1874
|
+
if (querySignals.asksLocation || querySignals.asksDefinition) {
|
|
1875
|
+
const perFile = selectedFileCounts.get(citation.path) || 0;
|
|
1876
|
+
const distinctFiles = selectedFileCounts.size;
|
|
1877
|
+
const targetDistinct = Math.min(3, sourcePerFileCounts.size);
|
|
1878
|
+
if (perFile >= 3 && distinctFiles < targetDistinct) {
|
|
1879
|
+
return false;
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
selectedForOutput.push(citation);
|
|
1883
|
+
selectedFileCounts.set(citation.path, (selectedFileCounts.get(citation.path) || 0) + 1);
|
|
1884
|
+
return true;
|
|
1885
|
+
};
|
|
1886
|
+
if (mode === 'search' && querySignals.asksSchema) {
|
|
1887
|
+
const schemaAffinity = (citation) => {
|
|
1888
|
+
const text = citation.snippet.toLowerCase();
|
|
1889
|
+
let affinity = 0;
|
|
1890
|
+
for (const term of querySignals.highSignalTerms) {
|
|
1891
|
+
const normalized = term.toLowerCase();
|
|
1892
|
+
const compact = normalized.replace(/\s+/g, '');
|
|
1893
|
+
if (text.includes(normalized))
|
|
1894
|
+
affinity += 1;
|
|
1895
|
+
else if (text.includes(compact))
|
|
1896
|
+
affinity += 0.85;
|
|
1897
|
+
}
|
|
1898
|
+
return affinity;
|
|
1899
|
+
};
|
|
1900
|
+
const schemaPreferred = sourceEvidence
|
|
1901
|
+
.filter((citation) => /^export\s+interface\b/i.test(citation.snippet) ||
|
|
1902
|
+
/^export\s+type\b/i.test(citation.snippet) ||
|
|
1903
|
+
/^[A-Za-z_][A-Za-z0-9_?]*\s*:\s*[^=,]+;?$/.test(citation.snippet))
|
|
1904
|
+
.sort((a, b) => {
|
|
1905
|
+
const affinityDiff = schemaAffinity(b) - schemaAffinity(a);
|
|
1906
|
+
if (affinityDiff !== 0)
|
|
1907
|
+
return affinityDiff;
|
|
1908
|
+
return b.weight - a.weight;
|
|
1909
|
+
});
|
|
1910
|
+
for (const citation of schemaPreferred) {
|
|
1911
|
+
if (selectedForOutput.length >= maxCitations)
|
|
1912
|
+
break;
|
|
1913
|
+
pushSelected(citation);
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1030
1916
|
if (mode === 'comparison' && terms.length >= 2) {
|
|
1031
1917
|
for (const term of terms.slice(0, 2)) {
|
|
1032
1918
|
const firstForTerm = sourceEvidence.find((citation) => citation.term === term);
|
|
1033
1919
|
if (firstForTerm) {
|
|
1034
|
-
|
|
1920
|
+
pushSelected(firstForTerm);
|
|
1035
1921
|
}
|
|
1036
1922
|
}
|
|
1037
1923
|
}
|
|
1924
|
+
else if (mode === 'search' && terms.length > 0) {
|
|
1925
|
+
const discriminativeHighSignalTerms = querySignals.highSignalTerms.filter((term) => {
|
|
1926
|
+
const docFreq = termFileHits.get(term) || 0;
|
|
1927
|
+
return docFreq / matchedFileTotal <= 0.65;
|
|
1928
|
+
});
|
|
1929
|
+
const preferredTerms = discriminativeHighSignalTerms.length > 0
|
|
1930
|
+
? discriminativeHighSignalTerms.slice(0, 8)
|
|
1931
|
+
: querySignals.highSignalTerms.length > 0
|
|
1932
|
+
? querySignals.highSignalTerms.slice(0, 8)
|
|
1933
|
+
: terms
|
|
1934
|
+
.filter((term) => !LOW_SIGNAL_TERMS.has(term) && term.length >= 4)
|
|
1935
|
+
.slice(0, 8);
|
|
1936
|
+
for (const term of preferredTerms) {
|
|
1937
|
+
const firstForTerm = sourceEvidence.find((citation) => {
|
|
1938
|
+
if (citation.term !== term)
|
|
1939
|
+
return false;
|
|
1940
|
+
const normalized = (citation.term || '').toLowerCase();
|
|
1941
|
+
return !GENERIC_OUTPUT_TERMS.has(normalized);
|
|
1942
|
+
});
|
|
1943
|
+
if (!firstForTerm)
|
|
1944
|
+
continue;
|
|
1945
|
+
pushSelected(firstForTerm);
|
|
1946
|
+
if (selectedForOutput.length >= maxCitations)
|
|
1947
|
+
break;
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1038
1950
|
for (const citation of sourceEvidence) {
|
|
1039
1951
|
if (selectedForOutput.length >= maxCitations)
|
|
1040
1952
|
break;
|
|
1041
|
-
if (
|
|
1953
|
+
if (commandFocusQuery &&
|
|
1954
|
+
(querySignals.asksLocation || querySignals.asksHow) &&
|
|
1955
|
+
selectedForOutput.some((item) => item.path.includes(`/commands/${commandFocusQuery}.`)) &&
|
|
1956
|
+
citation.path.includes('/commands/') &&
|
|
1957
|
+
!citation.path.includes(`/commands/${commandFocusQuery}.`)) {
|
|
1042
1958
|
continue;
|
|
1043
1959
|
}
|
|
1044
|
-
|
|
1960
|
+
pushSelected(citation);
|
|
1045
1961
|
}
|
|
1046
1962
|
const finalCitations = selectedForOutput.map(({ path, line, snippet, term }) => ({
|
|
1047
1963
|
path,
|
|
@@ -1055,11 +1971,16 @@ async function askCommand(question, options = {}) {
|
|
|
1055
1971
|
matchedLines: sourceEvidence.length,
|
|
1056
1972
|
brainCandidates: brainResults.entries.length,
|
|
1057
1973
|
};
|
|
1974
|
+
const lineCacheForAnswer = new Map();
|
|
1975
|
+
for (const [filePath, data] of rawFileMatches.entries()) {
|
|
1976
|
+
lineCacheForAnswer.set(filePath, data.lines);
|
|
1977
|
+
}
|
|
1058
1978
|
const truth = evaluateTruthAssessment(mode, normalizedQuestion, terms, sourceEvidence, sourcePerFileCounts, sourceTermCounts);
|
|
1059
1979
|
const answer = buildAnswer(mode, question, terms, finalCitations, stats, sourceTermCounts, sourcePerFileCounts, truth, {
|
|
1060
1980
|
cliPackageName,
|
|
1061
1981
|
knownCommands: knownCliCommands,
|
|
1062
1982
|
featureBullets,
|
|
1983
|
+
lineCache: lineCacheForAnswer,
|
|
1063
1984
|
});
|
|
1064
1985
|
emitAskResult(answer, {
|
|
1065
1986
|
json: options.json,
|