@neurcode-ai/cli 0.9.25 → 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.
Files changed (40) hide show
  1. package/dist/api-client.d.ts +29 -0
  2. package/dist/api-client.d.ts.map +1 -1
  3. package/dist/api-client.js +28 -2
  4. package/dist/api-client.js.map +1 -1
  5. package/dist/commands/ask.d.ts +1 -0
  6. package/dist/commands/ask.d.ts.map +1 -1
  7. package/dist/commands/ask.js +1952 -1587
  8. package/dist/commands/ask.js.map +1 -1
  9. package/dist/commands/brain.d.ts.map +1 -1
  10. package/dist/commands/brain.js +315 -0
  11. package/dist/commands/brain.js.map +1 -1
  12. package/dist/commands/policy.d.ts +3 -0
  13. package/dist/commands/policy.d.ts.map +1 -0
  14. package/dist/commands/policy.js +148 -0
  15. package/dist/commands/policy.js.map +1 -0
  16. package/dist/commands/ship.d.ts +1 -0
  17. package/dist/commands/ship.d.ts.map +1 -1
  18. package/dist/commands/ship.js +93 -0
  19. package/dist/commands/ship.js.map +1 -1
  20. package/dist/commands/simulate.d.ts +10 -0
  21. package/dist/commands/simulate.d.ts.map +1 -0
  22. package/dist/commands/simulate.js +96 -0
  23. package/dist/commands/simulate.js.map +1 -0
  24. package/dist/commands/verify.d.ts.map +1 -1
  25. package/dist/commands/verify.js +85 -51
  26. package/dist/commands/verify.js.map +1 -1
  27. package/dist/index.js +26 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/utils/ask-cache.d.ts +10 -0
  30. package/dist/utils/ask-cache.d.ts.map +1 -1
  31. package/dist/utils/ask-cache.js.map +1 -1
  32. package/dist/utils/breakage-simulator.d.ts +53 -0
  33. package/dist/utils/breakage-simulator.d.ts.map +1 -0
  34. package/dist/utils/breakage-simulator.js +323 -0
  35. package/dist/utils/breakage-simulator.js.map +1 -0
  36. package/dist/utils/policy-packs.d.ts +31 -0
  37. package/dist/utils/policy-packs.d.ts.map +1 -0
  38. package/dist/utils/policy-packs.js +277 -0
  39. package/dist/utils/policy-packs.js.map +1 -0
  40. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.askCommand = askCommand;
4
+ const child_process_1 = require("child_process");
4
5
  const fs_1 = require("fs");
5
6
  const path_1 = require("path");
6
7
  const config_1 = require("../config");
@@ -26,60 +27,269 @@ catch {
26
27
  };
27
28
  }
28
29
  const MAX_SCAN_FILES = (() => {
29
- const raw = Number(process.env.NEURCODE_ASK_MAX_SCAN_FILES || '900');
30
+ const raw = Number(process.env.NEURCODE_ASK_MAX_SCAN_FILES || '2200');
30
31
  if (!Number.isFinite(raw))
31
- return 900;
32
- return Math.max(120, Math.min(Math.trunc(raw), 2000));
32
+ return 2200;
33
+ return Math.max(300, Math.min(Math.trunc(raw), 8000));
33
34
  })();
34
35
  const MAX_FILE_BYTES = 512 * 1024;
35
- const MAX_RAW_CITATIONS = 120;
36
- const PRIMARY_SOURCE_EXTENSIONS = new Set([
37
- 'ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs',
38
- 'py', 'go', 'rs', 'java', 'kt', 'swift', 'rb', 'php', 'cs',
39
- 'json', 'yaml', 'yml', 'toml', 'md', 'sql', 'graphql', 'gql',
40
- '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',
41
55
  ]);
42
56
  const STOP_WORDS = new Set([
43
- 'the', 'and', 'for', 'with', 'that', 'this', 'what', 'where', 'when', 'which', 'into',
57
+ 'the', 'and', 'for', 'with', 'that', 'this', 'what', 'where', 'when', 'which',
44
58
  'from', 'your', 'about', 'there', 'their', 'them', 'have', 'does', 'is', 'are', 'was',
45
- 'were', 'any', 'all', 'read', 'tell', 'me', 'its', 'it', 'instead', 'than', 'then',
46
- 'workflow', 'codebase', 'repo', 'repository', 'used', 'use', 'mentioned', 'mention', 'whether',
47
- 'list', 'show', 'like', 'can', 'type', 'types', 'using', 'etc', 'cmd', 'cmds', 'command', 'commands',
48
- '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',
49
62
  ]);
50
63
  const LOW_SIGNAL_TERMS = new Set([
51
- 'used', 'use', 'using', 'mentioned', 'mention', 'where', 'tell', 'read', 'check', 'find', 'search',
52
- 'workflow', 'repo', 'repository', 'codebase', 'anywhere', 'can', 'type', 'types', 'list', 'show', 'like',
53
- 'neurcode', 'cli', 'ask', 'file', 'files', 'path', 'filepath', 'header', 'added', 'add', 'request', 'requests',
54
- 'flag', 'flags', 'option', 'options',
55
- 'defined', 'define', 'implemented', 'implement', 'called', 'call', 'computed', 'compute', 'resolved', 'resolve',
56
- 'lookup', 'lookups', 'decide', 'decides',
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',
57
77
  ]);
