@neurcode-ai/cli 0.9.26 → 0.9.27

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.
@@ -27,60 +27,269 @@ catch {
27
27
  };
28
28
  }
29
29
  const MAX_SCAN_FILES = (() => {
30
- const raw = Number(process.env.NEURCODE_ASK_MAX_SCAN_FILES || '900');
30
+ const raw = Number(process.env.NEURCODE_ASK_MAX_SCAN_FILES || '2200');
31
31
  if (!Number.isFinite(raw))
32
- return 900;
33
- return Math.max(120, Math.min(Math.trunc(raw), 2000));
32
+ return 2200;
33
+ return Math.max(300, Math.min(Math.trunc(raw), 8000));
34
34
  })();
35
35
  const MAX_FILE_BYTES = 512 * 1024;
36
- const MAX_RAW_CITATIONS = 120;
37
- const PRIMARY_SOURCE_EXTENSIONS = new Set([
38
- 'ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs',
39
- 'py', 'go', 'rs', 'java', 'kt', 'swift', 'rb', 'php', 'cs',
40
- 'json', 'yaml', 'yml', 'toml', 'md', 'sql', 'graphql', 'gql',
41
- 'sh', 'bash', 'zsh', 'ps1', 'env', 'html', 'css', 'scss', 'less', 'prisma',
36
+ const MAX_RAW_CITATIONS = 220;
37
+ const RG_MAX_MATCHES = 3500;
38
+ const FETCH_TIMEOUT_MS = (() => {
39
+ const raw = Number(process.env.NEURCODE_ASK_EXTERNAL_TIMEOUT_MS || '9000');
40
+ if (!Number.isFinite(raw))
41
+ return 9000;
42
+ return Math.max(3000, Math.min(Math.trunc(raw), 30000));
43
+ })();
44
+ const REPO_SCOPE_TERMS = new Set([
45
+ 'repo', 'repository', 'codebase', 'file', 'files', 'path', 'paths', 'module', 'modules',
46
+ 'function', 'class', 'interface', 'type', 'schema', 'command', 'commands', 'flag', 'option',
47
+ 'middleware', 'route', 'service', 'api', 'org', 'organization', 'tenant', 'tenancy',
48
+ 'plan', 'verify', 'ask', 'ship', 'apply', 'watch', 'session', 'cache', 'brain', 'diff',
49
+ ]);
50
+ const EXTERNAL_WORLD_TERMS = new Set([
51
+ 'capital', 'population', 'gdp', 'weather', 'temperature', 'forecast', 'stock', 'price',
52
+ 'exchange', 'currency', 'president', 'prime minister', 'news', 'election', 'sports',
53
+ 'bitcoin', 'btc', 'ethereum', 'eth', 'usd', 'eur', 'inr', 'jpy',
54
+ 'fifa', 'world cup', 'olympics', 'cricket', 'nba', 'nfl',
42
55
  ]);
43
56
  const STOP_WORDS = new Set([
44
- 'the', 'and', 'for', 'with', 'that', 'this', 'what', 'where', 'when', 'which', 'into',
57
+ 'the', 'and', 'for', 'with', 'that', 'this', 'what', 'where', 'when', 'which',
45
58
  'from', 'your', 'about', 'there', 'their', 'them', 'have', 'does', 'is', 'are', 'was',
46
- 'were', 'any', 'all', 'read', 'tell', 'me', 'its', 'it', 'instead', 'than', 'then',
47
- 'workflow', 'codebase', 'repo', 'repository', 'used', 'use', 'mentioned', 'mention', 'whether',
48
- 'list', 'show', 'like', 'can', 'type', 'types', 'using', 'etc', 'cmd', 'cmds', 'command', 'commands',
49
- 'system', 'platform', 'latest', 'package', 'packages',
59
+ 'were', 'any', 'all', 'tell', 'me', 'its', 'it', 'than', 'then', 'workflow', 'codebase',
60
+ 'repo', 'repository', 'used', 'use', 'using', 'list', 'show', 'like', 'can', 'type',
61
+ 'types', 'package', 'packages', 'give', 'need', 'please', 'how', 'work', 'works', 'working',
50
62
  ]);
51
63
  const LOW_SIGNAL_TERMS = new Set([
52
- 'used', 'use', 'using', 'mentioned', 'mention', 'where', 'tell', 'read', 'check', 'find', 'search',
53
- 'workflow', 'repo', 'repository', 'codebase', 'anywhere', 'can', 'type', 'types', 'list', 'show', 'like',
54
- 'neurcode', 'cli', 'ask', 'file', 'files', 'path', 'filepath', 'header', 'added', 'add', 'request', 'requests',
55
- 'flag', 'flags', 'option', 'options',
56
- 'defined', 'define', 'implemented', 'implement', 'called', 'call', 'computed', 'compute', 'resolved', 'resolve',
57
- 'lookup', 'lookups', 'decide', 'decides',
64
+ 'used', 'use', 'using', 'where', 'tell', 'read', 'check', 'find', 'search',
65
+ 'workflow', 'repo', 'repository', 'codebase', 'anywhere', 'can', 'type', 'types',
66
+ 'list', 'show', 'like', 'neurcode', 'cli', 'file', 'files', 'path', 'paths',
67
+ 'resolved', 'resolve', 'defined', 'define', 'implemented', 'implement', 'called', 'call',
68
+ 'how', 'work', 'works', 'working',
69
+ ]);
70
+ const SUBCOMMAND_STOP_TERMS = new Set([
71
+ 'command', 'commands', 'subcommand', 'subcommands',
72
+ 'option', 'options', 'flag', 'flags',
73
+ 'what', 'where', 'when', 'why', 'who', 'which', 'how',
74
+ 'does', 'do', 'did', 'can', 'could', 'should', 'would', 'will',
75
+ 'work', 'works', 'working', 'flow', 'trace', 'compute', 'computed',
76
+ 'implementation', 'implement', 'internals', 'logic', 'behavior', 'behaviour',
58
77
  ]);
59
- const GENERIC_OUTPUT_TERMS = new Set(['file', 'files', 'path', 'filepath']);
60
- const REPO_SCOPE_TERMS = [
61
- 'repo', 'repository', 'codebase', 'neurcode', 'cli', 'command', 'commands', 'ship', 'plan', 'ask', 'verify',
62
- 'apply', 'watch', 'config', 'login', 'logout', 'whoami', 'doctor', 'module', 'file', 'files', 'path',
63
- 'middleware', 'api', 'service', 'services', 'branch', 'commit', 'cache', 'brain', 'org', 'organization',
64
- 'tenant', 'tenancy', 'x-org-id', 'readme', 'docs',
65
- ];
66
- const EXTERNAL_WORLD_TERMS = [
67
- 'capital', 'exchange rate', 'stock price', 'weather', 'temperature', 'forecast', 'news', 'election',
68
- 'president', 'prime minister', 'population', 'gdp', 'sports score', 'match result', 'bitcoin price',
69
- 'usd', 'inr', 'eur', 'jpy', 'currency',
70
- ];
71
- const REALTIME_WORLD_TERMS = ['right now', 'currently', 'today', 'tomorrow', 'yesterday', 'live', 'real time'];
72
78
  const CLI_COMMAND_NAMES = new Set([
73
79
  'check', 'refactor', 'security', 'brain', 'login', 'logout', 'init', 'doctor',
74
80
  'whoami', 'config', 'map', 'ask', 'plan', 'ship', 'apply', 'allow', 'watch',
75
81
  'session', 'verify', 'prompt', 'revert',
76
82
  ]);
83
+ function normalizeFilePath(filePath) {
84
+ return filePath.replace(/\\/g, '/').replace(/^\.\//, '');
85
+ }
86
+ function isIgnoredSearchPath(path) {
87
+ const normalized = normalizeFilePath(path).toLowerCase();
88
+ if (!normalized)
89
+ return true;
90
+ if (normalized.startsWith('dist/') ||
91
+ normalized.includes('/dist/') ||
92
+ normalized.startsWith('build/') ||
93
+ normalized.includes('/build/') ||
94
+ normalized.startsWith('out/') ||
95
+ normalized.includes('/out/') ||
96
+ normalized.startsWith('.next/') ||
97
+ normalized.includes('/.next/')) {
98
+ return true;
99
+ }
100
+ if (normalized.includes('.pnpm-store/') ||
101
+ normalized.startsWith('node_modules/') ||
102
+ normalized.includes('/node_modules/') ||
103
+ normalized.startsWith('.git/') ||
104
+ normalized.includes('/.git/') ||
105
+ normalized.startsWith('.neurcode/') ||
106
+ normalized.includes('/.neurcode/') ||
107
+ normalized.includes('/coverage/') ||
108
+ normalized.includes('/.cache/') ||
109
+ normalized.endsWith('.min.js') ||
110
+ normalized.endsWith('.map')) {
111
+ return true;
112
+ }
113
+ return false;
114
+ }
115
+ function escapeRegExp(input) {
116
+ return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
117
+ }
118
+ function normalizeSnippet(line) {
119
+ return line
120
+ .replace(/\t/g, ' ')
121
+ .replace(/\s+/g, ' ')
122
+ .trim()
123
+ .slice(0, 260);
124
+ }
125
+ function buildQueryProfile(searchTerms) {
126
+ const normalizedQuestion = searchTerms.normalizedQuestion;
127
+ const commandFocus = detectCommandFocus(normalizedQuestion);
128
+ return {
129
+ asksLocation: /\b(where|which file|location|defined|implemented|called|computed|resolved)\b/.test(normalizedQuestion),
130
+ asksHow: /\b(how|flow|trace|explain|why)\b/.test(normalizedQuestion),
131
+ asksList: /\b(list|show|which|what)\b/.test(normalizedQuestion),
132
+ asksRegistration: /\b(register|registered|registration|mapped|wired|hooked)\b/.test(normalizedQuestion),
133
+ codeFocused: /\b(command|commands|flag|option|api|middleware|route|service|class|function|interface|type|schema|field|cache|tenant|org|auth|verify|plan|apply|ship)\b/.test(normalizedQuestion),
134
+ commandFocus,
135
+ subcommandFocus: detectSubcommandFocus(normalizedQuestion, commandFocus),
136
+ highSignalSet: new Set(searchTerms.highSignalTerms.map((term) => term.toLowerCase())),
137
+ };
138
+ }
139
+ function isLikelyDocumentationPath(path) {
140
+ const normalized = normalizeFilePath(path).toLowerCase();
141
+ if (!normalized)
142
+ return false;
143
+ if (normalized === 'readme.md' || normalized.endsWith('/readme.md'))
144
+ return true;
145
+ if (normalized.startsWith('docs/') || normalized.includes('/docs/'))
146
+ return true;
147
+ if (normalized.includes('/documentation/'))
148
+ return true;
149
+ if (normalized.includes('/sitemap'))
150
+ return true;
151
+ if (normalized.endsWith('.md') || normalized.endsWith('.mdx'))
152
+ return true;
153
+ return false;
154
+ }
155
+ function isLikelyCodeSnippet(snippet) {
156
+ const value = snippet.trim();
157
+ if (!value)
158
+ return false;
159
+ if (/^\s*\/[/*]/.test(value))
160
+ return true;
161
+ if (/[{}();=]/.test(value))
162
+ return true;
163
+ if (/\b(import|export|const|let|var|function|class|interface|type|enum|return|await|if|else|switch|case|try|catch|throw)\b/.test(value)) {
164
+ return true;
165
+ }
166
+ if (/\b[a-zA-Z_][a-zA-Z0-9_]*\s*\(/.test(value))
167
+ return true;
168
+ if (/\.\w+\(/.test(value))
169
+ return true;
170
+ if (/=>/.test(value))
171
+ return true;
172
+ return false;
173
+ }
174
+ function isLikelyDocSnippet(snippet) {
175
+ const value = snippet.trim();
176
+ if (!value)
177
+ return false;
178
+ if (/^<\w+/.test(value) || /<\/\w+>/.test(value))
179
+ return true;
180
+ if (/\b(className|href|to: ['"]\/docs\/|#ask-command)\b/.test(value))
181
+ return true;
182
+ if (/^#{1,6}\s/.test(value))
183
+ return true;
184
+ if (/^[-*]\s/.test(value))
185
+ return true;
186
+ return false;
187
+ }
188
+ function isPromptExampleSnippet(snippet, normalizedQuestion, highSignalTerms) {
189
+ const snippetLower = snippet.toLowerCase();
190
+ if (!snippetLower)
191
+ return false;
192
+ if (/\bneurcode\s+(ask|plan|verify|ship|apply)\s+["`]/i.test(snippet))
193
+ return true;
194
+ if (snippetLower.includes('?') && /\b(where|what|how|why|which)\b/.test(snippetLower)) {
195
+ const overlaps = highSignalTerms.filter((term) => term && snippetLower.includes(term.toLowerCase())).length;
196
+ if (overlaps >= Math.min(3, Math.max(2, highSignalTerms.length)))
197
+ return true;
198
+ }
199
+ const normalizedSnippet = (0, plan_cache_1.normalizeIntent)(snippetLower);
200
+ if (normalizedQuestion.length >= 24 && normalizedSnippet.includes(normalizedQuestion.slice(0, 28))) {
201
+ return true;
202
+ }
203
+ return false;
204
+ }
205
+ function tokenizeQuestion(question) {
206
+ return (0, plan_cache_1.normalizeIntent)(question)
207
+ .replace(/[^a-z0-9_\-\s]/g, ' ')
208
+ .split(/\s+/)
209
+ .map((token) => token.trim())
210
+ .filter((token) => token.length >= 3 && !STOP_WORDS.has(token));
211
+ }
212
+ function extractQuotedPhrases(question) {
213
+ const out = [];
214
+ const seen = new Set();
215
+ const re = /["'`](.{2,100}?)["'`]/g;
216
+ for (const match of question.matchAll(re)) {
217
+ const value = (0, plan_cache_1.normalizeIntent)(match[1] || '').trim();
218
+ if (!value || seen.has(value))
219
+ continue;
220
+ seen.add(value);
221
+ out.push(value);
222
+ }
223
+ return out;
224
+ }
225
+ function extractCodeIdentifiers(question) {
226
+ const matches = question.match(/[A-Za-z_][A-Za-z0-9_\-]{2,}/g) || [];
227
+ const out = [];
228
+ const seen = new Set();
229
+ for (const token of matches) {
230
+ const normalized = token.trim();
231
+ const key = normalized.toLowerCase();
232
+ if (!normalized || STOP_WORDS.has(key))
233
+ continue;
234
+ if (seen.has(key))
235
+ continue;
236
+ seen.add(key);
237
+ out.push(normalized);
238
+ }
239
+ return out.slice(0, 16);
240
+ }
241
+ function buildSearchTerms(question) {
242
+ const normalizedQuestion = (0, plan_cache_1.normalizeIntent)(question);
243
+ const tokens = tokenizeQuestion(question);
244
+ const quotedPhrases = extractQuotedPhrases(question);
245
+ const identifiers = extractCodeIdentifiers(question);
246
+ const highSignalTerms = tokens
247
+ .filter((token) => !LOW_SIGNAL_TERMS.has(token))
248
+ .slice(0, 18);
249
+ const phraseTerms = [];
250
+ for (let i = 0; i < highSignalTerms.length - 1; i++) {
251
+ const phrase = `${highSignalTerms[i]} ${highSignalTerms[i + 1]}`;
252
+ if (phrase.length >= 7)
253
+ phraseTerms.push(phrase);
254
+ if (phraseTerms.length >= 8)
255
+ break;
256
+ }
257
+ const all = [
258
+ ...quotedPhrases,
259
+ ...identifiers,
260
+ ...highSignalTerms,
261
+ ...phraseTerms,
262
+ ];
263
+ const seen = new Set();
264
+ const rgTerms = [];
265
+ for (const term of all) {
266
+ const normalized = (0, plan_cache_1.normalizeIntent)(term).trim();
267
+ if (!normalized)
268
+ continue;
269
+ if (seen.has(normalized))
270
+ continue;
271
+ seen.add(normalized);
272
+ rgTerms.push(normalized);
273
+ }
274
+ return {
275
+ normalizedQuestion,
276
+ tokens,
277
+ highSignalTerms,
278
+ quotedPhrases,
279
+ identifiers,
280
+ rgTerms: rgTerms.slice(0, 22),
281
+ };
282
+ }
77
283
  function scanFiles(dir, maxFiles = MAX_SCAN_FILES) {
78
284
  const files = [];
79
285
  const ignoreDirs = new Set([
80
- 'node_modules', '.git', '.next', 'dist', 'build', '.turbo', '.cache', 'coverage',
81
- '.neurcode', '.vscode', '.pnpm-store', '.npm', '.yarn',
286
+ 'node_modules', '.git', '.next', 'dist', 'build', '.turbo', '.cache',
287
+ 'coverage', '.neurcode', '.vscode', '.pnpm-store', '.yarn', '.npm',
288
+ ]);
289
+ const ignoreExts = new Set([
290
+ 'map', 'log', 'lock', 'png', 'jpg', 'jpeg', 'gif', 'ico', 'svg',
291
+ 'woff', 'woff2', 'ttf', 'eot', 'pdf',
82
292
  ]);
83
- const ignoreExts = new Set(['map', 'log', 'lock', 'png', 'jpg', 'jpeg', 'gif', 'ico', 'svg', 'woff', 'woff2', 'ttf', 'eot', 'pdf']);
84
293
  const walk = (current) => {
85
294
  if (files.length >= maxFiles)
86
295
  return;
@@ -94,10 +303,6 @@ function scanFiles(dir, maxFiles = MAX_SCAN_FILES) {
94
303
  for (const entry of entries) {
95
304
  if (files.length >= maxFiles)
96
305
  break;
97
- if (entry.startsWith('.') && !entry.startsWith('.env')) {
98
- if (ignoreDirs.has(entry))
99
- continue;
100
- }
101
306
  const fullPath = (0, path_1.join)(current, entry);
102
307
  let st;
103
308
  try {
@@ -109,6 +314,8 @@ function scanFiles(dir, maxFiles = MAX_SCAN_FILES) {
109
314
  if (st.isDirectory()) {
110
315
  if (ignoreDirs.has(entry))
111
316
  continue;
317
+ if (entry.startsWith('.') && entry !== '.env')
318
+ continue;
112
319
  walk(fullPath);
113
320
  continue;
114
321
  }
@@ -119,374 +326,103 @@ function scanFiles(dir, maxFiles = MAX_SCAN_FILES) {
119
326
  const ext = entry.includes('.') ? entry.split('.').pop()?.toLowerCase() || '' : '';
120
327
  if (ext && ignoreExts.has(ext))
121
328
  continue;
122
- const rel = fullPath.slice(dir.length + 1).replace(/\\/g, '/');
329
+ const rel = normalizeFilePath(fullPath.slice(dir.length + 1));
123
330
  files.push(rel);
124
331
  }
125
332
  };
126
333
  walk(dir);
127
334
  return files.slice(0, maxFiles);
128
335
  }
129
- function tokenizeQuestion(question) {
130
- return (0, plan_cache_1.normalizeIntent)(question)
131
- .replace(/[^a-z0-9_\-\s]/g, ' ')
132
- .split(/\s+/)
133
- .map((token) => token.trim())
134
- .filter((token) => token.length >= 3 && !STOP_WORDS.has(token));
135
- }
136
- function escapeRegExp(input) {
137
- return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
138
- }
139
- function normalizeTerm(raw) {
140
- const normalized = raw
141
- .toLowerCase()
142
- .replace(/['"`]/g, '')
143
- .replace(/[^a-z0-9_\-\s]/g, ' ')
144
- .replace(/\s+/g, ' ')
145
- .trim();
146
- if (!normalized)
147
- return '';
148
- return normalized
149
- .split(' ')
150
- .map((token) => token.replace(/^[-_]+|[-_]+$/g, ''))
151
- .filter(Boolean)
152
- .join(' ');
153
- }
154
- function extractQuotedTerms(question) {
155
- const seen = new Set();
156
- const terms = [];
157
- const re = /["'`](.{2,80}?)["'`]/g;
158
- for (const match of question.matchAll(re)) {
159
- const term = normalizeTerm(match[1] || '');
160
- if (!term || seen.has(term))
161
- continue;
162
- seen.add(term);
163
- terms.push(term);
164
- }
165
- return terms;
166
- }
167
- function extractIdentityTerms(question) {
168
- const normalized = (0, plan_cache_1.normalizeIntent)(question);
169
- const matches = normalized.match(/(?:organization[\s_-]*id|org[\s_-]*id|orgid|organizationid|user[\s_-]*id|userid|userid)/g);
170
- if (!matches)
171
- return [];
172
- const seen = new Set();
173
- const ordered = [];
174
- for (const raw of matches) {
175
- const term = normalizeTerm(raw);
176
- if (seen.has(term))
177
- continue;
178
- seen.add(term);
179
- ordered.push(term);
180
- }
181
- return ordered;
182
- }
183
- function extractComparisonTerms(question) {
184
- const quoted = extractQuotedTerms(question);
185
- if (quoted.length >= 2)
186
- return quoted.slice(0, 2);
187
- const normalized = (0, plan_cache_1.normalizeIntent)(question);
188
- const hasComparisonJoiner = normalized.includes('instead of') ||
189
- normalized.includes(' versus ') ||
190
- normalized.includes(' vs ') ||
191
- normalized.includes(' compared to ');
192
- if (!hasComparisonJoiner)
193
- return [];
194
- const identities = extractIdentityTerms(question);
195
- if (identities.length >= 2)
196
- return identities.slice(0, 2);
197
- return [];
198
- }
199
- function extractPhraseTerms(question, maxTerms = 6) {
200
- const tokens = (0, plan_cache_1.normalizeIntent)(question)
201
- .replace(/[^a-z0-9_\-\s]/g, ' ')
202
- .split(/\s+/)
203
- .map((token) => token.trim())
204
- .filter((token) => token.length >= 3 && !STOP_WORDS.has(token) && !LOW_SIGNAL_TERMS.has(token));
205
- const phrases = [];
206
- const seen = new Set();
207
- for (let i = 0; i < tokens.length - 1; i++) {
208
- const phrase = `${tokens[i]} ${tokens[i + 1]}`;
209
- if (seen.has(phrase))
210
- continue;
211
- seen.add(phrase);
212
- phrases.push(phrase);
213
- if (phrases.length >= maxTerms)
214
- break;
215
- }
216
- return phrases;
217
- }
218
- function buildTermMatchers(term, weight) {
219
- const normalized = normalizeTerm(term);
220
- if (!normalized)
221
- return [];
222
- const tokens = normalized.split(/\s+/).filter(Boolean);
223
- const out = [];
224
- const push = (label, regexSource) => {
225
- out.push({
226
- label,
227
- regex: new RegExp(regexSource, 'i'),
228
- weight,
229
- });
230
- };
231
- if (tokens.length === 1) {
232
- const token = escapeRegExp(tokens[0]);
233
- push(normalized, `\\b${token}\\b`);
234
- if (/^[a-z0-9_]+$/.test(tokens[0]) && tokens[0].length >= 5) {
235
- push(normalized, `\\b${token}[a-z0-9_]*\\b`);
236
- push(normalized, `\\b[a-z0-9_]*${token}[a-z0-9_]*\\b`);
237
- }
238
- return out;
239
- }
240
- const tokenChain = tokens.map((t) => escapeRegExp(t)).join('[\\s_-]*');
241
- push(normalized, `\\b${tokenChain}\\b`);
242
- push(normalized, `\\b${escapeRegExp(tokens.join(''))}\\b`);
243
- push(normalized, `\\b${escapeRegExp(tokens.join('_'))}\\b`);
244
- push(normalized, `\\b${escapeRegExp(tokens.join('-'))}\\b`);
245
- return out;
246
- }
247
- function expandSearchTerms(terms) {
248
- const expanded = new Set();
249
- for (const rawTerm of terms) {
250
- const term = normalizeTerm(rawTerm);
251
- if (!term)
336
+ function countTermHits(text, terms) {
337
+ if (!text || terms.length === 0)
338
+ return 0;
339
+ let hits = 0;
340
+ const lower = text.toLowerCase();
341
+ for (const term of terms) {
342
+ const normalized = term.toLowerCase().trim();
343
+ if (!normalized)
252
344
  continue;
253
- expanded.add(term);
254
- if (term.includes(' ')) {
345
+ if (normalized.includes(' ')) {
346
+ if (lower.includes(normalized))
347
+ hits += 1;
255
348
  continue;
256
349
  }
257
- if (term.endsWith('ies') && term.length > 4) {
258
- expanded.add(`${term.slice(0, -3)}y`);
259
- }
260
- if (term.endsWith('s') && term.length > 3) {
261
- expanded.add(term.slice(0, -1));
262
- }
263
- else if (term.length > 3) {
264
- expanded.add(`${term}s`);
265
- }
266
- if (term.endsWith('ing') && term.length > 5) {
267
- expanded.add(term.slice(0, -3));
268
- }
269
- if (term.endsWith('ed') && term.length > 4) {
270
- expanded.add(term.slice(0, -2));
271
- }
272
- if (term.includes('-')) {
273
- expanded.add(term.replace(/-/g, ' '));
274
- expanded.add(term.replace(/-/g, ''));
275
- }
276
- if (term.endsWith('cache')) {
277
- expanded.add(`${term}d`);
278
- }
279
- if (term.endsWith('cached')) {
280
- expanded.add(term.slice(0, -1)); // cached -> cache
281
- }
282
- if (term.includes('cache') && term.includes('-')) {
283
- expanded.add(term.replace(/cache\b/, 'cached'));
284
- expanded.add(term.replace(/cached\b/, 'cache'));
285
- }
350
+ const pattern = new RegExp(`(?:^|[^a-z0-9])${escapeRegExp(normalized)}(?:$|[^a-z0-9])`, 'i');
351
+ if (pattern.test(lower))
352
+ hits += 1;
286
353
  }
287
- return [...expanded];
354
+ return hits;
288
355
  }
289
- function buildMatchers(question) {
290
- const comparisonTerms = extractComparisonTerms(question);
291
- if (comparisonTerms.length >= 2) {
292
- const matchers = [
293
- ...buildTermMatchers(comparisonTerms[0], 1.0),
294
- ...buildTermMatchers(comparisonTerms[1], 0.9),
295
- ];
356
+ function classifyQuestionScope(question, terms) {
357
+ const normalized = terms.normalizedQuestion;
358
+ const repoSignal = countTermHits(normalized, [...REPO_SCOPE_TERMS]);
359
+ const externalSignal = countTermHits(normalized, [...EXTERNAL_WORLD_TERMS]);
360
+ const hasCodeLikeSyntax = /[`][^`]+[`]/.test(question) ||
361
+ /\b[a-z0-9_\-/]+\.(ts|tsx|js|jsx|py|go|java|rb|php|cs|json|yml|yaml|toml|sql|md)\b/i.test(question) ||
362
+ /\b[A-Za-z_][A-Za-z0-9_]*\s*\(/.test(question) ||
363
+ /--[a-z0-9-]+/i.test(question);
364
+ const mentionsKnownCommand = [...CLI_COMMAND_NAMES].some((cmd) => new RegExp(`\\b${escapeRegExp(cmd)}\\b`, 'i').test(normalized));
365
+ const asksGlobalFact = /\b(capital|population|gdp|weather|forecast|stock|currency|exchange rate|president|prime minister|who is|who was|who won|what is|where is|when did|world cup|olympics|fifa|nba|nfl|cricket)\b/.test(normalized);
366
+ if ((externalSignal >= 1 || asksGlobalFact) && repoSignal === 0 && !hasCodeLikeSyntax && !mentionsKnownCommand) {
296
367
  return {
297
- mode: 'comparison',
298
- terms: comparisonTerms.slice(0, 2),
299
- matchers,
368
+ kind: 'external',
369
+ reasons: ['Question appears to be outside repository scope.'],
300
370
  };
301
371
  }
302
- const quoted = extractQuotedTerms(question);
303
- const identityTerms = extractIdentityTerms(question);
304
- const phraseTerms = extractPhraseTerms(question);
305
- const keywords = tokenizeQuestion(question).slice(0, 8);
306
- const quotedSet = new Set(quoted.map((term) => normalizeTerm(term)));
307
- const baseTerms = [...new Set([...quoted, ...phraseTerms, ...identityTerms, ...keywords].map(normalizeTerm).filter(Boolean))];
308
- const filteredTerms = baseTerms.filter((term) => quotedSet.has(term) || !LOW_SIGNAL_TERMS.has(term));
309
- const terms = expandSearchTerms(filteredTerms.length > 0 ? filteredTerms : baseTerms).filter(Boolean);
310
- const matchers = terms.flatMap((term) => buildTermMatchers(term, quoted.includes(term) ? 0.9 : 0.55));
311
- return {
312
- mode: 'search',
313
- terms,
314
- matchers,
315
- };
316
- }
317
- function derivePathHints(question) {
318
- const normalized = (0, plan_cache_1.normalizeIntent)(question);
319
- const hints = [];
320
- if (/\bcli|command|terminal\b/.test(normalized)) {
321
- hints.push('packages/cli/src/commands/');
322
- hints.push('packages/cli/src/');
323
- hints.push('packages/cli/');
324
- }
325
- if (/\bapi|backend|server|route|middleware\b/.test(normalized)) {
326
- hints.push('services/api/');
327
- }
328
- if (/\bdashboard|landing|docs|frontend|ui|pricing\b/.test(normalized)) {
329
- hints.push('web/dashboard/');
330
- hints.push('docs/');
331
- hints.push('README.md');
332
- }
333
- if (/\bfeature|capabilit|offer|platform\b/.test(normalized)) {
334
- hints.push('docs/');
335
- hints.push('README.md');
336
- }
337
- if (/\binstall|setup|upgrade|update\b/.test(normalized)) {
338
- hints.push('README.md');
339
- hints.push('docs/');
340
- hints.push('packages/cli/');
341
- }
342
- if (/\btenant|tenancy|single|multi|organization|org\b/.test(normalized)) {
343
- hints.push('services/api/src/lib/');
344
- hints.push('services/api/src/routes/');
345
- hints.push('packages/cli/src/');
346
- }
347
- if (/\brequest|requests|header|inject|injected\b/.test(normalized)) {
348
- hints.push('packages/cli/src/api-client.ts');
349
- hints.push('services/api/src/middleware/');
350
- }
351
- if (/(github action|\bci\b)/.test(normalized)) {
352
- hints.push('.github/workflows/');
353
- hints.push('packages/action/');
354
- hints.push('actions/');
355
- }
356
- return [...new Set(hints)];
357
- }
358
- function extractCodeLikeIdentifiers(question) {
359
- const quoted = extractQuotedTerms(question);
360
- const tokens = question.match(/[A-Za-z_][A-Za-z0-9_]{2,}/g) || [];
361
- const seen = new Set();
362
- const output = [];
363
- for (const raw of [...quoted, ...tokens]) {
364
- const normalized = raw.trim();
365
- if (!normalized)
366
- continue;
367
- const key = normalized.toLowerCase();
368
- if (STOP_WORDS.has(key) || LOW_SIGNAL_TERMS.has(key))
369
- continue;
370
- if (seen.has(key))
371
- continue;
372
- seen.add(key);
373
- output.push(normalized);
372
+ if (repoSignal > 0 || hasCodeLikeSyntax || mentionsKnownCommand) {
373
+ return { kind: 'repo', reasons: [] };
374
374
  }
375
- return output.slice(0, 8);
376
- }
377
- function deriveQuerySignals(question, normalizedQuestion, terms) {
378
- const asksLocation = /\b(where|which file|in which file|filepath|file path|location)\b/.test(normalizedQuestion);
379
- const asksHow = /\b(how|flow|trace|walk me through|explain)\b/.test(normalizedQuestion);
380
- const asksCommandSurface = /\b(command|commands|subcommand|subcommands|flag|flags|option|options)\b/.test(normalizedQuestion);
381
- const asksSchema = /\b(field|fields|key|keys|schema|interface|input|output|parameter|parameters)\b/.test(normalizedQuestion);
382
- const asksList = /\b(list|show|which|available)\b/.test(normalizedQuestion) &&
383
- /\b(command|commands|subcommand|subcommands|files|fields|features)\b/.test(normalizedQuestion);
384
- const asksDefinition = /\b(defined|implemented|called|computed|resolved)\b/.test(normalizedQuestion);
385
- const identifiers = extractCodeLikeIdentifiers(question);
386
- const highSignalTerms = terms.filter((term) => !LOW_SIGNAL_TERMS.has(term) && term.length >= 3);
387
375
  return {
388
- asksLocation,
389
- asksHow,
390
- asksList,
391
- asksDefinition,
392
- asksCommandSurface,
393
- asksSchema,
394
- identifiers,
395
- highSignalTerms,
376
+ kind: 'ambiguous',
377
+ reasons: ['Question does not clearly reference repository context.'],
396
378
  };
397
379
  }
398
- function extractAnchorTerms(question) {
399
- const quoted = extractQuotedTerms(question);
400
- const special = (question.match(/[A-Za-z0-9_-]{4,}/g) || []).filter((token) => /[_-]/.test(token) || /[A-Z]/.test(token));
401
- const normalized = [...new Set([...quoted, ...special].map(normalizeTerm).filter(Boolean))]
402
- .filter((term) => !STOP_WORDS.has(term) && !LOW_SIGNAL_TERMS.has(term));
403
- return expandSearchTerms(normalized).slice(0, 12);
404
- }
405
- function looksLikeImportLine(rawLine) {
406
- const trimmed = rawLine.trim();
407
- return /^import\s+/.test(trimmed) || /^export\s+\{/.test(trimmed);
408
- }
409
- function countQuestionTermHits(normalizedQuestion, terms) {
410
- let hits = 0;
411
- const seen = new Set();
412
- for (const term of terms) {
413
- const normalizedTerm = term.toLowerCase().trim();
414
- if (!normalizedTerm || seen.has(normalizedTerm))
415
- continue;
416
- seen.add(normalizedTerm);
417
- if (normalizedTerm.includes(' ')) {
418
- if (!normalizedQuestion.includes(normalizedTerm))
419
- continue;
420
- hits += 1;
421
- continue;
422
- }
423
- const boundaryPattern = new RegExp(`(?:^|[^a-z0-9])${escapeRegExp(normalizedTerm)}(?:$|[^a-z0-9])`);
424
- if (boundaryPattern.test(normalizedQuestion)) {
425
- hits += 1;
426
- }
380
+ function detectCommandFocus(normalizedQuestion) {
381
+ const compound = normalizedQuestion.match(/\b([a-z][a-z0-9-]*)\s+([a-z][a-z0-9-]*)\s+command\b/);
382
+ if (compound?.[1] && CLI_COMMAND_NAMES.has(compound[1])) {
383
+ return compound[1];
427
384
  }
428
- return hits;
385
+ const direct = normalizedQuestion.match(/\bneurcode\s+([a-z][a-z0-9-]*)\b/);
386
+ if (direct?.[1] && CLI_COMMAND_NAMES.has(direct[1])) {
387
+ return direct[1];
388
+ }
389
+ const singular = normalizedQuestion.match(/\b([a-z][a-z0-9-]*)\s+command\b/);
390
+ if (singular?.[1] && CLI_COMMAND_NAMES.has(singular[1])) {
391
+ return singular[1];
392
+ }
393
+ const mentioned = [...CLI_COMMAND_NAMES].filter((cmd) => new RegExp(`\\b${escapeRegExp(cmd)}\\b`, 'i').test(normalizedQuestion));
394
+ if (mentioned.length === 1)
395
+ return mentioned[0];
396
+ return null;
429
397
  }
430
- function assessQuestionScope(normalizedQuestion) {
431
- const repoHits = countQuestionTermHits(normalizedQuestion, REPO_SCOPE_TERMS);
432
- const externalHits = countQuestionTermHits(normalizedQuestion, EXTERNAL_WORLD_TERMS);
433
- const realtimeHits = countQuestionTermHits(normalizedQuestion, REALTIME_WORLD_TERMS);
434
- const currencyPairPattern = /\b[a-z]{3}\s*(?:to|\/)\s*[a-z]{3}\b/;
435
- const hasCurrencyPair = currencyPairPattern.test(normalizedQuestion);
436
- const hasWorldEntity = /\b(france|india|usa|united states|china|germany|europe|asia|africa)\b/.test(normalizedQuestion);
437
- const looksExternal = hasCurrencyPair ||
438
- externalHits > 0 ||
439
- (hasWorldEntity && /\bcapital|population|gdp|president|prime minister\b/.test(normalizedQuestion)) ||
440
- (realtimeHits > 0 && /\bexchange rate|weather|stock|price|news|capital|population\b/.test(normalizedQuestion));
441
- if (looksExternal && repoHits === 0) {
442
- const reasons = ['This question appears to require external world knowledge rather than repository evidence.'];
443
- if (hasCurrencyPair) {
444
- reasons.push('Detected a currency/exchange-rate query, which is outside repo-grounded scope.');
445
- }
446
- if (realtimeHits > 0) {
447
- reasons.push('Detected real-time wording (for example "right now"/"currently"), which is not answerable from local files.');
448
- }
449
- return { isOutOfScope: true, reasons };
450
- }
451
- return { isOutOfScope: false, reasons: [] };
398
+ function isLikelySubcommandToken(value, options) {
399
+ const token = (0, plan_cache_1.normalizeIntent)(value).trim().toLowerCase();
400
+ if (!token || token.length < 3)
401
+ return false;
402
+ if (!/^[a-z][a-z0-9-]*$/.test(token))
403
+ return false;
404
+ if (!options?.allowKnownCommand && CLI_COMMAND_NAMES.has(token))
405
+ return false;
406
+ if (STOP_WORDS.has(token) || LOW_SIGNAL_TERMS.has(token) || SUBCOMMAND_STOP_TERMS.has(token))
407
+ return false;
408
+ return true;
452
409
  }
453
- function buildOutOfScopeAnswerPayload(question, normalizedQuestion, reasons) {
454
- const truthReasons = [
455
- 'Neurcode Ask is repository-grounded and does not use web/external knowledge.',
456
- ...reasons,
457
- ];
458
- return {
459
- question,
460
- questionNormalized: normalizedQuestion,
461
- mode: 'search',
462
- answer: [
463
- 'I can only answer from this repository.',
464
- 'This question looks external/realtime, so I cannot answer it from repo evidence.',
465
- 'Try a repo-scoped variant, for example: `Where is org id injected in CLI requests?`',
466
- ].join('\n'),
467
- findings: [
468
- 'Scope guard triggered: external/non-repo query.',
469
- 'No repo files were scanned to avoid returning misleading answers.',
470
- ],
471
- confidence: 'low',
472
- truth: {
473
- status: 'insufficient',
474
- score: 0.05,
475
- reasons: truthReasons,
476
- sourceCitations: 0,
477
- sourceFiles: 0,
478
- minCitationsRequired: 2,
479
- minFilesRequired: 1,
480
- },
481
- citations: [],
482
- generatedAt: new Date().toISOString(),
483
- stats: {
484
- scannedFiles: 0,
485
- matchedFiles: 0,
486
- matchedLines: 0,
487
- brainCandidates: 0,
488
- },
489
- };
410
+ function detectSubcommandFocus(normalizedQuestion, commandFocus) {
411
+ if (!commandFocus)
412
+ return null;
413
+ const explicit = normalizedQuestion.match(new RegExp(`\\b${escapeRegExp(commandFocus)}\\s+([a-z][a-z0-9-]*)\\s+command\\b`));
414
+ if (explicit?.[1] && isLikelySubcommandToken(explicit[1], { allowKnownCommand: true })) {
415
+ return explicit[1].toLowerCase();
416
+ }
417
+ const directAfter = normalizedQuestion.match(new RegExp(`\\b${escapeRegExp(commandFocus)}\\s+([a-z][a-z0-9-]*)\\b`));
418
+ if (directAfter?.[1] && isLikelySubcommandToken(directAfter[1])) {
419
+ return directAfter[1].toLowerCase();
420
+ }
421
+ const explicitSubcommand = normalizedQuestion.match(new RegExp(`\\b([a-z][a-z0-9-]*)\\s+subcommand\\b.*\\b${escapeRegExp(commandFocus)}\\b`));
422
+ if (explicitSubcommand?.[1] && isLikelySubcommandToken(explicitSubcommand[1])) {
423
+ return explicitSubcommand[1].toLowerCase();
424
+ }
425
+ return null;
490
426
  }
491
427
  function parseOwnershipLookbackDays(normalizedQuestion) {
492
428
  if (/\bquarter\b/.test(normalizedQuestion))
@@ -511,10 +447,21 @@ function buildOwnershipDeterministicAnswer(cwd, question, normalizedQuestion) {
511
447
  const asksOwnership = /\b(who|owner|owners|authored|touched|touch)\b/.test(normalizedQuestion);
512
448
  if (!asksOwnership)
513
449
  return null;
450
+ const repoSignal = countTermHits(normalizedQuestion, [...REPO_SCOPE_TERMS]);
451
+ const externalSignal = countTermHits(normalizedQuestion, [...EXTERNAL_WORLD_TERMS]);
452
+ const hasCodeLikeSyntax = /[`][^`]+[`]/.test(question) ||
453
+ /\b[a-z0-9_\-/]+\.(ts|tsx|js|jsx|py|go|java|rb|php|cs|json|yml|yaml|toml|sql|md)\b/i.test(question) ||
454
+ /\b[A-Za-z_][A-Za-z0-9_]*\s*\(/.test(question) ||
455
+ /--[a-z0-9-]+/i.test(question);
456
+ const ownershipExternalPattern = /\b(who won|who is|who was|world cup|olympics|fifa|nba|nfl|cricket)\b/.test(normalizedQuestion);
457
+ if ((repoSignal === 0 && !hasCodeLikeSyntax) || ownershipExternalPattern || externalSignal > 0) {
458
+ return null;
459
+ }
514
460
  const ignoreTerms = new Set([
515
- 'who', 'owner', 'owners', 'authored', 'touched', 'touch', 'last', 'quarter', 'recent', 'recently',
516
- 'file', 'files', 'module', 'modules', 'repo', 'repository', 'codebase', 'this', 'that', 'these', 'those',
517
- 'for', 'from', 'with', 'about', 'during', 'before', 'after', 'show', 'list', 'what', 'which', 'where',
461
+ 'who', 'owner', 'owners', 'authored', 'touched', 'touch', 'last', 'quarter',
462
+ 'recent', 'recently', 'file', 'files', 'module', 'modules', 'repo', 'repository',
463
+ 'codebase', 'this', 'that', 'these', 'those', 'for', 'from', 'with', 'about',
464
+ 'during', 'before', 'after', 'show', 'list', 'what', 'which', 'where',
518
465
  ]);
519
466
  const focusTerms = (0, plan_cache_1.normalizeIntent)(question)
520
467
  .split(/\s+/)
@@ -543,21 +490,20 @@ function buildOwnershipDeterministicAnswer(cwd, question, normalizedQuestion) {
543
490
  }
544
491
  if (!currentAuthor)
545
492
  continue;
546
- const normalizedPath = line.replace(/\\/g, '/').replace(/^\.\//, '');
547
- if (!normalizedPath || normalizedPath.startsWith('.git/') || normalizedPath.startsWith('node_modules/')) {
493
+ const normalizedPath = normalizeFilePath(line);
494
+ if (!normalizedPath || isIgnoredSearchPath(normalizedPath)) {
548
495
  continue;
549
496
  }
550
497
  if (focusTerms.length > 0 && !focusTerms.some((term) => (0, plan_cache_1.normalizeIntent)(normalizedPath).includes(term))) {
551
498
  continue;
552
499
  }
553
500
  authorTouches.set(currentAuthor, (authorTouches.get(currentAuthor) || 0) + 1);
554
- const perFile = fileTouches.get(normalizedPath) || new Map();
555
- perFile.set(currentAuthor, (perFile.get(currentAuthor) || 0) + 1);
556
- fileTouches.set(normalizedPath, perFile);
501
+ const byFile = fileTouches.get(normalizedPath) || new Map();
502
+ byFile.set(currentAuthor, (byFile.get(currentAuthor) || 0) + 1);
503
+ fileTouches.set(normalizedPath, byFile);
557
504
  }
558
- if (authorTouches.size === 0) {
505
+ if (authorTouches.size === 0)
559
506
  return null;
560
- }
561
507
  const topContributors = [...authorTouches.entries()]
562
508
  .sort((a, b) => b[1] - a[1])
563
509
  .slice(0, 5)
@@ -570,10 +516,6 @@ function buildOwnershipDeterministicAnswer(cwd, question, normalizedQuestion) {
570
516
  })
571
517
  .slice(0, 6);
572
518
  const targetLabel = focusTerms.length > 0 ? `files matching "${focusTerms.slice(0, 4).join(', ')}"` : 'this repository';
573
- const answer = [
574
- `Top contributors for ${targetLabel} in the last ${sinceDays} day(s):`,
575
- ...topContributors.map((entry) => ` • ${entry.author} (${entry.touches} touches)`),
576
- ].join('\n');
577
519
  const citations = topFiles.map(([path, owners]) => {
578
520
  const summary = [...owners.entries()]
579
521
  .sort((a, b) => b[1] - a[1])
@@ -587,1050 +529,1330 @@ function buildOwnershipDeterministicAnswer(cwd, question, normalizedQuestion) {
587
529
  snippet: `Recent git touches (last ${sinceDays}d): ${summary}`,
588
530
  };
589
531
  });
590
- const sourceFiles = new Set(citations.map((citation) => citation.path)).size;
591
- const score = Math.min(0.98, 0.65 + Math.min(sourceFiles, 6) * 0.05);
532
+ const sourceFiles = new Set(citations.map((c) => c.path)).size;
592
533
  return {
593
- reason: 'ownership_git_history',
594
- payload: {
595
- question,
596
- questionNormalized: normalizedQuestion,
597
- mode: 'search',
598
- answer,
599
- findings: [
600
- `Derived from git history over the last ${sinceDays} day(s).`,
601
- `Focus: ${targetLabel}.`,
602
- ],
603
- confidence: topContributors.length >= 2 ? 'high' : 'medium',
604
- truth: {
605
- status: 'grounded',
606
- score,
607
- reasons: ['Answer is grounded in local git history for this repository.'],
534
+ question,
535
+ questionNormalized: normalizedQuestion,
536
+ mode: 'search',
537
+ answer: [
538
+ `I checked local git history for the last ${sinceDays} day(s).`,
539
+ `Top contributors for ${targetLabel}:`,
540
+ ...topContributors.map((entry) => ` • ${entry.author} (${entry.touches} touches)`),
541
+ ].join('\n'),
542
+ findings: [
543
+ `Derived from git history over ${sinceDays} day(s).`,
544
+ `Focus: ${targetLabel}.`,
545
+ ],
546
+ confidence: topContributors.length >= 2 ? 'high' : 'medium',
547
+ proof: {
548
+ topFiles: [...new Set(citations.map((citation) => citation.path))].slice(0, 5),
549
+ evidenceCount: citations.length,
550
+ coverage: {
608
551
  sourceCitations: citations.length,
609
552
  sourceFiles,
610
- minCitationsRequired: 1,
611
- minFilesRequired: 1,
612
- },
613
- citations,
614
- generatedAt: new Date().toISOString(),
615
- stats: {
616
- scannedFiles: 0,
617
553
  matchedFiles: sourceFiles,
618
554
  matchedLines: citations.length,
619
- brainCandidates: 0,
620
555
  },
621
556
  },
622
- };
623
- }
624
- function tryBuildDeterministicAnswer(cwd, question, normalizedQuestion) {
625
- const scope = assessQuestionScope(normalizedQuestion);
626
- if (scope.isOutOfScope) {
627
- return {
628
- payload: buildOutOfScopeAnswerPayload(question, normalizedQuestion, scope.reasons),
629
- reason: 'out_of_scope',
630
- };
631
- }
632
- const ownership = buildOwnershipDeterministicAnswer(cwd, question, normalizedQuestion);
633
- if (ownership) {
634
- return ownership;
635
- }
636
- return null;
637
- }
638
- function normalizeSnippet(line) {
639
- return line
640
- .replace(/\t/g, ' ')
641
- .replace(/\s+/g, ' ')
642
- .trim()
643
- .slice(0, 220);
557
+ truth: {
558
+ status: 'grounded',
559
+ score: Math.min(0.98, 0.66 + Math.min(sourceFiles, 6) * 0.05),
560
+ reasons: ['Answer is grounded in local git history.'],
561
+ sourceCitations: citations.length,
562
+ sourceFiles,
563
+ minCitationsRequired: 1,
564
+ minFilesRequired: 1,
565
+ },
566
+ citations,
567
+ generatedAt: new Date().toISOString(),
568
+ stats: {
569
+ scannedFiles: 0,
570
+ matchedFiles: sourceFiles,
571
+ matchedLines: citations.length,
572
+ brainCandidates: 0,
573
+ },
574
+ };
644
575
  }
645
- function addAnchorCandidates(fileTree, candidateSet, pathPriority, normalizedQuestion) {
646
- const asSet = new Set(fileTree);
647
- const pinned = [
648
- 'README.md',
649
- 'packages/cli/package.json',
650
- 'packages/cli/src/index.ts',
651
- 'packages/cli/src/api-client.ts',
652
- 'services/api/src/lib/org-context.ts',
653
- 'services/api/src/lib/user-org.ts',
654
- 'docs/cli-commands.md',
655
- 'docs/enterprise-setup.md',
576
+ function buildCommandRegistrationDeterministicAnswer(cwd, question, searchTerms, maxCitations) {
577
+ const profile = buildQueryProfile(searchTerms);
578
+ if (!profile.asksRegistration || !profile.commandFocus)
579
+ return null;
580
+ const commandName = profile.commandFocus;
581
+ const handlerName = `${commandName}Command`;
582
+ const directPattern = `\\.command\\(['"\`]${escapeRegExp(commandName)}['"\`]\\)`;
583
+ const handlerPattern = `\\b${escapeRegExp(handlerName)}\\b`;
584
+ const handlerCallPattern = `\\b(?:await\\s+)?${escapeRegExp(handlerName)}\\s*\\(`;
585
+ const rawMatches = [
586
+ ...runRipgrepSearch(cwd, directPattern),
587
+ ...runRipgrepSearch(cwd, handlerCallPattern),
588
+ ...runRipgrepSearch(cwd, handlerPattern),
656
589
  ];
657
- for (const path of pinned) {
658
- if (!asSet.has(path))
590
+ if (rawMatches.length === 0)
591
+ return null;
592
+ const dedup = new Map();
593
+ const directRegex = new RegExp(directPattern, 'i');
594
+ const handlerRegex = new RegExp(handlerPattern, 'i');
595
+ const handlerCallRegex = new RegExp(handlerCallPattern, 'i');
596
+ for (const match of rawMatches) {
597
+ if (isIgnoredSearchPath(match.path))
659
598
  continue;
660
- candidateSet.add(path);
661
- pathPriority.set(path, Math.max(pathPriority.get(path) || 0, 0.22));
599
+ if (isLikelyDocumentationPath(match.path))
600
+ continue;
601
+ let score = 0.45;
602
+ if (directRegex.test(match.snippet))
603
+ score += 7.2;
604
+ if (handlerCallRegex.test(match.snippet))
605
+ score += 3.2;
606
+ if (handlerRegex.test(match.snippet))
607
+ score += 1.4;
608
+ const pathLower = match.path.toLowerCase();
609
+ if (pathLower === 'packages/cli/src/index.ts')
610
+ score += 4.6;
611
+ if (pathLower.includes(`/commands/${commandName}.`))
612
+ score += 2.8;
613
+ if (pathLower.includes('/commands/'))
614
+ score += 0.55;
615
+ if (/\.action\(/.test(match.snippet))
616
+ score += 1.1;
617
+ if (/^\s*import\s+/.test(match.snippet))
618
+ score += 0.7;
619
+ if (score <= 0)
620
+ continue;
621
+ const key = `${match.path}:${match.line}`;
622
+ const next = {
623
+ path: match.path,
624
+ line: match.line,
625
+ snippet: match.snippet,
626
+ term: commandName,
627
+ score,
628
+ matchedTerms: [commandName],
629
+ };
630
+ const existing = dedup.get(key);
631
+ if (!existing || next.score > existing.score) {
632
+ dedup.set(key, next);
633
+ }
662
634
  }
663
- const docsLikelyUseful = /\b(feature|capabilit|offer|platform|install|setup|tenant|tenancy|architecture)\b/.test(normalizedQuestion);
664
- if (!docsLikelyUseful)
665
- return;
666
- let addedDocs = 0;
667
- for (const path of fileTree) {
668
- if (addedDocs >= 24)
669
- break;
670
- if (path === 'README.md' || (path.startsWith('docs/') && path.endsWith('.md'))) {
671
- candidateSet.add(path);
672
- pathPriority.set(path, Math.max(pathPriority.get(path) || 0, 0.18));
673
- addedDocs++;
635
+ const scored = [...dedup.values()].sort((a, b) => b.score - a.score);
636
+ if (scored.length === 0)
637
+ return null;
638
+ const citations = selectTopCitations(scored, Math.min(maxCitations, 10), searchTerms);
639
+ if (citations.length === 0)
640
+ return null;
641
+ const directCitations = citations.filter((citation) => directRegex.test(citation.snippet));
642
+ const sourceFiles = new Set(citations.map((citation) => citation.path)).size;
643
+ const topFiles = [...new Set(citations.map((citation) => citation.path))].slice(0, 5);
644
+ const answerLines = [];
645
+ if (directCitations.length > 0) {
646
+ const first = directCitations[0];
647
+ answerLines.push(`The \`${commandName}\` command is registered at ${first.path}:${first.line}.`);
648
+ answerLines.push('Supporting wiring references:');
649
+ for (const citation of citations.slice(0, 5)) {
650
+ answerLines.push(` • ${citation.path}:${citation.line} — ${normalizeSnippet(citation.snippet)}`);
674
651
  }
675
652
  }
653
+ else {
654
+ answerLines.push(`I found related wiring for \`${commandName}\`, but no direct \`.command('${commandName}')\` line yet.`);
655
+ answerLines.push('Closest references:');
656
+ for (const citation of citations.slice(0, 5)) {
657
+ answerLines.push(` • ${citation.path}:${citation.line} — ${normalizeSnippet(citation.snippet)}`);
658
+ }
659
+ }
660
+ const truthStatus = directCitations.length > 0 ? 'grounded' : 'insufficient';
661
+ const truthScore = directCitations.length > 0
662
+ ? Math.min(0.98, 0.74 + Math.min(citations.length, 6) * 0.03)
663
+ : 0.33;
664
+ return {
665
+ question,
666
+ questionNormalized: searchTerms.normalizedQuestion,
667
+ mode: 'search',
668
+ answer: answerLines.join('\n'),
669
+ findings: [
670
+ `Direct registration hits: ${directCitations.length}.`,
671
+ `Total wiring citations: ${citations.length} across ${sourceFiles} file(s).`,
672
+ ],
673
+ confidence: truthStatus === 'grounded' ? 'high' : 'low',
674
+ proof: {
675
+ topFiles,
676
+ evidenceCount: citations.length,
677
+ coverage: {
678
+ sourceCitations: citations.length,
679
+ sourceFiles,
680
+ matchedFiles: sourceFiles,
681
+ matchedLines: citations.length,
682
+ },
683
+ },
684
+ truth: {
685
+ status: truthStatus,
686
+ score: Number(truthScore.toFixed(2)),
687
+ reasons: truthStatus === 'grounded'
688
+ ? ['Command registration is grounded in direct command declaration evidence.']
689
+ : [`No direct \`.command('${commandName}')\` declaration was found.`],
690
+ sourceCitations: citations.length,
691
+ sourceFiles,
692
+ minCitationsRequired: 1,
693
+ minFilesRequired: 1,
694
+ },
695
+ citations,
696
+ generatedAt: new Date().toISOString(),
697
+ stats: {
698
+ scannedFiles: 0,
699
+ matchedFiles: sourceFiles,
700
+ matchedLines: citations.length,
701
+ brainCandidates: 0,
702
+ },
703
+ };
676
704
  }
677
- function readCliPackageName(cwd) {
678
- const pkgPath = (0, path_1.join)(cwd, 'packages/cli/package.json');
679
- if (!(0, fs_1.existsSync)(pkgPath))
705
+ function buildCommandInventoryDeterministicAnswer(cwd, question, searchTerms, maxCitations) {
706
+ const normalized = searchTerms.normalizedQuestion;
707
+ const asksInventory = /\b(list|show|what|which)\b/.test(normalized) &&
708
+ /\bcommands?\b/.test(normalized) &&
709
+ /\b(neurcode|cli|available|all)\b/.test(normalized);
710
+ if (!asksInventory)
680
711
  return null;
681
- try {
682
- const parsed = JSON.parse((0, fs_1.readFileSync)(pkgPath, 'utf-8'));
683
- if (typeof parsed.name !== 'string' || !parsed.name.trim())
684
- return null;
685
- return parsed.name.trim();
686
- }
687
- catch {
712
+ const matches = runRipgrepSearch(cwd, `\\.command\\(['"\`][a-z][a-z0-9-]*['"\`]\\)`)
713
+ .filter((row) => !isIgnoredSearchPath(row.path))
714
+ .filter((row) => !isLikelyDocumentationPath(row.path))
715
+ .filter((row) => normalizeFilePath(row.path) === 'packages/cli/src/index.ts');
716
+ if (matches.length === 0)
688
717
  return null;
718
+ const byCommand = new Map();
719
+ for (const row of matches.sort((a, b) => a.path.localeCompare(b.path) || a.line - b.line)) {
720
+ const match = row.snippet.match(/\.command\(['"`]([a-z][a-z0-9-]*)['"`]\)/i);
721
+ if (!match?.[1])
722
+ continue;
723
+ const command = match[1].trim().toLowerCase();
724
+ if (!command || byCommand.has(command))
725
+ continue;
726
+ byCommand.set(command, {
727
+ path: row.path,
728
+ line: row.line,
729
+ term: command,
730
+ snippet: row.snippet,
731
+ });
689
732
  }
733
+ const commands = [...byCommand.entries()]
734
+ .sort((a, b) => a[0].localeCompare(b[0]));
735
+ if (commands.length === 0)
736
+ return null;
737
+ const citations = commands
738
+ .slice(0, Math.max(6, Math.min(maxCitations, 30)))
739
+ .map(([, citation]) => citation);
740
+ const sourceFiles = new Set(citations.map((citation) => citation.path)).size;
741
+ const topFiles = [...new Set(citations.map((citation) => citation.path))].slice(0, 5);
742
+ const answerLines = [
743
+ `I found ${commands.length} Neurcode CLI command registrations in this repository.`,
744
+ 'Top commands and registration points:',
745
+ ...commands.slice(0, 14).map(([command, citation]) => ` • ${command} — ${citation.path}:${citation.line}`),
746
+ '',
747
+ 'If you want, ask `where is <command> command registered` for wiring details.',
748
+ ];
749
+ return {
750
+ question,
751
+ questionNormalized: normalized,
752
+ mode: 'search',
753
+ answer: answerLines.join('\n'),
754
+ findings: [
755
+ `Detected ${commands.length} command registration(s).`,
756
+ `Evidence spans ${sourceFiles} file(s).`,
757
+ ],
758
+ confidence: 'high',
759
+ proof: {
760
+ topFiles,
761
+ evidenceCount: citations.length,
762
+ coverage: {
763
+ sourceCitations: citations.length,
764
+ sourceFiles,
765
+ matchedFiles: sourceFiles,
766
+ matchedLines: citations.length,
767
+ },
768
+ },
769
+ truth: {
770
+ status: 'grounded',
771
+ score: 0.93,
772
+ reasons: ['Command list is grounded in direct `.command(...)` declarations.'],
773
+ sourceCitations: citations.length,
774
+ sourceFiles,
775
+ minCitationsRequired: 1,
776
+ minFilesRequired: 1,
777
+ },
778
+ citations,
779
+ generatedAt: new Date().toISOString(),
780
+ stats: {
781
+ scannedFiles: 0,
782
+ matchedFiles: sourceFiles,
783
+ matchedLines: citations.length,
784
+ brainCandidates: 0,
785
+ },
786
+ };
690
787
  }
691
- function extractCommandsFromCliIndex(cwd) {
692
- const indexPath = (0, path_1.join)(cwd, 'packages/cli/src/index.ts');
693
- if (!(0, fs_1.existsSync)(indexPath))
694
- return [];
788
+ function collectCommandSubcommandBlockEvidence(cwd, commandPath, subcommand, searchTerms, maxCitations) {
789
+ const fullPath = (0, path_1.join)(cwd, commandPath);
790
+ if (!(0, fs_1.existsSync)(fullPath))
791
+ return null;
695
792
  let content = '';
696
793
  try {
697
- content = (0, fs_1.readFileSync)(indexPath, 'utf-8');
794
+ content = (0, fs_1.readFileSync)(fullPath, 'utf-8');
698
795
  }
699
796
  catch {
700
- return [];
797
+ return null;
701
798
  }
702
- const commandSet = new Set();
703
- const topByVar = new Map();
704
- for (const match of content.matchAll(/(?:const\s+([A-Za-z_$][\w$]*)\s*=\s*)?program\s*\r?\n\s*\.command\('([^']+)'\)/g)) {
705
- const varName = match[1];
706
- const top = (match[2] || '').trim().split(/\s+/)[0];
707
- if (!top)
708
- continue;
709
- commandSet.add(`neurcode ${top}`);
710
- if (varName) {
711
- topByVar.set(varName, top);
799
+ const lines = content.split(/\r?\n/);
800
+ const subcommandDeclRegex = new RegExp(`\\.command\\(['"\`]${escapeRegExp(subcommand)}(?:\\s+\\[[^\\]]+\\])?['"\`]\\)`, 'i');
801
+ const anchorIdx = lines.findIndex((line) => subcommandDeclRegex.test(line));
802
+ if (anchorIdx < 0)
803
+ return null;
804
+ let endIdx = lines.length;
805
+ for (let i = anchorIdx + 1; i < lines.length; i++) {
806
+ if (/^\s*\.command\(['"`][a-z][a-z0-9-]*(?:\s+\[[^\]]+\])?['"`]\)/i.test(lines[i])) {
807
+ endIdx = i;
808
+ break;
712
809
  }
713
810
  }
714
- for (const match of content.matchAll(/([A-Za-z_$][\w$]*)\s*\r?\n\s*\.command\('([^']+)'\)/g)) {
715
- const parentVar = match[1];
716
- const parent = topByVar.get(parentVar);
717
- if (!parent)
811
+ const relevantTerms = [...searchTerms.highSignalTerms, ...searchTerms.identifiers]
812
+ .map((term) => (0, plan_cache_1.normalizeIntent)(term))
813
+ .filter((term) => term.length >= 3 && !LOW_SIGNAL_TERMS.has(term) && term !== subcommand)
814
+ .slice(0, 14);
815
+ let focusRegex = null;
816
+ const focusPattern = buildPatternFromTerms(relevantTerms);
817
+ if (focusPattern) {
818
+ try {
819
+ focusRegex = new RegExp(focusPattern, 'i');
820
+ }
821
+ catch {
822
+ focusRegex = null;
823
+ }
824
+ }
825
+ const citations = [];
826
+ const seen = new Set();
827
+ const pushLine = (lineIdx, term) => {
828
+ if (lineIdx < 0 || lineIdx >= lines.length)
829
+ return;
830
+ const snippet = normalizeSnippet(lines[lineIdx] || '');
831
+ if (!snippet)
832
+ return;
833
+ const key = `${commandPath}:${lineIdx + 1}`;
834
+ if (seen.has(key))
835
+ return;
836
+ seen.add(key);
837
+ citations.push({
838
+ path: commandPath,
839
+ line: lineIdx + 1,
840
+ term,
841
+ snippet,
842
+ });
843
+ };
844
+ pushLine(anchorIdx, subcommand);
845
+ for (let i = anchorIdx; i < endIdx; i++) {
846
+ if (citations.length >= maxCitations)
847
+ break;
848
+ const snippet = normalizeSnippet(lines[i] || '');
849
+ if (!snippet)
718
850
  continue;
719
- const sub = (match[2] || '').trim().split(/\s+/)[0];
720
- if (!sub)
851
+ const isAction = /\.action\(/.test(snippet);
852
+ const isDescription = /\.description\(/.test(snippet);
853
+ const hasFocus = focusRegex ? focusRegex.test(snippet) : false;
854
+ const hasFlowCall = /\b(?:get|load|read|list|compute|count|find|search|refresh|record|write|print|format|clear|delete|close|set)[A-Za-z0-9_]*\s*\(/.test(snippet);
855
+ const hasStateSignal = /\b(cache|memory|context|scope|stats|entries|bytes|payload|store|index)\b/i.test(snippet);
856
+ const hasOutputSignal = /\bJSON\.stringify\b|\bconsole\.(?:log|warn|error)\b|^\s*return\b/.test(snippet);
857
+ const hasOptionSignal = /\.option\(/.test(snippet);
858
+ if (!isAction && !isDescription && !hasFocus && !hasFlowCall && !hasStateSignal && !hasOutputSignal && !hasOptionSignal) {
721
859
  continue;
722
- commandSet.add(`neurcode ${parent} ${sub}`);
860
+ }
861
+ pushLine(i, hasStateSignal ? 'state' : undefined);
862
+ }
863
+ if (!citations.some((citation) => /\.action\(/.test(citation.snippet))) {
864
+ for (let i = anchorIdx; i < endIdx; i++) {
865
+ if (/\.action\(/.test(lines[i] || '')) {
866
+ pushLine(i, 'action');
867
+ break;
868
+ }
869
+ }
723
870
  }
724
- return [...commandSet].slice(0, 40);
871
+ if (citations.length === 0)
872
+ return null;
873
+ return {
874
+ anchorLine: anchorIdx + 1,
875
+ citations: citations.slice(0, maxCitations),
876
+ };
725
877
  }
726
- function extractFeatureBulletsFromMarkdown(markdown, limit, headingPattern) {
727
- const lines = markdown.split(/\r?\n/);
728
- let start = lines.findIndex((line) => headingPattern.test(line));
729
- if (start < 0)
730
- return [];
878
+ function extractCommandOperationNames(citations) {
731
879
  const out = [];
732
880
  const seen = new Set();
733
- for (let i = start + 1; i < lines.length; i++) {
734
- const line = lines[i];
735
- if (/^##\s+/.test(line))
736
- break;
737
- const rich = line.match(/^- \*\*(.+?)\*\*\s*-\s*(.+)$/);
738
- if (rich?.[1]) {
739
- const text = `${formatInsightSnippet(rich[1])} - ${formatInsightSnippet(rich[2])}`.trim();
740
- const key = text.toLowerCase();
741
- if (key && !seen.has(key)) {
742
- seen.add(key);
743
- out.push(text);
744
- }
745
- }
746
- else {
747
- const simple = line.match(/^- (.+)$/);
748
- if (!simple?.[1])
881
+ const ignored = new Set([
882
+ 'if', 'for', 'while', 'switch', 'catch', 'return',
883
+ 'async', 'argument',
884
+ 'console', 'log', 'warn', 'error', 'JSON', 'Promise',
885
+ 'Map', 'Set', 'Array', 'Object', 'String', 'Number', 'Date',
886
+ 'command', 'description', 'option', 'action',
887
+ 'filter', 'map', 'slice', 'sort', 'join', 'push',
888
+ ]);
889
+ for (const citation of citations) {
890
+ for (const match of citation.snippet.matchAll(/\b([A-Za-z_][A-Za-z0-9_]*)\s*\(/g)) {
891
+ const operation = match[1];
892
+ if (!operation || ignored.has(operation))
749
893
  continue;
750
- const text = formatInsightSnippet(simple[1]);
751
- if (!text)
894
+ if (operation.length < 3)
752
895
  continue;
753
- const key = text.toLowerCase();
754
- if (!seen.has(key)) {
755
- seen.add(key);
756
- out.push(text);
757
- }
896
+ const looksLikeCodeOperation = /[A-Z_]/.test(operation) ||
897
+ /^(get|load|read|list|compute|count|find|search|refresh|record|write|print|format|clear|delete|close|set|diagnose|resolve|build|create|update)/i.test(operation);
898
+ if (!looksLikeCodeOperation)
899
+ continue;
900
+ if (seen.has(operation))
901
+ continue;
902
+ seen.add(operation);
903
+ out.push(operation);
904
+ if (out.length >= 10)
905
+ return out;
758
906
  }
759
- if (out.length >= limit)
760
- break;
761
907
  }
762
908
  return out;
763
909
  }
764
- function extractFeatureBulletsFromReadme(cwd, limit) {
765
- const readmePath = (0, path_1.join)(cwd, 'README.md');
766
- if (!(0, fs_1.existsSync)(readmePath))
910
+ function buildCommandSubcommandFlowDeterministicAnswer(cwd, question, searchTerms, maxCitations) {
911
+ const profile = buildQueryProfile(searchTerms);
912
+ if (!profile.commandFocus || !profile.subcommandFocus)
913
+ return null;
914
+ if (profile.asksRegistration || profile.asksList)
915
+ return null;
916
+ const asksFlowLike = profile.asksHow ||
917
+ /\b(flow|trace|internals?|compute|works?|working|steps?|logic|behavior|behaviour)\b/.test(searchTerms.normalizedQuestion);
918
+ if (!asksFlowLike)
919
+ return null;
920
+ const commandName = profile.commandFocus;
921
+ const subcommand = profile.subcommandFocus;
922
+ const commandPath = `packages/cli/src/commands/${commandName}.ts`;
923
+ const blockEvidence = collectCommandSubcommandBlockEvidence(cwd, commandPath, subcommand, searchTerms, Math.min(Math.max(maxCitations, 8), 16));
924
+ if (!blockEvidence || blockEvidence.citations.length === 0)
925
+ return null;
926
+ const registrationCitations = runRipgrepSearch(cwd, `\\.command\\(['"\`]${escapeRegExp(commandName)}['"\`]\\)`)
927
+ .filter((hit) => normalizeFilePath(hit.path) === 'packages/cli/src/index.ts')
928
+ .slice(0, 1)
929
+ .map((hit) => ({
930
+ path: hit.path,
931
+ line: hit.line,
932
+ term: commandName,
933
+ snippet: hit.snippet,
934
+ }));
935
+ const subcommandDeclRegex = new RegExp(`\\.command\\(['"\`]${escapeRegExp(subcommand)}(?:\\s+\\[[^\\]]+\\])?['"\`]\\)`, 'i');
936
+ const scored = new Map();
937
+ for (const citation of registrationCitations) {
938
+ const key = `${citation.path}:${citation.line}`;
939
+ scored.set(key, {
940
+ ...citation,
941
+ score: 7.2,
942
+ matchedTerms: [commandName],
943
+ });
944
+ }
945
+ for (const citation of blockEvidence.citations) {
946
+ const key = `${citation.path}:${citation.line}`;
947
+ let score = 3.8;
948
+ if (subcommandDeclRegex.test(citation.snippet))
949
+ score += 6.2;
950
+ if (/\.action\(/.test(citation.snippet))
951
+ score += 4.2;
952
+ if (/\.description\(/.test(citation.snippet))
953
+ score += 1.0;
954
+ if (/\b(cache|memory|context|scope|stats|entries|bytes|payload|store|index)\b/i.test(citation.snippet)) {
955
+ score += 2.4;
956
+ }
957
+ if (/\b(?:get|load|read|list|compute|count|find|search|refresh|record|write|print|format|clear|delete|close|set)[A-Za-z0-9_]*\s*\(/.test(citation.snippet)) {
958
+ score += 1.8;
959
+ }
960
+ if (/\bJSON\.stringify\b|\bconsole\.(?:log|warn|error)\b|^\s*return\b/.test(citation.snippet)) {
961
+ score += 0.9;
962
+ }
963
+ scored.set(key, {
964
+ ...citation,
965
+ score,
966
+ matchedTerms: [commandName, subcommand],
967
+ });
968
+ }
969
+ const citations = [...scored.values()]
970
+ .sort((a, b) => b.score - a.score)
971
+ .slice(0, Math.min(Math.max(maxCitations, 8), 16))
972
+ .map(({ path, line, snippet, term }) => ({ path, line, snippet, term }));
973
+ if (citations.length === 0)
974
+ return null;
975
+ const hasSubcommandDeclaration = citations.some((citation) => subcommandDeclRegex.test(citation.snippet));
976
+ const hasActionBlock = citations.some((citation) => /\.action\(/.test(citation.snippet));
977
+ if (!hasSubcommandDeclaration)
978
+ return null;
979
+ const sourceFiles = new Set(citations.map((citation) => citation.path)).size;
980
+ const topFiles = [...new Set(citations.map((citation) => citation.path))].slice(0, 5);
981
+ const operations = extractCommandOperationNames(citations.filter((citation) => citation.path === commandPath));
982
+ const answerLines = [
983
+ `Short answer: \`${commandName} ${subcommand}\` is implemented in ${commandPath}:${blockEvidence.anchorLine}.`,
984
+ '',
985
+ 'What I verified in code:',
986
+ ...citations.slice(0, 8).map((citation) => ` • ${explainEvidenceCitation(citation)}`),
987
+ ];
988
+ if (operations.length > 0) {
989
+ answerLines.push('');
990
+ answerLines.push('Key operations in this flow:');
991
+ for (const operation of operations.slice(0, 8)) {
992
+ answerLines.push(` • ${operation}()`);
993
+ }
994
+ }
995
+ answerLines.push('');
996
+ answerLines.push(`If you want, I can trace control flow inside ${commandPath} line-by-line.`);
997
+ const truthScore = Math.min(0.97, 0.73 +
998
+ (hasSubcommandDeclaration ? 0.1 : 0) +
999
+ (hasActionBlock ? 0.08 : 0) +
1000
+ Math.min(citations.length * 0.01, 0.06));
1001
+ return {
1002
+ question,
1003
+ questionNormalized: searchTerms.normalizedQuestion,
1004
+ mode: 'search',
1005
+ answer: answerLines.join('\n'),
1006
+ findings: [
1007
+ `Command focus: ${commandName}, subcommand focus: ${subcommand}.`,
1008
+ `Subcommand block anchor: ${commandPath}:${blockEvidence.anchorLine}.`,
1009
+ `Evidence lines: ${citations.length} across ${sourceFiles} file(s).`,
1010
+ ],
1011
+ confidence: truthScore >= 0.9 ? 'high' : 'medium',
1012
+ proof: {
1013
+ topFiles,
1014
+ evidenceCount: citations.length,
1015
+ coverage: {
1016
+ sourceCitations: citations.length,
1017
+ sourceFiles,
1018
+ matchedFiles: sourceFiles,
1019
+ matchedLines: citations.length,
1020
+ },
1021
+ },
1022
+ truth: {
1023
+ status: hasActionBlock ? 'grounded' : 'insufficient',
1024
+ score: Number(truthScore.toFixed(2)),
1025
+ reasons: hasActionBlock
1026
+ ? ['Command subcommand flow is grounded in direct command action block evidence.']
1027
+ : ['Subcommand declaration found but action block evidence is limited.'],
1028
+ sourceCitations: citations.length,
1029
+ sourceFiles,
1030
+ minCitationsRequired: 2,
1031
+ minFilesRequired: 1,
1032
+ },
1033
+ citations,
1034
+ generatedAt: new Date().toISOString(),
1035
+ stats: {
1036
+ scannedFiles: 0,
1037
+ matchedFiles: sourceFiles,
1038
+ matchedLines: citations.length,
1039
+ brainCandidates: 0,
1040
+ },
1041
+ };
1042
+ }
1043
+ function buildAskCacheFlowDeterministicAnswer(cwd, question, searchTerms, maxCitations) {
1044
+ const normalized = searchTerms.normalizedQuestion;
1045
+ const asksAskCacheFlow = /\bask\b/.test(normalized) &&
1046
+ /\bcache\b/.test(normalized) &&
1047
+ /\b(how|flow|work|works|working|exact|steps|internals?|mechanism)\b/.test(normalized);
1048
+ if (!asksAskCacheFlow)
1049
+ return null;
1050
+ const probes = [
1051
+ { tag: 'hash', pattern: 'computeAskQuestionHash\\(' },
1052
+ { tag: 'key', pattern: 'computeAskCacheKey\\(' },
1053
+ { tag: 'exact', pattern: 'readCachedAsk\\(' },
1054
+ { tag: 'near', pattern: 'findNearCachedAsk\\(' },
1055
+ { tag: 'drift', pattern: 'getChangedWorkingTreePaths\\(' },
1056
+ { tag: 'write', pattern: 'writeCachedAsk\\(' },
1057
+ ];
1058
+ const raw = [];
1059
+ for (const probe of probes) {
1060
+ const hits = runRipgrepSearch(cwd, probe.pattern);
1061
+ for (const hit of hits) {
1062
+ const normalizedPath = normalizeFilePath(hit.path);
1063
+ if (normalizedPath !== 'packages/cli/src/commands/ask.ts' && normalizedPath !== 'packages/cli/src/utils/ask-cache.ts') {
1064
+ continue;
1065
+ }
1066
+ raw.push({ ...hit, tag: probe.tag });
1067
+ }
1068
+ }
1069
+ if (raw.length === 0)
1070
+ return null;
1071
+ const scoreByTag = {
1072
+ hash: 2.3,
1073
+ key: 2.0,
1074
+ exact: 4.8,
1075
+ near: 4.2,
1076
+ drift: 2.6,
1077
+ write: 4.6,
1078
+ };
1079
+ const dedup = new Map();
1080
+ for (const hit of raw) {
1081
+ const key = `${hit.path}:${hit.line}`;
1082
+ const score = (scoreByTag[hit.tag] || 0) + (hit.path.endsWith('/commands/ask.ts') ? 1.1 : 0.35);
1083
+ const next = {
1084
+ path: hit.path,
1085
+ line: hit.line,
1086
+ snippet: hit.snippet,
1087
+ term: hit.tag,
1088
+ score,
1089
+ matchedTerms: [hit.tag],
1090
+ };
1091
+ const existing = dedup.get(key);
1092
+ if (!existing || existing.score < next.score) {
1093
+ dedup.set(key, next);
1094
+ }
1095
+ }
1096
+ const scored = [...dedup.values()].sort((a, b) => b.score - a.score);
1097
+ if (scored.length === 0)
1098
+ return null;
1099
+ const citations = selectTopCitations(scored, Math.min(Math.max(maxCitations, 8), 16), searchTerms);
1100
+ if (citations.length === 0)
1101
+ return null;
1102
+ const hasExact = citations.some((citation) => /readCachedAsk\s*\(/.test(citation.snippet));
1103
+ const hasNear = citations.some((citation) => /findNearCachedAsk\s*\(/.test(citation.snippet));
1104
+ const hasWrite = citations.some((citation) => /writeCachedAsk\s*\(/.test(citation.snippet));
1105
+ const hasHash = citations.some((citation) => /computeAskQuestionHash\s*\(|computeAskCacheKey\s*\(/.test(citation.snippet));
1106
+ const hasDrift = citations.some((citation) => /getChangedWorkingTreePaths\s*\(/.test(citation.snippet));
1107
+ const verifiedSteps = [];
1108
+ if (hasHash)
1109
+ verifiedSteps.push('Normalize question + context into deterministic hash and cache key.');
1110
+ if (hasExact)
1111
+ verifiedSteps.push('Attempt exact cache hit first (`readCachedAsk`).');
1112
+ if (hasNear)
1113
+ verifiedSteps.push('On exact miss, attempt semantic near-hit reuse (`findNearCachedAsk`).');
1114
+ if (hasDrift)
1115
+ verifiedSteps.push('Near-hit safety checks include working-tree drift via changed paths.');
1116
+ if (hasWrite)
1117
+ verifiedSteps.push('On fresh retrieval, write the result back for future asks (`writeCachedAsk`).');
1118
+ const sourceFiles = new Set(citations.map((citation) => citation.path)).size;
1119
+ const topFiles = [...new Set(citations.map((citation) => citation.path))].slice(0, 5);
1120
+ const answerLines = [
1121
+ 'Short answer: ask cache uses an exact-hit -> near-hit -> fresh-retrieval -> write-back flow.',
1122
+ '',
1123
+ 'Verified implementation steps:',
1124
+ ...verifiedSteps.map((step, idx) => ` ${idx + 1}. ${step}`),
1125
+ '',
1126
+ 'Grounding evidence:',
1127
+ ...citations.slice(0, 8).map((citation) => ` • ${citation.path}:${citation.line} — ${normalizeSnippet(citation.snippet)}`),
1128
+ ];
1129
+ const truthScore = (hasExact && hasNear && hasWrite) ? 0.94 : 0.66;
1130
+ return {
1131
+ question,
1132
+ questionNormalized: normalized,
1133
+ mode: 'search',
1134
+ answer: answerLines.join('\n'),
1135
+ findings: [
1136
+ `Verified cache-flow steps: ${verifiedSteps.length}.`,
1137
+ `Evidence spans ${sourceFiles} file(s).`,
1138
+ ],
1139
+ confidence: truthScore >= 0.9 ? 'high' : 'medium',
1140
+ proof: {
1141
+ topFiles,
1142
+ evidenceCount: citations.length,
1143
+ coverage: {
1144
+ sourceCitations: citations.length,
1145
+ sourceFiles,
1146
+ matchedFiles: sourceFiles,
1147
+ matchedLines: citations.length,
1148
+ },
1149
+ },
1150
+ truth: {
1151
+ status: 'grounded',
1152
+ score: truthScore,
1153
+ reasons: ['Ask cache flow is grounded in direct cache function calls in the CLI implementation.'],
1154
+ sourceCitations: citations.length,
1155
+ sourceFiles,
1156
+ minCitationsRequired: 2,
1157
+ minFilesRequired: 1,
1158
+ },
1159
+ citations,
1160
+ generatedAt: new Date().toISOString(),
1161
+ stats: {
1162
+ scannedFiles: 0,
1163
+ matchedFiles: sourceFiles,
1164
+ matchedLines: citations.length,
1165
+ brainCandidates: 0,
1166
+ },
1167
+ };
1168
+ }
1169
+ function buildPatternFromTerms(terms) {
1170
+ const parts = terms
1171
+ .map((term) => term.trim())
1172
+ .filter((term) => term.length >= 2)
1173
+ .sort((a, b) => b.length - a.length)
1174
+ .slice(0, 16)
1175
+ .map((term) => {
1176
+ const escaped = escapeRegExp(term);
1177
+ if (/^[a-z0-9_]+$/i.test(term)) {
1178
+ return `\\b${escaped}\\b`;
1179
+ }
1180
+ if (term.includes(' ')) {
1181
+ return escaped.replace(/\\\s+/g, '\\s+');
1182
+ }
1183
+ return escaped;
1184
+ });
1185
+ if (parts.length === 0)
1186
+ return '';
1187
+ return `(?:${parts.join('|')})`;
1188
+ }
1189
+ function parseRgLine(line) {
1190
+ const match = line.match(/^(.*?):(\d+):(.*)$/);
1191
+ if (!match)
1192
+ return null;
1193
+ const rawPath = normalizeFilePath(match[1]);
1194
+ if (isIgnoredSearchPath(rawPath))
1195
+ return null;
1196
+ const lineNumber = Number(match[2]);
1197
+ if (!rawPath || !Number.isFinite(lineNumber) || lineNumber <= 0)
1198
+ return null;
1199
+ return {
1200
+ path: rawPath,
1201
+ line: lineNumber,
1202
+ snippet: normalizeSnippet(match[3] || ''),
1203
+ };
1204
+ }
1205
+ function runRipgrepSearch(cwd, pattern) {
1206
+ if (!pattern)
1207
+ return [];
1208
+ const args = [
1209
+ '--line-number',
1210
+ '--no-heading',
1211
+ '--color', 'never',
1212
+ '--max-count', String(RG_MAX_MATCHES),
1213
+ '--max-columns', '400',
1214
+ '--smart-case',
1215
+ '--hidden',
1216
+ '--glob', '!**/node_modules/**',
1217
+ '--glob', '!**/.git/**',
1218
+ '--glob', '!**/dist/**',
1219
+ '--glob', '!**/build/**',
1220
+ '--glob', '!**/out/**',
1221
+ '--glob', '!**/.next/**',
1222
+ '--glob', '!**/coverage/**',
1223
+ '--glob', '!**/.neurcode/**',
1224
+ '--glob', '!**/.pnpm-store/**',
1225
+ '--glob', '!*.lock',
1226
+ '--glob', '!*.map',
1227
+ '--',
1228
+ pattern,
1229
+ '.',
1230
+ ];
1231
+ const result = (0, child_process_1.spawnSync)('rg', args, {
1232
+ cwd,
1233
+ encoding: 'utf-8',
1234
+ maxBuffer: 1024 * 1024 * 80,
1235
+ stdio: ['ignore', 'pipe', 'pipe'],
1236
+ });
1237
+ const status = result.status ?? 1;
1238
+ if (status !== 0 && status !== 1) {
767
1239
  return [];
768
- try {
769
- const content = (0, fs_1.readFileSync)(readmePath, 'utf-8');
770
- return extractFeatureBulletsFromMarkdown(content, limit, /^#{2,3}\s+.*(features|capabilities|what it does|highlights|offerings|product surface|platform surface|overview)/i);
771
1240
  }
772
- catch {
1241
+ const stdout = result.stdout || '';
1242
+ if (!stdout.trim())
773
1243
  return [];
1244
+ const out = [];
1245
+ for (const raw of stdout.split(/\r?\n/)) {
1246
+ const parsed = parseRgLine(raw.trim());
1247
+ if (!parsed)
1248
+ continue;
1249
+ out.push(parsed);
1250
+ if (out.length >= RG_MAX_MATCHES)
1251
+ break;
774
1252
  }
1253
+ return out;
775
1254
  }
776
- function extractFeatureBulletsFromDocs(cwd, limit) {
777
- const docsDir = (0, path_1.join)(cwd, 'docs');
778
- if (!(0, fs_1.existsSync)(docsDir))
779
- return [];
1255
+ function fallbackScanMatches(cwd, fileTree, pattern, maxMatches = 1200) {
780
1256
  const out = [];
781
- const seen = new Set();
782
- let files = [];
1257
+ if (!pattern)
1258
+ return out;
1259
+ let regex;
783
1260
  try {
784
- files = (0, fs_1.readdirSync)(docsDir).filter((name) => name.toLowerCase().endsWith('.md')).slice(0, 40);
1261
+ regex = new RegExp(pattern, 'i');
785
1262
  }
786
1263
  catch {
787
- return [];
1264
+ return out;
788
1265
  }
789
- for (const fileName of files) {
790
- if (out.length >= limit)
1266
+ for (const filePath of fileTree) {
1267
+ if (out.length >= maxMatches)
791
1268
  break;
792
- const fullPath = (0, path_1.join)(docsDir, fileName);
1269
+ if (isIgnoredSearchPath(filePath))
1270
+ continue;
1271
+ const fullPath = (0, path_1.join)(cwd, filePath);
793
1272
  let content = '';
794
1273
  try {
1274
+ const st = (0, fs_1.statSync)(fullPath);
1275
+ if (st.size > MAX_FILE_BYTES)
1276
+ continue;
795
1277
  content = (0, fs_1.readFileSync)(fullPath, 'utf-8');
796
1278
  }
797
1279
  catch {
798
1280
  continue;
799
1281
  }
800
- const extracted = extractFeatureBulletsFromMarkdown(content, limit, /^#{2,3}\s+.*(features|capabilities|what it does|highlights|offerings|product surface|platform surface|overview)/i);
801
- for (const bullet of extracted) {
802
- const key = bullet.toLowerCase();
803
- if (seen.has(key))
1282
+ const lines = content.split(/\r?\n/);
1283
+ for (let idx = 0; idx < lines.length; idx++) {
1284
+ const line = lines[idx];
1285
+ if (!line)
804
1286
  continue;
805
- seen.add(key);
806
- out.push(bullet);
807
- if (out.length >= limit)
1287
+ if (!regex.test(line))
1288
+ continue;
1289
+ out.push({
1290
+ path: filePath,
1291
+ line: idx + 1,
1292
+ snippet: normalizeSnippet(line),
1293
+ });
1294
+ if (out.length >= maxMatches)
808
1295
  break;
809
1296
  }
810
1297
  }
811
1298
  return out;
812
1299
  }
813
- function extractFeatureBulletsForRepo(cwd, limit) {
814
- const readmeBullets = extractFeatureBulletsFromReadme(cwd, limit);
815
- if (readmeBullets.length >= limit) {
816
- return readmeBullets.slice(0, limit);
817
- }
818
- const docsBullets = extractFeatureBulletsFromDocs(cwd, limit);
819
- return [...new Set([...readmeBullets, ...docsBullets])].slice(0, limit);
820
- }
821
- function extractNeurcodeCommandsFromCitations(citations) {
822
- const commandSet = new Set();
823
- for (const citation of citations) {
824
- const snippet = citation.snippet || '';
825
- for (const match of snippet.matchAll(/\.command\('([^']+)'\)/g)) {
826
- const command = (match[1] || '').trim().split(/\s+/)[0];
827
- if (!command)
828
- continue;
829
- commandSet.add(`neurcode ${command}`);
1300
+ function collectRepoEvidence(cwd, fileTree, searchTerms, pathBoostScores) {
1301
+ const pattern = buildPatternFromTerms(searchTerms.rgTerms);
1302
+ const fromRg = runRipgrepSearch(cwd, pattern);
1303
+ const rawMatches = fromRg.length > 0 ? fromRg : fallbackScanMatches(cwd, fileTree, pattern);
1304
+ const profile = buildQueryProfile(searchTerms);
1305
+ const asksLocation = profile.asksLocation;
1306
+ const asksHow = profile.asksHow;
1307
+ const asksList = profile.asksList;
1308
+ const asksRegistration = profile.asksRegistration;
1309
+ const commandFocus = profile.commandFocus;
1310
+ const subcommandFocus = profile.subcommandFocus;
1311
+ const subcommandDeclRegex = subcommandFocus
1312
+ ? new RegExp(`\\.command\\(['"\`]${escapeRegExp(subcommandFocus)}(?:\\s+\\[[^\\]]+\\])?['"\`]\\)`, 'i')
1313
+ : null;
1314
+ const codeFocused = profile.codeFocused;
1315
+ const matchedTerms = new Set();
1316
+ const dedup = new Map();
1317
+ for (const match of rawMatches) {
1318
+ if (isIgnoredSearchPath(match.path))
1319
+ continue;
1320
+ const pathLower = match.path.toLowerCase();
1321
+ const snippetLower = (match.snippet || '').toLowerCase();
1322
+ const isDocPath = isLikelyDocumentationPath(match.path);
1323
+ const docSnippet = isLikelyDocSnippet(match.snippet);
1324
+ const codeSnippet = isLikelyCodeSnippet(match.snippet);
1325
+ const promptExample = isPromptExampleSnippet(match.snippet, searchTerms.normalizedQuestion, searchTerms.highSignalTerms);
1326
+ if (asksLocation && codeFocused && (pathLower.endsWith('.md') || pathLower.endsWith('.txt'))) {
1327
+ continue;
830
1328
  }
831
- for (const match of snippet.matchAll(/`neurcode\s+([^`]+)`/g)) {
832
- const command = (match[1] || '').trim();
833
- if (!command)
834
- continue;
835
- commandSet.add(`neurcode ${command}`);
1329
+ const termHits = searchTerms.rgTerms.filter((term) => {
1330
+ const normalized = term.toLowerCase();
1331
+ if (normalized.includes(' '))
1332
+ return snippetLower.includes(normalized);
1333
+ const pattern = new RegExp(`(?:^|[^a-z0-9])${escapeRegExp(normalized)}(?:$|[^a-z0-9])`, 'i');
1334
+ return pattern.test(snippetLower);
1335
+ });
1336
+ const highSignalHits = searchTerms.highSignalTerms.filter((term) => {
1337
+ const normalized = term.toLowerCase();
1338
+ if (normalized.includes(' '))
1339
+ return snippetLower.includes(normalized);
1340
+ const pattern = new RegExp(`(?:^|[^a-z0-9])${escapeRegExp(normalized)}(?:$|[^a-z0-9])`, 'i');
1341
+ return pattern.test(snippetLower);
1342
+ });
1343
+ const identifierHits = searchTerms.identifiers.filter((identifier) => new RegExp(`\\b${escapeRegExp(identifier)}\\b`, 'i').test(match.snippet));
1344
+ const pathHits = searchTerms.highSignalTerms.filter((term) => pathLower.includes(term)).length;
1345
+ const quotedHits = searchTerms.quotedPhrases.filter((phrase) => snippetLower.includes(phrase.toLowerCase())).length;
1346
+ let score = 0;
1347
+ score += termHits.length * 1.15;
1348
+ score += highSignalHits.length * 1.65;
1349
+ score += identifierHits.length * 2.05;
1350
+ score += pathHits * (asksLocation ? 1.95 : 1.1);
1351
+ score += quotedHits * 2.1;
1352
+ if (codeFocused && codeSnippet) {
1353
+ score += 0.95;
836
1354
  }
837
- for (const plainMatch of snippet.matchAll(/\bneurcode\s+([a-z][a-z0-9-]*(?:\s+[a-z][a-z0-9-]*)?)\b/g)) {
838
- if (!plainMatch?.[1])
839
- continue;
840
- commandSet.add(`neurcode ${plainMatch[1].trim()}`);
1355
+ if (codeFocused && !codeSnippet) {
1356
+ score -= 1.35;
841
1357
  }
842
- }
843
- return [...commandSet].slice(0, 20);
844
- }
845
- function extractCommandFocus(normalizedQuestion) {
846
- const toKnownCommand = (candidate) => {
847
- if (!candidate)
848
- return null;
849
- const normalized = candidate.toLowerCase().trim();
850
- return CLI_COMMAND_NAMES.has(normalized) ? normalized : null;
851
- };
852
- const direct = normalizedQuestion.match(/\bneurcode\s+([a-z][a-z0-9-]*)\b/);
853
- const directCommand = toKnownCommand(direct?.[1] || null);
854
- if (directCommand)
855
- return directCommand;
856
- const scoped = normalizedQuestion.match(/\b(?:under|for|within|inside|in)\s+(?:neurcode\s+)?([a-z][a-z0-9-]*)\b/);
857
- const scopedCommand = toKnownCommand(scoped?.[1] || null);
858
- if (scopedCommand)
859
- return scopedCommand;
860
- const commandPattern = normalizedQuestion.match(/\b([a-z][a-z0-9-]*)\s+command\b/);
861
- const commandPhrase = toKnownCommand(commandPattern?.[1] || null);
862
- if (commandPhrase)
863
- return commandPhrase;
864
- const mentionedCommands = [...CLI_COMMAND_NAMES].filter((command) => new RegExp(`\\b${escapeRegExp(command)}\\b`, 'i').test(normalizedQuestion));
865
- if (mentionedCommands.length !== 1)
866
- return null;
867
- 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);
868
- if (hasImplementationSignals) {
869
- return mentionedCommands[0];
870
- }
871
- return null;
872
- }
873
- function extractInstallCommandsFromCitations(citations) {
874
- const installSet = new Set();
875
- for (const citation of citations) {
876
- const snippet = citation.snippet || '';
877
- const matches = snippet.match(/\b(?:npm|pnpm|yarn)\s+(?:install|add)\s+-g\s+[@a-z0-9_.\-/]+(?:@latest)?\b/gi) || [];
878
- for (const match of matches) {
879
- installSet.add(match.trim());
1358
+ if (docSnippet) {
1359
+ score -= codeFocused ? 1.9 : 0.6;
880
1360
  }
881
- }
882
- return [...installSet].slice(0, 6);
883
- }
884
- function formatInsightSnippet(snippet) {
885
- return snippet
886
- .replace(/^[-*]\s+/, '')
887
- .replace(/^\/\/\s?/, '')
888
- .replace(/^\/\*\s?/, '')
889
- .replace(/\*\/$/, '')
890
- .replace(/^\*\s+/, '')
891
- .replace(/\s+/g, ' ')
892
- .trim()
893
- .slice(0, 180);
894
- }
895
- function countTermHitsInText(text, terms) {
896
- if (!text || terms.length === 0)
897
- return 0;
898
- let hits = 0;
899
- const isCompactText = !/\s/.test(text);
900
- const compactText = isCompactText ? text.replace(/[^a-z0-9]/g, '') : '';
901
- for (const term of terms) {
902
- const normalized = term.toLowerCase().trim();
903
- if (!normalized)
904
- continue;
905
- if (normalized.includes(' ')) {
906
- if (text.includes(normalized))
907
- hits += 1;
908
- continue;
1361
+ if (isDocPath && codeFocused) {
1362
+ score -= asksLocation ? 3.7 : 2.2;
909
1363
  }
910
- const pattern = new RegExp(`(?:^|[^a-z0-9])${escapeRegExp(normalized)}(?:$|[^a-z0-9])`, 'i');
911
- if (pattern.test(text)) {
912
- hits += 1;
913
- continue;
1364
+ if (promptExample) {
1365
+ score -= codeFocused ? 4.4 : 2.2;
914
1366
  }
915
- if (isCompactText) {
916
- const compactTerm = normalized.replace(/[^a-z0-9]/g, '');
917
- if (compactTerm.length >= 3 && compactText.includes(compactTerm)) {
918
- hits += 1;
919
- }
1367
+ if (/\b(?:export\s+)?(?:function|class|const|interface|type|enum)\b/.test(match.snippet)) {
1368
+ score += 0.85;
920
1369
  }
921
- }
922
- return hits;
923
- }
924
- function findInterfaceFieldBlock(lines, startLine) {
925
- const startIndex = Math.max(0, startLine - 1);
926
- const lookbackStart = Math.max(0, startIndex - 14);
927
- const lookaheadEnd = Math.min(lines.length - 1, startIndex + 10);
928
- let headerIndex = -1;
929
- let interfaceName = '';
930
- for (let i = startIndex; i >= lookbackStart; i--) {
931
- const match = lines[i].match(/^\s*(?:export\s+)?interface\s+([A-Za-z_][A-Za-z0-9_]*)\b/);
932
- if (!match?.[1])
933
- continue;
934
- headerIndex = i;
935
- interfaceName = match[1];
936
- break;
937
- }
938
- if (headerIndex < 0) {
939
- for (let i = startIndex; i <= lookaheadEnd; i++) {
940
- const match = lines[i].match(/^\s*(?:export\s+)?interface\s+([A-Za-z_][A-Za-z0-9_]*)\b/);
941
- if (!match?.[1])
942
- continue;
943
- headerIndex = i;
944
- interfaceName = match[1];
945
- break;
1370
+ if (asksLocation && /\b(?:function|class|interface|type)\b/.test(match.snippet)) {
1371
+ score += 0.75;
946
1372
  }
947
- }
948
- if (headerIndex < 0 || !interfaceName)
949
- return null;
950
- let opened = false;
951
- let depth = 0;
952
- const fields = [];
953
- const seen = new Set();
954
- for (let i = headerIndex; i < Math.min(lines.length, headerIndex + 120); i++) {
955
- const line = lines[i];
956
- if (!opened) {
957
- if (line.includes('{')) {
958
- opened = true;
959
- depth = 1;
960
- }
961
- continue;
1373
+ if (asksHow && /\b(if|else|return|await|for|while|switch|try|catch)\b/.test(match.snippet)) {
1374
+ score += 0.55;
1375
+ }
1376
+ if (asksList && /\.command\(|\.option\(|\bneurcode\s+[a-z]/i.test(match.snippet)) {
1377
+ score += 0.7;
1378
+ }
1379
+ if (asksRegistration && /(?:^|[^a-z])(?:register|registered|registration)(?:$|[^a-z])/i.test(match.snippet)) {
1380
+ score += 0.8;
962
1381
  }
963
- const fieldMatch = line.match(/^\s*([A-Za-z_][A-Za-z0-9_?]*)\s*:/);
964
- if (fieldMatch?.[1]) {
965
- const field = fieldMatch[1].replace(/\?$/, '');
966
- if (!seen.has(field)) {
967
- seen.add(field);
968
- fields.push(field);
1382
+ if (asksRegistration && /\.command\(|program\.command\(/i.test(match.snippet)) {
1383
+ score += 2.3;
1384
+ }
1385
+ if (asksLocation && profile.highSignalSet.has('middleware') && pathLower.includes('/middleware/')) {
1386
+ score += 1.9;
1387
+ }
1388
+ if (asksLocation && profile.highSignalSet.has('middleware') && !pathLower.includes('middleware')) {
1389
+ score -= 1.7;
1390
+ }
1391
+ if (asksLocation && profile.highSignalSet.has('auth') && /(?:^|\/)auth(?:[-_.]|\/|\.|$)/.test(pathLower)) {
1392
+ score += 1.25;
1393
+ }
1394
+ if (asksLocation && profile.highSignalSet.has('orgid') && /\borgid\b|organizationid/i.test(match.snippet)) {
1395
+ score += 1.45;
1396
+ }
1397
+ if (commandFocus) {
1398
+ if (new RegExp(`\\.command\\(['"\`]${escapeRegExp(commandFocus)}['"\`]\\)`, 'i').test(match.snippet)) {
1399
+ score += 6.2;
1400
+ }
1401
+ if (asksRegistration && pathLower === 'packages/cli/src/index.ts') {
1402
+ score += 2.8;
1403
+ }
1404
+ if (pathLower.includes(`/commands/${commandFocus}.`)) {
1405
+ score += asksHow ? 3.2 : 1.8;
1406
+ if (subcommandDeclRegex && subcommandDeclRegex.test(match.snippet)) {
1407
+ score += 4.1;
1408
+ }
1409
+ }
1410
+ else if (pathLower.includes('/commands/')) {
1411
+ score -= asksHow ? 1.85 : 0.45;
1412
+ }
1413
+ if (asksHow && pathLower.includes('/commands/') && !pathLower.includes(`/commands/${commandFocus}.`) && pathLower !== 'packages/cli/src/index.ts') {
1414
+ score -= 1.05;
1415
+ }
1416
+ if (commandFocus !== 'ask' && pathLower.endsWith('/commands/ask.ts')) {
1417
+ score -= 3.2;
969
1418
  }
970
1419
  }
971
- for (const ch of line) {
972
- if (ch === '{')
973
- depth += 1;
974
- if (ch === '}')
975
- depth -= 1;
1420
+ if (codeFocused && (pathLower.endsWith('.md') || pathLower.startsWith('docs/'))) {
1421
+ score -= 1.35;
1422
+ }
1423
+ if (codeFocused && (pathLower.endsWith('.txt') || pathLower.includes('audit'))) {
1424
+ score -= 1.8;
1425
+ }
1426
+ if (codeFocused && /(?:^|\/)(pnpm-lock\.yaml|package-lock\.json|yarn\.lock)$/i.test(pathLower)) {
1427
+ score -= 4.5;
1428
+ }
1429
+ if (codeFocused && /\bneurcode\s+(ask|plan|verify|ship)\s+["`]/i.test(match.snippet)) {
1430
+ score -= 2.4;
1431
+ }
1432
+ if (codeFocused && /\?/.test(match.snippet) && /\b(neurcode|ask|plan)\b/i.test(match.snippet)) {
1433
+ score -= 1.1;
1434
+ }
1435
+ if (codeFocused && /\bnew Set\(\[/.test(match.snippet)) {
1436
+ score -= 1.1;
1437
+ }
1438
+ if (codeFocused && /\\b\([^)]*\|[^)]*\)/.test(match.snippet) && /\.test\(/.test(match.snippet)) {
1439
+ score -= 1.45;
1440
+ }
1441
+ if (pathLower.includes('/commands/')) {
1442
+ score += 0.15;
1443
+ }
1444
+ if (asksRegistration && pathLower.endsWith('/commands/ask.ts')) {
1445
+ score -= 2.4;
1446
+ }
1447
+ if (asksLocation && codeFocused && pathLower.endsWith('/commands/ask.ts')) {
1448
+ score -= 2.9;
1449
+ }
1450
+ const boost = pathBoostScores.get(match.path) || 0;
1451
+ if (boost > 0) {
1452
+ const cappedBoost = codeFocused && isDocPath
1453
+ ? Math.min(boost, 0.06)
1454
+ : Math.min(boost, 0.45);
1455
+ score += cappedBoost;
1456
+ }
1457
+ if (score <= 0)
1458
+ continue;
1459
+ for (const term of highSignalHits) {
1460
+ matchedTerms.add(term);
1461
+ }
1462
+ const dominantTerm = highSignalHits[0] ||
1463
+ identifierHits[0] ||
1464
+ termHits[0] ||
1465
+ '';
1466
+ const key = `${match.path}:${match.line}`;
1467
+ const next = {
1468
+ path: match.path,
1469
+ line: match.line,
1470
+ snippet: match.snippet,
1471
+ term: dominantTerm || undefined,
1472
+ score,
1473
+ matchedTerms: [...new Set([...highSignalHits, ...identifierHits, ...termHits])],
1474
+ };
1475
+ const existing = dedup.get(key);
1476
+ if (!existing || existing.score < next.score) {
1477
+ dedup.set(key, next);
976
1478
  }
977
- if (depth <= 0)
978
- break;
979
1479
  }
980
- if (fields.length === 0)
981
- return null;
982
- return { interfaceName, fields };
1480
+ const scoredCitations = [...dedup.values()]
1481
+ .sort((a, b) => b.score - a.score)
1482
+ .slice(0, MAX_RAW_CITATIONS);
1483
+ return {
1484
+ scoredCitations,
1485
+ matchedTerms,
1486
+ scannedFiles: fileTree.length,
1487
+ };
983
1488
  }
984
- function splitIdentifierTokens(value) {
985
- if (!value)
1489
+ function selectTopCitations(scored, maxCitations, searchTerms) {
1490
+ if (scored.length === 0)
986
1491
  return [];
987
- const tokens = value
988
- .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
989
- .replace(/[_\-\s]+/g, ' ')
990
- .toLowerCase()
991
- .split(/\s+/)
992
- .map((token) => token.trim())
993
- .filter((token) => token.length >= 2);
994
- return [...new Set(tokens)];
995
- }
996
- function buildSchemaFieldSummaries(citations, lineCache, terms) {
997
- const scored = [];
998
- const seen = new Set();
999
- const highSignalTerms = terms.filter((term) => !LOW_SIGNAL_TERMS.has(term));
1000
- const queryTermSet = new Set(highSignalTerms.map((term) => term.toLowerCase()));
1001
- for (const citation of citations) {
1002
- const lines = lineCache.get(citation.path);
1003
- if (!lines || lines.length === 0)
1004
- continue;
1005
- const parsed = findInterfaceFieldBlock(lines, citation.line);
1006
- if (!parsed)
1007
- continue;
1008
- const interfaceKey = `${citation.path}:${parsed.interfaceName}`;
1009
- if (seen.has(interfaceKey))
1010
- continue;
1011
- const ifaceLower = parsed.interfaceName.toLowerCase();
1012
- const ifaceTokens = splitIdentifierTokens(parsed.interfaceName);
1013
- const ifaceText = `${ifaceLower} ${ifaceTokens.join(' ')}`.trim();
1014
- const fieldText = parsed.fields.join(' ').toLowerCase();
1015
- const nameHits = countTermHitsInText(ifaceText, highSignalTerms);
1016
- const fieldHits = countTermHitsInText(fieldText, highSignalTerms);
1017
- let relevanceScore = nameHits * 2 + fieldHits;
1018
- if (queryTermSet.has('plan') && ifaceText.includes('plan'))
1019
- relevanceScore += 0.8;
1020
- if (queryTermSet.has('cache') && ifaceText.includes('cache'))
1021
- relevanceScore += 0.8;
1022
- if (queryTermSet.has('key') && ifaceText.includes('key'))
1023
- relevanceScore += 1.2;
1024
- if ((queryTermSet.has('field') || queryTermSet.has('fields')) && parsed.fields.length > 0) {
1025
- relevanceScore += Math.min(1, parsed.fields.length / 8);
1026
- }
1027
- if (highSignalTerms.length > 0 && relevanceScore === 0) {
1028
- continue;
1492
+ const profile = buildQueryProfile(searchTerms);
1493
+ const asksLocationCode = profile.asksLocation && profile.codeFocused;
1494
+ const structuralTerms = new Set([
1495
+ 'middleware', 'route', 'routes', 'service', 'services', 'controller', 'controllers',
1496
+ 'command', 'commands', 'schema', 'model', 'models', 'db', 'database', 'api', 'auth',
1497
+ 'cache', 'plan', 'verify', 'ship', 'apply', 'watch',
1498
+ ]);
1499
+ const sorted = [...scored].sort((a, b) => b.score - a.score);
1500
+ const topScore = sorted[0]?.score || 0;
1501
+ const scoreFloor = asksLocationCode ? topScore * 0.45 : topScore * 0.18;
1502
+ let candidates = sorted.filter((citation) => citation.score >= scoreFloor);
1503
+ if (candidates.length === 0) {
1504
+ candidates = sorted.slice(0, maxCitations * 3);
1505
+ }
1506
+ if (asksLocationCode) {
1507
+ const anchorTerms = searchTerms.highSignalTerms.filter((term) => structuralTerms.has(term.toLowerCase()));
1508
+ if (anchorTerms.length > 0) {
1509
+ const anchored = candidates.filter((citation) => {
1510
+ const pathLower = citation.path.toLowerCase();
1511
+ return anchorTerms.some((term) => pathLower.includes(term.toLowerCase()));
1512
+ });
1513
+ if (anchored.length >= Math.min(2, maxCitations)) {
1514
+ candidates = anchored;
1515
+ }
1516
+ }
1517
+ const strict = candidates.filter((citation) => !isLikelyDocumentationPath(citation.path) &&
1518
+ !isLikelyDocSnippet(citation.snippet) &&
1519
+ !isPromptExampleSnippet(citation.snippet, searchTerms.normalizedQuestion, searchTerms.highSignalTerms) &&
1520
+ isLikelyCodeSnippet(citation.snippet));
1521
+ if (strict.length >= Math.min(maxCitations, 3)) {
1522
+ candidates = strict;
1523
+ }
1524
+ else {
1525
+ const merged = [];
1526
+ const seen = new Set();
1527
+ for (const citation of [...strict, ...candidates]) {
1528
+ const key = `${citation.path}:${citation.line}`;
1529
+ if (seen.has(key))
1530
+ continue;
1531
+ seen.add(key);
1532
+ merged.push(citation);
1533
+ }
1534
+ candidates = merged;
1535
+ }
1536
+ }
1537
+ if (profile.codeFocused && !asksLocationCode) {
1538
+ const identifierAnchors = searchTerms.identifiers
1539
+ .map((term) => (0, plan_cache_1.normalizeIntent)(term))
1540
+ .filter((term) => term.length >= 3);
1541
+ const termAnchors = searchTerms.highSignalTerms
1542
+ .map((term) => term.toLowerCase().trim())
1543
+ .filter((term) => term.length >= 3 && !LOW_SIGNAL_TERMS.has(term));
1544
+ const anchors = [...new Set([...identifierAnchors, ...termAnchors])].slice(0, 8);
1545
+ if (anchors.length > 0) {
1546
+ const anchored = candidates.filter((citation) => {
1547
+ const pathLower = citation.path.toLowerCase();
1548
+ const snippetLower = citation.snippet.toLowerCase();
1549
+ return anchors.some((term) => {
1550
+ if (pathLower.includes(term))
1551
+ return true;
1552
+ const pattern = new RegExp(`(?:^|[^a-z0-9])${escapeRegExp(term)}(?:$|[^a-z0-9])`, 'i');
1553
+ return pattern.test(snippetLower);
1554
+ });
1555
+ });
1556
+ if (anchored.length >= Math.min(maxCitations, 4)) {
1557
+ candidates = anchored;
1558
+ }
1029
1559
  }
1030
- seen.add(interfaceKey);
1031
- scored.push({
1032
- score: relevanceScore,
1033
- summary: `${parsed.interfaceName}: ${parsed.fields.slice(0, 12).join(', ')}`,
1560
+ }
1561
+ if (profile.commandFocus) {
1562
+ const commandNeedle = `/commands/${profile.commandFocus}.`;
1563
+ const commandAnchored = candidates.filter((citation) => {
1564
+ const pathLower = citation.path.toLowerCase();
1565
+ return pathLower.includes(commandNeedle) || pathLower === 'packages/cli/src/index.ts';
1034
1566
  });
1567
+ if (commandAnchored.length >= Math.min(maxCitations, 3)) {
1568
+ candidates = commandAnchored;
1569
+ }
1035
1570
  }
1036
- const ranked = scored.sort((a, b) => b.score - a.score);
1037
- if (ranked.length === 0)
1038
- return [];
1039
- const topScore = ranked[0].score;
1040
- let minScore = topScore >= 2 ? topScore * 0.6 : 0;
1041
- const strictExactFieldRequest = queryTermSet.has('exact') &&
1042
- (queryTermSet.has('field') || queryTermSet.has('fields'));
1043
- if (strictExactFieldRequest) {
1044
- minScore = Math.max(minScore, topScore - 0.5);
1045
- }
1046
- return ranked
1047
- .filter((item) => item.score >= minScore)
1048
- .slice(0, 6)
1049
- .map((item) => item.summary);
1050
- }
1051
- function extractInsightLines(citations, limit) {
1052
- const out = [];
1053
- const seen = new Set();
1054
- for (const citation of citations) {
1055
- const line = formatInsightSnippet(citation.snippet || '');
1056
- if (!line || line.length < 10)
1057
- continue;
1058
- if (/^#+\s/.test(line))
1059
- continue;
1060
- if (/^name:\s+/i.test(line))
1061
- continue;
1062
- const tooNoisy = /[{}[\];=><]/.test(line) && !/\b(multi-tenant|single-tenant|organization|install|command|feature)\b/i.test(line);
1063
- if (tooNoisy)
1064
- continue;
1065
- const key = line.toLowerCase();
1066
- if (seen.has(key))
1067
- continue;
1068
- seen.add(key);
1069
- out.push(line);
1070
- if (out.length >= limit)
1071
- break;
1571
+ const byFile = new Map();
1572
+ for (const citation of candidates) {
1573
+ const bucket = byFile.get(citation.path) || [];
1574
+ bucket.push(citation);
1575
+ byFile.set(citation.path, bucket);
1072
1576
  }
1073
- return out;
1074
- }
1075
- function isCommandCatalogIntent(normalizedQuestion) {
1076
- const mentionsCommandSurface = /\b(cli|cmd|cmds|command|commands|subcommand|subcommands)\b/.test(normalizedQuestion);
1077
- if (!mentionsCommandSurface)
1078
- return false;
1079
- const listIntent = /\blist\b/.test(normalizedQuestion) ||
1080
- /\bshow\b/.test(normalizedQuestion) ||
1081
- /\bavailable\b/.test(normalizedQuestion) ||
1082
- /\ball commands?\b/.test(normalizedQuestion) ||
1083
- /\bwhich commands\b/.test(normalizedQuestion) ||
1084
- /\bwhat commands\b/.test(normalizedQuestion) ||
1085
- /\bwhat can i (type|run)\b/.test(normalizedQuestion) ||
1086
- /\bcan i type\b/.test(normalizedQuestion) ||
1087
- /\bcmds\b/.test(normalizedQuestion);
1088
- if (!listIntent)
1089
- return false;
1090
- const specificIntent = /\bwhere\b/.test(normalizedQuestion) ||
1091
- /\bhow\b/.test(normalizedQuestion) ||
1092
- /\bwhy\b/.test(normalizedQuestion) ||
1093
- /\bwhen\b/.test(normalizedQuestion) ||
1094
- /\bwhich file\b/.test(normalizedQuestion) ||
1095
- /\bin which file\b/.test(normalizedQuestion) ||
1096
- /\bfilepath\b/.test(normalizedQuestion) ||
1097
- /\bfile path\b/.test(normalizedQuestion) ||
1098
- /\binject(?:ed)?\b/.test(normalizedQuestion) ||
1099
- /\bhandle(?:s|d)?\b/.test(normalizedQuestion) ||
1100
- /\bused?\b/.test(normalizedQuestion) ||
1101
- /\bflow\b/.test(normalizedQuestion);
1102
- return !specificIntent;
1103
- }
1104
- function isPrimarySourcePath(filePath) {
1105
- const normalized = filePath.trim().replace(/\\/g, '/').toLowerCase();
1106
- if (!normalized)
1107
- return false;
1108
- if (normalized.startsWith('.neurcode/') ||
1109
- normalized.startsWith('.github/') ||
1110
- normalized.startsWith('.git/') ||
1111
- normalized.startsWith('node_modules/') ||
1112
- normalized.startsWith('dist/') ||
1113
- normalized.startsWith('build/') ||
1114
- normalized.startsWith('coverage/')) {
1115
- return false;
1577
+ for (const list of byFile.values()) {
1578
+ list.sort((a, b) => b.score - a.score);
1116
1579
  }
1117
- if (normalized.includes('/dist/') || normalized.includes('/build/') || normalized.includes('/coverage/')) {
1118
- return false;
1580
+ const selected = [];
1581
+ // Pass 1: strongest line per file.
1582
+ for (const [_, list] of [...byFile.entries()].sort((a, b) => b[1][0].score - a[1][0].score)) {
1583
+ if (selected.length >= maxCitations)
1584
+ break;
1585
+ selected.push(list.shift());
1119
1586
  }
1120
- const ext = normalized.includes('.') ? normalized.split('.').pop() || '' : '';
1121
- if (!ext) {
1122
- // Allow extensionless source-like files (e.g. Dockerfile, Makefile).
1123
- return true;
1587
+ // Pass 2: fill remaining by global score.
1588
+ const remainder = [...byFile.values()].flat().sort((a, b) => b.score - a.score);
1589
+ for (const citation of remainder) {
1590
+ if (selected.length >= maxCitations)
1591
+ break;
1592
+ selected.push(citation);
1124
1593
  }
1125
- return PRIMARY_SOURCE_EXTENSIONS.has(ext);
1594
+ return selected.map(({ path, line, snippet, term }) => ({ path, line, snippet, term }));
1126
1595
  }
1127
- function isBroadQuestion(normalizedQuestion) {
1128
- return (/\banywhere\b/.test(normalizedQuestion) ||
1129
- /\bacross\b/.test(normalizedQuestion) ||
1130
- /\bworkflow\b/.test(normalizedQuestion) ||
1131
- /\bentire\b/.test(normalizedQuestion) ||
1132
- /\bwhole\b/.test(normalizedQuestion) ||
1133
- /\ball\b/.test(normalizedQuestion) ||
1134
- /\bin repo\b/.test(normalizedQuestion));
1135
- }
1136
- function calibrateConfidence(truth) {
1137
- if (truth.status === 'insufficient')
1138
- return 'low';
1139
- if (truth.score >= 0.78)
1140
- return 'high';
1141
- if (truth.score >= 0.52)
1142
- return 'medium';
1143
- return 'low';
1144
- }
1145
- function evaluateTruthAssessment(mode, normalizedQuestion, terms, sourceCitations, sourcePerFileCounts, sourceTermCounts) {
1146
- const broadQuestion = isBroadQuestion(normalizedQuestion);
1147
- const minCitationsRequired = mode === 'comparison' ? 2 : 2;
1148
- let minFilesRequired = mode === 'comparison' ? 2 : 1;
1149
- const sourceCitationCount = sourceCitations.length;
1150
- const sourceFileCount = sourcePerFileCounts.size;
1151
- const hasStrongSingleFileEvidence = sourceFileCount === 1 && sourceCitationCount >= 6;
1152
- if (broadQuestion) {
1153
- minFilesRequired = hasStrongSingleFileEvidence ? minFilesRequired : Math.max(minFilesRequired, 2);
1154
- }
1596
+ function evaluateTruth(normalizedQuestion, highSignalTerms, citations) {
1597
+ const sourceCitations = citations.length;
1598
+ const sourceFiles = new Set(citations.map((c) => c.path)).size;
1599
+ const asksLocation = /\b(where|which file|location|defined|implemented|called|computed|resolved)\b/.test(normalizedQuestion);
1600
+ const asksList = /\b(list|all|available|commands|files|features)\b/.test(normalizedQuestion);
1601
+ const commandFocus = detectCommandFocus(normalizedQuestion);
1602
+ const subcommandFocus = detectSubcommandFocus(normalizedQuestion, commandFocus);
1603
+ const minCitationsRequired = asksLocation ? 1 : asksList ? 3 : 2;
1604
+ const minFilesRequired = asksLocation ? 1 : 2;
1605
+ const matchedHighSignalTerms = highSignalTerms.filter((term) => {
1606
+ const normalized = term.toLowerCase();
1607
+ return citations.some((citation) => {
1608
+ const haystack = `${citation.path} ${citation.snippet}`.toLowerCase();
1609
+ if (normalized.includes(' '))
1610
+ return haystack.includes(normalized);
1611
+ const pattern = new RegExp(`(?:^|[^a-z0-9])${escapeRegExp(normalized)}(?:$|[^a-z0-9])`, 'i');
1612
+ return pattern.test(haystack);
1613
+ });
1614
+ });
1615
+ const coverage = highSignalTerms.length === 0
1616
+ ? (sourceCitations > 0 ? 1 : 0)
1617
+ : matchedHighSignalTerms.length / highSignalTerms.length;
1618
+ const citationScore = Math.min(1, sourceCitations / Math.max(minCitationsRequired, 3));
1619
+ const fileScore = Math.min(1, sourceFiles / Math.max(minFilesRequired, 2));
1620
+ let score = Math.max(0, Math.min(1, citationScore * 0.45 + fileScore * 0.35 + coverage * 0.2));
1155
1621
  const reasons = [];
1156
- if (sourceCitationCount === 0) {
1157
- reasons.push('No direct source-file evidence was found for the query terms.');
1158
- }
1159
- if (sourceCitationCount < minCitationsRequired) {
1160
- reasons.push(`Only ${sourceCitationCount} source citation(s) found (minimum ${minCitationsRequired} required).`);
1161
- }
1162
- if (sourceFileCount < minFilesRequired) {
1163
- reasons.push(`Evidence spans ${sourceFileCount} file(s) (minimum ${minFilesRequired} required for this question).`);
1164
- }
1165
- if (mode === 'comparison' && terms.length >= 2) {
1166
- const missingTerms = terms.filter((term) => (sourceTermCounts.get(term) || 0) === 0);
1167
- if (missingTerms.length > 0) {
1168
- reasons.push(`Missing direct source evidence for term(s): ${missingTerms.join(', ')}.`);
1169
- }
1170
- }
1171
- if (mode === 'search') {
1172
- const highSignalTerms = terms.filter((term) => !LOW_SIGNAL_TERMS.has(term));
1173
- if (highSignalTerms.length > 0) {
1174
- const matchedHighSignalTerms = highSignalTerms.filter((term) => (sourceTermCounts.get(term) || 0) > 0);
1175
- if (matchedHighSignalTerms.length === 0) {
1176
- reasons.push('No high-signal query terms were grounded in source citations.');
1622
+ if (sourceCitations < minCitationsRequired) {
1623
+ reasons.push(`Only ${sourceCitations} citation(s) found (minimum ${minCitationsRequired} expected).`);
1624
+ }
1625
+ if (sourceFiles < minFilesRequired) {
1626
+ reasons.push(`Evidence spans ${sourceFiles} file(s) (minimum ${minFilesRequired} expected).`);
1627
+ }
1628
+ if (highSignalTerms.length > 0 && coverage < 0.4) {
1629
+ reasons.push('Important query terms were not well represented in matched evidence.');
1630
+ }
1631
+ if (commandFocus) {
1632
+ const commandNeedle = `/commands/${commandFocus}.`;
1633
+ const commandFileCitations = citations.filter((citation) => citation.path.toLowerCase().includes(commandNeedle));
1634
+ const registrationHits = citations.filter((citation) => citation.path.toLowerCase() === 'packages/cli/src/index.ts' &&
1635
+ new RegExp(`\\.command\\(['"\`]${escapeRegExp(commandFocus)}['"\`]\\)`, 'i').test(citation.snippet));
1636
+ const anchoredCount = commandFileCitations.length + registrationHits.length;
1637
+ const anchoredRatio = sourceCitations > 0 ? anchoredCount / sourceCitations : 0;
1638
+ if (anchoredCount === 0) {
1639
+ reasons.push(`No direct evidence found in ${commandNeedle} or command registration wiring.`);
1640
+ }
1641
+ else if (sourceCitations >= 2 && anchoredRatio < 0.35) {
1642
+ reasons.push('Top evidence is weakly anchored to the referenced command implementation.');
1643
+ }
1644
+ if (subcommandFocus) {
1645
+ const subcommandDeclRegex = new RegExp(`\\.command\\(['"\`]${escapeRegExp(subcommandFocus)}(?:\\s+\\[[^\\]]+\\])?['"\`]\\)`, 'i');
1646
+ const hasSubcommandDeclaration = citations.some((citation) => citation.path.toLowerCase().includes(commandNeedle) &&
1647
+ subcommandDeclRegex.test(citation.snippet));
1648
+ if (!hasSubcommandDeclaration) {
1649
+ reasons.push(`No direct \`${subcommandFocus}\` subcommand declaration found in ${commandNeedle}.`);
1177
1650
  }
1178
1651
  }
1179
- }
1180
- let dominantShare = 0;
1181
- if (sourceCitationCount > 0) {
1182
- const highest = Math.max(...sourcePerFileCounts.values());
1183
- dominantShare = highest / sourceCitationCount;
1184
- }
1185
- if (broadQuestion && dominantShare > 0.85 && sourceFileCount < 3 && !hasStrongSingleFileEvidence) {
1186
- reasons.push('Evidence is highly concentrated in one file; broader coverage is required for this query.');
1187
- }
1188
- const citationScore = Math.min(1, sourceCitationCount / Math.max(minCitationsRequired, 4));
1189
- const fileScore = Math.min(1, sourceFileCount / Math.max(minFilesRequired, 3));
1190
- let termCoverageScore = 0;
1191
- if (mode === 'comparison' && terms.length >= 2) {
1192
- termCoverageScore = terms.filter((term) => (sourceTermCounts.get(term) || 0) > 0).length / terms.length;
1193
- }
1194
- else if (mode === 'search') {
1195
- const highSignalTerms = terms.filter((term) => !LOW_SIGNAL_TERMS.has(term));
1196
- if (highSignalTerms.length === 0) {
1197
- termCoverageScore = sourceCitationCount > 0 ? 1 : 0;
1652
+ if (anchoredRatio < 0.35) {
1653
+ score *= 0.68;
1198
1654
  }
1199
- else {
1200
- termCoverageScore = highSignalTerms.filter((term) => (sourceTermCounts.get(term) || 0) > 0).length / highSignalTerms.length;
1655
+ else if (anchoredRatio < 0.55) {
1656
+ score *= 0.86;
1201
1657
  }
1202
1658
  }
1203
- const concentrationPenalty = dominantShare > 0.9 ? 0.08 : dominantShare > 0.75 ? 0.04 : 0;
1204
- let score = citationScore * 0.45 + fileScore * 0.35 + termCoverageScore * 0.2 - concentrationPenalty;
1205
- if (!Number.isFinite(score))
1206
- score = 0;
1207
- score = Math.max(0, Math.min(score, 1));
1208
- if (score < 0.42 && reasons.length === 0) {
1209
- reasons.push('Evidence quality score is below the confidence threshold.');
1659
+ if (score < 0.4 && reasons.length === 0) {
1660
+ reasons.push('Evidence quality is below the confidence threshold.');
1210
1661
  }
1211
1662
  return {
1212
- status: reasons.length > 0 ? 'insufficient' : 'grounded',
1663
+ status: reasons.length === 0 ? 'grounded' : 'insufficient',
1213
1664
  score,
1214
1665
  reasons,
1215
- sourceCitations: sourceCitationCount,
1216
- sourceFiles: sourceFileCount,
1666
+ sourceCitations,
1667
+ sourceFiles,
1217
1668
  minCitationsRequired,
1218
1669
  minFilesRequired,
1219
1670
  };
1220
1671
  }
1221
- function buildAnswer(mode, question, terms, citations, stats, termCounts, perFileCounts, truth, context = {}) {
1222
- const confidence = calibrateConfidence(truth);
1223
- const normalizedQuestion = (0, plan_cache_1.normalizeIntent)(question);
1224
- const findings = [];
1225
- let answer = '';
1226
- const asksCommandCatalog = isCommandCatalogIntent(normalizedQuestion);
1227
- const asksInstall = /\b(install|upgrade|update|latest)\b/.test(normalizedQuestion);
1228
- const asksFeatures = /\b(feature|features|capability|capabilities|offers?)\b/.test(normalizedQuestion);
1229
- const asksSchema = /\b(field|fields|key|keys|schema|interface|input|output|parameter|parameters)\b/.test(normalizedQuestion);
1230
- const asksTenancy = /\b(tenant|tenancy|single|multi|organization)\b/.test(normalizedQuestion);
1231
- const asksLocation = /\b(where|which file|in which file|filepath|file path|location|defined|implemented|called|computed|resolved)\b/.test(normalizedQuestion);
1232
- const asksHow = /\bhow\b/.test(normalizedQuestion);
1233
- const asksDecision = /\b(decide|decision|verdict|pass|fail)\b/.test(normalizedQuestion);
1234
- const asksCommandSurface = /\b(command|commands|subcommand|subcommands|flag|flags|option|options)\b/.test(normalizedQuestion);
1235
- const asksSingleCommandLookup = /\b(what|which)\s+command\b/.test(normalizedQuestion) &&
1236
- !/\b(commands|subcommands)\b/.test(normalizedQuestion);
1237
- const asksOrgRequestInjection = /\b(inject|injected|header|request|requests)\b/.test(normalizedQuestion) &&
1238
- /\b(org|organization)\b/.test(normalizedQuestion);
1239
- const extractedCommandMatches = extractNeurcodeCommandsFromCitations(citations);
1240
- const knownCommands = context.knownCommands || [];
1241
- const knownCommandSet = new Set(knownCommands);
1242
- const knownRoots = new Set(knownCommands.map((command) => command.split(/\s+/).slice(0, 2).join(' ')));
1243
- const filteredExtractedMatches = knownCommands.length > 0
1244
- ? extractedCommandMatches.filter((command) => {
1245
- if (knownCommandSet.has(command))
1246
- return true;
1247
- const root = command.split(/\s+/).slice(0, 2).join(' ');
1248
- return knownRoots.has(root);
1249
- })
1250
- : extractedCommandMatches;
1251
- const commandMatches = [
1252
- ...knownCommands,
1253
- ...filteredExtractedMatches,
1254
- ].filter((value, index, arr) => arr.indexOf(value) === index);
1255
- const commandFocus = extractCommandFocus(normalizedQuestion);
1256
- const installMatches = extractInstallCommandsFromCitations(citations);
1257
- const insightLines = extractInsightLines(citations, 5);
1258
- const highSignalAnswerTerms = terms.filter((term) => !LOW_SIGNAL_TERMS.has(term) && term.length >= 3).slice(0, 10);
1259
- const anchorTerms = extractAnchorTerms(question);
1260
- const queriedFlags = (question.match(/--[a-z0-9-]+/gi) || []).map((flag) => flag.toLowerCase());
1261
- const pathAnchorTerms = (0, plan_cache_1.normalizeIntent)(question)
1262
- .replace(/[^a-z0-9_\-\s]/g, ' ')
1263
- .split(/\s+/)
1264
- .map((token) => token.trim())
1265
- .filter((token) => token.length >= 3 &&
1266
- !STOP_WORDS.has(token) &&
1267
- ![
1268
- 'where', 'which', 'file', 'files', 'path', 'paths', 'location',
1269
- 'defined', 'implemented', 'called', 'computed', 'resolved',
1270
- ].includes(token))
1271
- .slice(0, 10);
1272
- const citationRelevanceScore = (citation) => {
1273
- const snippetLower = (citation.snippet || '').toLowerCase();
1274
- let score = countTermHitsInText(snippetLower, highSignalAnswerTerms);
1275
- score += countTermHitsInText(citation.path.toLowerCase(), highSignalAnswerTerms) * 0.5;
1276
- const pathAnchorHits = countTermHitsInText(citation.path.toLowerCase(), pathAnchorTerms);
1277
- score += pathAnchorHits * 0.9;
1278
- if (asksLocation && pathAnchorTerms.length > 0 && pathAnchorHits === 0) {
1279
- score -= 0.35;
1280
- }
1281
- const anchorHits = countTermHitsInText(snippetLower, anchorTerms);
1282
- score += anchorHits * 1.4;
1283
- if ((asksLocation || asksHow) && anchorTerms.length > 0 && anchorHits === 0) {
1284
- score -= 0.45;
1285
- }
1286
- if (queriedFlags.length > 0) {
1287
- const flagHits = queriedFlags.filter((flag) => snippetLower.includes(flag)).length;
1288
- score += flagHits * 2;
1289
- if (asksLocation && flagHits === 0)
1290
- score -= 0.4;
1291
- if (/\.option\(/i.test(snippetLower) && flagHits > 0)
1292
- score += 2.5;
1293
- }
1294
- if (asksCommandSurface) {
1295
- if (/\.command\(|\bcommand\(/i.test(snippetLower))
1296
- score += 2;
1297
- if (/\.option\(|--[a-z0-9-]+/i.test(snippetLower))
1298
- score += 1.4;
1299
- if (citation.path.includes('/commands/') || citation.path.endsWith('/index.ts'))
1300
- score += 0.6;
1301
- }
1302
- if (commandFocus) {
1303
- if (citation.path.includes(`/commands/${commandFocus}.`)) {
1304
- score += 2.1;
1305
- }
1306
- else if ((asksLocation || asksHow) && citation.path.includes('/commands/')) {
1307
- score -= 1.0;
1308
- }
1309
- if (citation.path.endsWith('/index.ts') &&
1310
- new RegExp(`\\.command\\('${escapeRegExp(commandFocus)}'\\)`, 'i').test(citation.snippet)) {
1311
- score += 1.4;
1312
- }
1313
- }
1314
- if ((asksLocation || asksHow) && looksLikeImportLine(citation.snippet)) {
1315
- score -= 0.8;
1316
- }
1317
- if ((asksLocation || asksHow) && /^\s*[?:]?\s*['"`].+['"`][,;]?\s*$/i.test(citation.snippet)) {
1318
- score -= 0.9;
1319
- }
1320
- if (asksLocation && /console\.log\(/i.test(citation.snippet)) {
1321
- score -= 1.0;
1322
- }
1323
- if (asksDecision) {
1324
- if (/\b(verdict|exitcode|policydecision)\b/i.test(snippetLower) || /\bif\s*\(|\?\s*'[^']+'\s*:\s*'[^']+'/i.test(citation.snippet)) {
1325
- score += 1.1;
1326
- }
1327
- if (/(log|roi|estimat|time saved|badge|banner)/i.test(snippetLower)) {
1328
- score -= 0.9;
1329
- }
1330
- }
1331
- return score;
1332
- };
1333
- const multiSignals = citations.filter((citation) => /\bmulti[- ]tenant|x-org-id|organization[_ -]?id|org-scoped\b/i.test(citation.snippet)).length;
1334
- const singleSignals = citations.filter((citation) => /\bsingle[- ]tenant|single-user\b/i.test(citation.snippet)).length;
1335
- if (mode === 'comparison' && terms.length >= 2) {
1336
- const left = terms[0];
1337
- const right = terms[1];
1338
- const leftCount = termCounts.get(left) || 0;
1339
- const rightCount = termCounts.get(right) || 0;
1340
- if (leftCount > 0 && rightCount > 0) {
1341
- answer = `Found both "${left}" (${leftCount}) and "${right}" (${rightCount}) references in the scanned scope.`;
1342
- }
1343
- else if (leftCount > 0) {
1344
- answer = `Found "${left}" references (${leftCount}) and no direct "${right}" references in the scanned scope.`;
1345
- }
1346
- else if (rightCount > 0) {
1347
- answer = `No direct "${left}" references found; "${right}" appears ${rightCount} time(s) in the scanned scope.`;
1348
- }
1349
- else {
1350
- answer = `No direct references to "${left}" or "${right}" were found in the scanned scope.`;
1351
- }
1352
- findings.push(`Compared terms: "${left}" vs "${right}"`);
1353
- findings.push(`Matches by term: ${left}=${leftCount}, ${right}=${rightCount}`);
1672
+ function calibrateConfidence(truth) {
1673
+ if (truth.status === 'insufficient')
1674
+ return 'low';
1675
+ if (truth.score >= 0.78)
1676
+ return 'high';
1677
+ if (truth.score >= 0.55)
1678
+ return 'medium';
1679
+ return 'low';
1680
+ }
1681
+ function isPrimarySourcePath(path) {
1682
+ const normalized = path.toLowerCase();
1683
+ if (isIgnoredSearchPath(normalized))
1684
+ return false;
1685
+ if (isLikelyDocumentationPath(normalized))
1686
+ return false;
1687
+ if (normalized === 'license' || normalized.startsWith('license.'))
1688
+ return false;
1689
+ if (normalized.endsWith('/license') || normalized.includes('/license.'))
1690
+ return false;
1691
+ if (normalized.startsWith('changelog') || normalized.includes('/changelog'))
1692
+ return false;
1693
+ if (normalized.endsWith('pnpm-lock.yaml'))
1694
+ return false;
1695
+ if (normalized.endsWith('package-lock.json'))
1696
+ return false;
1697
+ if (normalized.endsWith('yarn.lock'))
1698
+ return false;
1699
+ if (normalized.endsWith('.md'))
1700
+ return false;
1701
+ if (normalized.endsWith('.txt'))
1702
+ return false;
1703
+ if (normalized === 'readme.md')
1704
+ return false;
1705
+ if (normalized.startsWith('docs/'))
1706
+ return false;
1707
+ if (normalized.includes('/__tests__/'))
1708
+ return false;
1709
+ if (normalized.includes('.test.') || normalized.includes('.spec.'))
1710
+ return false;
1711
+ return true;
1712
+ }
1713
+ function formatCitationLocation(citation) {
1714
+ return `${citation.path}:${citation.line}`;
1715
+ }
1716
+ function explainEvidenceCitation(citation) {
1717
+ const location = formatCitationLocation(citation);
1718
+ const snippet = normalizeSnippet(citation.snippet);
1719
+ if (!snippet)
1720
+ return `${location}`;
1721
+ if (/\.command\(/.test(snippet)) {
1722
+ return `${location} registers CLI wiring: ${snippet}`;
1354
1723
  }
1355
- else if (citations.length === 0) {
1356
- answer = 'I could not find direct grounded evidence for that in the files I scanned.';
1357
- findings.push('Try adding a module/file hint, for example: "in packages/cli" or "in services/api auth middleware".');
1724
+ if (/^\s*import\b/.test(snippet)) {
1725
+ return `${location} connects module dependency: ${snippet}`;
1358
1726
  }
1359
- else if (asksInstall) {
1360
- const manifestInstall = context.cliPackageName ? `npm install -g ${context.cliPackageName}@latest` : null;
1361
- const primaryInstall = manifestInstall || installMatches[0];
1362
- if (!primaryInstall) {
1363
- answer = 'I found CLI references, but not an explicit install command in the matched evidence.';
1364
- }
1365
- else {
1366
- answer = [
1367
- 'Here is the install command I found for the CLI:',
1368
- `\`${primaryInstall}\``,
1369
- installMatches.length > 1 ? `Also seen: ${installMatches.slice(1, 3).map((cmd) => `\`${cmd}\``).join(', ')}` : '',
1370
- ].filter(Boolean).join('\n');
1371
- }
1372
- }
1373
- else if (asksFeatures && (context.featureBullets || []).length > 0) {
1374
- const bullets = (context.featureBullets || []).slice(0, 6).map((line) => ` • ${line}`);
1375
- answer = ['Here are the main platform features I could verify from the repo:', ...bullets].join('\n');
1376
- }
1377
- else if (asksFeatures) {
1378
- const inferredFeatures = [];
1379
- const signalText = citations.map((citation) => `${citation.path} ${citation.snippet}`.toLowerCase());
1380
- const hasSignal = (patterns) => {
1381
- return signalText.some((line) => patterns.some((pattern) => pattern.test(line)));
1382
- };
1383
- const preferredCommands = [
1384
- 'neurcode ask',
1385
- 'neurcode plan',
1386
- 'neurcode apply',
1387
- 'neurcode verify',
1388
- 'neurcode ship',
1389
- 'neurcode watch',
1390
- 'neurcode session',
1391
- 'neurcode revert',
1392
- 'neurcode check',
1393
- 'neurcode login',
1394
- 'neurcode config',
1395
- 'neurcode init',
1396
- ];
1397
- const primaryCommands = [
1398
- ...preferredCommands.filter((command) => commandMatches.includes(command)),
1399
- ...commandMatches.filter((command) => !preferredCommands.includes(command)),
1400
- ]
1401
- .filter((command) => !command.startsWith('neurcode session ') && !command.startsWith('neurcode revert '))
1402
- .slice(0, 12);
1403
- if (primaryCommands.length > 0) {
1404
- inferredFeatures.push(`CLI workflows: ${primaryCommands.join(', ')}`);
1405
- }
1406
- if (hasSignal([/\bverify\b/, /\bpolicy\b/, /\bscope[_ -]?guard\b/, /\bgovernance\b/])) {
1407
- inferredFeatures.push('Governance and policy verification gates.');
1408
- }
1409
- if (hasSignal([/\bship\b/, /\bmerge confidence\b/, /\bauto-remediation\b/])) {
1410
- inferredFeatures.push('Ship workflow with automated remediation and merge confidence scoring.');
1411
- }
1412
- if (hasSignal([/\bask\b/, /\bcitation\b/, /\bgrounded\b/, /\bbrain\b/])) {
1413
- inferredFeatures.push('Repository-grounded Q&A with cached context retrieval.');
1414
- }
1415
- if (hasSignal([/\bsession\b/, /\brevert\b/, /\bhistory\b/, /\btime machine\b/])) {
1416
- inferredFeatures.push('Session/history tracking with revert support.');
1417
- }
1418
- if (hasSignal([/\bx-org-id\b/, /\bmulti[- ]tenant\b/, /\borganization[_ -]?id\b/])) {
1419
- inferredFeatures.push('Multi-tenant organization-scoped isolation and identity context.');
1420
- }
1421
- if (inferredFeatures.length > 0) {
1422
- answer = ['Based on repository evidence, these platform capabilities are available:', ...inferredFeatures.map((line) => ` • ${line}`)].join('\n');
1423
- }
1424
- else {
1425
- answer = 'I found code evidence, but not enough explicit feature documentation to give a precise feature list yet.';
1426
- }
1727
+ if (/\b(orgid|organizationid|userid|token|auth)\b/i.test(snippet) && /=/.test(snippet)) {
1728
+ return `${location} sets auth/org context: ${snippet}`;
1427
1729
  }
1428
- else if (asksSchema && citations.length > 0) {
1429
- const lineCache = context.lineCache || new Map();
1430
- const schemaFieldSummaries = buildSchemaFieldSummaries(citations, lineCache, terms);
1431
- if (schemaFieldSummaries.length > 0) {
1432
- const bullets = schemaFieldSummaries.map((line) => ` • ${line}`);
1433
- answer = ['From the matched code, these fields/signatures are relevant:', ...bullets].join('\n');
1434
- }
1435
- else {
1436
- const schemaLines = citations
1437
- .map((citation) => formatInsightSnippet(citation.snippet))
1438
- .filter((line) => /^export\s+interface\b/i.test(line) ||
1439
- /^export\s+type\b/i.test(line) ||
1440
- (/^[A-Za-z_][A-Za-z0-9_?]*\s*:\s*[^=,]+;?$/.test(line) &&
1441
- !line.includes('{') &&
1442
- !line.includes('=>')));
1443
- const selected = [...new Set(schemaLines)].slice(0, 8);
1444
- if (selected.length > 0) {
1445
- const bullets = selected.map((line) => ` • ${line}`);
1446
- answer = ['From the matched code, these fields/signatures are relevant:', ...bullets].join('\n');
1447
- }
1448
- else {
1449
- answer = 'I found cache/schema-related code, but not enough direct field declarations in the top evidence.';
1450
- }
1451
- }
1730
+ if (/\breturn\b/.test(snippet)) {
1731
+ return `${location} returns runtime behavior: ${snippet}`;
1452
1732
  }
1453
- else if (asksSingleCommandLookup && commandMatches.length > 0) {
1454
- const commandQueryTerms = [...new Set(tokenizeQuestion(question).flatMap((term) => {
1455
- const out = [term.toLowerCase()];
1456
- if (term.endsWith('s') && term.length > 4)
1457
- out.push(term.slice(0, -1).toLowerCase());
1458
- if (term.endsWith('ing') && term.length > 5)
1459
- out.push(term.slice(0, -3).toLowerCase());
1460
- if (term.endsWith('ed') && term.length > 4)
1461
- out.push(term.slice(0, -2).toLowerCase());
1462
- return out;
1463
- }))];
1464
- const scored = commandMatches
1465
- .map((command) => {
1466
- const normalized = command.toLowerCase();
1467
- let score = countTermHitsInText(normalized, commandQueryTerms);
1468
- if (/\bend(s|ed|ing)?\b/.test(normalizedQuestion) && /\bend\b/.test(normalized))
1469
- score += 0.8;
1470
- if (commandFocus &&
1471
- (normalized === `neurcode ${commandFocus}` || normalized.startsWith(`neurcode ${commandFocus} `))) {
1472
- score += 1.2;
1473
- }
1474
- return { command, score };
1475
- })
1476
- .sort((a, b) => b.score - a.score);
1477
- const selected = (scored.filter((item) => item.score > 0).slice(0, 3).map((item) => item.command));
1478
- const top = selected.length > 0 ? selected : [scored[0].command];
1479
- answer = top.length === 1
1480
- ? `Use \`${top[0]}\`.`
1481
- : ['Most relevant commands:', ...top.map((command) => ` • \`${command}\``)].join('\n');
1482
- }
1483
- else if (asksCommandCatalog && commandMatches.length > 0) {
1484
- const scopedCommands = commandFocus
1485
- ? commandMatches.filter((command) => command === `neurcode ${commandFocus}` || command.startsWith(`neurcode ${commandFocus} `))
1486
- : commandMatches;
1487
- const normalizedCommands = (scopedCommands.length > 0 ? scopedCommands : commandMatches)
1488
- .filter((command) => /^neurcode\s+[a-z]/.test(command))
1489
- .slice(0, 22);
1490
- const commandBullets = normalizedCommands.map((command) => ` • \`${command}\``);
1491
- answer = ['Here are the CLI commands I could verify from the repo:', ...commandBullets].join('\n');
1492
- }
1493
- else if (asksLocation && citations.length > 0) {
1494
- const focusedCitations = citations.filter((citation) => {
1495
- const term = (citation.term || '').toLowerCase();
1496
- return term.length === 0 || !GENERIC_OUTPUT_TERMS.has(term);
1497
- });
1498
- const locationPool = focusedCitations.length > 0 ? [...focusedCitations] : [...citations];
1499
- locationPool.sort((a, b) => citationRelevanceScore(b) - citationRelevanceScore(a));
1500
- if (asksOrgRequestInjection) {
1501
- const score = (citation) => {
1502
- const snippet = citation.snippet.toLowerCase();
1503
- let value = 0;
1504
- if (snippet.includes('x-org-id'))
1505
- value += 4;
1506
- if (snippet.includes('auto-inject') || snippet.includes('inject'))
1507
- value += 2;
1508
- if (snippet.includes('headers[') || snippet.includes('header'))
1509
- value += 2;
1510
- if (snippet.includes('request'))
1511
- value += 1;
1512
- if (citation.path.includes('api-client.ts'))
1513
- value += 2;
1514
- return value;
1515
- };
1516
- locationPool.sort((a, b) => score(b) - score(a));
1517
- }
1518
- const prefersImplementationLines = /\b(defined|implemented|called|computed|resolved|lookup)\b/.test(normalizedQuestion);
1519
- const queriedFlags = (question.match(/--[a-z0-9-]+/gi) || []).map((flag) => flag.toLowerCase());
1520
- const declarationLike = prefersImplementationLines
1521
- ? locationPool.filter((citation) => /\b(?:export\s+)?(?:function|const|let|var|class|interface|type)\b/i.test(citation.snippet))
1733
+ if (/\bawait\b/.test(snippet)) {
1734
+ return `${location} performs async flow step: ${snippet}`;
1735
+ }
1736
+ return `${location} ${snippet}`;
1737
+ }
1738
+ function collectUniquePaths(citations, limit, skipFirst) {
1739
+ const out = [];
1740
+ const seen = new Set();
1741
+ const start = skipFirst ? 1 : 0;
1742
+ for (let i = start; i < citations.length; i++) {
1743
+ const path = citations[i]?.path;
1744
+ if (!path || seen.has(path))
1745
+ continue;
1746
+ seen.add(path);
1747
+ out.push(path);
1748
+ if (out.length >= limit)
1749
+ break;
1750
+ }
1751
+ return out;
1752
+ }
1753
+ function buildRepoAnswerPayload(question, searchTerms, citations, stats, truth) {
1754
+ const profile = buildQueryProfile(searchTerms);
1755
+ const asksLocation = profile.asksLocation;
1756
+ const asksHow = profile.asksHow;
1757
+ const asksList = profile.asksList;
1758
+ const primary = citations[0];
1759
+ const primaryLocation = primary ? formatCitationLocation(primary) : null;
1760
+ const flowPaths = collectUniquePaths(citations, 4, false);
1761
+ const relatedPaths = collectUniquePaths(citations, 4, true);
1762
+ const evidenceLines = citations
1763
+ .slice(0, 6)
1764
+ .map((citation) => ` • ${explainEvidenceCitation(citation)}`);
1765
+ let answer = '';
1766
+ if (citations.length === 0) {
1767
+ answer = [
1768
+ 'Short answer: I do not have enough direct repository evidence for this yet.',
1769
+ '',
1770
+ 'What will improve accuracy:',
1771
+ ' Add a folder/file hint (for example: `in packages/cli/src/commands`).',
1772
+ ' • Mention exact identifiers (function/class/flag names) if you have them.',
1773
+ ].join('\n');
1774
+ }
1775
+ else if (asksLocation) {
1776
+ const relatedContext = relatedPaths.length > 0
1777
+ ? ['', 'Related context worth checking:', ...relatedPaths.map((path) => ` • ${path}`)]
1522
1778
  : [];
1523
- const orderedLocationPool = declarationLike.length >= 2
1524
- ? [...declarationLike, ...locationPool.filter((citation) => !declarationLike.includes(citation))]
1525
- : locationPool;
1526
- const commandFocusedLocationPool = commandFocus
1527
- ? [...orderedLocationPool].sort((a, b) => {
1528
- const score = (citation) => {
1529
- let value = 0;
1530
- if (citation.path.includes(`/commands/${commandFocus}.`))
1531
- value += 3;
1532
- if (citation.path.endsWith('/index.ts') &&
1533
- new RegExp(`\\.command\\('${escapeRegExp(commandFocus)}'\\)`, 'i').test(citation.snippet)) {
1534
- value += 2;
1535
- }
1536
- if (citation.path.includes('/commands/') && !citation.path.includes(`/commands/${commandFocus}.`)) {
1537
- value -= 0.5;
1538
- }
1539
- if (queriedFlags.length > 0) {
1540
- const snippetLower = citation.snippet.toLowerCase();
1541
- const matchedFlagCount = queriedFlags.filter((flag) => snippetLower.includes(flag)).length;
1542
- value += matchedFlagCount * 2;
1543
- if (/\.option\(/i.test(citation.snippet) && matchedFlagCount > 0)
1544
- value += 2;
1545
- }
1546
- return value;
1547
- };
1548
- return score(b) - score(a);
1549
- })
1550
- : orderedLocationPool;
1551
- const locations = commandFocusedLocationPool
1552
- .slice(0, 5)
1553
- .map((citation) => ` • ${citation.path}:${citation.line} — ${formatInsightSnippet(citation.snippet)}`);
1554
- answer = ['I found the relevant references here:', ...locations].join('\n');
1555
- }
1556
- else if (asksHow && insightLines.length > 0) {
1557
- const focusedInsights = [...citations]
1558
- .sort((a, b) => citationRelevanceScore(b) - citationRelevanceScore(a))
1559
- .filter((citation) => citationRelevanceScore(citation) >= 1)
1560
- .map((citation) => formatInsightSnippet(citation.snippet))
1561
- .filter((line) => !/^const\s+[A-Za-z_$][\w$]*\s*=\s*\/.+\/[a-z]*\.test\(/i.test(line))
1562
- .filter((line) => !/^[A-Za-z_$][\w$]*,\s*$/.test(line))
1563
- .filter((line) => !/^['"`].+['"`],?\s*$/.test(line));
1564
- const decisionFilteredInsights = asksDecision
1565
- ? focusedInsights.filter((line) => !/(log|roi|estimat|time saved|badge|banner|record[a-z]*event|telemetry|metric)/i.test(line))
1566
- : focusedInsights;
1567
- const decisionCoreInsights = asksDecision
1568
- ? decisionFilteredInsights.filter((line) => /\b(verdict|policydecision|exitcode|pass|fail|warn)\b/i.test(line) ||
1569
- /\?\s*'[^']+'\s*:\s*'[^']+'/.test(line))
1779
+ answer = [
1780
+ `Short answer: ${primaryLocation} is the strongest direct location match.`,
1781
+ '',
1782
+ 'What I verified in code:',
1783
+ ...evidenceLines,
1784
+ ...relatedContext,
1785
+ '',
1786
+ `If you want, I can trace upstream callers and downstream usage from ${primaryLocation}.`,
1787
+ ].join('\n');
1788
+ }
1789
+ else if (asksHow) {
1790
+ const flowSummary = flowPaths.length > 1
1791
+ ? flowPaths.join(' -> ')
1792
+ : flowPaths[0] || (primaryLocation || 'the top match');
1793
+ const relatedContext = relatedPaths.length > 0
1794
+ ? ['', 'Related context:', ...relatedPaths.map((path) => ` • ${path}`)]
1570
1795
  : [];
1571
- const selectedInsights = decisionCoreInsights.length > 0
1572
- ? [...new Set(decisionCoreInsights)].slice(0, 5)
1573
- : decisionFilteredInsights.length > 0
1574
- ? [...new Set(decisionFilteredInsights)].slice(0, 5)
1575
- : focusedInsights.length > 0
1576
- ? [...new Set(focusedInsights)].slice(0, 5)
1577
- : insightLines;
1578
- const bullets = selectedInsights.map((line) => ` • ${line}`);
1579
- answer = ['From the matched code, this is how it works:', ...bullets].join('\n');
1580
- }
1581
- else if (asksTenancy && (multiSignals > 0 || singleSignals > 0)) {
1582
- if (multiSignals >= singleSignals + 2) {
1583
- answer = 'This codebase is multi-tenant, with organization-scoped flows (for example org IDs / `x-org-id` context).';
1584
- }
1585
- else if (singleSignals >= multiSignals + 2) {
1586
- answer = 'This codebase currently looks single-tenant from the scanned evidence.';
1587
- }
1588
- else {
1589
- answer = 'I see mixed tenancy signals, but it leans multi-tenant in current runtime paths.';
1590
- }
1591
- }
1592
- else if (insightLines.length > 0) {
1593
- const bullets = insightLines.map((line) => ` • ${line}`);
1594
- answer = truth.status === 'insufficient'
1595
- ? ['I found partial evidence, but not enough for a fully definitive answer yet.', 'What I can confirm so far:', ...bullets].join('\n')
1596
- : ['From this repo, here is what I found:', ...bullets].join('\n');
1796
+ answer = [
1797
+ `Short answer: the implementation flow is centered around ${flowSummary}.`,
1798
+ '',
1799
+ 'Evidence-backed breakdown:',
1800
+ ...evidenceLines,
1801
+ ...relatedContext,
1802
+ ].join('\n');
1803
+ }
1804
+ else if (asksList) {
1805
+ answer = [
1806
+ `Short answer: I verified ${truth.sourceCitations} evidence line(s) across ${truth.sourceFiles} file(s).`,
1807
+ '',
1808
+ 'Most relevant items:',
1809
+ ...evidenceLines,
1810
+ '',
1811
+ 'If you want, I can rank these by execution order next.',
1812
+ ].join('\n');
1597
1813
  }
1598
1814
  else {
1599
- answer = `I found grounded evidence in ${stats.matchedFiles} file(s), but it is mostly low-level implementation detail.`;
1600
- }
1601
- if (asksCommandCatalog && commandMatches.length === 0 && citations.length > 0) {
1602
- findings.push('Command-style question detected, but no command declarations were found in the matched lines.');
1815
+ const corroborating = relatedPaths.slice(0, 2);
1816
+ const corroboratingText = corroborating.length > 0 ? corroborating.join(', ') : 'nearby modules';
1817
+ answer = [
1818
+ `Short answer: ${primaryLocation} is the strongest anchor, corroborated by ${corroboratingText}.`,
1819
+ '',
1820
+ 'What I can confirm directly from the codebase:',
1821
+ ...evidenceLines,
1822
+ ].join('\n');
1823
+ }
1824
+ const findings = [
1825
+ `Matched ${stats.matchedLines} evidence line(s) across ${stats.matchedFiles} primary source file(s).`,
1826
+ ];
1827
+ if (primaryLocation) {
1828
+ findings.push(`Primary evidence anchor: ${primaryLocation}`);
1603
1829
  }
1604
- if (asksInstall && installMatches.length === 0 && !context.cliPackageName) {
1605
- findings.push('Install question detected, but no package name could be resolved from packages/cli/package.json.');
1830
+ if (flowPaths.length > 1) {
1831
+ findings.push(`Inferred file flow: ${flowPaths.join(' -> ')}`);
1606
1832
  }
1607
- const topFiles = [...perFileCounts.entries()]
1608
- .sort((a, b) => b[1] - a[1])
1609
- .slice(0, 3)
1610
- .map(([file, count]) => `${file} (${count})`);
1611
- if (topFiles.length > 0) {
1612
- findings.push(`Most relevant files: ${topFiles.join(', ')}`);
1833
+ if (searchTerms.highSignalTerms.length > 0) {
1834
+ findings.push(`High-signal terms used: ${searchTerms.highSignalTerms.slice(0, 8).join(', ')}`);
1613
1835
  }
1614
- findings.push(`Scanned ${stats.scannedFiles} file(s); matched ${stats.matchedFiles} file(s).`);
1615
1836
  if (truth.status === 'insufficient') {
1616
1837
  findings.push(...truth.reasons);
1617
- findings.push('Add scope hints (module/file/path) to improve precision and grounding coverage.');
1838
+ findings.push('Add a tighter module/file hint to improve grounding precision.');
1618
1839
  }
1840
+ const topFiles = [...citations.reduce((acc, citation) => {
1841
+ acc.set(citation.path, (acc.get(citation.path) || 0) + 1);
1842
+ return acc;
1843
+ }, new Map())]
1844
+ .sort((a, b) => b[1] - a[1])
1845
+ .map(([path]) => path)
1846
+ .slice(0, 5);
1619
1847
  return {
1620
1848
  question,
1621
- questionNormalized: normalizedQuestion,
1622
- mode,
1849
+ questionNormalized: searchTerms.normalizedQuestion,
1850
+ mode: 'search',
1623
1851
  answer,
1624
1852
  findings,
1625
- confidence,
1853
+ confidence: calibrateConfidence(truth),
1626
1854
  proof: {
1627
- topFiles: [...citations.reduce((acc, citation) => {
1628
- acc.set(citation.path, (acc.get(citation.path) || 0) + 1);
1629
- return acc;
1630
- }, new Map()).entries()]
1631
- .sort((a, b) => b[1] - a[1])
1632
- .slice(0, 5)
1633
- .map(([path]) => path),
1855
+ topFiles,
1634
1856
  evidenceCount: citations.length,
1635
1857
  coverage: {
1636
1858
  sourceCitations: truth.sourceCitations,
@@ -1653,6 +1875,168 @@ function buildAnswer(mode, question, terms, citations, stats, termCounts, perFil
1653
1875
  stats,
1654
1876
  };
1655
1877
  }
1878
+ function stripHtml(value) {
1879
+ return value
1880
+ .replace(/<[^>]*>/g, ' ')
1881
+ .replace(/&quot;/g, '"')
1882
+ .replace(/&#39;/g, "'")
1883
+ .replace(/&amp;/g, '&')
1884
+ .replace(/\s+/g, ' ')
1885
+ .trim();
1886
+ }
1887
+ const EXTERNAL_LLM_BASE_URL = (process.env.NEURCODE_ASK_EXTERNAL_BASE_URL || 'https://api.deepinfra.com/v1/openai').replace(/\/+$/, '');
1888
+ const EXTERNAL_LLM_MODEL = process.env.NEURCODE_ASK_EXTERNAL_MODEL || 'deepseek-ai/DeepSeek-V3.2';
1889
+ const EXTERNAL_LLM_KEY_ENV_NAMES = [
1890
+ 'DEEPINFRA_API_KEY',
1891
+ 'NEURCODE_DEEPINFRA_API_KEY',
1892
+ 'DEEPSEEK_API_KEY',
1893
+ 'NEURCODE_DEEPSEEK_API_KEY',
1894
+ ];
1895
+ function resolveExternalLlmApiKey() {
1896
+ for (const envName of EXTERNAL_LLM_KEY_ENV_NAMES) {
1897
+ const value = process.env[envName];
1898
+ if (value && value.trim())
1899
+ return value.trim();
1900
+ }
1901
+ return null;
1902
+ }
1903
+ function inferExternalAnswerConfidence(text) {
1904
+ const normalized = (0, plan_cache_1.normalizeIntent)(text);
1905
+ if (!normalized)
1906
+ return 'low';
1907
+ if (/\b(i am not sure|i'm not sure|unsure|unknown|cannot verify|can't verify|not certain|might|may|possibly|likely|probably)\b/.test(normalized)) {
1908
+ return 'low';
1909
+ }
1910
+ if (text.split(/\s+/).length <= 28)
1911
+ return 'high';
1912
+ return 'medium';
1913
+ }
1914
+ async function fetchExternalLlmAnswer(question) {
1915
+ if (process.env.NEURCODE_ASK_DISABLE_EXTERNAL_WEB === '1') {
1916
+ return null;
1917
+ }
1918
+ const apiKey = resolveExternalLlmApiKey();
1919
+ if (!apiKey)
1920
+ return null;
1921
+ const controller = new AbortController();
1922
+ const timer = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
1923
+ try {
1924
+ const response = await fetch(`${EXTERNAL_LLM_BASE_URL}/chat/completions`, {
1925
+ method: 'POST',
1926
+ signal: controller.signal,
1927
+ headers: {
1928
+ 'Content-Type': 'application/json',
1929
+ 'Authorization': `Bearer ${apiKey}`,
1930
+ },
1931
+ body: JSON.stringify({
1932
+ model: EXTERNAL_LLM_MODEL,
1933
+ messages: [
1934
+ {
1935
+ role: 'system',
1936
+ content: [
1937
+ 'You answer non-codebase factual questions for a CLI assistant.',
1938
+ 'Return a direct factual answer in at most 2 short sentences.',
1939
+ 'If uncertain, explicitly say you are not sure instead of guessing.',
1940
+ 'Do not include markdown or code fences.',
1941
+ ].join(' '),
1942
+ },
1943
+ {
1944
+ role: 'user',
1945
+ content: question,
1946
+ },
1947
+ ],
1948
+ temperature: 0.1,
1949
+ max_tokens: 220,
1950
+ }),
1951
+ });
1952
+ if (!response.ok)
1953
+ return null;
1954
+ const payload = await response.json();
1955
+ const content = payload.choices?.[0]?.message?.content;
1956
+ if (typeof content !== 'string')
1957
+ return null;
1958
+ const text = stripHtml(content).replace(/^short answer:\s*/i, '').trim();
1959
+ if (!text)
1960
+ return null;
1961
+ return {
1962
+ text,
1963
+ source: `${EXTERNAL_LLM_MODEL} via DeepInfra`,
1964
+ confidence: inferExternalAnswerConfidence(text),
1965
+ };
1966
+ }
1967
+ catch {
1968
+ return null;
1969
+ }
1970
+ finally {
1971
+ clearTimeout(timer);
1972
+ }
1973
+ }
1974
+ async function buildExternalAnswerPayload(question, normalizedQuestion, reasons) {
1975
+ const external = await fetchExternalLlmAnswer(question);
1976
+ const hasExternalLlmKey = Boolean(resolveExternalLlmApiKey());
1977
+ const shortAnswer = external?.text
1978
+ ? `Short answer: ${external.text}`
1979
+ : hasExternalLlmKey
1980
+ ? 'Short answer: I could not get a reliable external response right now.'
1981
+ : 'Short answer: I could not answer this external question because DeepSeek is not configured (set DEEPINFRA_API_KEY).';
1982
+ const sourceNote = external?.source
1983
+ ? `Source used: ${external.source}`
1984
+ : hasExternalLlmKey
1985
+ ? 'DeepSeek request failed or returned no answer.'
1986
+ : 'DeepSeek not configured. Set DEEPINFRA_API_KEY to enable external free-flow answers.';
1987
+ const truthScore = external
1988
+ ? external.confidence === 'high'
1989
+ ? 0.42
1990
+ : external.confidence === 'medium'
1991
+ ? 0.34
1992
+ : 0.24
1993
+ : 0.08;
1994
+ return {
1995
+ question,
1996
+ questionNormalized: normalizedQuestion,
1997
+ mode: 'search',
1998
+ answer: [
1999
+ shortAnswer,
2000
+ '',
2001
+ 'I am strongest on repository questions. Come back with a codebase question and I will answer with file/line citations.',
2002
+ ].join('\n'),
2003
+ findings: [
2004
+ 'Question appears to be outside repository scope.',
2005
+ sourceNote,
2006
+ ],
2007
+ confidence: external ? (external.confidence === 'high' ? 'medium' : 'low') : 'low',
2008
+ proof: {
2009
+ topFiles: [],
2010
+ evidenceCount: 0,
2011
+ coverage: {
2012
+ sourceCitations: 0,
2013
+ sourceFiles: 0,
2014
+ matchedFiles: 0,
2015
+ matchedLines: 0,
2016
+ },
2017
+ },
2018
+ truth: {
2019
+ status: 'insufficient',
2020
+ score: Number(truthScore.toFixed(2)),
2021
+ reasons: [
2022
+ 'Answer is not grounded in repository files.',
2023
+ ...reasons,
2024
+ ],
2025
+ sourceCitations: 0,
2026
+ sourceFiles: 0,
2027
+ minCitationsRequired: 2,
2028
+ minFilesRequired: 1,
2029
+ },
2030
+ citations: [],
2031
+ generatedAt: new Date().toISOString(),
2032
+ stats: {
2033
+ scannedFiles: 0,
2034
+ matchedFiles: 0,
2035
+ matchedLines: 0,
2036
+ brainCandidates: 0,
2037
+ },
2038
+ };
2039
+ }
1656
2040
  function emitAskResult(result, options) {
1657
2041
  if (options.json) {
1658
2042
  console.log(JSON.stringify({
@@ -1679,66 +2063,49 @@ function emitAskResult(result, options) {
1679
2063
  console.log(chalk.yellow(result.answer));
1680
2064
  }
1681
2065
  const showProof = options.proof === true;
1682
- if (!options.verbose && !showProof) {
2066
+ const showVerbose = options.verbose === true;
2067
+ if (!showProof && !showVerbose) {
1683
2068
  console.log(chalk.dim(`\nConfidence: ${result.confidence.toUpperCase()}`));
1684
- if (result.truth.status === 'insufficient' && result.citations.length === 0 && result.truth.reasons.length > 0) {
2069
+ if (result.truth.status === 'insufficient' && result.truth.reasons.length > 0) {
1685
2070
  console.log(chalk.yellow('\nWhy confidence is limited:'));
1686
2071
  for (const reason of result.truth.reasons.slice(0, 2)) {
1687
2072
  console.log(chalk.yellow(` • ${reason}`));
1688
2073
  }
1689
- console.log(chalk.dim('\nTip: add `--proof` for quick evidence digest or `--verbose` for full detail.'));
2074
+ console.log(chalk.dim('\nTip: add `--proof` for concise evidence or `--verbose` for full evidence output.'));
1690
2075
  }
1691
2076
  return;
1692
2077
  }
1693
- if (showProof && !options.verbose) {
1694
- const proof = result.proof;
1695
- if (proof) {
2078
+ if (showProof) {
2079
+ if (result.proof) {
1696
2080
  console.log(chalk.bold.white('\nProof:'));
1697
- const topFiles = proof.topFiles.slice(0, 4);
1698
- if (topFiles.length > 0) {
1699
- console.log(chalk.cyan(` • Top files: ${topFiles.join(', ')}`));
1700
- }
1701
- console.log(chalk.cyan(` • Evidence: citations=${proof.coverage.sourceCitations}, source_files=${proof.coverage.sourceFiles}, matched_lines=${proof.coverage.matchedLines}`));
1702
- }
1703
- const tenancyQuestion = /\b(tenant|tenancy|single|multi|organization|org)\b/.test(result.questionNormalized);
1704
- let citations = result.citations.slice(0, Math.min(options.maxCitations, 12));
1705
- if (tenancyQuestion) {
1706
- const focused = citations.filter((citation) => /\b(multi[- ]tenant|x-org-id|organization[_ -]?id|org[_ -]?id)\b/i.test(citation.snippet));
1707
- if (focused.length >= 3) {
1708
- citations = focused;
1709
- }
1710
- if (/multi-tenant/i.test(result.answer)) {
1711
- citations = citations.filter((citation) => !/\bsingle-user platform\b/i.test(citation.snippet));
2081
+ if (result.proof.topFiles.length > 0) {
2082
+ console.log(chalk.cyan(` • Top files: ${result.proof.topFiles.slice(0, 5).join(', ')}`));
1712
2083
  }
2084
+ console.log(chalk.cyan(` • Coverage: citations=${result.proof.coverage.sourceCitations}, files=${result.proof.coverage.sourceFiles}, matched_lines=${result.proof.coverage.matchedLines}`));
1713
2085
  }
1714
- citations = citations.slice(0, Math.min(options.maxCitations, 6));
1715
- if (citations.length > 0) {
2086
+ const proofCitations = result.citations.slice(0, Math.min(options.maxCitations, 6));
2087
+ if (proofCitations.length > 0) {
1716
2088
  console.log(chalk.bold.white('\nKey Evidence:'));
1717
- citations.forEach((citation, idx) => {
2089
+ proofCitations.forEach((citation, idx) => {
1718
2090
  console.log(chalk.dim(` ${idx + 1}. ${citation.path}:${citation.line} ${citation.snippet}`));
1719
2091
  });
1720
2092
  }
1721
- const truthLabel = result.truth.status === 'grounded' ? chalk.green('GROUNDED') : chalk.yellow('INSUFFICIENT');
1722
- console.log(chalk.dim(`\nTruth Mode: ${truthLabel} (score=${result.truth.score.toFixed(2)}, source_citations=${result.truth.sourceCitations}, source_files=${result.truth.sourceFiles})`));
1723
- console.log(chalk.dim(`Confidence: ${result.confidence.toUpperCase()} | scanned=${result.stats.scannedFiles} matched=${result.stats.matchedFiles}`));
1724
- return;
1725
2093
  }
1726
- if (result.findings.length > 0) {
1727
- console.log(chalk.bold.white('\nFindings:'));
1728
- for (const finding of result.findings) {
1729
- console.log(chalk.cyan(` • ${finding}`));
2094
+ if (showVerbose) {
2095
+ if (result.findings.length > 0) {
2096
+ console.log(chalk.bold.white('\nFindings:'));
2097
+ for (const finding of result.findings) {
2098
+ console.log(chalk.cyan(` • ${finding}`));
2099
+ }
2100
+ }
2101
+ const verboseCitations = result.citations.slice(0, options.maxCitations);
2102
+ if (verboseCitations.length > 0) {
2103
+ console.log(chalk.bold.white('\nEvidence:'));
2104
+ verboseCitations.forEach((citation, idx) => {
2105
+ const prefix = citation.term ? `${citation.term} ` : '';
2106
+ console.log(chalk.dim(` ${idx + 1}. ${citation.path}:${citation.line} ${prefix}${citation.snippet}`));
2107
+ });
1730
2108
  }
1731
- }
1732
- const citations = result.citations.slice(0, options.maxCitations);
1733
- if (citations.length > 0) {
1734
- console.log(chalk.bold.white('\nEvidence:'));
1735
- citations.forEach((citation, idx) => {
1736
- const prefix = citation.term ? `${citation.term} ` : '';
1737
- console.log(chalk.dim(` ${idx + 1}. ${citation.path}:${citation.line} ${prefix}${citation.snippet}`));
1738
- });
1739
- }
1740
- else {
1741
- console.log(chalk.yellow('\nEvidence: no direct matching lines found.'));
1742
2109
  }
1743
2110
  const truthLabel = result.truth.status === 'grounded' ? chalk.green('GROUNDED') : chalk.yellow('INSUFFICIENT');
1744
2111
  console.log(chalk.dim(`\nTruth Mode: ${truthLabel} (score=${result.truth.score.toFixed(2)}, source_citations=${result.truth.sourceCitations}, source_files=${result.truth.sourceFiles})`));
@@ -1759,13 +2126,96 @@ async function askCommand(question, options = {}) {
1759
2126
  const scope = { orgId: orgId || null, projectId: projectId || null };
1760
2127
  const maxCitations = Math.max(3, Math.min(options.maxCitations || 12, 30));
1761
2128
  const shouldUseCache = options.cache !== false && process.env.NEURCODE_ASK_NO_CACHE !== '1';
1762
- const normalizedQuestion = (0, plan_cache_1.normalizeIntent)(question);
1763
- const deterministic = tryBuildDeterministicAnswer(cwd, question, normalizedQuestion);
1764
- if (deterministic) {
1765
- if (!options.json) {
1766
- console.log(chalk.dim(`🧠 Asking repo context in ${cwd}...`));
2129
+ const searchTerms = buildSearchTerms(question);
2130
+ const ownershipAnswer = buildOwnershipDeterministicAnswer(cwd, question, searchTerms.normalizedQuestion);
2131
+ if (ownershipAnswer) {
2132
+ emitAskResult(ownershipAnswer, {
2133
+ json: options.json,
2134
+ maxCitations,
2135
+ fromPlan: options.fromPlan,
2136
+ verbose: options.verbose,
2137
+ proof: options.proof,
2138
+ });
2139
+ if (orgId && projectId) {
2140
+ (0, brain_context_1.recordBrainProgressEvent)(cwd, scope, {
2141
+ type: 'ask',
2142
+ note: `mode=deterministic;reason=ownership_git_history;truth=${ownershipAnswer.truth.status};score=${ownershipAnswer.truth.score.toFixed(2)}`,
2143
+ });
1767
2144
  }
1768
- emitAskResult(deterministic.payload, {
2145
+ return;
2146
+ }
2147
+ const scopeAssessment = classifyQuestionScope(question, searchTerms);
2148
+ if (scopeAssessment.kind === 'external') {
2149
+ const externalPayload = await buildExternalAnswerPayload(question, searchTerms.normalizedQuestion, scopeAssessment.reasons);
2150
+ emitAskResult(externalPayload, {
2151
+ json: options.json,
2152
+ maxCitations,
2153
+ fromPlan: options.fromPlan,
2154
+ verbose: options.verbose,
2155
+ proof: options.proof,
2156
+ });
2157
+ if (orgId && projectId) {
2158
+ (0, brain_context_1.recordBrainProgressEvent)(cwd, scope, {
2159
+ type: 'ask',
2160
+ note: `mode=external;truth=${externalPayload.truth.status};score=${externalPayload.truth.score.toFixed(2)}`,
2161
+ });
2162
+ }
2163
+ return;
2164
+ }
2165
+ const registrationAnswer = buildCommandRegistrationDeterministicAnswer(cwd, question, searchTerms, maxCitations);
2166
+ if (registrationAnswer) {
2167
+ emitAskResult(registrationAnswer, {
2168
+ json: options.json,
2169
+ maxCitations,
2170
+ fromPlan: options.fromPlan,
2171
+ verbose: options.verbose,
2172
+ proof: options.proof,
2173
+ });
2174
+ if (orgId && projectId) {
2175
+ (0, brain_context_1.recordBrainProgressEvent)(cwd, scope, {
2176
+ type: 'ask',
2177
+ note: `mode=deterministic;reason=command_registration;truth=${registrationAnswer.truth.status};score=${registrationAnswer.truth.score.toFixed(2)}`,
2178
+ });
2179
+ }
2180
+ return;
2181
+ }
2182
+ const commandInventoryAnswer = buildCommandInventoryDeterministicAnswer(cwd, question, searchTerms, maxCitations);
2183
+ if (commandInventoryAnswer) {
2184
+ emitAskResult(commandInventoryAnswer, {
2185
+ json: options.json,
2186
+ maxCitations,
2187
+ fromPlan: options.fromPlan,
2188
+ verbose: options.verbose,
2189
+ proof: options.proof,
2190
+ });
2191
+ if (orgId && projectId) {
2192
+ (0, brain_context_1.recordBrainProgressEvent)(cwd, scope, {
2193
+ type: 'ask',
2194
+ note: `mode=deterministic;reason=command_inventory;truth=${commandInventoryAnswer.truth.status};score=${commandInventoryAnswer.truth.score.toFixed(2)}`,
2195
+ });
2196
+ }
2197
+ return;
2198
+ }
2199
+ const commandSubcommandFlowAnswer = buildCommandSubcommandFlowDeterministicAnswer(cwd, question, searchTerms, maxCitations);
2200
+ if (commandSubcommandFlowAnswer) {
2201
+ emitAskResult(commandSubcommandFlowAnswer, {
2202
+ json: options.json,
2203
+ maxCitations,
2204
+ fromPlan: options.fromPlan,
2205
+ verbose: options.verbose,
2206
+ proof: options.proof,
2207
+ });
2208
+ if (orgId && projectId) {
2209
+ (0, brain_context_1.recordBrainProgressEvent)(cwd, scope, {
2210
+ type: 'ask',
2211
+ note: `mode=deterministic;reason=command_subcommand_flow;truth=${commandSubcommandFlowAnswer.truth.status};score=${commandSubcommandFlowAnswer.truth.score.toFixed(2)}`,
2212
+ });
2213
+ }
2214
+ return;
2215
+ }
2216
+ const askCacheFlowAnswer = buildAskCacheFlowDeterministicAnswer(cwd, question, searchTerms, maxCitations);
2217
+ if (askCacheFlowAnswer) {
2218
+ emitAskResult(askCacheFlowAnswer, {
1769
2219
  json: options.json,
1770
2220
  maxCitations,
1771
2221
  fromPlan: options.fromPlan,
@@ -1775,7 +2225,7 @@ async function askCommand(question, options = {}) {
1775
2225
  if (orgId && projectId) {
1776
2226
  (0, brain_context_1.recordBrainProgressEvent)(cwd, scope, {
1777
2227
  type: 'ask',
1778
- note: `mode=deterministic;reason=${deterministic.reason};truth=${deterministic.payload.truth.status};score=${deterministic.payload.truth.score.toFixed(2)}`,
2228
+ note: `mode=deterministic;reason=ask_cache_flow;truth=${askCacheFlowAnswer.truth.status};score=${askCacheFlowAnswer.truth.score.toFixed(2)}`,
1779
2229
  });
1780
2230
  }
1781
2231
  return;
@@ -1802,12 +2252,12 @@ async function askCommand(question, options = {}) {
1802
2252
  });
1803
2253
  }
1804
2254
  catch {
1805
- // non-blocking
2255
+ // Non-blocking.
1806
2256
  }
1807
2257
  }
1808
2258
  if (shouldUseCache && orgId && projectId) {
1809
2259
  const questionHash = (0, ask_cache_1.computeAskQuestionHash)({
1810
- question: normalizedQuestion,
2260
+ question: searchTerms.normalizedQuestion,
1811
2261
  contextHash: staticContext.hash,
1812
2262
  });
1813
2263
  const exactKey = (0, ask_cache_1.computeAskCacheKey)({
@@ -1824,7 +2274,7 @@ async function askCommand(question, options = {}) {
1824
2274
  const exactOutput = {
1825
2275
  ...exact.output,
1826
2276
  question,
1827
- questionNormalized: normalizedQuestion,
2277
+ questionNormalized: searchTerms.normalizedQuestion,
1828
2278
  };
1829
2279
  emitAskResult(exactOutput, {
1830
2280
  json: options.json,
@@ -1844,18 +2294,18 @@ async function askCommand(question, options = {}) {
1844
2294
  orgId,
1845
2295
  projectId,
1846
2296
  repo: repoFingerprint,
1847
- question: normalizedQuestion,
2297
+ question: searchTerms.normalizedQuestion,
1848
2298
  policyVersionHash,
1849
2299
  neurcodeVersion,
1850
2300
  contextHash: staticContext.hash,
1851
2301
  changedPaths: (0, ask_cache_1.getChangedWorkingTreePaths)(cwd),
1852
- minSimilarity: 0.68,
2302
+ minSimilarity: 0.72,
1853
2303
  });
1854
2304
  if (near) {
1855
2305
  const nearOutput = {
1856
2306
  ...near.entry.output,
1857
2307
  question,
1858
- questionNormalized: normalizedQuestion,
2308
+ questionNormalized: searchTerms.normalizedQuestion,
1859
2309
  };
1860
2310
  const reasonText = near.reason === 'safe_repo_drift_similar_question'
1861
2311
  ? 'Using near-cached answer (safe repo drift)'
@@ -1879,408 +2329,24 @@ async function askCommand(question, options = {}) {
1879
2329
  console.log(chalk.dim(`🧠 Asking repo context in ${cwd}...`));
1880
2330
  }
1881
2331
  const brainResults = orgId && projectId
1882
- ? (0, brain_context_1.searchBrainContextEntries)(cwd, scope, normalizedQuestion, { limit: 48 })
2332
+ ? (0, brain_context_1.searchBrainContextEntries)(cwd, scope, searchTerms.normalizedQuestion, { limit: 64 })
1883
2333
  : { entries: [], totalIndexedFiles: 0 };
1884
- const cliPackageName = readCliPackageName(cwd);
1885
- const knownCliCommands = extractCommandsFromCliIndex(cwd);
1886
- const featureBullets = extractFeatureBulletsForRepo(cwd, 10);
1887
- if (!options.json && brainResults.entries.length > 0) {
1888
- const top = brainResults.entries.filter((entry) => entry.score > 0).length;
1889
- console.log(chalk.dim(`🧠 Brain retrieval: ${top} relevant file summaries from ${brainResults.totalIndexedFiles} indexed files`));
1890
- }
1891
- const { mode, terms, matchers } = buildMatchers(question);
1892
- const pathHints = derivePathHints(question);
1893
- const mentionsAskCommand = /\bask\b/.test(normalizedQuestion);
1894
- const asksQuestionAnsweringInternals = /\b(question|questions|answer|answers|cache|brain|citation|grounded)\b/.test(normalizedQuestion);
1895
- if (matchers.length === 0) {
1896
- console.error(chalk.red('❌ Could not derive useful search terms from the question.'));
1897
- process.exit(1);
1898
- }
1899
- const candidateSet = new Set();
1900
- const pathPriority = new Map();
2334
+ const pathBoostScores = new Map();
1901
2335
  for (const entry of brainResults.entries) {
1902
- candidateSet.add(entry.path);
1903
- pathPriority.set(entry.path, (pathPriority.get(entry.path) || 0) + entry.score);
1904
- }
1905
- addAnchorCandidates(fileTree, candidateSet, pathPriority, normalizedQuestion);
1906
- const tokenHints = tokenizeQuestion(question);
1907
- 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) &&
1908
- !/\b(feature|features|install|setup|readme|docs|documentation)\b/.test(normalizedQuestion);
1909
- if (candidateSet.size < 80) {
1910
- for (const filePath of fileTree) {
1911
- const normalizedPath = filePath.toLowerCase();
1912
- let score = pathPriority.get(filePath) || 0;
1913
- for (const token of tokenHints) {
1914
- if (normalizedPath.includes(token))
1915
- score += 0.2;
1916
- }
1917
- if (normalizedPath.startsWith('.github/')) {
1918
- score -= 0.2;
1919
- }
1920
- if (normalizedPath.startsWith('scripts/')) {
1921
- score -= 0.1;
1922
- }
1923
- if (codeFocusedQuestion && (filePath === 'README.md' || normalizedPath.startsWith('docs/'))) {
1924
- score -= 0.35;
1925
- }
1926
- if (!mentionsAskCommand &&
1927
- !asksQuestionAnsweringInternals &&
1928
- (filePath.endsWith('/commands/ask.ts') || filePath.endsWith('/utils/ask-cache.ts'))) {
1929
- score -= 0.45;
1930
- }
1931
- if (pathHints.some((hint) => normalizedPath.startsWith(hint))) {
1932
- score += 0.45;
1933
- }
1934
- else if (pathHints.length > 0) {
1935
- score -= 0.08;
1936
- }
1937
- if (/\b(feature|features|capability|capabilities|offers?|platform)\b/.test(normalizedQuestion)) {
1938
- if (filePath === 'README.md' || normalizedPath.startsWith('docs/')) {
1939
- score += 0.55;
1940
- }
1941
- if (normalizedPath.startsWith('web/') || normalizedPath.startsWith('packages/dashboard/')) {
1942
- score -= 0.25;
1943
- }
1944
- }
1945
- if (score > 0) {
1946
- candidateSet.add(filePath);
1947
- pathPriority.set(filePath, score);
1948
- }
1949
- }
1950
- }
1951
- if (candidateSet.size < 40) {
1952
- for (const filePath of fileTree.slice(0, Math.min(fileTree.length, MAX_SCAN_FILES))) {
1953
- candidateSet.add(filePath);
1954
- }
1955
- }
1956
- const selfReferenceFiles = new Set([
1957
- 'packages/cli/src/commands/ask.ts',
1958
- 'packages/cli/src/utils/ask-cache.ts',
1959
- ]);
1960
- const prioritized = [...candidateSet]
1961
- .filter((filePath) => (0, fs_1.existsSync)((0, path_1.join)(cwd, filePath)))
1962
- .filter((filePath) => mentionsAskCommand || !selfReferenceFiles.has(filePath))
1963
- .sort((a, b) => (pathPriority.get(b) || 0) - (pathPriority.get(a) || 0));
1964
- const hinted = pathHints.length > 0
1965
- ? prioritized.filter((filePath) => pathHints.some((hint) => filePath.startsWith(hint)))
1966
- : [];
1967
- const nonHinted = pathHints.length > 0
1968
- ? prioritized.filter((filePath) => !pathHints.some((hint) => filePath.startsWith(hint)))
1969
- : prioritized;
1970
- const candidates = [...hinted, ...nonHinted].slice(0, MAX_SCAN_FILES);
1971
- const querySignals = deriveQuerySignals(question, normalizedQuestion, terms);
1972
- const commandFocusQuery = extractCommandFocus(normalizedQuestion);
1973
- const anchorTerms = extractAnchorTerms(question);
1974
- const hasTemporalIntent = /\b(before|after|during|when)\b/.test(normalizedQuestion);
1975
- const highSignalSet = new Set(querySignals.highSignalTerms);
1976
- const rawFileMatches = new Map();
1977
- const termFileHits = new Map();
1978
- let scannedFiles = 0;
1979
- for (const filePath of candidates) {
1980
- const fullPath = (0, path_1.join)(cwd, filePath);
1981
- let content = '';
1982
- try {
1983
- const st = (0, fs_1.statSync)(fullPath);
1984
- if (st.size > MAX_FILE_BYTES)
1985
- continue;
1986
- content = (0, fs_1.readFileSync)(fullPath, 'utf-8');
1987
- }
1988
- catch {
1989
- continue;
1990
- }
1991
- scannedFiles++;
1992
- const lines = content.split(/\r?\n/);
1993
- const lineTerms = new Map();
1994
- const matchedTerms = new Set();
1995
- for (let idx = 0; idx < lines.length; idx++) {
1996
- const rawLine = lines[idx];
1997
- if (!rawLine || rawLine.trim().length === 0)
1998
- continue;
1999
- for (const matcher of matchers) {
2000
- if (!matcher.regex.test(rawLine))
2001
- continue;
2002
- if (!lineTerms.has(idx))
2003
- lineTerms.set(idx, new Set());
2004
- lineTerms.get(idx).add(matcher.label);
2005
- matchedTerms.add(matcher.label);
2006
- }
2007
- }
2008
- if (lineTerms.size === 0)
2009
- continue;
2010
- rawFileMatches.set(filePath, {
2011
- lines,
2012
- lineTerms,
2013
- pathScore: pathPriority.get(filePath) || 0,
2014
- hintBoost: pathHints.some((hint) => filePath.startsWith(hint)) ? 0.25 : 0,
2015
- });
2016
- for (const term of matchedTerms) {
2017
- termFileHits.set(term, (termFileHits.get(term) || 0) + 1);
2018
- }
2019
- }
2020
- const matchedFileTotal = Math.max(rawFileMatches.size, 1);
2021
- const termWeight = new Map();
2022
- for (const term of terms) {
2023
- const docFreq = termFileHits.get(term) || 0;
2024
- const idfWeight = Math.log((matchedFileTotal + 1) / (docFreq + 1)) + 0.25;
2025
- const highSignalBoost = highSignalSet.has(term) ? 0.35 : 0;
2026
- termWeight.set(term, Math.max(0.08, idfWeight + highSignalBoost));
2027
- }
2028
- const citations = [];
2029
- for (const [filePath, fileData] of rawFileMatches.entries()) {
2030
- const { lines, lineTerms, pathScore, hintBoost } = fileData;
2031
- for (const [lineIdx, directTerms] of lineTerms.entries()) {
2032
- if (citations.length >= MAX_RAW_CITATIONS)
2033
- break;
2034
- const rawLine = lines[lineIdx] || '';
2035
- const snippet = normalizeSnippet(rawLine);
2036
- if (!snippet)
2037
- continue;
2038
- const windowTerms = new Set();
2039
- const contextRadius = querySignals.asksLocation || querySignals.asksDefinition ? 6 : 2;
2040
- for (let i = Math.max(0, lineIdx - contextRadius); i <= Math.min(lines.length - 1, lineIdx + contextRadius); i++) {
2041
- const termsAtLine = lineTerms.get(i);
2042
- if (!termsAtLine)
2043
- continue;
2044
- for (const term of termsAtLine)
2045
- windowTerms.add(term);
2046
- }
2047
- const highSignalWindow = [...windowTerms].filter((term) => highSignalSet.has(term));
2048
- let score = pathScore + hintBoost;
2049
- for (const term of windowTerms) {
2050
- score += termWeight.get(term) || 0.1;
2051
- }
2052
- score += highSignalWindow.length * 0.7;
2053
- score += windowTerms.size * 0.12;
2054
- if (querySignals.highSignalTerms.length > 0 && highSignalWindow.length === 0) {
2055
- score -= 0.65;
2056
- }
2057
- if ((querySignals.asksLocation || querySignals.asksDefinition) && querySignals.highSignalTerms.length >= 2 && highSignalWindow.length < 2) {
2058
- score -= 0.9;
2059
- }
2060
- const anchorHits = countTermHitsInText(rawLine.toLowerCase(), anchorTerms);
2061
- score += anchorHits * 1.2;
2062
- if ((querySignals.asksLocation || querySignals.asksDefinition) && anchorTerms.length > 0 && anchorHits === 0) {
2063
- score -= 0.6;
2064
- }
2065
- if (querySignals.asksLocation || querySignals.asksDefinition) {
2066
- if (looksLikeImportLine(rawLine))
2067
- score -= 0.75;
2068
- if (/\b(?:export\s+)?(?:function|const|let|var|class|interface|type)\b/.test(rawLine) &&
2069
- countTermHitsInText(rawLine.toLowerCase(), querySignals.highSignalTerms) >= 1) {
2070
- score += 0.9;
2071
- }
2072
- if (querySignals.asksDefinition && /console\.log\(/.test(rawLine)) {
2073
- score -= 0.6;
2074
- }
2075
- for (const identifier of querySignals.identifiers) {
2076
- const escaped = escapeRegExp(identifier);
2077
- if (new RegExp(`\\b${escaped}\\s*\\(`, 'i').test(rawLine)) {
2078
- score += 0.95;
2079
- }
2080
- if (new RegExp(`\\b(?:function|const|let|var|class|interface|type)\\s+${escaped}\\b`, 'i').test(rawLine)) {
2081
- score += 0.8;
2082
- }
2083
- }
2084
- if (hasTemporalIntent) {
2085
- if (/\b[A-Za-z_][A-Za-z0-9_]*\s*\(/.test(rawLine))
2086
- score += 0.75;
2087
- if (/^\s*export\s+(interface|type)\b/.test(rawLine))
2088
- score -= 0.9;
2089
- if (/^\s*const\s+[A-Z0-9_]+\s*=/.test(rawLine))
2090
- score -= 0.7;
2091
- }
2092
- }
2093
- if (querySignals.asksHow && /\b(if|else|return|await|for|while|switch)\b/.test(rawLine)) {
2094
- score += 0.2;
2095
- }
2096
- if (querySignals.asksHow) {
2097
- const trimmed = rawLine.trim();
2098
- if (trimmed.startsWith('//') || trimmed.startsWith('*'))
2099
- score -= 0.45;
2100
- if (/\.description\(/.test(rawLine))
2101
- score -= 0.4;
2102
- }
2103
- if (querySignals.asksSchema) {
2104
- if (/^\s*export\s+interface\s+/i.test(rawLine) || /^\s*export\s+type\s+/i.test(rawLine))
2105
- score += 1.1;
2106
- if (/^\s*[A-Za-z_][A-Za-z0-9_?]*\s*:\s*/.test(rawLine))
2107
- score += 0.8;
2108
- if (filePath.endsWith('.md'))
2109
- score -= 0.5;
2110
- }
2111
- if (rawLine.length > 180 && rawLine.includes(',') && /['"`].*['"`].*['"`]/.test(rawLine)) {
2112
- score -= 0.35;
2113
- }
2114
- if ((querySignals.asksLocation || querySignals.asksDefinition) && /Usage:\s*neurcode/i.test(rawLine)) {
2115
- score -= 0.6;
2116
- }
2117
- if ((querySignals.asksLocation || querySignals.asksDefinition) && /^\s*type:\s*['"`][a-z0-9_-]+['"`],?\s*$/i.test(rawLine)) {
2118
- score -= 0.7;
2119
- }
2120
- if (querySignals.asksCommandSurface) {
2121
- if (/\.command\(|\bcommand\(/i.test(rawLine))
2122
- score += 1.2;
2123
- if (/\.option\(|--[a-z0-9-]+/i.test(rawLine))
2124
- score += 0.8;
2125
- if (filePath.endsWith('/index.ts') || filePath.includes('/commands/'))
2126
- score += 0.35;
2127
- }
2128
- if (commandFocusQuery) {
2129
- if (filePath.includes(`/commands/${commandFocusQuery}.`)) {
2130
- score += 1.4;
2131
- }
2132
- else if ((querySignals.asksHow || querySignals.asksLocation) && filePath.includes('/commands/')) {
2133
- score -= 1.2;
2134
- }
2135
- if (filePath.endsWith('/index.ts') &&
2136
- new RegExp(`\\.command\\('${escapeRegExp(commandFocusQuery)}'\\)`, 'i').test(rawLine)) {
2137
- score += 1.1;
2138
- }
2139
- }
2140
- const dominantTerm = [...directTerms].sort((a, b) => (termWeight.get(b) || 0) - (termWeight.get(a) || 0))[0] ||
2141
- [...windowTerms].sort((a, b) => (termWeight.get(b) || 0) - (termWeight.get(a) || 0))[0] ||
2142
- '';
2143
- if (score <= 0)
2144
- continue;
2145
- citations.push({
2146
- path: filePath,
2147
- line: lineIdx + 1,
2148
- snippet,
2149
- term: dominantTerm,
2150
- weight: score,
2151
- });
2152
- }
2153
- }
2154
- citations.sort((a, b) => b.weight - a.weight);
2155
- const sourceEvidence = citations.filter((citation) => isPrimarySourcePath(citation.path));
2156
- const sourcePerFileCounts = new Map();
2157
- const sourceTermCounts = new Map();
2158
- for (const citation of sourceEvidence) {
2159
- sourcePerFileCounts.set(citation.path, (sourcePerFileCounts.get(citation.path) || 0) + 1);
2160
- if (citation.term) {
2161
- sourceTermCounts.set(citation.term, (sourceTermCounts.get(citation.term) || 0) + 1);
2162
- }
2163
- }
2164
- const selectedForOutput = [];
2165
- const selectedFileCounts = new Map();
2166
- const pushSelected = (citation) => {
2167
- if (selectedForOutput.length >= maxCitations)
2168
- return false;
2169
- if (selectedForOutput.some((existing) => existing.path === citation.path && existing.line === citation.line && existing.term === citation.term)) {
2170
- return false;
2171
- }
2172
- if (querySignals.asksLocation || querySignals.asksDefinition) {
2173
- const perFile = selectedFileCounts.get(citation.path) || 0;
2174
- const distinctFiles = selectedFileCounts.size;
2175
- const targetDistinct = Math.min(3, sourcePerFileCounts.size);
2176
- if (perFile >= 3 && distinctFiles < targetDistinct) {
2177
- return false;
2178
- }
2179
- }
2180
- selectedForOutput.push(citation);
2181
- selectedFileCounts.set(citation.path, (selectedFileCounts.get(citation.path) || 0) + 1);
2182
- return true;
2183
- };
2184
- if (mode === 'search' && querySignals.asksSchema) {
2185
- const schemaAffinity = (citation) => {
2186
- const text = citation.snippet.toLowerCase();
2187
- let affinity = 0;
2188
- for (const term of querySignals.highSignalTerms) {
2189
- const normalized = term.toLowerCase();
2190
- const compact = normalized.replace(/\s+/g, '');
2191
- if (text.includes(normalized))
2192
- affinity += 1;
2193
- else if (text.includes(compact))
2194
- affinity += 0.85;
2195
- }
2196
- return affinity;
2197
- };
2198
- const schemaPreferred = sourceEvidence
2199
- .filter((citation) => /^export\s+interface\b/i.test(citation.snippet) ||
2200
- /^export\s+type\b/i.test(citation.snippet) ||
2201
- /^[A-Za-z_][A-Za-z0-9_?]*\s*:\s*[^=,]+;?$/.test(citation.snippet))
2202
- .sort((a, b) => {
2203
- const affinityDiff = schemaAffinity(b) - schemaAffinity(a);
2204
- if (affinityDiff !== 0)
2205
- return affinityDiff;
2206
- return b.weight - a.weight;
2207
- });
2208
- for (const citation of schemaPreferred) {
2209
- if (selectedForOutput.length >= maxCitations)
2210
- break;
2211
- pushSelected(citation);
2212
- }
2336
+ pathBoostScores.set(entry.path, Math.max(pathBoostScores.get(entry.path) || 0, entry.score));
2213
2337
  }
2214
- if (mode === 'comparison' && terms.length >= 2) {
2215
- for (const term of terms.slice(0, 2)) {
2216
- const firstForTerm = sourceEvidence.find((citation) => citation.term === term);
2217
- if (firstForTerm) {
2218
- pushSelected(firstForTerm);
2219
- }
2220
- }
2221
- }
2222
- else if (mode === 'search' && terms.length > 0) {
2223
- const discriminativeHighSignalTerms = querySignals.highSignalTerms.filter((term) => {
2224
- const docFreq = termFileHits.get(term) || 0;
2225
- return docFreq / matchedFileTotal <= 0.65;
2226
- });
2227
- const preferredTerms = discriminativeHighSignalTerms.length > 0
2228
- ? discriminativeHighSignalTerms.slice(0, 8)
2229
- : querySignals.highSignalTerms.length > 0
2230
- ? querySignals.highSignalTerms.slice(0, 8)
2231
- : terms
2232
- .filter((term) => !LOW_SIGNAL_TERMS.has(term) && term.length >= 4)
2233
- .slice(0, 8);
2234
- for (const term of preferredTerms) {
2235
- const firstForTerm = sourceEvidence.find((citation) => {
2236
- if (citation.term !== term)
2237
- return false;
2238
- const normalized = (citation.term || '').toLowerCase();
2239
- return !GENERIC_OUTPUT_TERMS.has(normalized);
2240
- });
2241
- if (!firstForTerm)
2242
- continue;
2243
- pushSelected(firstForTerm);
2244
- if (selectedForOutput.length >= maxCitations)
2245
- break;
2246
- }
2247
- }
2248
- for (const citation of sourceEvidence) {
2249
- if (selectedForOutput.length >= maxCitations)
2250
- break;
2251
- if (commandFocusQuery &&
2252
- (querySignals.asksLocation || querySignals.asksHow) &&
2253
- selectedForOutput.some((item) => item.path.includes(`/commands/${commandFocusQuery}.`)) &&
2254
- citation.path.includes('/commands/') &&
2255
- !citation.path.includes(`/commands/${commandFocusQuery}.`)) {
2256
- continue;
2257
- }
2258
- pushSelected(citation);
2259
- }
2260
- const finalCitations = selectedForOutput.map(({ path, line, snippet, term }) => ({
2261
- path,
2262
- line,
2263
- snippet,
2264
- term,
2265
- }));
2338
+ const evidence = collectRepoEvidence(cwd, fileTree, searchTerms, pathBoostScores);
2339
+ const sourceEvidence = evidence.scoredCitations.filter((citation) => isPrimarySourcePath(citation.path));
2340
+ const finalCitations = selectTopCitations(sourceEvidence, maxCitations, searchTerms);
2266
2341
  const stats = {
2267
- scannedFiles,
2268
- matchedFiles: sourcePerFileCounts.size,
2342
+ scannedFiles: evidence.scannedFiles,
2343
+ matchedFiles: new Set(sourceEvidence.map((citation) => citation.path)).size,
2269
2344
  matchedLines: sourceEvidence.length,
2270
2345
  brainCandidates: brainResults.entries.length,
2271
2346
  };
2272
- const lineCacheForAnswer = new Map();
2273
- for (const [filePath, data] of rawFileMatches.entries()) {
2274
- lineCacheForAnswer.set(filePath, data.lines);
2275
- }
2276
- const truth = evaluateTruthAssessment(mode, normalizedQuestion, terms, sourceEvidence, sourcePerFileCounts, sourceTermCounts);
2277
- const answer = buildAnswer(mode, question, terms, finalCitations, stats, sourceTermCounts, sourcePerFileCounts, truth, {
2278
- cliPackageName,
2279
- knownCommands: knownCliCommands,
2280
- featureBullets,
2281
- lineCache: lineCacheForAnswer,
2282
- });
2283
- emitAskResult(answer, {
2347
+ const truth = evaluateTruth(searchTerms.normalizedQuestion, searchTerms.highSignalTerms, finalCitations);
2348
+ const payload = buildRepoAnswerPayload(question, searchTerms, finalCitations, stats, truth);
2349
+ emitAskResult(payload, {
2284
2350
  json: options.json,
2285
2351
  maxCitations,
2286
2352
  fromPlan: options.fromPlan,
@@ -2290,12 +2356,12 @@ async function askCommand(question, options = {}) {
2290
2356
  if (orgId && projectId) {
2291
2357
  (0, brain_context_1.recordBrainProgressEvent)(cwd, scope, {
2292
2358
  type: 'ask',
2293
- note: `mode=${mode};truth=${truth.status};score=${truth.score.toFixed(2)};matched_files=${stats.matchedFiles};matched_lines=${stats.matchedLines}`,
2359
+ note: `mode=retrieval;truth=${truth.status};score=${truth.score.toFixed(2)};matched_files=${stats.matchedFiles};matched_lines=${stats.matchedLines}`,
2294
2360
  });
2295
2361
  }
2296
2362
  if (shouldUseCache && orgId && projectId) {
2297
2363
  const questionHash = (0, ask_cache_1.computeAskQuestionHash)({
2298
- question: normalizedQuestion,
2364
+ question: searchTerms.normalizedQuestion,
2299
2365
  contextHash: staticContext.hash,
2300
2366
  });
2301
2367
  const key = (0, ask_cache_1.computeAskCacheKey)({
@@ -2317,10 +2383,10 @@ async function askCommand(question, options = {}) {
2317
2383
  questionHash,
2318
2384
  policyVersionHash,
2319
2385
  neurcodeVersion,
2320
- question: normalizedQuestion,
2386
+ question: searchTerms.normalizedQuestion,
2321
2387
  contextHash: staticContext.hash,
2322
2388
  },
2323
- output: answer,
2389
+ output: payload,
2324
2390
  evidencePaths: finalCitations.map((citation) => citation.path),
2325
2391
  });
2326
2392
  }