@neurcode-ai/cli 0.9.23 → 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,9 +50,29 @@ 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', 'file', 'files', 'path', 'filepath', 'header', 'added', 'add', 'request', 'requests',
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',
49
57
  ]);
50
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',
75
+ ]);
51
76
  function scanFiles(dir, maxFiles = MAX_SCAN_FILES) {
52
77
  const files = [];
53
78
  const ignoreDirs = new Set([
@@ -111,12 +136,19 @@ function escapeRegExp(input) {
111
136
  return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
112
137
  }
113
138
  function normalizeTerm(raw) {
114
- return raw
139
+ const normalized = raw
115
140
  .toLowerCase()
116
141
  .replace(/['"`]/g, '')
117
142
  .replace(/[^a-z0-9_\-\s]/g, ' ')
118
143
  .replace(/\s+/g, ' ')
119
144
  .trim();
145
+ if (!normalized)
146
+ return '';
147
+ return normalized
148
+ .split(' ')
149
+ .map((token) => token.replace(/^[-_]+|[-_]+$/g, ''))
150
+ .filter(Boolean)
151
+ .join(' ');
120
152
  }
121
153
  function extractQuotedTerms(question) {
122
154
  const seen = new Set();
@@ -163,6 +195,25 @@ function extractComparisonTerms(question) {
163
195
  return identities.slice(0, 2);
164
196
  return [];
165
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
+ }
166
217
  function buildTermMatchers(term, weight) {
167
218
  const normalized = normalizeTerm(term);
168
219
  if (!normalized)
@@ -179,6 +230,10 @@ function buildTermMatchers(term, weight) {
179
230
  if (tokens.length === 1) {
180
231
  const token = escapeRegExp(tokens[0]);
181
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
+ }
182
237
  return out;
183
238
  }
184
239
  const tokenChain = tokens.map((t) => escapeRegExp(t)).join('[\\s_-]*');
@@ -213,6 +268,20 @@ function expandSearchTerms(terms) {
213
268
  if (term.endsWith('ed') && term.length > 4) {
214
269
  expanded.add(term.slice(0, -2));
215
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
+ }
216
285
  }
217
286
  return [...expanded];
218
287
  }
@@ -231,9 +300,10 @@ function buildMatchers(question) {
231
300
  }
232
301
  const quoted = extractQuotedTerms(question);
233
302
  const identityTerms = extractIdentityTerms(question);
303
+ const phraseTerms = extractPhraseTerms(question);
234
304
  const keywords = tokenizeQuestion(question).slice(0, 8);
235
305
  const quotedSet = new Set(quoted.map((term) => normalizeTerm(term)));
236
- const baseTerms = [...new Set([...quoted, ...identityTerms, ...keywords].map(normalizeTerm).filter(Boolean))];
306
+ const baseTerms = [...new Set([...quoted, ...phraseTerms, ...identityTerms, ...keywords].map(normalizeTerm).filter(Boolean))];
237
307
  const filteredTerms = baseTerms.filter((term) => quotedSet.has(term) || !LOW_SIGNAL_TERMS.has(term));
238
308
  const terms = expandSearchTerms(filteredTerms.length > 0 ? filteredTerms : baseTerms).filter(Boolean);
239
309
  const matchers = terms.flatMap((term) => buildTermMatchers(term, quoted.includes(term) ? 0.9 : 0.55));
@@ -284,6 +354,149 @@ function derivePathHints(question) {
284
354
  }
285
355
  return [...new Set(hints)];
286
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
+ }
287
500
  function normalizeSnippet(line) {
288
501
  return line
289
502
  .replace(/\t/g, ' ')
@@ -444,6 +657,34 @@ function extractNeurcodeCommandsFromCitations(citations) {
444
657
  }
445
658
  return [...commandSet].slice(0, 20);
446
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
+ }
447
688
  function extractInstallCommandsFromCitations(citations) {
448
689
  const installSet = new Set();
449
690
  for (const citation of citations) {
@@ -466,6 +707,162 @@ function formatInsightSnippet(snippet) {
466
707
  .trim()
467
708
  .slice(0, 180);
468
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
+ }
469
866
  function extractInsightLines(citations, limit) {
470
867
  const out = [];
471
868
  const seen = new Set();
@@ -498,8 +895,8 @@ function isCommandCatalogIntent(normalizedQuestion) {
498
895
  /\bshow\b/.test(normalizedQuestion) ||
499
896
  /\bavailable\b/.test(normalizedQuestion) ||
500
897
  /\ball commands?\b/.test(normalizedQuestion) ||
501
- /\bwhich commands?\b/.test(normalizedQuestion) ||
502
- /\bwhat commands?\b/.test(normalizedQuestion) ||
898
+ /\bwhich commands\b/.test(normalizedQuestion) ||
899
+ /\bwhat commands\b/.test(normalizedQuestion) ||
503
900
  /\bwhat can i (type|run)\b/.test(normalizedQuestion) ||
504
901
  /\bcan i type\b/.test(normalizedQuestion) ||
505
902
  /\bcmds\b/.test(normalizedQuestion);
@@ -644,17 +1041,110 @@ function buildAnswer(mode, question, terms, citations, stats, termCounts, perFil
644
1041
  const asksCommandCatalog = isCommandCatalogIntent(normalizedQuestion);
645
1042
  const asksInstall = /\b(install|upgrade|update|latest)\b/.test(normalizedQuestion);
646
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);
647
1045
  const asksTenancy = /\b(tenant|tenancy|single|multi|organization)\b/.test(normalizedQuestion);
648
- const asksLocation = /\b(where|which file|in which file|filepath|file path|location)\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);
649
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);
650
1052
  const asksOrgRequestInjection = /\b(inject|injected|header|request|requests)\b/.test(normalizedQuestion) &&
651
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;
652
1066
  const commandMatches = [
653
- ...extractNeurcodeCommandsFromCitations(citations),
654
- ...(context.knownCommands || []),
1067
+ ...knownCommands,
1068
+ ...filteredExtractedMatches,
655
1069
  ].filter((value, index, arr) => arr.indexOf(value) === index);
1070
+ const commandFocus = extractCommandFocus(normalizedQuestion);
656
1071
  const installMatches = extractInstallCommandsFromCitations(citations);
657
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
+ };
658
1148
  const multiSignals = citations.filter((citation) => /\bmulti[- ]tenant|x-org-id|organization[_ -]?id|org-scoped\b/i.test(citation.snippet)).length;
659
1149
  const singleSignals = citations.filter((citation) => /\bsingle[- ]tenant|single-user\b/i.test(citation.snippet)).length;
660
1150
  if (mode === 'comparison' && terms.length >= 2) {
@@ -699,8 +1189,66 @@ function buildAnswer(mode, question, terms, citations, stats, termCounts, perFil
699
1189
  const bullets = (context.featureBullets || []).slice(0, 6).map((line) => ` • ${line}`);
700
1190
  answer = ['Here are the main platform features I could verify from the repo:', ...bullets].join('\n');
701
1191
  }
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
+ }
702
1247
  else if (asksCommandCatalog && commandMatches.length > 0) {
703
- const normalizedCommands = commandMatches
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)
704
1252
  .filter((command) => /^neurcode\s+[a-z]/.test(command))
705
1253
  .slice(0, 22);
706
1254
  const commandBullets = normalizedCommands.map((command) => ` • \`${command}\``);
@@ -712,6 +1260,7 @@ function buildAnswer(mode, question, terms, citations, stats, termCounts, perFil
712
1260
  return term.length === 0 || !GENERIC_OUTPUT_TERMS.has(term);
713
1261
  });
714
1262
  const locationPool = focusedCitations.length > 0 ? [...focusedCitations] : [...citations];
1263
+ locationPool.sort((a, b) => citationRelevanceScore(b) - citationRelevanceScore(a));
715
1264
  if (asksOrgRequestInjection) {
716
1265
  const score = (citation) => {
717
1266
  const snippet = citation.snippet.toLowerCase();
@@ -730,13 +1279,67 @@ function buildAnswer(mode, question, terms, citations, stats, termCounts, perFil
730
1279
  };
731
1280
  locationPool.sort((a, b) => score(b) - score(a));
732
1281
  }
733
- const locations = locationPool
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
734
1316
  .slice(0, 5)
735
1317
  .map((citation) => ` • ${citation.path}:${citation.line} — ${formatInsightSnippet(citation.snippet)}`);
736
1318
  answer = ['I found the relevant references here:', ...locations].join('\n');
737
1319
  }
738
1320
  else if (asksHow && insightLines.length > 0) {
739
- const bullets = insightLines.map((line) => ` • ${line}`);
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}`);
740
1343
  answer = ['From the matched code, this is how it works:', ...bullets].join('\n');
741
1344
  }
742
1345
  else if (asksTenancy && (multiSignals > 0 || singleSignals > 0)) {
@@ -870,6 +1473,25 @@ async function askCommand(question, options = {}) {
870
1473
  const maxCitations = Math.max(3, Math.min(options.maxCitations || 12, 30));
871
1474
  const shouldUseCache = options.cache !== false && process.env.NEURCODE_ASK_NO_CACHE !== '1';
872
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
+ }
873
1495
  if (process.stdout.isTTY && !process.env.CI) {
874
1496
  (0, neurcode_context_1.ensureDefaultLocalContextFile)(cwd);
875
1497
  }
@@ -979,6 +1601,7 @@ async function askCommand(question, options = {}) {
979
1601
  const { mode, terms, matchers } = buildMatchers(question);
980
1602
  const pathHints = derivePathHints(question);
981
1603
  const mentionsAskCommand = /\bask\b/.test(normalizedQuestion);
1604
+ const asksQuestionAnsweringInternals = /\b(question|questions|answer|answers|cache|brain|citation|grounded)\b/.test(normalizedQuestion);
982
1605
  if (matchers.length === 0) {
983
1606
  console.error(chalk.red('❌ Could not derive useful search terms from the question.'));
984
1607
  process.exit(1);
@@ -991,6 +1614,8 @@ async function askCommand(question, options = {}) {
991
1614
  }
992
1615
  addAnchorCandidates(fileTree, candidateSet, pathPriority, normalizedQuestion);
993
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);
994
1619
  if (candidateSet.size < 80) {
995
1620
  for (const filePath of fileTree) {
996
1621
  const normalizedPath = filePath.toLowerCase();
@@ -1005,7 +1630,12 @@ async function askCommand(question, options = {}) {
1005
1630
  if (normalizedPath.startsWith('scripts/')) {
1006
1631
  score -= 0.1;
1007
1632
  }
1008
- 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'))) {
1009
1639
  score -= 0.45;
1010
1640
  }
1011
1641
  if (pathHints.some((hint) => normalizedPath.startsWith(hint))) {
@@ -1040,11 +1670,15 @@ async function askCommand(question, options = {}) {
1040
1670
  ? prioritized.filter((filePath) => !pathHints.some((hint) => filePath.startsWith(hint)))
1041
1671
  : prioritized;
1042
1672
  const candidates = [...hinted, ...nonHinted].slice(0, MAX_SCAN_FILES);
1043
- 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();
1044
1680
  let scannedFiles = 0;
1045
1681
  for (const filePath of candidates) {
1046
- if (citations.length >= MAX_RAW_CITATIONS)
1047
- break;
1048
1682
  const fullPath = (0, path_1.join)(cwd, filePath);
1049
1683
  let content = '';
1050
1684
  try {
@@ -1058,34 +1692,165 @@ async function askCommand(question, options = {}) {
1058
1692
  }
1059
1693
  scannedFiles++;
1060
1694
  const lines = content.split(/\r?\n/);
1695
+ const lineTerms = new Map();
1696
+ const matchedTerms = new Set();
1061
1697
  for (let idx = 0; idx < lines.length; idx++) {
1062
- if (citations.length >= MAX_RAW_CITATIONS)
1063
- break;
1064
- const line = lines[idx];
1065
- if (!line || line.trim().length === 0)
1698
+ const rawLine = lines[idx];
1699
+ if (!rawLine || rawLine.trim().length === 0)
1066
1700
  continue;
1067
1701
  for (const matcher of matchers) {
1068
- if (!matcher.regex.test(line))
1069
- continue;
1070
- const snippet = normalizeSnippet(line);
1071
- if (!snippet)
1702
+ if (!matcher.regex.test(rawLine))
1072
1703
  continue;
1073
- const evidence = {
1074
- path: filePath,
1075
- line: idx + 1,
1076
- snippet,
1077
- term: matcher.label,
1078
- weight: matcher.weight +
1079
- (pathPriority.get(filePath) || 0) +
1080
- (pathHints.some((hint) => filePath.startsWith(hint)) ? 0.25 : 0),
1081
- };
1082
- const dedupeKey = `${evidence.path}:${evidence.line}:${evidence.term || ''}`;
1083
- 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)
1084
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;
1773
+ }
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;
1085
1793
  }
1086
- citations.push(evidence);
1087
- break;
1088
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;
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
+ });
1089
1854
  }
1090
1855
  }
1091
1856
  citations.sort((a, b) => b.weight - a.weight);
@@ -1099,20 +1864,75 @@ async function askCommand(question, options = {}) {
1099
1864
  }
1100
1865
  }
1101
1866
  const selectedForOutput = [];
1102
- const wantsOrgRequestInjection = /\b(inject|injected|header|request|requests)\b/.test(normalizedQuestion) &&
1103
- /\b(org|organization)\b/.test(normalizedQuestion);
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
+ }
1104
1916
  if (mode === 'comparison' && terms.length >= 2) {
1105
1917
  for (const term of terms.slice(0, 2)) {
1106
1918
  const firstForTerm = sourceEvidence.find((citation) => citation.term === term);
1107
1919
  if (firstForTerm) {
1108
- selectedForOutput.push(firstForTerm);
1920
+ pushSelected(firstForTerm);
1109
1921
  }
1110
1922
  }
1111
1923
  }
1112
1924
  else if (mode === 'search' && terms.length > 0) {
1113
- const preferredTerms = terms
1114
- .filter((term) => !LOW_SIGNAL_TERMS.has(term) && term.length >= 4)
1115
- .slice(0, 8);
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);
1116
1936
  for (const term of preferredTerms) {
1117
1937
  const firstForTerm = sourceEvidence.find((citation) => {
1118
1938
  if (citation.term !== term)
@@ -1122,32 +1942,22 @@ async function askCommand(question, options = {}) {
1122
1942
  });
1123
1943
  if (!firstForTerm)
1124
1944
  continue;
1125
- if (selectedForOutput.some((existing) => existing.path === firstForTerm.path && existing.line === firstForTerm.line && existing.term === firstForTerm.term)) {
1126
- continue;
1127
- }
1128
- selectedForOutput.push(firstForTerm);
1945
+ pushSelected(firstForTerm);
1129
1946
  if (selectedForOutput.length >= maxCitations)
1130
1947
  break;
1131
1948
  }
1132
1949
  }
1133
- if (wantsOrgRequestInjection && selectedForOutput.length < maxCitations) {
1134
- const targeted = sourceEvidence.filter((citation) => /x-org-id|org[_ -]?id|headers?\[|auto-?inject|request/i.test(citation.snippet));
1135
- for (const citation of targeted) {
1136
- if (selectedForOutput.length >= maxCitations)
1137
- break;
1138
- if (selectedForOutput.some((existing) => existing.path === citation.path && existing.line === citation.line && existing.term === citation.term)) {
1139
- continue;
1140
- }
1141
- selectedForOutput.push(citation);
1142
- }
1143
- }
1144
1950
  for (const citation of sourceEvidence) {
1145
1951
  if (selectedForOutput.length >= maxCitations)
1146
1952
  break;
1147
- 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}.`)) {
1148
1958
  continue;
1149
1959
  }
1150
- selectedForOutput.push(citation);
1960
+ pushSelected(citation);
1151
1961
  }
1152
1962
  const finalCitations = selectedForOutput.map(({ path, line, snippet, term }) => ({
1153
1963
  path,
@@ -1161,11 +1971,16 @@ async function askCommand(question, options = {}) {
1161
1971
  matchedLines: sourceEvidence.length,
1162
1972
  brainCandidates: brainResults.entries.length,
1163
1973
  };
1974
+ const lineCacheForAnswer = new Map();
1975
+ for (const [filePath, data] of rawFileMatches.entries()) {
1976
+ lineCacheForAnswer.set(filePath, data.lines);
1977
+ }
1164
1978
  const truth = evaluateTruthAssessment(mode, normalizedQuestion, terms, sourceEvidence, sourcePerFileCounts, sourceTermCounts);
1165
1979
  const answer = buildAnswer(mode, question, terms, finalCitations, stats, sourceTermCounts, sourcePerFileCounts, truth, {
1166
1980
  cliPackageName,
1167
1981
  knownCommands: knownCliCommands,
1168
1982
  featureBullets,
1983
+ lineCache: lineCacheForAnswer,
1169
1984
  });
1170
1985
  emitAskResult(answer, {
1171
1986
  json: options.json,