58
- const GENERIC_OUTPUT_TERMS = new Set(['file', 'files', 'path', 'filepath']);
59
- const REPO_SCOPE_TERMS = [
60
- 'repo', 'repository', 'codebase', 'neurcode', 'cli', 'command', 'commands', 'ship', 'plan', 'ask', 'verify',
61
- 'apply', 'watch', 'config', 'login', 'logout', 'whoami', 'doctor', 'module', 'file', 'files', 'path',
62
- 'middleware', 'api', 'service', 'services', 'branch', 'commit', 'cache', 'brain', 'org', 'organization',
63
- 'tenant', 'tenancy', 'x-org-id', 'readme', 'docs',
64
- ];
65
- const EXTERNAL_WORLD_TERMS = [
66
- 'capital', 'exchange rate', 'stock price', 'weather', 'temperature', 'forecast', 'news', 'election',
67
- 'president', 'prime minister', 'population', 'gdp', 'sports score', 'match result', 'bitcoin price',
68
- 'usd', 'inr', 'eur', 'jpy', 'currency',
69
- ];
70
- const REALTIME_WORLD_TERMS = ['right now', 'currently', 'today', 'tomorrow', 'yesterday', 'live', 'real time'];
71
78
  const CLI_COMMAND_NAMES = new Set([
72
79
  'check', 'refactor', 'security', 'brain', 'login', 'logout', 'init', 'doctor',
73
80
  'whoami', 'config', 'map', 'ask', 'plan', 'ship', 'apply', 'allow', 'watch',
74
81
  'session', 'verify', 'prompt', 'revert',
75
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
+ }
76
283
  function scanFiles(dir, maxFiles = MAX_SCAN_FILES) {
77
284
  const files = [];
78
285
  const ignoreDirs = new Set([
79
- 'node_modules', '.git', '.next', 'dist', 'build', '.turbo', '.cache', 'coverage',
80
- '.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',
81
292
  ]);
82
- const ignoreExts = new Set(['map', 'log', 'lock', 'png', 'jpg', 'jpeg', 'gif', 'ico', 'svg', 'woff', 'woff2', 'ttf', 'eot', 'pdf']);
83
293
  const walk = (current) => {
84
294
  if (files.length >= maxFiles)
85
295
  return;
@@ -93,10 +303,6 @@ function scanFiles(dir, maxFiles = MAX_SCAN_FILES) {
93
303
  for (const entry of entries) {
94
304
  if (files.length >= maxFiles)
95
305
  break;
96
- if (entry.startsWith('.') && !entry.startsWith('.env')) {
97
- if (ignoreDirs.has(entry))
98
- continue;
99
- }
100
306
  const fullPath = (0, path_1.join)(current, entry);
101
307
  let st;
102
308
  try {
@@ -108,6 +314,8 @@ function scanFiles(dir, maxFiles = MAX_SCAN_FILES) {
108
314
  if (st.isDirectory()) {
109
315
  if (ignoreDirs.has(entry))
110
316
  continue;
317
+ if (entry.startsWith('.') && entry !== '.env')
318
+ continue;
111
319
  walk(fullPath);
112
320
  continue;
113
321
  }
@@ -118,1275 +326,1541 @@ function scanFiles(dir, maxFiles = MAX_SCAN_FILES) {
118
326
  const ext = entry.includes('.') ? entry.split('.').pop()?.toLowerCase() || '' : '';
119
327
  if (ext && ignoreExts.has(ext))
120
328
  continue;
121
- const rel = fullPath.slice(dir.length + 1).replace(/\\/g, '/');
329
+ const rel = normalizeFilePath(fullPath.slice(dir.length + 1));
122
330
  files.push(rel);
123
331
  }
124
332
  };
125
333
  walk(dir);
126
334
  return files.slice(0, maxFiles);
127
335
  }
128
- function tokenizeQuestion(question) {
129
- return (0, plan_cache_1.normalizeIntent)(question)
130
- .replace(/[^a-z0-9_\-\s]/g, ' ')
131
- .split(/\s+/)
132
- .map((token) => token.trim())
133
- .filter((token) => token.length >= 3 && !STOP_WORDS.has(token));
134
- }
135
- function escapeRegExp(input) {
136
- return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
137
- }
138
- function normalizeTerm(raw) {
139
- const normalized = raw
140
- .toLowerCase()
141
- .replace(/['"`]/g, '')
142
- .replace(/[^a-z0-9_\-\s]/g, ' ')
143
- .replace(/\s+/g, ' ')
144
- .trim();
145
- if (!normalized)
146
- return '';
147
- return normalized
148
- .split(' ')
149
- .map((token) => token.replace(/^[-_]+|[-_]+$/g, ''))
150
- .filter(Boolean)
151
- .join(' ');
152
- }
153
- function extractQuotedTerms(question) {
154
- const seen = new Set();
155
- const terms = [];
156
- const re = /["'`](.{2,80}?)["'`]/g;
157
- for (const match of question.matchAll(re)) {
158
- const term = normalizeTerm(match[1] || '');
159
- if (!term || seen.has(term))
160
- continue;
161
- seen.add(term);
162
- terms.push(term);
163
- }
164
- return terms;
165
- }
166
- function extractIdentityTerms(question) {
167
- const normalized = (0, plan_cache_1.normalizeIntent)(question);
168
- const matches = normalized.match(/(?:organization[\s_-]*id|org[\s_-]*id|orgid|organizationid|user[\s_-]*id|userid|userid)/g);
169
- if (!matches)
170
- return [];
171
- const seen = new Set();
172
- const ordered = [];
173
- for (const raw of matches) {
174
- const term = normalizeTerm(raw);
175
- if (seen.has(term))
176
- continue;
177
- seen.add(term);
178
- ordered.push(term);
179
- }
180
- return ordered;
181
- }
182
- function extractComparisonTerms(question) {
183
- const quoted = extractQuotedTerms(question);
184
- if (quoted.length >= 2)
185
- return quoted.slice(0, 2);
186
- const normalized = (0, plan_cache_1.normalizeIntent)(question);
187
- const hasComparisonJoiner = normalized.includes('instead of') ||
188
- normalized.includes(' versus ') ||
189
- normalized.includes(' vs ') ||
190
- normalized.includes(' compared to ');
191
- if (!hasComparisonJoiner)
192
- return [];
193
- const identities = extractIdentityTerms(question);
194
- if (identities.length >= 2)
195
- return identities.slice(0, 2);
196
- return [];
197
- }
198
- function extractPhraseTerms(question, maxTerms = 6) {
199
- const tokens = (0, plan_cache_1.normalizeIntent)(question)
200
- .replace(/[^a-z0-9_\-\s]/g, ' ')
201
- .split(/\s+/)
202
- .map((token) => token.trim())
203
- .filter((token) => token.length >= 3 && !STOP_WORDS.has(token) && !LOW_SIGNAL_TERMS.has(token));
204
- const phrases = [];
205
- const seen = new Set();
206
- for (let i = 0; i < tokens.length - 1; i++) {
207
- const phrase = `${tokens[i]} ${tokens[i + 1]}`;
208
- if (seen.has(phrase))
209
- continue;
210
- seen.add(phrase);
211
- phrases.push(phrase);
212
- if (phrases.length >= maxTerms)
213
- break;
214
- }
215
- return phrases;
216
- }
217
- function buildTermMatchers(term, weight) {
218
- const normalized = normalizeTerm(term);
219
- if (!normalized)
220
- return [];
221
- const tokens = normalized.split(/\s+/).filter(Boolean);
222
- const out = [];
223
- const push = (label, regexSource) => {
224
- out.push({
225
- label,
226
- regex: new RegExp(regexSource, 'i'),
227
- weight,
228
- });
229
- };
230
- if (tokens.length === 1) {
231
- const token = escapeRegExp(tokens[0]);
232
- push(normalized, `\\b${token}\\b`);
233
- if (/^[a-z0-9_]+$/.test(tokens[0]) && tokens[0].length >= 5) {
234
- push(normalized, `\\b${token}[a-z0-9_]*\\b`);
235
- push(normalized, `\\b[a-z0-9_]*${token}[a-z0-9_]*\\b`);
236
- }
237
- return out;
238
- }
239
- const tokenChain = tokens.map((t) => escapeRegExp(t)).join('[\\s_-]*');
240
- push(normalized, `\\b${tokenChain}\\b`);
241
- push(normalized, `\\b${escapeRegExp(tokens.join(''))}\\b`);
242
- push(normalized, `\\b${escapeRegExp(tokens.join('_'))}\\b`);
243
- push(normalized, `\\b${escapeRegExp(tokens.join('-'))}\\b`);
244
- return out;
245
- }
246
- function expandSearchTerms(terms) {
247
- const expanded = new Set();
248
- for (const rawTerm of terms) {
249
- const term = normalizeTerm(rawTerm);
250
- 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)
251
344
  continue;
252
- expanded.add(term);
253
- if (term.includes(' ')) {
345
+ if (normalized.includes(' ')) {
346
+ if (lower.includes(normalized))
347
+ hits += 1;
254
348
  continue;
255
349
  }
256
- if (term.endsWith('ies') && term.length > 4) {
257
- expanded.add(`${term.slice(0, -3)}y`);
258
- }
259
- if (term.endsWith('s') && term.length > 3) {
260
- expanded.add(term.slice(0, -1));
261
- }
262
- else if (term.length > 3) {
263
- expanded.add(`${term}s`);
264
- }
265
- if (term.endsWith('ing') && term.length > 5) {
266
- expanded.add(term.slice(0, -3));
267
- }
268
- if (term.endsWith('ed') && term.length > 4) {
269
- expanded.add(term.slice(0, -2));
270
- }
271
- if (term.includes('-')) {
272
- expanded.add(term.replace(/-/g, ' '));
273
- expanded.add(term.replace(/-/g, ''));
274
- }
275
- if (term.endsWith('cache')) {
276
- expanded.add(`${term}d`);
277
- }
278
- if (term.endsWith('cached')) {
279
- expanded.add(term.slice(0, -1)); // cached -> cache
280
- }
281
- if (term.includes('cache') && term.includes('-')) {
282
- expanded.add(term.replace(/cache\b/, 'cached'));
283
- expanded.add(term.replace(/cached\b/, 'cache'));
284
- }
350
+ const pattern = new RegExp(`(?:^|[^a-z0-9])${escapeRegExp(normalized)}(?:$|[^a-z0-9])`, 'i');
351
+ if (pattern.test(lower))
352
+ hits += 1;
285
353
  }
286
- return [...expanded];
354
+ return hits;
287
355
  }
288
- function buildMatchers(question) {
289
- const comparisonTerms = extractComparisonTerms(question);
290
- if (comparisonTerms.length >= 2) {
291
- const matchers = [
292
- ...buildTermMatchers(comparisonTerms[0], 1.0),
293
- ...buildTermMatchers(comparisonTerms[1], 0.9),
294
- ];
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) {
295
367
  return {
296
- mode: 'comparison',
297
- terms: comparisonTerms.slice(0, 2),
298
- matchers,
368
+ kind: 'external',
369
+ reasons: ['Question appears to be outside repository scope.'],
299
370
  };
300
371
  }
301
- const quoted = extractQuotedTerms(question);
302
- const identityTerms = extractIdentityTerms(question);
303
- const phraseTerms = extractPhraseTerms(question);
304
- const keywords = tokenizeQuestion(question).slice(0, 8);
305
- const quotedSet = new Set(quoted.map((term) => normalizeTerm(term)));
306
- const baseTerms = [...new Set([...quoted, ...phraseTerms, ...identityTerms, ...keywords].map(normalizeTerm).filter(Boolean))];
307
- const filteredTerms = baseTerms.filter((term) => quotedSet.has(term) || !LOW_SIGNAL_TERMS.has(term));
308
- const terms = expandSearchTerms(filteredTerms.length > 0 ? filteredTerms : baseTerms).filter(Boolean);
309
- const matchers = terms.flatMap((term) => buildTermMatchers(term, quoted.includes(term) ? 0.9 : 0.55));
372
+ if (repoSignal > 0 || hasCodeLikeSyntax || mentionsKnownCommand) {
373
+ return { kind: 'repo', reasons: [] };
374
+ }
310
375
  return {
311
- mode: 'search',
312
- terms,
313
- matchers,
376
+ kind: 'ambiguous',
377
+ reasons: ['Question does not clearly reference repository context.'],
314
378
  };
315
379
  }
316
- function derivePathHints(question) {
317
- const normalized = (0, plan_cache_1.normalizeIntent)(question);
318
- const hints = [];
319
- if (/\bcli|command|terminal\b/.test(normalized)) {
320
- hints.push('packages/cli/src/commands/');
321
- hints.push('packages/cli/src/');
322
- hints.push('packages/cli/');
323
- }
324
- if (/\bapi|backend|server|route|middleware\b/.test(normalized)) {
325
- hints.push('services/api/');
326
- }
327
- if (/\bdashboard|landing|docs|frontend|ui|pricing\b/.test(normalized)) {
328
- hints.push('web/dashboard/');
329
- hints.push('docs/');
330
- hints.push('README.md');
331
- }
332
- if (/\bfeature|capabilit|offer|platform\b/.test(normalized)) {
333
- hints.push('docs/');
334
- hints.push('README.md');
335
- }
336
- if (/\binstall|setup|upgrade|update\b/.test(normalized)) {
337
- hints.push('README.md');
338
- hints.push('docs/');
339
- hints.push('packages/cli/');
340
- }
341
- if (/\btenant|tenancy|single|multi|organization|org\b/.test(normalized)) {
342
- hints.push('services/api/src/lib/');
343
- hints.push('services/api/src/routes/');
344
- hints.push('packages/cli/src/');
345
- }
346
- if (/\brequest|requests|header|inject|injected\b/.test(normalized)) {
347
- hints.push('packages/cli/src/api-client.ts');
348
- hints.push('services/api/src/middleware/');
349
- }
350
- if (/(github action|\bci\b)/.test(normalized)) {
351
- hints.push('.github/workflows/');
352
- hints.push('packages/action/');
353
- hints.push('actions/');
354
- }
355
- return [...new Set(hints)];
356
- }
357
- function extractCodeLikeIdentifiers(question) {
358
- const quoted = extractQuotedTerms(question);
359
- const tokens = question.match(/[A-Za-z_][A-Za-z0-9_]{2,}/g) || [];
360
- const seen = new Set();
361
- const output = [];
362
- for (const raw of [...quoted, ...tokens]) {
363
- const normalized = raw.trim();
364
- if (!normalized)
365
- continue;
366
- const key = normalized.toLowerCase();
367
- if (STOP_WORDS.has(key) || LOW_SIGNAL_TERMS.has(key))
368
- continue;
369
- if (seen.has(key))
370
- continue;
371
- seen.add(key);
372
- output.push(normalized);
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];
384
+ }
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];
373
392
  }
374
- return output.slice(0, 8);
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;
375
397
  }
376
- function deriveQuerySignals(question, normalizedQuestion, terms) {
377
- const asksLocation = /\b(where|which file|in which file|filepath|file path|location)\b/.test(normalizedQuestion);
378
- const asksHow = /\b(how|flow|trace|walk me through|explain)\b/.test(normalizedQuestion);
379
- const asksCommandSurface = /\b(command|commands|subcommand|subcommands|flag|flags|option|options)\b/.test(normalizedQuestion);
380
- const asksSchema = /\b(field|fields|key|keys|schema|interface|input|output|parameter|parameters)\b/.test(normalizedQuestion);
381
- const asksList = /\b(list|show|which|available)\b/.test(normalizedQuestion) &&
382
- /\b(command|commands|subcommand|subcommands|files|fields|features)\b/.test(normalizedQuestion);
383
- const asksDefinition = /\b(defined|implemented|called|computed|resolved)\b/.test(normalizedQuestion);
384
- const identifiers = extractCodeLikeIdentifiers(question);
385
- const highSignalTerms = terms.filter((term) => !LOW_SIGNAL_TERMS.has(term) && term.length >= 3);
386
- return {
387
- asksLocation,
388
- asksHow,
389
- asksList,
390
- asksDefinition,
391
- asksCommandSurface,
392
- asksSchema,
393
- identifiers,
394
- highSignalTerms,
395
- };
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;
396
409
  }
397
- function extractAnchorTerms(question) {
398
- const quoted = extractQuotedTerms(question);
399
- const special = (question.match(/[A-Za-z0-9_-]{4,}/g) || []).filter((token) => /[_-]/.test(token) || /[A-Z]/.test(token));
400
- const normalized = [...new Set([...quoted, ...special].map(normalizeTerm).filter(Boolean))]
401
- .filter((term) => !STOP_WORDS.has(term) && !LOW_SIGNAL_TERMS.has(term));
402
- return expandSearchTerms(normalized).slice(0, 12);
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;
403
426
  }
404
- function looksLikeImportLine(rawLine) {
405
- const trimmed = rawLine.trim();
406
- return /^import\s+/.test(trimmed) || /^export\s+\{/.test(trimmed);
427
+ function parseOwnershipLookbackDays(normalizedQuestion) {
428
+ if (/\bquarter\b/.test(normalizedQuestion))
429
+ return 120;
430
+ if (/\bhalf[-\s]?year\b/.test(normalizedQuestion))
431
+ return 180;
432
+ if (/\byear\b/.test(normalizedQuestion))
433
+ return 365;
434
+ if (/\bmonth\b/.test(normalizedQuestion))
435
+ return 30;
436
+ if (/\bweek\b/.test(normalizedQuestion))
437
+ return 14;
438
+ const explicitDays = normalizedQuestion.match(/\b(\d{1,4})\s*days?\b/);
439
+ if (explicitDays) {
440
+ const parsed = Number(explicitDays[1]);
441
+ if (Number.isFinite(parsed))
442
+ return Math.max(1, Math.min(parsed, 3650));
443
+ }
444
+ return 90;
407
445
  }
408
- function countQuestionTermHits(normalizedQuestion, terms) {
409
- let hits = 0;
410
- const seen = new Set();
411
- for (const term of terms) {
412
- const normalizedTerm = term.toLowerCase().trim();
413
- if (!normalizedTerm || seen.has(normalizedTerm))
446
+ function buildOwnershipDeterministicAnswer(cwd, question, normalizedQuestion) {
447
+ const asksOwnership = /\b(who|owner|owners|authored|touched|touch)\b/.test(normalizedQuestion);
448
+ if (!asksOwnership)
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
+ }
460
+ const ignoreTerms = new Set([
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',
465
+ ]);
466
+ const focusTerms = (0, plan_cache_1.normalizeIntent)(question)
467
+ .split(/\s+/)
468
+ .map((term) => term.trim())
469
+ .filter((term) => term.length >= 4 && !ignoreTerms.has(term));
470
+ const sinceDays = parseOwnershipLookbackDays(normalizedQuestion);
471
+ const result = (0, child_process_1.spawnSync)('git', ['log', `--since=${sinceDays}.days`, '--name-only', '--pretty=format:__AUTHOR__%an'], {
472
+ cwd,
473
+ encoding: 'utf-8',
474
+ maxBuffer: 1024 * 1024 * 60,
475
+ stdio: ['ignore', 'pipe', 'pipe'],
476
+ });
477
+ if ((result.status ?? 1) !== 0 || !result.stdout) {
478
+ return null;
479
+ }
480
+ const authorTouches = new Map();
481
+ const fileTouches = new Map();
482
+ let currentAuthor = '';
483
+ for (const rawLine of result.stdout.split(/\r?\n/)) {
484
+ const line = rawLine.trim();
485
+ if (!line)
414
486
  continue;
415
- seen.add(normalizedTerm);
416
- if (normalizedTerm.includes(' ')) {
417
- if (!normalizedQuestion.includes(normalizedTerm))
418
- continue;
419
- hits += 1;
487
+ if (line.startsWith('__AUTHOR__')) {
488
+ currentAuthor = line.replace('__AUTHOR__', '').trim() || 'Unknown';
420
489
  continue;
421
490
  }
422
- const boundaryPattern = new RegExp(`(?:^|[^a-z0-9])${escapeRegExp(normalizedTerm)}(?:$|[^a-z0-9])`);
423
- if (boundaryPattern.test(normalizedQuestion)) {
424
- hits += 1;
491
+ if (!currentAuthor)
492
+ continue;
493
+ const normalizedPath = normalizeFilePath(line);
494
+ if (!normalizedPath || isIgnoredSearchPath(normalizedPath)) {
495
+ continue;
496
+ }
497
+ if (focusTerms.length > 0 && !focusTerms.some((term) => (0, plan_cache_1.normalizeIntent)(normalizedPath).includes(term))) {
498
+ continue;
425
499
  }
500
+ authorTouches.set(currentAuthor, (authorTouches.get(currentAuthor) || 0) + 1);
501
+ const byFile = fileTouches.get(normalizedPath) || new Map();
502
+ byFile.set(currentAuthor, (byFile.get(currentAuthor) || 0) + 1);
503
+ fileTouches.set(normalizedPath, byFile);
426
504
  }
427
- return hits;
428
- }
429
- function assessQuestionScope(normalizedQuestion) {
430
- const repoHits = countQuestionTermHits(normalizedQuestion, REPO_SCOPE_TERMS);
431
- const externalHits = countQuestionTermHits(normalizedQuestion, EXTERNAL_WORLD_TERMS);
432
- const realtimeHits = countQuestionTermHits(normalizedQuestion, REALTIME_WORLD_TERMS);
433
- const currencyPairPattern = /\b[a-z]{3}\s*(?:to|\/)\s*[a-z]{3}\b/;
434
- const hasCurrencyPair = currencyPairPattern.test(normalizedQuestion);
435
- const hasWorldEntity = /\b(france|india|usa|united states|china|germany|europe|asia|africa)\b/.test(normalizedQuestion);
436
- const looksExternal = hasCurrencyPair ||
437
- externalHits > 0 ||
438
- (hasWorldEntity && /\bcapital|population|gdp|president|prime minister\b/.test(normalizedQuestion)) ||
439
- (realtimeHits > 0 && /\bexchange rate|weather|stock|price|news|capital|population\b/.test(normalizedQuestion));
440
- if (looksExternal && repoHits === 0) {
441
- const reasons = ['This question appears to require external world knowledge rather than repository evidence.'];
442
- if (hasCurrencyPair) {
443
- reasons.push('Detected a currency/exchange-rate query, which is outside repo-grounded scope.');
444
- }
445
- if (realtimeHits > 0) {
446
- reasons.push('Detected real-time wording (for example "right now"/"currently"), which is not answerable from local files.');
447
- }
448
- return { isOutOfScope: true, reasons };
449
- }
450
- return { isOutOfScope: false, reasons: [] };
451
- }
452
- function buildOutOfScopeAnswerPayload(question, normalizedQuestion, reasons) {
453
- const truthReasons = [
454
- 'Neurcode Ask is repository-grounded and does not use web/external knowledge.',
455
- ...reasons,
456
- ];
505
+ if (authorTouches.size === 0)
506
+ return null;
507
+ const topContributors = [...authorTouches.entries()]
508
+ .sort((a, b) => b[1] - a[1])
509
+ .slice(0, 5)
510
+ .map(([author, touches]) => ({ author, touches }));
511
+ const topFiles = [...fileTouches.entries()]
512
+ .sort((a, b) => {
513
+ const aCount = [...a[1].values()].reduce((sum, n) => sum + n, 0);
514
+ const bCount = [...b[1].values()].reduce((sum, n) => sum + n, 0);
515
+ return bCount - aCount;
516
+ })
517
+ .slice(0, 6);
518
+ const targetLabel = focusTerms.length > 0 ? `files matching "${focusTerms.slice(0, 4).join(', ')}"` : 'this repository';
519
+ const citations = topFiles.map(([path, owners]) => {
520
+ const summary = [...owners.entries()]
521
+ .sort((a, b) => b[1] - a[1])
522
+ .slice(0, 3)
523
+ .map(([author, touches]) => `${author}(${touches})`)
524
+ .join(', ');
525
+ return {
526
+ path,
527
+ line: 1,
528
+ term: focusTerms[0] || 'authorship',
529
+ snippet: `Recent git touches (last ${sinceDays}d): ${summary}`,
530
+ };
531
+ });
532
+ const sourceFiles = new Set(citations.map((c) => c.path)).size;
457
533
  return {
458
534
  question,
459
535
  questionNormalized: normalizedQuestion,
460
536
  mode: 'search',
461
537
  answer: [
462
- 'I can only answer from this repository.',
463
- 'This question looks external/realtime, so I cannot answer it from repo evidence.',
464
- 'Try a repo-scoped variant, for example: `Where is org id injected in CLI requests?`',
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)`),
465
541
  ].join('\n'),
466
542
  findings: [
467
- 'Scope guard triggered: external/non-repo query.',
468
- 'No repo files were scanned to avoid returning misleading answers.',
543
+ `Derived from git history over ${sinceDays} day(s).`,
544
+ `Focus: ${targetLabel}.`,
469
545
  ],
470
- confidence: 'low',
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: {
551
+ sourceCitations: citations.length,
552
+ sourceFiles,
553
+ matchedFiles: sourceFiles,
554
+ matchedLines: citations.length,
555
+ },
556
+ },
471
557
  truth: {
472
- status: 'insufficient',
473
- score: 0.05,
474
- reasons: truthReasons,
475
- sourceCitations: 0,
476
- sourceFiles: 0,
477
- minCitationsRequired: 2,
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,
478
564
  minFilesRequired: 1,
479
565
  },
480
- citations: [],
566
+ citations,
481
567
  generatedAt: new Date().toISOString(),
482
568
  stats: {
483
569
  scannedFiles: 0,
484
- matchedFiles: 0,
485
- matchedLines: 0,
570
+ matchedFiles: sourceFiles,
571
+ matchedLines: citations.length,
486
572
  brainCandidates: 0,
487
573
  },
488
574
  };
489
575
  }
490
- function tryBuildDeterministicAnswer(_cwd, question, normalizedQuestion) {
491
- const scope = assessQuestionScope(normalizedQuestion);
492
- if (scope.isOutOfScope) {
493
- return {
494
- payload: buildOutOfScopeAnswerPayload(question, normalizedQuestion, scope.reasons),
495
- reason: 'out_of_scope',
496
- };
497
- }
498
- return null;
499
- }
500
- function normalizeSnippet(line) {
501
- return line
502
- .replace(/\t/g, ' ')
503
- .replace(/\s+/g, ' ')
504
- .trim()
505
- .slice(0, 220);
506
- }
507
- function addAnchorCandidates(fileTree, candidateSet, pathPriority, normalizedQuestion) {
508
- const asSet = new Set(fileTree);
509
- const pinned = [
510
- 'README.md',
511
- 'packages/cli/package.json',
512
- 'packages/cli/src/index.ts',
513
- 'packages/cli/src/api-client.ts',
514
- 'services/api/src/lib/org-context.ts',
515
- 'services/api/src/lib/user-org.ts',
516
- 'docs/cli-commands.md',
517
- '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),
518
589
  ];
519
- for (const path of pinned) {
520
- 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))
521
598
  continue;
522
- candidateSet.add(path);
523
- pathPriority.set(path, Math.max(pathPriority.get(path) || 0, 0.22));
524
- }
525
- const docsLikelyUseful = /\b(feature|capabilit|offer|platform|install|setup|tenant|tenancy|architecture)\b/.test(normalizedQuestion);
526
- if (!docsLikelyUseful)
527
- return;
528
- let addedDocs = 0;
529
- for (const path of fileTree) {
530
- if (addedDocs >= 24)
531
- break;
532
- if (path === 'README.md' || (path.startsWith('docs/') && path.endsWith('.md'))) {
533
- candidateSet.add(path);
534
- pathPriority.set(path, Math.max(pathPriority.get(path) || 0, 0.18));
535
- addedDocs++;
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);
536
633
  }
537
634
  }
538
- }
539
- function readCliPackageName(cwd) {
540
- const pkgPath = (0, path_1.join)(cwd, 'packages/cli/package.json');
541
- if (!(0, fs_1.existsSync)(pkgPath))
635
+ const scored = [...dedup.values()].sort((a, b) => b.score - a.score);
636
+ if (scored.length === 0)
542
637
  return null;
543
- try {
544
- const parsed = JSON.parse((0, fs_1.readFileSync)(pkgPath, 'utf-8'));
545
- if (typeof parsed.name !== 'string' || !parsed.name.trim())
546
- return null;
547
- return parsed.name.trim();
548
- }
549
- catch {
638
+ const citations = selectTopCitations(scored, Math.min(maxCitations, 10), searchTerms);
639
+ if (citations.length === 0)
550
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)}`);
651
+ }
551
652
  }
552
- }
553
- function extractCommandsFromCliIndex(cwd) {
554
- const indexPath = (0, path_1.join)(cwd, 'packages/cli/src/index.ts');
555
- if (!(0, fs_1.existsSync)(indexPath))
556
- return [];
557
- let content = '';
558
- try {
559
- content = (0, fs_1.readFileSync)(indexPath, 'utf-8');
560
- }
561
- catch {
562
- return [];
563
- }
564
- const commandSet = new Set();
565
- const topByVar = new Map();
566
- for (const match of content.matchAll(/(?:const\s+([A-Za-z_$][\w$]*)\s*=\s*)?program\s*\r?\n\s*\.command\('([^']+)'\)/g)) {
567
- const varName = match[1];
568
- const top = (match[2] || '').trim().split(/\s+/)[0];
569
- if (!top)
570
- continue;
571
- commandSet.add(`neurcode ${top}`);
572
- if (varName) {
573
- topByVar.set(varName, top);
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)}`);
574
658
  }
575
659
  }
576
- for (const match of content.matchAll(/([A-Za-z_$][\w$]*)\s*\r?\n\s*\.command\('([^']+)'\)/g)) {
577
- const parentVar = match[1];
578
- const parent = topByVar.get(parentVar);
579
- if (!parent)
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
+ };
704
+ }
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)
711
+ return null;
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)
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])
580
722
  continue;
581
- const sub = (match[2] || '').trim().split(/\s+/)[0];
582
- if (!sub)
723
+ const command = match[1].trim().toLowerCase();
724
+ if (!command || byCommand.has(command))
583
725
  continue;
584
- commandSet.add(`neurcode ${parent} ${sub}`);
726
+ byCommand.set(command, {
727
+ path: row.path,
728
+ line: row.line,
729
+ term: command,
730
+ snippet: row.snippet,
731
+ });
585
732
  }
586
- return [...commandSet].slice(0, 40);
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
+ };
587
787
  }
588
- function extractFeatureBulletsFromReadme(cwd, limit) {
589
- const readmePath = (0, path_1.join)(cwd, 'README.md');
590
- if (!(0, fs_1.existsSync)(readmePath))
591
- 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;
592
792
  let content = '';
593
793
  try {
594
- content = (0, fs_1.readFileSync)(readmePath, 'utf-8');
794
+ content = (0, fs_1.readFileSync)(fullPath, 'utf-8');
595
795
  }
596
796
  catch {
597
- return [];
797
+ return null;
598
798
  }
599
799
  const lines = content.split(/\r?\n/);
600
- let start = lines.findIndex((line) => /^##\s+.*features/i.test(line));
601
- if (start < 0)
602
- start = lines.findIndex((line) => /^###\s+.*features/i.test(line));
603
- if (start < 0)
604
- return [];
605
- const out = [];
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;
809
+ }
810
+ }
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 = [];
606
826
  const seen = new Set();
607
- for (let i = start + 1; i < lines.length; i++) {
608
- const line = lines[i];
609
- if (/^##\s+/.test(line))
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)
610
847
  break;
611
- const rich = line.match(/^- \*\*(.+?)\*\*\s*-\s*(.+)$/);
612
- if (rich?.[1]) {
613
- const text = `${formatInsightSnippet(rich[1])} - ${formatInsightSnippet(rich[2])}`.trim();
614
- const key = text.toLowerCase();
615
- if (key && !seen.has(key)) {
616
- seen.add(key);
617
- out.push(text);
618
- }
848
+ const snippet = normalizeSnippet(lines[i] || '');
849
+ if (!snippet)
850
+ continue;
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) {
859
+ continue;
619
860
  }
620
- else {
621
- const simple = line.match(/^- (.+)$/);
622
- if (!simple?.[1])
623
- continue;
624
- const text = formatInsightSnippet(simple[1]);
625
- const key = text.toLowerCase();
626
- if (text && !seen.has(key)) {
627
- seen.add(key);
628
- out.push(text);
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;
629
868
  }
630
869
  }
631
- if (out.length >= limit)
632
- break;
633
870
  }
634
- return out;
871
+ if (citations.length === 0)
872
+ return null;
873
+ return {
874
+ anchorLine: anchorIdx + 1,
875
+ citations: citations.slice(0, maxCitations),
876
+ };
635
877
  }
636
- function extractNeurcodeCommandsFromCitations(citations) {
637
- const commandSet = new Set();
878
+ function extractCommandOperationNames(citations) {
879
+ const out = [];
880
+ const seen = new Set();
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
+ ]);
638
889
  for (const citation of citations) {
639
- const snippet = citation.snippet || '';
640
- for (const match of snippet.matchAll(/\.command\('([^']+)'\)/g)) {
641
- const command = (match[1] || '').trim().split(/\s+/)[0];
642
- if (!command)
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))
643
893
  continue;
644
- commandSet.add(`neurcode ${command}`);
645
- }
646
- for (const match of snippet.matchAll(/`neurcode\s+([^`]+)`/g)) {
647
- const command = (match[1] || '').trim();
648
- if (!command)
894
+ if (operation.length < 3)
649
895
  continue;
650
- commandSet.add(`neurcode ${command}`);
651
- }
652
- for (const plainMatch of snippet.matchAll(/\bneurcode\s+([a-z][a-z0-9-]*(?:\s+[a-z][a-z0-9-]*)?)\b/g)) {
653
- if (!plainMatch?.[1])
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)
654
899
  continue;
655
- commandSet.add(`neurcode ${plainMatch[1].trim()}`);
900
+ if (seen.has(operation))
901
+ continue;
902
+ seen.add(operation);
903
+ out.push(operation);
904
+ if (out.length >= 10)
905
+ return out;
656
906
  }
657
907
  }
658
- return [...commandSet].slice(0, 20);
908
+ return out;
659
909
  }
660
- function extractCommandFocus(normalizedQuestion) {
661
- const toKnownCommand = (candidate) => {
662
- if (!candidate)
663
- return null;
664
- const normalized = candidate.toLowerCase().trim();
665
- return CLI_COMMAND_NAMES.has(normalized) ? normalized : null;
666
- };
667
- const direct = normalizedQuestion.match(/\bneurcode\s+([a-z][a-z0-9-]*)\b/);
668
- const directCommand = toKnownCommand(direct?.[1] || null);
669
- if (directCommand)
670
- return directCommand;
671
- const scoped = normalizedQuestion.match(/\b(?:under|for|within|inside|in)\s+(?:neurcode\s+)?([a-z][a-z0-9-]*)\b/);
672
- const scopedCommand = toKnownCommand(scoped?.[1] || null);
673
- if (scopedCommand)
674
- return scopedCommand;
675
- const commandPattern = normalizedQuestion.match(/\b([a-z][a-z0-9-]*)\s+command\b/);
676
- const commandPhrase = toKnownCommand(commandPattern?.[1] || null);
677
- if (commandPhrase)
678
- return commandPhrase;
679
- const mentionedCommands = [...CLI_COMMAND_NAMES].filter((command) => new RegExp(`\\b${escapeRegExp(command)}\\b`, 'i').test(normalizedQuestion));
680
- if (mentionedCommands.length !== 1)
910
+ function buildCommandSubcommandFlowDeterministicAnswer(cwd, question, searchTerms, maxCitations) {
911
+ const profile = buildQueryProfile(searchTerms);
912
+ if (!profile.commandFocus || !profile.subcommandFocus)
681
913
  return null;
682
- const hasImplementationSignals = /\b(where|how|defined|implemented|called|computed|resolved|lookup|cache|flag|option|verdict|pass|fail|schema|field|fields|key|keys)\b/.test(normalizedQuestion);
683
- if (hasImplementationSignals) {
684
- return mentionedCommands[0];
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
+ });
685
944
  }
686
- return null;
687
- }
688
- function extractInstallCommandsFromCitations(citations) {
689
- const installSet = new Set();
690
- for (const citation of citations) {
691
- const snippet = citation.snippet || '';
692
- const matches = snippet.match(/\b(?:npm|pnpm|yarn)\s+(?:install|add)\s+-g\s+[@a-z0-9_.\-/]+(?:@latest)?\b/gi) || [];
693
- for (const match of matches) {
694
- installSet.add(match.trim());
695
- }
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
+ });
696
968
  }
697
- return [...installSet].slice(0, 6);
698
- }
699
- function formatInsightSnippet(snippet) {
700
- return snippet
701
- .replace(/^[-*]\s+/, '')
702
- .replace(/^\/\/\s?/, '')
703
- .replace(/^\/\*\s?/, '')
704
- .replace(/\*\/$/, '')
705
- .replace(/^\*\s+/, '')
706
- .replace(/\s+/g, ' ')
707
- .trim()
708
- .slice(0, 180);
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
+ };
709
1042
  }
710
- function countTermHitsInText(text, terms) {
711
- if (!text || terms.length === 0)
712
- return 0;
713
- let hits = 0;
714
- const isCompactText = !/\s/.test(text);
715
- const compactText = isCompactText ? text.replace(/[^a-z0-9]/g, '') : '';
716
- for (const term of terms) {
717
- const normalized = term.toLowerCase().trim();
718
- if (!normalized)
719
- continue;
720
- if (normalized.includes(' ')) {
721
- if (text.includes(normalized))
722
- hits += 1;
723
- continue;
724
- }
725
- const pattern = new RegExp(`(?:^|[^a-z0-9])${escapeRegExp(normalized)}(?:$|[^a-z0-9])`, 'i');
726
- if (pattern.test(text)) {
727
- hits += 1;
728
- continue;
729
- }
730
- if (isCompactText) {
731
- const compactTerm = normalized.replace(/[^a-z0-9]/g, '');
732
- if (compactTerm.length >= 3 && compactText.includes(compactTerm)) {
733
- hits += 1;
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;
734
1065
  }
1066
+ raw.push({ ...hit, tag: probe.tag });
735
1067
  }
736
1068
  }
737
- return hits;
738
- }
739
- function findInterfaceFieldBlock(lines, startLine) {
740
- const startIndex = Math.max(0, startLine - 1);
741
- const lookbackStart = Math.max(0, startIndex - 14);
742
- const lookaheadEnd = Math.min(lines.length - 1, startIndex + 10);
743
- let headerIndex = -1;
744
- let interfaceName = '';
745
- for (let i = startIndex; i >= lookbackStart; i--) {
746
- const match = lines[i].match(/^\s*(?:export\s+)?interface\s+([A-Za-z_][A-Za-z0-9_]*)\b/);
747
- if (!match?.[1])
748
- continue;
749
- headerIndex = i;
750
- interfaceName = match[1];
751
- break;
752
- }
753
- if (headerIndex < 0) {
754
- for (let i = startIndex; i <= lookaheadEnd; i++) {
755
- const match = lines[i].match(/^\s*(?:export\s+)?interface\s+([A-Za-z_][A-Za-z0-9_]*)\b/);
756
- if (!match?.[1])
757
- continue;
758
- headerIndex = i;
759
- interfaceName = match[1];
760
- break;
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);
761
1094
  }
762
1095
  }
763
- if (headerIndex < 0 || !interfaceName)
1096
+ const scored = [...dedup.values()].sort((a, b) => b.score - a.score);
1097
+ if (scored.length === 0)
764
1098
  return null;
765
- let opened = false;
766
- let depth = 0;
767
- const fields = [];
768
- const seen = new Set();
769
- for (let i = headerIndex; i < Math.min(lines.length, headerIndex + 120); i++) {
770
- const line = lines[i];
771
- if (!opened) {
772
- if (line.includes('{')) {
773
- opened = true;
774
- depth = 1;
775
- }
776
- continue;
777
- }
778
- const fieldMatch = line.match(/^\s*([A-Za-z_][A-Za-z0-9_?]*)\s*:/);
779
- if (fieldMatch?.[1]) {
780
- const field = fieldMatch[1].replace(/\?$/, '');
781
- if (!seen.has(field)) {
782
- seen.add(field);
783
- fields.push(field);
784
- }
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`;
785
1179
  }
786
- for (const ch of line) {
787
- if (ch === '{')
788
- depth += 1;
789
- if (ch === '}')
790
- depth -= 1;
1180
+ if (term.includes(' ')) {
1181
+ return escaped.replace(/\\\s+/g, '\\s+');
791
1182
  }
792
- if (depth <= 0)
793
- break;
794
- }
795
- if (fields.length === 0)
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)
796
1198
  return null;
797
- return { interfaceName, fields };
1199
+ return {
1200
+ path: rawPath,
1201
+ line: lineNumber,
1202
+ snippet: normalizeSnippet(match[3] || ''),
1203
+ };
798
1204
  }
799
- function splitIdentifierTokens(value) {
800
- if (!value)
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) {
801
1239
  return [];
802
- const tokens = value
803
- .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
804
- .replace(/[_\-\s]+/g, ' ')
805
- .toLowerCase()
806
- .split(/\s+/)
807
- .map((token) => token.trim())
808
- .filter((token) => token.length >= 2);
809
- return [...new Set(tokens)];
810
- }
811
- function buildSchemaFieldSummaries(citations, lineCache, terms) {
812
- const scored = [];
813
- const seen = new Set();
814
- const highSignalTerms = terms.filter((term) => !LOW_SIGNAL_TERMS.has(term));
815
- const queryTermSet = new Set(highSignalTerms.map((term) => term.toLowerCase()));
816
- for (const citation of citations) {
817
- const lines = lineCache.get(citation.path);
818
- if (!lines || lines.length === 0)
819
- continue;
820
- const parsed = findInterfaceFieldBlock(lines, citation.line);
821
- if (!parsed)
822
- continue;
823
- const interfaceKey = `${citation.path}:${parsed.interfaceName}`;
824
- if (seen.has(interfaceKey))
825
- continue;
826
- const ifaceLower = parsed.interfaceName.toLowerCase();
827
- const ifaceTokens = splitIdentifierTokens(parsed.interfaceName);
828
- const ifaceText = `${ifaceLower} ${ifaceTokens.join(' ')}`.trim();
829
- const fieldText = parsed.fields.join(' ').toLowerCase();
830
- const nameHits = countTermHitsInText(ifaceText, highSignalTerms);
831
- const fieldHits = countTermHitsInText(fieldText, highSignalTerms);
832
- let relevanceScore = nameHits * 2 + fieldHits;
833
- if (queryTermSet.has('plan') && ifaceText.includes('plan'))
834
- relevanceScore += 0.8;
835
- if (queryTermSet.has('cache') && ifaceText.includes('cache'))
836
- relevanceScore += 0.8;
837
- if (queryTermSet.has('key') && ifaceText.includes('key'))
838
- relevanceScore += 1.2;
839
- if ((queryTermSet.has('field') || queryTermSet.has('fields')) && parsed.fields.length > 0) {
840
- relevanceScore += Math.min(1, parsed.fields.length / 8);
841
- }
842
- if (highSignalTerms.length > 0 && relevanceScore === 0) {
843
- continue;
844
- }
845
- seen.add(interfaceKey);
846
- scored.push({
847
- score: relevanceScore,
848
- summary: `${parsed.interfaceName}: ${parsed.fields.slice(0, 12).join(', ')}`,
849
- });
850
1240
  }
851
- const ranked = scored.sort((a, b) => b.score - a.score);
852
- if (ranked.length === 0)
1241
+ const stdout = result.stdout || '';
1242
+ if (!stdout.trim())
853
1243
  return [];
854
- const topScore = ranked[0].score;
855
- let minScore = topScore >= 2 ? topScore * 0.6 : 0;
856
- const strictExactFieldRequest = queryTermSet.has('exact') &&
857
- (queryTermSet.has('field') || queryTermSet.has('fields'));
858
- if (strictExactFieldRequest) {
859
- minScore = Math.max(minScore, topScore - 0.5);
860
- }
861
- return ranked
862
- .filter((item) => item.score >= minScore)
863
- .slice(0, 6)
864
- .map((item) => item.summary);
865
- }
866
- function extractInsightLines(citations, limit) {
867
1244
  const out = [];
868
- const seen = new Set();
869
- for (const citation of citations) {
870
- const line = formatInsightSnippet(citation.snippet || '');
871
- if (!line || line.length < 10)
872
- continue;
873
- if (/^#+\s/.test(line))
874
- continue;
875
- if (/^name:\s+/i.test(line))
876
- continue;
877
- const tooNoisy = /[{}[\];=><]/.test(line) && !/\b(multi-tenant|single-tenant|organization|install|command|feature)\b/i.test(line);
878
- if (tooNoisy)
879
- continue;
880
- const key = line.toLowerCase();
881
- if (seen.has(key))
1245
+ for (const raw of stdout.split(/\r?\n/)) {
1246
+ const parsed = parseRgLine(raw.trim());
1247
+ if (!parsed)
882
1248
  continue;
883
- seen.add(key);
884
- out.push(line);
885
- if (out.length >= limit)
1249
+ out.push(parsed);
1250
+ if (out.length >= RG_MAX_MATCHES)
886
1251
  break;
887
1252
  }
888
1253
  return out;
889
1254
  }
890
- function isCommandCatalogIntent(normalizedQuestion) {
891
- const mentionsCommandSurface = /\b(cli|cmd|cmds|command|commands|subcommand|subcommands)\b/.test(normalizedQuestion);
892
- if (!mentionsCommandSurface)
893
- return false;
894
- const listIntent = /\blist\b/.test(normalizedQuestion) ||
895
- /\bshow\b/.test(normalizedQuestion) ||
896
- /\bavailable\b/.test(normalizedQuestion) ||
897
- /\ball commands?\b/.test(normalizedQuestion) ||
898
- /\bwhich commands\b/.test(normalizedQuestion) ||
899
- /\bwhat commands\b/.test(normalizedQuestion) ||
900
- /\bwhat can i (type|run)\b/.test(normalizedQuestion) ||
901
- /\bcan i type\b/.test(normalizedQuestion) ||
902
- /\bcmds\b/.test(normalizedQuestion);
903
- if (!listIntent)
904
- return false;
905
- const specificIntent = /\bwhere\b/.test(normalizedQuestion) ||
906
- /\bhow\b/.test(normalizedQuestion) ||
907
- /\bwhy\b/.test(normalizedQuestion) ||
908
- /\bwhen\b/.test(normalizedQuestion) ||
909
- /\bwhich file\b/.test(normalizedQuestion) ||
910
- /\bin which file\b/.test(normalizedQuestion) ||
911
- /\bfilepath\b/.test(normalizedQuestion) ||
912
- /\bfile path\b/.test(normalizedQuestion) ||
913
- /\binject(?:ed)?\b/.test(normalizedQuestion) ||
914
- /\bhandle(?:s|d)?\b/.test(normalizedQuestion) ||
915
- /\bused?\b/.test(normalizedQuestion) ||
916
- /\bflow\b/.test(normalizedQuestion);
917
- return !specificIntent;
918
- }
919
- function isPrimarySourcePath(filePath) {
920
- const normalized = filePath.trim().replace(/\\/g, '/').toLowerCase();
921
- if (!normalized)
922
- return false;
923
- if (normalized.startsWith('.neurcode/') ||
924
- normalized.startsWith('.github/') ||
925
- normalized.startsWith('.git/') ||
926
- normalized.startsWith('node_modules/') ||
927
- normalized.startsWith('dist/') ||
928
- normalized.startsWith('build/') ||
929
- normalized.startsWith('coverage/')) {
930
- return false;
1255
+ function fallbackScanMatches(cwd, fileTree, pattern, maxMatches = 1200) {
1256
+ const out = [];
1257
+ if (!pattern)
1258
+ return out;
1259
+ let regex;
1260
+ try {
1261
+ regex = new RegExp(pattern, 'i');
931
1262
  }
932
- if (normalized.includes('/dist/') || normalized.includes('/build/') || normalized.includes('/coverage/')) {
933
- return false;
1263
+ catch {
1264
+ return out;
934
1265
  }
935
- const ext = normalized.includes('.') ? normalized.split('.').pop() || '' : '';
936
- if (!ext) {
937
- // Allow extensionless source-like files (e.g. Dockerfile, Makefile).
938
- return true;
1266
+ for (const filePath of fileTree) {
1267
+ if (out.length >= maxMatches)
1268
+ break;
1269
+ if (isIgnoredSearchPath(filePath))
1270
+ continue;
1271
+ const fullPath = (0, path_1.join)(cwd, filePath);
1272
+ let content = '';
1273
+ try {
1274
+ const st = (0, fs_1.statSync)(fullPath);
1275
+ if (st.size > MAX_FILE_BYTES)
1276
+ continue;
1277
+ content = (0, fs_1.readFileSync)(fullPath, 'utf-8');
1278
+ }
1279
+ catch {
1280
+ continue;
1281
+ }
1282
+ const lines = content.split(/\r?\n/);
1283
+ for (let idx = 0; idx < lines.length; idx++) {
1284
+ const line = lines[idx];
1285
+ if (!line)
1286
+ continue;
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)
1295
+ break;
1296
+ }
939
1297
  }
940
- return PRIMARY_SOURCE_EXTENSIONS.has(ext);
941
- }
942
- function isBroadQuestion(normalizedQuestion) {
943
- return (/\banywhere\b/.test(normalizedQuestion) ||
944
- /\bacross\b/.test(normalizedQuestion) ||
945
- /\bworkflow\b/.test(normalizedQuestion) ||
946
- /\bentire\b/.test(normalizedQuestion) ||
947
- /\bwhole\b/.test(normalizedQuestion) ||
948
- /\ball\b/.test(normalizedQuestion) ||
949
- /\bin repo\b/.test(normalizedQuestion));
950
- }
951
- function calibrateConfidence(truth) {
952
- if (truth.status === 'insufficient')
953
- return 'low';
954
- if (truth.score >= 0.78)
955
- return 'high';
956
- if (truth.score >= 0.52)
957
- return 'medium';
958
- return 'low';
1298
+ return out;
959
1299
  }
960
- function evaluateTruthAssessment(mode, normalizedQuestion, terms, sourceCitations, sourcePerFileCounts, sourceTermCounts) {
961
- const broadQuestion = isBroadQuestion(normalizedQuestion);
962
- const minCitationsRequired = mode === 'comparison' ? 2 : 2;
963
- let minFilesRequired = mode === 'comparison' ? 2 : 1;
964
- const sourceCitationCount = sourceCitations.length;
965
- const sourceFileCount = sourcePerFileCounts.size;
966
- const hasStrongSingleFileEvidence = sourceFileCount === 1 && sourceCitationCount >= 6;
967
- if (broadQuestion) {
968
- minFilesRequired = hasStrongSingleFileEvidence ? minFilesRequired : Math.max(minFilesRequired, 2);
969
- }
970
- const reasons = [];
971
- if (sourceCitationCount === 0) {
972
- reasons.push('No direct source-file evidence was found for the query terms.');
973
- }
974
- if (sourceCitationCount < minCitationsRequired) {
975
- reasons.push(`Only ${sourceCitationCount} source citation(s) found (minimum ${minCitationsRequired} required).`);
976
- }
977
- if (sourceFileCount < minFilesRequired) {
978
- reasons.push(`Evidence spans ${sourceFileCount} file(s) (minimum ${minFilesRequired} required for this question).`);
979
- }
980
- if (mode === 'comparison' && terms.length >= 2) {
981
- const missingTerms = terms.filter((term) => (sourceTermCounts.get(term) || 0) === 0);
982
- if (missingTerms.length > 0) {
983
- reasons.push(`Missing direct source evidence for term(s): ${missingTerms.join(', ')}.`);
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;
1328
+ }
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;
1354
+ }
1355
+ if (codeFocused && !codeSnippet) {
1356
+ score -= 1.35;
1357
+ }
1358
+ if (docSnippet) {
1359
+ score -= codeFocused ? 1.9 : 0.6;
1360
+ }
1361
+ if (isDocPath && codeFocused) {
1362
+ score -= asksLocation ? 3.7 : 2.2;
1363
+ }
1364
+ if (promptExample) {
1365
+ score -= codeFocused ? 4.4 : 2.2;
1366
+ }
1367
+ if (/\b(?:export\s+)?(?:function|class|const|interface|type|enum)\b/.test(match.snippet)) {
1368
+ score += 0.85;
1369
+ }
1370
+ if (asksLocation && /\b(?:function|class|interface|type)\b/.test(match.snippet)) {
1371
+ score += 0.75;
1372
+ }
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;
1381
+ }
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;
1418
+ }
1419
+ }
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);
984
1478
  }
985
1479
  }
986
- if (mode === 'search') {
987
- const highSignalTerms = terms.filter((term) => !LOW_SIGNAL_TERMS.has(term));
988
- if (highSignalTerms.length > 0) {
989
- const matchedHighSignalTerms = highSignalTerms.filter((term) => (sourceTermCounts.get(term) || 0) > 0);
990
- if (matchedHighSignalTerms.length === 0) {
991
- reasons.push('No high-signal query terms were grounded in source citations.');
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
+ };
1488
+ }
1489
+ function selectTopCitations(scored, maxCitations, searchTerms) {
1490
+ if (scored.length === 0)
1491
+ return [];
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;
992
1558
  }
993
1559
  }
994
1560
  }
995
- let dominantShare = 0;
996
- if (sourceCitationCount > 0) {
997
- const highest = Math.max(...sourcePerFileCounts.values());
998
- dominantShare = highest / sourceCitationCount;
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';
1566
+ });
1567
+ if (commandAnchored.length >= Math.min(maxCitations, 3)) {
1568
+ candidates = commandAnchored;
1569
+ }
1570
+ }
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);
999
1576
  }
1000
- if (broadQuestion && dominantShare > 0.85 && sourceFileCount < 3 && !hasStrongSingleFileEvidence) {
1001
- reasons.push('Evidence is highly concentrated in one file; broader coverage is required for this query.');
1577
+ for (const list of byFile.values()) {
1578
+ list.sort((a, b) => b.score - a.score);
1002
1579
  }
1003
- const citationScore = Math.min(1, sourceCitationCount / Math.max(minCitationsRequired, 4));
1004
- const fileScore = Math.min(1, sourceFileCount / Math.max(minFilesRequired, 3));
1005
- let termCoverageScore = 0;
1006
- if (mode === 'comparison' && terms.length >= 2) {
1007
- termCoverageScore = terms.filter((term) => (sourceTermCounts.get(term) || 0) > 0).length / terms.length;
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());
1008
1586
  }
1009
- else if (mode === 'search') {
1010
- const highSignalTerms = terms.filter((term) => !LOW_SIGNAL_TERMS.has(term));
1011
- if (highSignalTerms.length === 0) {
1012
- termCoverageScore = sourceCitationCount > 0 ? 1 : 0;
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);
1593
+ }
1594
+ return selected.map(({ path, line, snippet, term }) => ({ path, line, snippet, term }));
1595
+ }
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));
1621
+ const reasons = [];
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}.`);
1650
+ }
1013
1651
  }
1014
- else {
1015
- termCoverageScore = highSignalTerms.filter((term) => (sourceTermCounts.get(term) || 0) > 0).length / highSignalTerms.length;
1652
+ if (anchoredRatio < 0.35) {
1653
+ score *= 0.68;
1654
+ }
1655
+ else if (anchoredRatio < 0.55) {
1656
+ score *= 0.86;
1016
1657
  }
1017
1658
  }
1018
- const concentrationPenalty = dominantShare > 0.9 ? 0.08 : dominantShare > 0.75 ? 0.04 : 0;
1019
- let score = citationScore * 0.45 + fileScore * 0.35 + termCoverageScore * 0.2 - concentrationPenalty;
1020
- if (!Number.isFinite(score))
1021
- score = 0;
1022
- score = Math.max(0, Math.min(score, 1));
1023
- if (score < 0.42 && reasons.length === 0) {
1024
- 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.');
1025
1661
  }
1026
1662
  return {
1027
- status: reasons.length > 0 ? 'insufficient' : 'grounded',
1663
+ status: reasons.length === 0 ? 'grounded' : 'insufficient',
1028
1664
  score,
1029
1665
  reasons,
1030
- sourceCitations: sourceCitationCount,
1031
- sourceFiles: sourceFileCount,
1666
+ sourceCitations,
1667
+ sourceFiles,
1032
1668
  minCitationsRequired,
1033
1669
  minFilesRequired,
1034
1670
  };
1035
1671
  }
