@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.
@@ -25,7 +25,12 @@ catch {
25
25
  white: (str) => str,
26
26
  };
27
27
  }
28
- const MAX_SCAN_FILES = 320;
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
- return raw
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 asksCommands = /\b(cli|command|cmd|subcommand|terminal)\b/.test(normalizedQuestion);
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
- ...extractNeurcodeCommandsFromCitations(citations),
615
- ...(context.knownCommands || []),
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 (asksCommands && commandMatches.length > 0) {
664
- const normalizedCommands = commandMatches
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 (asksCommands && commandMatches.length === 0 && citations.length > 0) {
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 (!mentionsAskCommand && (filePath.endsWith('/commands/ask.ts') || filePath.endsWith('/utils/ask-cache.ts'))) {
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 citations = [];
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
- if (citations.length >= MAX_RAW_CITATIONS)
991
- break;
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(line))
997
- continue;
998
- const snippet = normalizeSnippet(line);
999
- if (!snippet)
1702
+ if (!matcher.regex.test(rawLine))
1000
1703
  continue;
1001
- const evidence = {
1002
- path: filePath,
1003
- line: idx + 1,
1004
- snippet,
1005
- term: matcher.label,
1006
- weight: matcher.weight +
1007
- (pathPriority.get(filePath) || 0) +
1008
- (pathHints.some((hint) => filePath.startsWith(hint)) ? 0.25 : 0),
1009
- };
1010
- const dedupeKey = `${evidence.path}:${evidence.line}:${evidence.term || ''}`;
1011
- if (citations.some((item) => `${item.path}:${item.line}:${item.term || ''}` === dedupeKey)) {
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
- citations.push(evidence);
1015
- break;
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
- selectedForOutput.push(firstForTerm);
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 (selectedForOutput.some((existing) => existing.path === citation.path && existing.line === citation.line && existing.term === citation.term)) {
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
- selectedForOutput.push(citation);
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,