1036
- function buildAnswer(mode, question, terms, citations, stats, termCounts, perFileCounts, truth, context = {}) {
1037
- const confidence = calibrateConfidence(truth);
1038
- const normalizedQuestion = (0, plan_cache_1.normalizeIntent)(question);
1039
- const findings = [];
1040
- let answer = '';
1041
- const asksCommandCatalog = isCommandCatalogIntent(normalizedQuestion);
1042
- const asksInstall = /\b(install|upgrade|update|latest)\b/.test(normalizedQuestion);
1043
- const asksFeatures = /\b(feature|features|capability|capabilities|offers?)\b/.test(normalizedQuestion);
1044
- const asksSchema = /\b(field|fields|key|keys|schema|interface|input|output|parameter|parameters)\b/.test(normalizedQuestion);
1045
- const asksTenancy = /\b(tenant|tenancy|single|multi|organization)\b/.test(normalizedQuestion);
1046
- const asksLocation = /\b(where|which file|in which file|filepath|file path|location|defined|implemented|called|computed|resolved)\b/.test(normalizedQuestion);
1047
- const asksHow = /\bhow\b/.test(normalizedQuestion);
1048
- const asksDecision = /\b(decide|decision|verdict|pass|fail)\b/.test(normalizedQuestion);
1049
- const asksCommandSurface = /\b(command|commands|subcommand|subcommands|flag|flags|option|options)\b/.test(normalizedQuestion);
1050
- const asksSingleCommandLookup = /\b(what|which)\s+command\b/.test(normalizedQuestion) &&
1051
- !/\b(commands|subcommands)\b/.test(normalizedQuestion);
1052
- const asksOrgRequestInjection = /\b(inject|injected|header|request|requests)\b/.test(normalizedQuestion) &&
1053
- /\b(org|organization)\b/.test(normalizedQuestion);
1054
- const extractedCommandMatches = extractNeurcodeCommandsFromCitations(citations);
1055
- const knownCommands = context.knownCommands || [];
1056
- const knownCommandSet = new Set(knownCommands);
1057
- const knownRoots = new Set(knownCommands.map((command) => command.split(/\s+/).slice(0, 2).join(' ')));
1058
- const filteredExtractedMatches = knownCommands.length > 0
1059
- ? extractedCommandMatches.filter((command) => {
1060
- if (knownCommandSet.has(command))
1061
- return true;
1062
- const root = command.split(/\s+/).slice(0, 2).join(' ');
1063
- return knownRoots.has(root);
1064
- })
1065
- : extractedCommandMatches;
1066
- const commandMatches = [
1067
- ...knownCommands,
1068
- ...filteredExtractedMatches,
1069
- ].filter((value, index, arr) => arr.indexOf(value) === index);
1070
- const commandFocus = extractCommandFocus(normalizedQuestion);
1071
- const installMatches = extractInstallCommandsFromCitations(citations);
1072
- const insightLines = extractInsightLines(citations, 5);
1073
- const highSignalAnswerTerms = terms.filter((term) => !LOW_SIGNAL_TERMS.has(term) && term.length >= 3).slice(0, 10);
1074
- const anchorTerms = extractAnchorTerms(question);
1075
- const queriedFlags = (question.match(/--[a-z0-9-]+/gi) || []).map((flag) => flag.toLowerCase());
1076
- const pathAnchorTerms = (0, plan_cache_1.normalizeIntent)(question)
1077
- .replace(/[^a-z0-9_\-\s]/g, ' ')
1078
- .split(/\s+/)
1079
- .map((token) => token.trim())
1080
- .filter((token) => token.length >= 3 &&
1081
- !STOP_WORDS.has(token) &&
1082
- ![
1083
- 'where', 'which', 'file', 'files', 'path', 'paths', 'location',
1084
- 'defined', 'implemented', 'called', 'computed', 'resolved',
1085
- ].includes(token))
1086
- .slice(0, 10);
1087
- const citationRelevanceScore = (citation) => {
1088
- const snippetLower = (citation.snippet || '').toLowerCase();
1089
- let score = countTermHitsInText(snippetLower, highSignalAnswerTerms);
1090
- score += countTermHitsInText(citation.path.toLowerCase(), highSignalAnswerTerms) * 0.5;
1091
- const pathAnchorHits = countTermHitsInText(citation.path.toLowerCase(), pathAnchorTerms);
1092
- score += pathAnchorHits * 0.9;
1093
- if (asksLocation && pathAnchorTerms.length > 0 && pathAnchorHits === 0) {
1094
- score -= 0.35;
1095
- }
1096
- const anchorHits = countTermHitsInText(snippetLower, anchorTerms);
1097
- score += anchorHits * 1.4;
1098
- if ((asksLocation || asksHow) && anchorTerms.length > 0 && anchorHits === 0) {
1099
- score -= 0.45;
1100
- }
1101
- if (queriedFlags.length > 0) {
1102
- const flagHits = queriedFlags.filter((flag) => snippetLower.includes(flag)).length;
1103
- score += flagHits * 2;
1104
- if (asksLocation && flagHits === 0)
1105
- score -= 0.4;
1106
- if (/\.option\(/i.test(snippetLower) && flagHits > 0)
1107
- score += 2.5;
1108
- }
1109
- if (asksCommandSurface) {
1110
- if (/\.command\(|\bcommand\(/i.test(snippetLower))
1111
- score += 2;
1112
- if (/\.option\(|--[a-z0-9-]+/i.test(snippetLower))
1113
- score += 1.4;
1114
- if (citation.path.includes('/commands/') || citation.path.endsWith('/index.ts'))
1115
- score += 0.6;
1116
- }
1117
- if (commandFocus) {
1118
- if (citation.path.includes(`/commands/${commandFocus}.`)) {
1119
- score += 2.1;
1120
- }
1121
- else if ((asksLocation || asksHow) && citation.path.includes('/commands/')) {
1122
- score -= 1.0;
1123
- }
1124
- if (citation.path.endsWith('/index.ts') &&
1125
- new RegExp(`\\.command\\('${escapeRegExp(commandFocus)}'\\)`, 'i').test(citation.snippet)) {
1126
- score += 1.4;
1127
- }
1128
- }
1129
- if ((asksLocation || asksHow) && looksLikeImportLine(citation.snippet)) {
1130
- score -= 0.8;
1131
- }
1132
- if ((asksLocation || asksHow) && /^\s*[?:]?\s*['"`].+['"`][,;]?\s*$/i.test(citation.snippet)) {
1133
- score -= 0.9;
1134
- }
1135
- if (asksLocation && /console\.log\(/i.test(citation.snippet)) {
1136
- score -= 1.0;
1137
- }
1138
- if (asksDecision) {
1139
- if (/\b(verdict|exitcode|policydecision)\b/i.test(snippetLower) || /\bif\s*\(|\?\s*'[^']+'\s*:\s*'[^']+'/i.test(citation.snippet)) {
1140
- score += 1.1;
1141
- }
1142
- if (/(log|roi|estimat|time saved|badge|banner)/i.test(snippetLower)) {
1143
- score -= 0.9;
1144
- }
1145
- }
1146
- return score;
1147
- };
1148
- const multiSignals = citations.filter((citation) => /\bmulti[- ]tenant|x-org-id|organization[_ -]?id|org-scoped\b/i.test(citation.snippet)).length;
1149
- const singleSignals = citations.filter((citation) => /\bsingle[- ]tenant|single-user\b/i.test(citation.snippet)).length;
1150
- if (mode === 'comparison' && terms.length >= 2) {
1151
- const left = terms[0];
1152
- const right = terms[1];
1153
- const leftCount = termCounts.get(left) || 0;
1154
- const rightCount = termCounts.get(right) || 0;
1155
- if (leftCount > 0 && rightCount > 0) {
1156
- answer = `Found both "${left}" (${leftCount}) and "${right}" (${rightCount}) references in the scanned scope.`;
1157
- }
1158
- else if (leftCount > 0) {
1159
- answer = `Found "${left}" references (${leftCount}) and no direct "${right}" references in the scanned scope.`;
1160
- }
1161
- else if (rightCount > 0) {
1162
- answer = `No direct "${left}" references found; "${right}" appears ${rightCount} time(s) in the scanned scope.`;
1163
- }
1164
- else {
1165
- answer = `No direct references to "${left}" or "${right}" were found in the scanned scope.`;
1166
- }
1167
- findings.push(`Compared terms: "${left}" vs "${right}"`);
1168
- 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}`;
1169
1723
  }
1170
- else if (citations.length === 0) {
1171
- answer = 'I could not find direct grounded evidence for that in the files I scanned.';
1172
- 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}`;
1173
1726
  }
1174
- else if (asksInstall) {
1175
- const manifestInstall = context.cliPackageName ? `npm install -g ${context.cliPackageName}@latest` : null;
1176
- const primaryInstall = manifestInstall || installMatches[0];
1177
- if (!primaryInstall) {
1178
- answer = 'I found CLI references, but not an explicit install command in the matched evidence.';
1179
- }
1180
- else {
1181
- answer = [
1182
- 'Here is the install command I found for the CLI:',
1183
- `\`${primaryInstall}\``,
1184
- installMatches.length > 1 ? `Also seen: ${installMatches.slice(1, 3).map((cmd) => `\`${cmd}\``).join(', ')}` : '',
1185
- ].filter(Boolean).join('\n');
1186
- }
1727
+ if (/\b(orgid|organizationid|userid|token|auth)\b/i.test(snippet) && /=/.test(snippet)) {
1728
+ return `${location} sets auth/org context: ${snippet}`;
1187
1729
  }
1188
- else if (asksFeatures && (context.featureBullets || []).length > 0) {
1189
- const bullets = (context.featureBullets || []).slice(0, 6).map((line) => ` • ${line}`);
1190
- answer = ['Here are the main platform features I could verify from the repo:', ...bullets].join('\n');
1730
+ if (/\breturn\b/.test(snippet)) {
1731
+ return `${location} returns runtime behavior: ${snippet}`;
1191
1732
  }
1192
- else if (asksSchema && citations.length > 0) {
1193
- const lineCache = context.lineCache || new Map();
1194
- const schemaFieldSummaries = buildSchemaFieldSummaries(citations, lineCache, terms);
1195
- if (schemaFieldSummaries.length > 0) {
1196
- const bullets = schemaFieldSummaries.map((line) => ` • ${line}`);
1197
- answer = ['From the matched code, these fields/signatures are relevant:', ...bullets].join('\n');
1198
- }
1199
- else {
1200
- const schemaLines = citations
1201
- .map((citation) => formatInsightSnippet(citation.snippet))
1202
- .filter((line) => /^export\s+interface\b/i.test(line) ||
1203
- /^export\s+type\b/i.test(line) ||
1204
- (/^[A-Za-z_][A-Za-z0-9_?]*\s*:\s*[^=,]+;?$/.test(line) &&
1205
- !line.includes('{') &&
1206
- !line.includes('=>')));
1207
- const selected = [...new Set(schemaLines)].slice(0, 8);
1208
- if (selected.length > 0) {
1209
- const bullets = selected.map((line) => ` • ${line}`);
1210
- answer = ['From the matched code, these fields/signatures are relevant:', ...bullets].join('\n');
1211
- }
1212
- else {
1213
- answer = 'I found cache/schema-related code, but not enough direct field declarations in the top evidence.';
1214
- }
1215
- }
1733
+ if (/\bawait\b/.test(snippet)) {
1734
+ return `${location} performs async flow step: ${snippet}`;
1216
1735
  }
1217
- else if (asksSingleCommandLookup && commandMatches.length > 0) {
1218
- const commandQueryTerms = [...new Set(tokenizeQuestion(question).flatMap((term) => {
1219
- const out = [term.toLowerCase()];
1220
- if (term.endsWith('s') && term.length > 4)
1221
- out.push(term.slice(0, -1).toLowerCase());
1222
- if (term.endsWith('ing') && term.length > 5)
1223
- out.push(term.slice(0, -3).toLowerCase());
1224
- if (term.endsWith('ed') && term.length > 4)
1225
- out.push(term.slice(0, -2).toLowerCase());
1226
- return out;
1227
- }))];
1228
- const scored = commandMatches
1229
- .map((command) => {
1230
- const normalized = command.toLowerCase();
1231
- let score = countTermHitsInText(normalized, commandQueryTerms);
1232
- if (/\bend(s|ed|ing)?\b/.test(normalizedQuestion) && /\bend\b/.test(normalized))
1233
- score += 0.8;
1234
- if (commandFocus &&
1235
- (normalized === `neurcode ${commandFocus}` || normalized.startsWith(`neurcode ${commandFocus} `))) {
1236
- score += 1.2;
1237
- }
1238
- return { command, score };
1239
- })
1240
- .sort((a, b) => b.score - a.score);
1241
- const selected = (scored.filter((item) => item.score > 0).slice(0, 3).map((item) => item.command));
1242
- const top = selected.length > 0 ? selected : [scored[0].command];
1243
- answer = top.length === 1
1244
- ? `Use \`${top[0]}\`.`
1245
- : ['Most relevant commands:', ...top.map((command) => ` • \`${command}\``)].join('\n');
1246
- }
1247
- else if (asksCommandCatalog && commandMatches.length > 0) {
1248
- const scopedCommands = commandFocus
1249
- ? commandMatches.filter((command) => command === `neurcode ${commandFocus}` || command.startsWith(`neurcode ${commandFocus} `))
1250
- : commandMatches;
1251
- const normalizedCommands = (scopedCommands.length > 0 ? scopedCommands : commandMatches)
1252
- .filter((command) => /^neurcode\s+[a-z]/.test(command))
1253
- .slice(0, 22);
1254
- const commandBullets = normalizedCommands.map((command) => ` • \`${command}\``);
1255
- answer = ['Here are the CLI commands I could verify from the repo:', ...commandBullets].join('\n');
1256
- }
1257
- else if (asksLocation && citations.length > 0) {
1258
- const focusedCitations = citations.filter((citation) => {
1259
- const term = (citation.term || '').toLowerCase();
1260
- return term.length === 0 || !GENERIC_OUTPUT_TERMS.has(term);
1261
- });
1262
- const locationPool = focusedCitations.length > 0 ? [...focusedCitations] : [...citations];
1263
- locationPool.sort((a, b) => citationRelevanceScore(b) - citationRelevanceScore(a));
1264
- if (asksOrgRequestInjection) {
1265
- const score = (citation) => {
1266
- const snippet = citation.snippet.toLowerCase();
1267
- let value = 0;
1268
- if (snippet.includes('x-org-id'))
1269
- value += 4;
1270
- if (snippet.includes('auto-inject') || snippet.includes('inject'))
1271
- value += 2;
1272
- if (snippet.includes('headers[') || snippet.includes('header'))
1273
- value += 2;
1274
- if (snippet.includes('request'))
1275
- value += 1;
1276
- if (citation.path.includes('api-client.ts'))
1277
- value += 2;
1278
- return value;
1279
- };
1280
- locationPool.sort((a, b) => score(b) - score(a));
1281
- }
1282
- const prefersImplementationLines = /\b(defined|implemented|called|computed|resolved|lookup)\b/.test(normalizedQuestion);
1283
- const queriedFlags = (question.match(/--[a-z0-9-]+/gi) || []).map((flag) => flag.toLowerCase());
1284
- const declarationLike = prefersImplementationLines
1285
- ? locationPool.filter((citation) => /\b(?:export\s+)?(?:function|const|let|var|class|interface|type)\b/i.test(citation.snippet))
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}`)]
1286
1778
  : [];
1287
- const orderedLocationPool = declarationLike.length >= 2
1288
- ? [...declarationLike, ...locationPool.filter((citation) => !declarationLike.includes(citation))]
1289
- : locationPool;
1290
- const commandFocusedLocationPool = commandFocus
1291
- ? [...orderedLocationPool].sort((a, b) => {
1292
- const score = (citation) => {
1293
- let value = 0;
1294
- if (citation.path.includes(`/commands/${commandFocus}.`))
1295
- value += 3;
1296
- if (citation.path.endsWith('/index.ts') &&
1297
- new RegExp(`\\.command\\('${escapeRegExp(commandFocus)}'\\)`, 'i').test(citation.snippet)) {
1298
- value += 2;
1299
- }
1300
- if (citation.path.includes('/commands/') && !citation.path.includes(`/commands/${commandFocus}.`)) {
1301
- value -= 0.5;
1302
- }
1303
- if (queriedFlags.length > 0) {
1304
- const snippetLower = citation.snippet.toLowerCase();
1305
- const matchedFlagCount = queriedFlags.filter((flag) => snippetLower.includes(flag)).length;
1306
- value += matchedFlagCount * 2;
1307
- if (/\.option\(/i.test(citation.snippet) && matchedFlagCount > 0)
1308
- value += 2;
1309
- }
1310
- return value;
1311
- };
1312
- return score(b) - score(a);
1313
- })
1314
- : orderedLocationPool;
1315
- const locations = commandFocusedLocationPool
1316
- .slice(0, 5)
1317
- .map((citation) => ` • ${citation.path}:${citation.line} — ${formatInsightSnippet(citation.snippet)}`);
1318
- answer = ['I found the relevant references here:', ...locations].join('\n');
1319
- }
1320
- else if (asksHow && insightLines.length > 0) {
1321
- const focusedInsights = [...citations]
1322
- .sort((a, b) => citationRelevanceScore(b) - citationRelevanceScore(a))
1323
- .filter((citation) => citationRelevanceScore(citation) >= 1)
1324
- .map((citation) => formatInsightSnippet(citation.snippet))
1325
- .filter((line) => !/^const\s+[A-Za-z_$][\w$]*\s*=\s*\/.+\/[a-z]*\.test\(/i.test(line))
1326
- .filter((line) => !/^[A-Za-z_$][\w$]*,\s*$/.test(line))
1327
- .filter((line) => !/^['"`].+['"`],?\s*$/.test(line));
1328
- const decisionFilteredInsights = asksDecision
1329
- ? focusedInsights.filter((line) => !/(log|roi|estimat|time saved|badge|banner|record[a-z]*event|telemetry|metric)/i.test(line))
1330
- : focusedInsights;
1331
- const decisionCoreInsights = asksDecision
1332
- ? decisionFilteredInsights.filter((line) => /\b(verdict|policydecision|exitcode|pass|fail|warn)\b/i.test(line) ||
1333
- /\?\s*'[^']+'\s*:\s*'[^']+'/.test(line))
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}`)]
1334
1795
  : [];
1335
- const selectedInsights = decisionCoreInsights.length > 0
1336
- ? [...new Set(decisionCoreInsights)].slice(0, 5)
1337
- : decisionFilteredInsights.length > 0
1338
- ? [...new Set(decisionFilteredInsights)].slice(0, 5)
1339
- : focusedInsights.length > 0
1340
- ? [...new Set(focusedInsights)].slice(0, 5)
1341
- : insightLines;
1342
- const bullets = selectedInsights.map((line) => ` • ${line}`);
1343
- answer = ['From the matched code, this is how it works:', ...bullets].join('\n');
1344
- }
1345
- else if (asksTenancy && (multiSignals > 0 || singleSignals > 0)) {
1346
- if (multiSignals >= singleSignals + 2) {
1347
- answer = 'This codebase is multi-tenant, with organization-scoped flows (for example org IDs / `x-org-id` context).';
1348
- }
1349
- else if (singleSignals >= multiSignals + 2) {
1350
- answer = 'This codebase currently looks single-tenant from the scanned evidence.';
1351
- }
1352
- else {
1353
- answer = 'I see mixed tenancy signals, but it leans multi-tenant in current runtime paths.';
1354
- }
1355
- }
1356
- else if (insightLines.length > 0) {
1357
- const bullets = insightLines.map((line) => ` • ${line}`);
1358
- answer = truth.status === 'insufficient'
1359
- ? ['I found partial evidence, but not enough for a fully definitive answer yet.', 'What I can confirm so far:', ...bullets].join('\n')
1360
- : ['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');
1361
1813
  }
1362
1814
  else {
1363
- answer = `I found grounded evidence in ${stats.matchedFiles} file(s), but it is mostly low-level implementation detail.`;
1364
- }
1365
- if (asksCommandCatalog && commandMatches.length === 0 && citations.length > 0) {
1366
- 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}`);
1367
1829
  }
1368
- if (asksInstall && installMatches.length === 0 && !context.cliPackageName) {
1369
- 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(' -> ')}`);
1370
1832
  }
1371
- const topFiles = [...perFileCounts.entries()]
1372
- .sort((a, b) => b[1] - a[1])
1373
- .slice(0, 3)
1374
- .map(([file, count]) => `${file} (${count})`);
1375
- if (topFiles.length > 0) {
1376
- 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(', ')}`);
1377
1835
  }
1378
- findings.push(`Scanned ${stats.scannedFiles} file(s); matched ${stats.matchedFiles} file(s).`);
1379
1836
  if (truth.status === 'insufficient') {
1380
1837
  findings.push(...truth.reasons);
1381
- 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.');
1382
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);
1383
1847
  return {
1384
1848
  question,
1385
- questionNormalized: normalizedQuestion,
1386
- mode,
1849
+ questionNormalized: searchTerms.normalizedQuestion,
1850
+ mode: 'search',
1387
1851
  answer,
1388
1852
  findings,
1389
- confidence,
1853
+ confidence: calibrateConfidence(truth),
1854
+ proof: {
1855
+ topFiles,
1856
+ evidenceCount: citations.length,
1857
+ coverage: {
1858
+ sourceCitations: truth.sourceCitations,
1859
+ sourceFiles: truth.sourceFiles,
1860
+ matchedFiles: stats.matchedFiles,
1861
+ matchedLines: stats.matchedLines,
1862
+ },
1863
+ },
1390
1864
  truth: {
1391
1865
  status: truth.status,
1392
1866
  score: Number(truth.score.toFixed(2)),
@@ -1401,6 +1875,168 @@ function buildAnswer(mode, question, terms, citations, stats, termCounts, perFil
1401
1875
  stats,
1402
1876
  };
1403
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
+ }
1404
2040
  function emitAskResult(result, options) {
1405
2041
  if (options.json) {
1406
2042
  console.log(JSON.stringify({
@@ -1426,32 +2062,50 @@ function emitAskResult(result, options) {
1426
2062
  else {
1427
2063
  console.log(chalk.yellow(result.answer));
1428
2064
  }
1429
- if (!options.verbose) {
2065
+ const showProof = options.proof === true;
2066
+ const showVerbose = options.verbose === true;
2067
+ if (!showProof && !showVerbose) {
2068
+ console.log(chalk.dim(`\nConfidence: ${result.confidence.toUpperCase()}`));
1430
2069
  if (result.truth.status === 'insufficient' && result.truth.reasons.length > 0) {
1431
2070
  console.log(chalk.yellow('\nWhy confidence is limited:'));
1432
- for (const reason of result.truth.reasons.slice(0, 3)) {
2071
+ for (const reason of result.truth.reasons.slice(0, 2)) {
1433
2072
  console.log(chalk.yellow(` • ${reason}`));
1434
2073
  }
2074
+ console.log(chalk.dim('\nTip: add `--proof` for concise evidence or `--verbose` for full evidence output.'));
1435
2075
  }
1436
- console.log(chalk.dim('\nTip: add `--verbose` to show citations and full grounding details.'));
1437
2076
  return;
1438
2077
  }
1439
- if (result.findings.length > 0) {
1440
- console.log(chalk.bold.white('\nFindings:'));
1441
- for (const finding of result.findings) {
1442
- console.log(chalk.cyan(` • ${finding}`));
2078
+ if (showProof) {
2079
+ if (result.proof) {
2080
+ console.log(chalk.bold.white('\nProof:'));
2081
+ if (result.proof.topFiles.length > 0) {
2082
+ console.log(chalk.cyan(` • Top files: ${result.proof.topFiles.slice(0, 5).join(', ')}`));
2083
+ }
2084
+ console.log(chalk.cyan(` • Coverage: citations=${result.proof.coverage.sourceCitations}, files=${result.proof.coverage.sourceFiles}, matched_lines=${result.proof.coverage.matchedLines}`));
2085
+ }
2086
+ const proofCitations = result.citations.slice(0, Math.min(options.maxCitations, 6));
2087
+ if (proofCitations.length > 0) {
2088
+ console.log(chalk.bold.white('\nKey Evidence:'));
2089
+ proofCitations.forEach((citation, idx) => {
2090
+ console.log(chalk.dim(` ${idx + 1}. ${citation.path}:${citation.line} ${citation.snippet}`));
2091
+ });
1443
2092
  }
1444
2093
  }
1445
- const citations = result.citations.slice(0, options.maxCitations);
1446
- if (citations.length > 0) {
1447
- console.log(chalk.bold.white('\nEvidence:'));
1448
- citations.forEach((citation, idx) => {
1449
- const prefix = citation.term ? `${citation.term} ` : '';
1450
- console.log(chalk.dim(` ${idx + 1}. ${citation.path}:${citation.line} ${prefix}${citation.snippet}`));
1451
- });
1452
- }
1453
- else {
1454
- console.log(chalk.yellow('\nEvidence: no direct matching lines found.'));
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
+ });
2108
+ }
1455
2109
  }
1456
2110
  const truthLabel = result.truth.status === 'grounded' ? chalk.green('GROUNDED') : chalk.yellow('INSUFFICIENT');
1457
2111
  console.log(chalk.dim(`\nTruth Mode: ${truthLabel} (score=${result.truth.score.toFixed(2)}, source_citations=${result.truth.sourceCitations}, source_files=${result.truth.sourceFiles})`));
@@ -1472,22 +2126,106 @@ async function askCommand(question, options = {}) {
1472
2126
  const scope = { orgId: orgId || null, projectId: projectId || null };
1473
2127
  const maxCitations = Math.max(3, Math.min(options.maxCitations || 12, 30));
1474
2128
  const shouldUseCache = options.cache !== false && process.env.NEURCODE_ASK_NO_CACHE !== '1';
1475
- const normalizedQuestion = (0, plan_cache_1.normalizeIntent)(question);
1476
- const deterministic = tryBuildDeterministicAnswer(cwd, question, normalizedQuestion);
1477
- if (deterministic) {
1478
- if (!options.json) {
1479
- console.log(chalk.dim(`🧠 Asking repo context in ${cwd}...`));
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
+ });
2144
+ }
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
+ });
1480
2213
  }
1481
- emitAskResult(deterministic.payload, {
2214
+ return;
2215
+ }
2216
+ const askCacheFlowAnswer = buildAskCacheFlowDeterministicAnswer(cwd, question, searchTerms, maxCitations);
2217
+ if (askCacheFlowAnswer) {
2218
+ emitAskResult(askCacheFlowAnswer, {
1482
2219
  json: options.json,
1483
2220
  maxCitations,
1484
2221
  fromPlan: options.fromPlan,
1485
2222
  verbose: options.verbose,
2223
+ proof: options.proof,
1486
2224
  });
1487
2225
  if (orgId && projectId) {
1488
2226
  (0, brain_context_1.recordBrainProgressEvent)(cwd, scope, {
1489
2227
  type: 'ask',
1490
- 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)}`,
1491
2229
  });
1492
2230
  }
1493
2231
  return;
@@ -1514,12 +2252,12 @@ async function askCommand(question, options = {}) {
1514
2252
  });
1515
2253
  }
1516
2254
  catch {
1517
- // non-blocking
2255
+ // Non-blocking.
1518
2256
  }
1519
2257
  }
1520
2258
  if (shouldUseCache && orgId && projectId) {
1521
2259
  const questionHash = (0, ask_cache_1.computeAskQuestionHash)({
1522
- question: normalizedQuestion,
2260
+ question: searchTerms.normalizedQuestion,
1523
2261
  contextHash: staticContext.hash,
1524
2262
  });
1525
2263
  const exactKey = (0, ask_cache_1.computeAskCacheKey)({
@@ -1536,7 +2274,7 @@ async function askCommand(question, options = {}) {
1536
2274
  const exactOutput = {
1537
2275
  ...exact.output,
1538
2276
  question,
1539
- questionNormalized: normalizedQuestion,
2277
+ questionNormalized: searchTerms.normalizedQuestion,
1540
2278
  };
1541
2279
  emitAskResult(exactOutput, {
1542
2280
  json: options.json,
@@ -1544,6 +2282,7 @@ async function askCommand(question, options = {}) {
1544
2282
  cacheLabel: `Using cached answer (created: ${new Date(exact.createdAt).toLocaleString()})`,
1545
2283
  fromPlan: options.fromPlan,
1546
2284
  verbose: options.verbose,
2285
+ proof: options.proof,
1547
2286
  });
1548
2287
  (0, brain_context_1.recordBrainProgressEvent)(cwd, scope, {
1549
2288
  type: 'ask',
@@ -1555,18 +2294,18 @@ async function askCommand(question, options = {}) {
1555
2294
  orgId,
1556
2295
  projectId,
1557
2296
  repo: repoFingerprint,
1558
- question: normalizedQuestion,
2297
+ question: searchTerms.normalizedQuestion,
1559
2298
  policyVersionHash,
1560
2299
  neurcodeVersion,
1561
2300
  contextHash: staticContext.hash,
1562
2301
  changedPaths: (0, ask_cache_1.getChangedWorkingTreePaths)(cwd),
1563
- minSimilarity: 0.68,
2302
+ minSimilarity: 0.72,
1564
2303
  });
1565
2304
  if (near) {
1566
2305
  const nearOutput = {
1567
2306
  ...near.entry.output,
1568
2307
  question,
1569
- questionNormalized: normalizedQuestion,
2308
+ questionNormalized: searchTerms.normalizedQuestion,
1570
2309
  };
1571
2310
  const reasonText = near.reason === 'safe_repo_drift_similar_question'
1572
2311
  ? 'Using near-cached answer (safe repo drift)'
@@ -1577,6 +2316,7 @@ async function askCommand(question, options = {}) {
1577
2316
  cacheLabel: `${reasonText}, similarity ${near.similarity.toFixed(2)}, created: ${new Date(near.entry.createdAt).toLocaleString()}`,
1578
2317
  fromPlan: options.fromPlan,
1579
2318
  verbose: options.verbose,
2319
+ proof: options.proof,
1580
2320
  });
1581
2321
  (0, brain_context_1.recordBrainProgressEvent)(cwd, scope, {
1582
2322
  type: 'ask',
@@ -1589,414 +2329,39 @@ async function askCommand(question, options = {}) {
1589
2329
  console.log(chalk.dim(`🧠 Asking repo context in ${cwd}...`));
1590
2330
  }
1591
2331
  const brainResults = orgId && projectId
1592
- ? (0, brain_context_1.searchBrainContextEntries)(cwd, scope, normalizedQuestion, { limit: 48 })
2332
+ ? (0, brain_context_1.searchBrainContextEntries)(cwd, scope, searchTerms.normalizedQuestion, { limit: 64 })
1593
2333
  : { entries: [], totalIndexedFiles: 0 };
1594
- const cliPackageName = readCliPackageName(cwd);
1595
- const knownCliCommands = extractCommandsFromCliIndex(cwd);
1596
- const featureBullets = extractFeatureBulletsFromReadme(cwd, 10);
1597
- if (!options.json && brainResults.entries.length > 0) {
1598
- const top = brainResults.entries.filter((entry) => entry.score > 0).length;
1599
- console.log(chalk.dim(`🧠 Brain retrieval: ${top} relevant file summaries from ${brainResults.totalIndexedFiles} indexed files`));
1600
- }
1601
- const { mode, terms, matchers } = buildMatchers(question);
1602
- const pathHints = derivePathHints(question);
1603
- const mentionsAskCommand = /\bask\b/.test(normalizedQuestion);
1604
- const asksQuestionAnsweringInternals = /\b(question|questions|answer|answers|cache|brain|citation|grounded)\b/.test(normalizedQuestion);
1605
- if (matchers.length === 0) {
1606
- console.error(chalk.red('❌ Could not derive useful search terms from the question.'));
1607
- process.exit(1);
1608
- }
1609
- const candidateSet = new Set();
1610
- const pathPriority = new Map();
2334
+ const pathBoostScores = new Map();
1611
2335
  for (const entry of brainResults.entries) {
1612
- candidateSet.add(entry.path);
1613
- pathPriority.set(entry.path, (pathPriority.get(entry.path) || 0) + entry.score);
1614
- }
1615
- addAnchorCandidates(fileTree, candidateSet, pathPriority, normalizedQuestion);
1616
- const tokenHints = tokenizeQuestion(question);
1617
- const codeFocusedQuestion = /\b(how|where|which file|defined|implemented|called|computed|resolved|function|class|api|command|flow|trace|why|field|fields|key|keys|schema|interface|parameter|parameters)\b/.test(normalizedQuestion) &&
1618
- !/\b(feature|features|install|setup|readme|docs|documentation)\b/.test(normalizedQuestion);
1619
- if (candidateSet.size < 80) {
1620
- for (const filePath of fileTree) {
1621
- const normalizedPath = filePath.toLowerCase();
1622
- let score = pathPriority.get(filePath) || 0;
1623
- for (const token of tokenHints) {
1624
- if (normalizedPath.includes(token))
1625
- score += 0.2;
1626
- }
1627
- if (normalizedPath.startsWith('.github/')) {
1628
- score -= 0.2;
1629
- }
1630
- if (normalizedPath.startsWith('scripts/')) {
1631
- score -= 0.1;
1632
- }
1633
- if (codeFocusedQuestion && (filePath === 'README.md' || normalizedPath.startsWith('docs/'))) {
1634
- score -= 0.35;
1635
- }
1636
- if (!mentionsAskCommand &&
1637
- !asksQuestionAnsweringInternals &&
1638
- (filePath.endsWith('/commands/ask.ts') || filePath.endsWith('/utils/ask-cache.ts'))) {
1639
- score -= 0.45;
1640
- }
1641
- if (pathHints.some((hint) => normalizedPath.startsWith(hint))) {
1642
- score += 0.45;
1643
- }
1644
- else if (pathHints.length > 0) {
1645
- score -= 0.08;
1646
- }
1647
- if (score > 0) {
1648
- candidateSet.add(filePath);
1649
- pathPriority.set(filePath, score);
1650
- }
1651
- }
1652
- }
1653
- if (candidateSet.size < 40) {
1654
- for (const filePath of fileTree.slice(0, Math.min(fileTree.length, MAX_SCAN_FILES))) {
1655
- candidateSet.add(filePath);
1656
- }
1657
- }
1658
- const selfReferenceFiles = new Set([
1659
- 'packages/cli/src/commands/ask.ts',
1660
- 'packages/cli/src/utils/ask-cache.ts',
1661
- ]);
1662
- const prioritized = [...candidateSet]
1663
- .filter((filePath) => (0, fs_1.existsSync)((0, path_1.join)(cwd, filePath)))
1664
- .filter((filePath) => mentionsAskCommand || !selfReferenceFiles.has(filePath))
1665
- .sort((a, b) => (pathPriority.get(b) || 0) - (pathPriority.get(a) || 0));
1666
- const hinted = pathHints.length > 0
1667
- ? prioritized.filter((filePath) => pathHints.some((hint) => filePath.startsWith(hint)))
1668
- : [];
1669
- const nonHinted = pathHints.length > 0
1670
- ? prioritized.filter((filePath) => !pathHints.some((hint) => filePath.startsWith(hint)))
1671
- : prioritized;
1672
- const candidates = [...hinted, ...nonHinted].slice(0, MAX_SCAN_FILES);
1673
- const querySignals = deriveQuerySignals(question, normalizedQuestion, terms);
1674
- const commandFocusQuery = extractCommandFocus(normalizedQuestion);
1675
- const anchorTerms = extractAnchorTerms(question);
1676
- const hasTemporalIntent = /\b(before|after|during|when)\b/.test(normalizedQuestion);
1677
- const highSignalSet = new Set(querySignals.highSignalTerms);
1678
- const rawFileMatches = new Map();
1679
- const termFileHits = new Map();
1680
- let scannedFiles = 0;
1681
- for (const filePath of candidates) {
1682
- const fullPath = (0, path_1.join)(cwd, filePath);
1683
- let content = '';
1684
- try {
1685
- const st = (0, fs_1.statSync)(fullPath);
1686
- if (st.size > MAX_FILE_BYTES)
1687
- continue;
1688
- content = (0, fs_1.readFileSync)(fullPath, 'utf-8');
1689
- }
1690
- catch {
1691
- continue;
1692
- }
1693
- scannedFiles++;
1694
- const lines = content.split(/\r?\n/);
1695
- const lineTerms = new Map();
1696
- const matchedTerms = new Set();
1697
- for (let idx = 0; idx < lines.length; idx++) {
1698
- const rawLine = lines[idx];
1699
- if (!rawLine || rawLine.trim().length === 0)
1700
- continue;
1701
- for (const matcher of matchers) {
1702
- if (!matcher.regex.test(rawLine))
1703
- continue;
1704
- if (!lineTerms.has(idx))
1705
- lineTerms.set(idx, new Set());
1706
- lineTerms.get(idx).add(matcher.label);
1707
- matchedTerms.add(matcher.label);
1708
- }
1709
- }
1710
- if (lineTerms.size === 0)
1711
- continue;
1712
- rawFileMatches.set(filePath, {
1713
- lines,
1714
- lineTerms,
1715
- pathScore: pathPriority.get(filePath) || 0,
1716
- hintBoost: pathHints.some((hint) => filePath.startsWith(hint)) ? 0.25 : 0,
1717
- });
1718
- for (const term of matchedTerms) {
1719
- termFileHits.set(term, (termFileHits.get(term) || 0) + 1);
1720
- }
1721
- }
1722
- const matchedFileTotal = Math.max(rawFileMatches.size, 1);
1723
- const termWeight = new Map();
1724
- for (const term of terms) {
1725
- const docFreq = termFileHits.get(term) || 0;
1726
- const idfWeight = Math.log((matchedFileTotal + 1) / (docFreq + 1)) + 0.25;
1727
- const highSignalBoost = highSignalSet.has(term) ? 0.35 : 0;
1728
- termWeight.set(term, Math.max(0.08, idfWeight + highSignalBoost));
1729
- }
1730
- const citations = [];
1731
- for (const [filePath, fileData] of rawFileMatches.entries()) {
1732
- const { lines, lineTerms, pathScore, hintBoost } = fileData;
1733
- for (const [lineIdx, directTerms] of lineTerms.entries()) {
1734
- if (citations.length >= MAX_RAW_CITATIONS)
1735
- break;
1736
- const rawLine = lines[lineIdx] || '';
1737
- const snippet = normalizeSnippet(rawLine);
1738
- if (!snippet)
1739
- continue;
1740
- const windowTerms = new Set();
1741
- const contextRadius = querySignals.asksLocation || querySignals.asksDefinition ? 6 : 2;
1742
- for (let i = Math.max(0, lineIdx - contextRadius); i <= Math.min(lines.length - 1, lineIdx + contextRadius); i++) {
1743
- const termsAtLine = lineTerms.get(i);
1744
- if (!termsAtLine)
1745
- continue;
1746
- for (const term of termsAtLine)
1747
- windowTerms.add(term);
1748
- }
1749
- const highSignalWindow = [...windowTerms].filter((term) => highSignalSet.has(term));
1750
- let score = pathScore + hintBoost;
1751
- for (const term of windowTerms) {
1752
- score += termWeight.get(term) || 0.1;
1753
- }
1754
- score += highSignalWindow.length * 0.7;
1755
- score += windowTerms.size * 0.12;
1756
- if (querySignals.highSignalTerms.length > 0 && highSignalWindow.length === 0) {
1757
- score -= 0.65;
1758
- }
1759
- if ((querySignals.asksLocation || querySignals.asksDefinition) && querySignals.highSignalTerms.length >= 2 && highSignalWindow.length < 2) {
1760
- score -= 0.9;
1761
- }
1762
- const anchorHits = countTermHitsInText(rawLine.toLowerCase(), anchorTerms);
1763
- score += anchorHits * 1.2;
1764
- if ((querySignals.asksLocation || querySignals.asksDefinition) && anchorTerms.length > 0 && anchorHits === 0) {
1765
- score -= 0.6;
1766
- }
1767
- if (querySignals.asksLocation || querySignals.asksDefinition) {
1768
- if (looksLikeImportLine(rawLine))
1769
- score -= 0.75;
1770
- if (/\b(?:export\s+)?(?:function|const|let|var|class|interface|type)\b/.test(rawLine) &&
1771
- countTermHitsInText(rawLine.toLowerCase(), querySignals.highSignalTerms) >= 1) {
1772
- score += 0.9;
1773
- }
1774
- if (querySignals.asksDefinition && /console\.log\(/.test(rawLine)) {
1775
- score -= 0.6;
1776
- }
1777
- for (const identifier of querySignals.identifiers) {
1778
- const escaped = escapeRegExp(identifier);
1779
- if (new RegExp(`\\b${escaped}\\s*\\(`, 'i').test(rawLine)) {
1780
- score += 0.95;
1781
- }
1782
- if (new RegExp(`\\b(?:function|const|let|var|class|interface|type)\\s+${escaped}\\b`, 'i').test(rawLine)) {
1783
- score += 0.8;
1784
- }
1785
- }
1786
- if (hasTemporalIntent) {
1787
- if (/\b[A-Za-z_][A-Za-z0-9_]*\s*\(/.test(rawLine))
1788
- score += 0.75;
1789
- if (/^\s*export\s+(interface|type)\b/.test(rawLine))
1790
- score -= 0.9;
1791
- if (/^\s*const\s+[A-Z0-9_]+\s*=/.test(rawLine))
1792
- score -= 0.7;
1793
- }
1794
- }
1795
- if (querySignals.asksHow && /\b(if|else|return|await|for|while|switch)\b/.test(rawLine)) {
1796
- score += 0.2;
1797
- }
1798
- if (querySignals.asksHow) {
1799
- const trimmed = rawLine.trim();
1800
- if (trimmed.startsWith('//') || trimmed.startsWith('*'))
1801
- score -= 0.45;
1802
- if (/\.description\(/.test(rawLine))
1803
- score -= 0.4;
1804
- }
1805
- if (querySignals.asksSchema) {
1806
- if (/^\s*export\s+interface\s+/i.test(rawLine) || /^\s*export\s+type\s+/i.test(rawLine))
1807
- score += 1.1;
1808
- if (/^\s*[A-Za-z_][A-Za-z0-9_?]*\s*:\s*/.test(rawLine))
1809
- score += 0.8;
1810
- if (filePath.endsWith('.md'))
1811
- score -= 0.5;
1812
- }
1813
- if (rawLine.length > 180 && rawLine.includes(',') && /['"`].*['"`].*['"`]/.test(rawLine)) {
1814
- score -= 0.35;
1815
- }
1816
- if ((querySignals.asksLocation || querySignals.asksDefinition) && /Usage:\s*neurcode/i.test(rawLine)) {
1817
- score -= 0.6;
1818
- }
1819
- if ((querySignals.asksLocation || querySignals.asksDefinition) && /^\s*type:\s*['"`][a-z0-9_-]+['"`],?\s*$/i.test(rawLine)) {
1820
- score -= 0.7;
1821
- }
1822
- if (querySignals.asksCommandSurface) {
1823
- if (/\.command\(|\bcommand\(/i.test(rawLine))
1824
- score += 1.2;
1825
- if (/\.option\(|--[a-z0-9-]+/i.test(rawLine))
1826
- score += 0.8;
1827
- if (filePath.endsWith('/index.ts') || filePath.includes('/commands/'))
1828
- score += 0.35;
1829
- }
1830
- if (commandFocusQuery) {
1831
- if (filePath.includes(`/commands/${commandFocusQuery}.`)) {
1832
- score += 1.4;
1833
- }
1834
- else if ((querySignals.asksHow || querySignals.asksLocation) && filePath.includes('/commands/')) {
1835
- score -= 1.2;
1836
- }
1837
- if (filePath.endsWith('/index.ts') &&
1838
- new RegExp(`\\.command\\('${escapeRegExp(commandFocusQuery)}'\\)`, 'i').test(rawLine)) {
1839
- score += 1.1;
1840
- }
1841
- }
1842
- const dominantTerm = [...directTerms].sort((a, b) => (termWeight.get(b) || 0) - (termWeight.get(a) || 0))[0] ||
1843
- [...windowTerms].sort((a, b) => (termWeight.get(b) || 0) - (termWeight.get(a) || 0))[0] ||
1844
- '';
1845
- if (score <= 0)
1846
- continue;
1847
- citations.push({
1848
- path: filePath,
1849
- line: lineIdx + 1,
1850
- snippet,
1851
- term: dominantTerm,
1852
- weight: score,
1853
- });
1854
- }
1855
- }
1856
- citations.sort((a, b) => b.weight - a.weight);
1857
- const sourceEvidence = citations.filter((citation) => isPrimarySourcePath(citation.path));
1858
- const sourcePerFileCounts = new Map();
1859
- const sourceTermCounts = new Map();
1860
- for (const citation of sourceEvidence) {
1861
- sourcePerFileCounts.set(citation.path, (sourcePerFileCounts.get(citation.path) || 0) + 1);
1862
- if (citation.term) {
1863
- sourceTermCounts.set(citation.term, (sourceTermCounts.get(citation.term) || 0) + 1);
1864
- }
1865
- }
1866
- const selectedForOutput = [];
1867
- const selectedFileCounts = new Map();
1868
- const pushSelected = (citation) => {
1869
- if (selectedForOutput.length >= maxCitations)
1870
- return false;
1871
- if (selectedForOutput.some((existing) => existing.path === citation.path && existing.line === citation.line && existing.term === citation.term)) {
1872
- return false;
1873
- }
1874
- if (querySignals.asksLocation || querySignals.asksDefinition) {
1875
- const perFile = selectedFileCounts.get(citation.path) || 0;
1876
- const distinctFiles = selectedFileCounts.size;
1877
- const targetDistinct = Math.min(3, sourcePerFileCounts.size);
1878
- if (perFile >= 3 && distinctFiles < targetDistinct) {
1879
- return false;
1880
- }
1881
- }
1882
- selectedForOutput.push(citation);
1883
- selectedFileCounts.set(citation.path, (selectedFileCounts.get(citation.path) || 0) + 1);
1884
- return true;
1885
- };
1886
- if (mode === 'search' && querySignals.asksSchema) {
1887
- const schemaAffinity = (citation) => {
1888
- const text = citation.snippet.toLowerCase();
1889
- let affinity = 0;
1890
- for (const term of querySignals.highSignalTerms) {
1891
- const normalized = term.toLowerCase();
1892
- const compact = normalized.replace(/\s+/g, '');
1893
- if (text.includes(normalized))
1894
- affinity += 1;
1895
- else if (text.includes(compact))
1896
- affinity += 0.85;
1897
- }
1898
- return affinity;
1899
- };
1900
- const schemaPreferred = sourceEvidence
1901
- .filter((citation) => /^export\s+interface\b/i.test(citation.snippet) ||
1902
- /^export\s+type\b/i.test(citation.snippet) ||
1903
- /^[A-Za-z_][A-Za-z0-9_?]*\s*:\s*[^=,]+;?$/.test(citation.snippet))
1904
- .sort((a, b) => {
1905
- const affinityDiff = schemaAffinity(b) - schemaAffinity(a);
1906
- if (affinityDiff !== 0)
1907
- return affinityDiff;
1908
- return b.weight - a.weight;
1909
- });
1910
- for (const citation of schemaPreferred) {
1911
- if (selectedForOutput.length >= maxCitations)
1912
- break;
1913
- pushSelected(citation);
1914
- }
2336
+ pathBoostScores.set(entry.path, Math.max(pathBoostScores.get(entry.path) || 0, entry.score));
1915
2337
  }
1916
- if (mode === 'comparison' && terms.length >= 2) {
1917
- for (const term of terms.slice(0, 2)) {
1918
- const firstForTerm = sourceEvidence.find((citation) => citation.term === term);
1919
- if (firstForTerm) {
1920
- pushSelected(firstForTerm);
1921
- }
1922
- }
1923
- }
1924
- else if (mode === 'search' && terms.length > 0) {
1925
- const discriminativeHighSignalTerms = querySignals.highSignalTerms.filter((term) => {
1926
- const docFreq = termFileHits.get(term) || 0;
1927
- return docFreq / matchedFileTotal <= 0.65;
1928
- });
1929
- const preferredTerms = discriminativeHighSignalTerms.length > 0
1930
- ? discriminativeHighSignalTerms.slice(0, 8)
1931
- : querySignals.highSignalTerms.length > 0
1932
- ? querySignals.highSignalTerms.slice(0, 8)
1933
- : terms
1934
- .filter((term) => !LOW_SIGNAL_TERMS.has(term) && term.length >= 4)
1935
- .slice(0, 8);
1936
- for (const term of preferredTerms) {
1937
- const firstForTerm = sourceEvidence.find((citation) => {
1938
- if (citation.term !== term)
1939
- return false;
1940
- const normalized = (citation.term || '').toLowerCase();
1941
- return !GENERIC_OUTPUT_TERMS.has(normalized);
1942
- });
1943
- if (!firstForTerm)
1944
- continue;
1945
- pushSelected(firstForTerm);
1946
- if (selectedForOutput.length >= maxCitations)
1947
- break;
1948
- }
1949
- }
1950
- for (const citation of sourceEvidence) {
1951
- if (selectedForOutput.length >= maxCitations)
1952
- break;
1953
- if (commandFocusQuery &&
1954
- (querySignals.asksLocation || querySignals.asksHow) &&
1955
- selectedForOutput.some((item) => item.path.includes(`/commands/${commandFocusQuery}.`)) &&
1956
- citation.path.includes('/commands/') &&
1957
- !citation.path.includes(`/commands/${commandFocusQuery}.`)) {
1958
- continue;
1959
- }
1960
- pushSelected(citation);
1961
- }
1962
- const finalCitations = selectedForOutput.map(({ path, line, snippet, term }) => ({
1963
- path,
1964
- line,
1965
- snippet,
1966
- term,
1967
- }));
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);
1968
2341
  const stats = {
1969
- scannedFiles,
1970
- matchedFiles: sourcePerFileCounts.size,
2342
+ scannedFiles: evidence.scannedFiles,
2343
+ matchedFiles: new Set(sourceEvidence.map((citation) => citation.path)).size,
1971
2344
  matchedLines: sourceEvidence.length,
1972
2345
  brainCandidates: brainResults.entries.length,
1973
2346
  };
1974
- const lineCacheForAnswer = new Map();
1975
- for (const [filePath, data] of rawFileMatches.entries()) {
1976
- lineCacheForAnswer.set(filePath, data.lines);
1977
- }
1978
- const truth = evaluateTruthAssessment(mode, normalizedQuestion, terms, sourceEvidence, sourcePerFileCounts, sourceTermCounts);
1979
- const answer = buildAnswer(mode, question, terms, finalCitations, stats, sourceTermCounts, sourcePerFileCounts, truth, {
1980
- cliPackageName,
1981
- knownCommands: knownCliCommands,
1982
- featureBullets,
1983
- lineCache: lineCacheForAnswer,
1984
- });
1985
- emitAskResult(answer, {
2347
+ const truth = evaluateTruth(searchTerms.normalizedQuestion, searchTerms.highSignalTerms, finalCitations);
2348
+ const payload = buildRepoAnswerPayload(question, searchTerms, finalCitations, stats, truth);
2349
+ emitAskResult(payload, {
1986
2350
  json: options.json,
1987
2351
  maxCitations,
1988
2352
  fromPlan: options.fromPlan,
1989
2353
  verbose: options.verbose,
2354
+ proof: options.proof,
1990
2355
  });
1991
2356
  if (orgId && projectId) {
1992
2357
  (0, brain_context_1.recordBrainProgressEvent)(cwd, scope, {
1993
2358
  type: 'ask',
1994
- 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}`,
1995
2360
  });
1996
2361
  }
1997
2362
  if (shouldUseCache && orgId && projectId) {
1998
2363
  const questionHash = (0, ask_cache_1.computeAskQuestionHash)({
1999
- question: normalizedQuestion,
2364
+ question: searchTerms.normalizedQuestion,
2000
2365
  contextHash: staticContext.hash,
2001
2366
  });
2002
2367
  const key = (0, ask_cache_1.computeAskCacheKey)({
@@ -2018,10 +2383,10 @@ async function askCommand(question, options = {}) {
2018
2383
  questionHash,
2019
2384
  policyVersionHash,
2020
2385
  neurcodeVersion,
2021
- question: normalizedQuestion,
2386
+ question: searchTerms.normalizedQuestion,
2022
2387
  contextHash: staticContext.hash,
2023
2388
  },
2024
- output: answer,
2389
+ output: payload,
2025
2390
  evidencePaths: finalCitations.map((citation) => citation.path),
2026
2391
  });
2027
2392
  }