@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.
- package/dist/api-client.d.ts +29 -0
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +28 -2
- package/dist/api-client.js.map +1 -1
- package/dist/commands/ask.d.ts +1 -0
- package/dist/commands/ask.d.ts.map +1 -1
- package/dist/commands/ask.js +1952 -1587
- package/dist/commands/ask.js.map +1 -1
- package/dist/commands/brain.d.ts.map +1 -1
- package/dist/commands/brain.js +315 -0
- package/dist/commands/brain.js.map +1 -1
- package/dist/commands/policy.d.ts +3 -0
- package/dist/commands/policy.d.ts.map +1 -0
- package/dist/commands/policy.js +148 -0
- package/dist/commands/policy.js.map +1 -0
- package/dist/commands/ship.d.ts +1 -0
- package/dist/commands/ship.d.ts.map +1 -1
- package/dist/commands/ship.js +93 -0
- package/dist/commands/ship.js.map +1 -1
- package/dist/commands/simulate.d.ts +10 -0
- package/dist/commands/simulate.d.ts.map +1 -0
- package/dist/commands/simulate.js +96 -0
- package/dist/commands/simulate.js.map +1 -0
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +85 -51
- package/dist/commands/verify.js.map +1 -1
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -1
- package/dist/utils/ask-cache.d.ts +10 -0
- package/dist/utils/ask-cache.d.ts.map +1 -1
- package/dist/utils/ask-cache.js.map +1 -1
- package/dist/utils/breakage-simulator.d.ts +53 -0
- package/dist/utils/breakage-simulator.d.ts.map +1 -0
- package/dist/utils/breakage-simulator.js +323 -0
- package/dist/utils/breakage-simulator.js.map +1 -0
- package/dist/utils/policy-packs.d.ts +31 -0
- package/dist/utils/policy-packs.d.ts.map +1 -0
- package/dist/utils/policy-packs.js +277 -0
- package/dist/utils/policy-packs.js.map +1 -0
- package/package.json +1 -1
package/dist/commands/ask.js
CHANGED
|
@@ -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 || '
|
|
30
|
+
const raw = Number(process.env.NEURCODE_ASK_MAX_SCAN_FILES || '2200');
|
|
30
31
|
if (!Number.isFinite(raw))
|
|
31
|
-
return
|
|
32
|
-
return Math.max(
|
|
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 =
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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',
|
|
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', '
|
|
46
|
-
'
|
|
47
|
-
'
|
|
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', '
|
|
52
|
-
'workflow', 'repo', 'repository', 'codebase', 'anywhere', 'can', 'type', 'types',
|
|
53
|
-
'
|
|
54
|
-
'
|
|
55
|
-
'
|
|
56
|
-
|
|
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',
|
|
80
|
-
'.neurcode', '.vscode', '.pnpm-store', '.
|
|
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)
|
|
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
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
253
|
-
|
|
345
|
+
if (normalized.includes(' ')) {
|
|
346
|
+
if (lower.includes(normalized))
|
|
347
|
+
hits += 1;
|
|
254
348
|
continue;
|
|
255
349
|
}
|
|
256
|
-
|
|
257
|
-
|
|
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
|
|
354
|
+
return hits;
|
|
287
355
|
}
|
|
288
|
-
function
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
matchers,
|
|
368
|
+
kind: 'external',
|
|
369
|
+
reasons: ['Question appears to be outside repository scope.'],
|
|
299
370
|
};
|
|
300
371
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
matchers,
|
|
376
|
+
kind: 'ambiguous',
|
|
377
|
+
reasons: ['Question does not clearly reference repository context.'],
|
|
314
378
|
};
|
|
315
379
|
}
|
|
316
|
-
function
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
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
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
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
|
|
405
|
-
|
|
406
|
-
|
|
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
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
-
|
|
416
|
-
|
|
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
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
(
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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
|
-
|
|
468
|
-
|
|
543
|
+
`Derived from git history over ${sinceDays} day(s).`,
|
|
544
|
+
`Focus: ${targetLabel}.`,
|
|
469
545
|
],
|
|
470
|
-
confidence: '
|
|
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: '
|
|
473
|
-
score: 0.05,
|
|
474
|
-
reasons:
|
|
475
|
-
sourceCitations:
|
|
476
|
-
sourceFiles
|
|
477
|
-
minCitationsRequired:
|
|
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:
|
|
485
|
-
matchedLines:
|
|
570
|
+
matchedFiles: sourceFiles,
|
|
571
|
+
matchedLines: citations.length,
|
|
486
572
|
brainCandidates: 0,
|
|
487
573
|
},
|
|
488
574
|
};
|
|
489
575
|
}
|
|
490
|
-
function
|
|
491
|
-
const
|
|
492
|
-
if (
|
|
493
|
-
return
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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
|
-
|
|
520
|
-
|
|
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
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
if (
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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
|
-
|
|
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
|
-
|
|
544
|
-
|
|
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
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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
|
|
582
|
-
if (!
|
|
723
|
+
const command = match[1].trim().toLowerCase();
|
|
724
|
+
if (!command || byCommand.has(command))
|
|
583
725
|
continue;
|
|
584
|
-
|
|
726
|
+
byCommand.set(command, {
|
|
727
|
+
path: row.path,
|
|
728
|
+
line: row.line,
|
|
729
|
+
term: command,
|
|
730
|
+
snippet: row.snippet,
|
|
731
|
+
});
|
|
585
732
|
}
|
|
586
|
-
|
|
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
|
|
589
|
-
const
|
|
590
|
-
if (!(0, fs_1.existsSync)(
|
|
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)(
|
|
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
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
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
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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
|
|
612
|
-
if (
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
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
|
-
|
|
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
|
|
637
|
-
const
|
|
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
|
|
640
|
-
|
|
641
|
-
|
|
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
|
-
|
|
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
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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
|
-
|
|
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
|
|
908
|
+
return out;
|
|
659
909
|
}
|
|
660
|
-
function
|
|
661
|
-
const
|
|
662
|
-
|
|
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
|
-
|
|
683
|
-
|
|
684
|
-
|
|
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
|
-
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
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
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
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
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
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
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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
|
-
|
|
1096
|
+
const scored = [...dedup.values()].sort((a, b) => b.score - a.score);
|
|
1097
|
+
if (scored.length === 0)
|
|
764
1098
|
return null;
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
const
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
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
|
-
|
|
787
|
-
|
|
788
|
-
depth += 1;
|
|
789
|
-
if (ch === '}')
|
|
790
|
-
depth -= 1;
|
|
1180
|
+
if (term.includes(' ')) {
|
|
1181
|
+
return escaped.replace(/\\\s+/g, '\\s+');
|
|
791
1182
|
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
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 {
|
|
1199
|
+
return {
|
|
1200
|
+
path: rawPath,
|
|
1201
|
+
line: lineNumber,
|
|
1202
|
+
snippet: normalizeSnippet(match[3] || ''),
|
|
1203
|
+
};
|
|
798
1204
|
}
|
|
799
|
-
function
|
|
800
|
-
if (!
|
|
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
|
|
852
|
-
if (
|
|
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
|
|
869
|
-
|
|
870
|
-
|
|
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
|
-
|
|
884
|
-
out.
|
|
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
|
|
891
|
-
const
|
|
892
|
-
if (!
|
|
893
|
-
return
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
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
|
-
|
|
933
|
-
return
|
|
1263
|
+
catch {
|
|
1264
|
+
return out;
|
|
934
1265
|
}
|
|
935
|
-
const
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
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
|
|
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
|
|
961
|
-
const
|
|
962
|
-
const
|
|
963
|
-
|
|
964
|
-
const
|
|
965
|
-
const
|
|
966
|
-
const
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
const
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
const
|
|
982
|
-
|
|
983
|
-
|
|
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
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
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
|
-
|
|
996
|
-
|
|
997
|
-
const
|
|
998
|
-
|
|
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
|
-
|
|
1001
|
-
|
|
1577
|
+
for (const list of byFile.values()) {
|
|
1578
|
+
list.sort((a, b) => b.score - a.score);
|
|
1002
1579
|
}
|
|
1003
|
-
const
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
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
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
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
|
-
|
|
1015
|
-
|
|
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
|
-
|
|
1019
|
-
|
|
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
|
|
1663
|
+
status: reasons.length === 0 ? 'grounded' : 'insufficient',
|
|
1028
1664
|
score,
|
|
1029
1665
|
reasons,
|
|
1030
|
-
sourceCitations
|
|
1031
|
-
sourceFiles
|
|
1666
|
+
sourceCitations,
|
|
1667
|
+
sourceFiles,
|
|
1032
1668
|
minCitationsRequired,
|
|
1033
1669
|
minFilesRequired,
|
|
1034
1670
|
};
|
|
1035
1671
|
}
|
|
1036
|
-
function
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
const
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
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
|
-
|
|
1171
|
-
|
|
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
|
-
|
|
1175
|
-
|
|
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
|
-
|
|
1189
|
-
|
|
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
|
-
|
|
1193
|
-
|
|
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
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
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
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
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
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
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
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
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 (
|
|
1369
|
-
findings.push(
|
|
1830
|
+
if (flowPaths.length > 1) {
|
|
1831
|
+
findings.push(`Inferred file flow: ${flowPaths.join(' -> ')}`);
|
|
1370
1832
|
}
|
|
1371
|
-
|
|
1372
|
-
.
|
|
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
|
|
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(/"/g, '"')
|
|
1882
|
+
.replace(/'/g, "'")
|
|
1883
|
+
.replace(/&/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
|
-
|
|
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,
|
|
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 (
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
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
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
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
|
|
1476
|
-
const
|
|
1477
|
-
if (
|
|
1478
|
-
|
|
1479
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
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.
|
|
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:
|
|
2332
|
+
? (0, brain_context_1.searchBrainContextEntries)(cwd, scope, searchTerms.normalizedQuestion, { limit: 64 })
|
|
1593
2333
|
: { entries: [], totalIndexedFiles: 0 };
|
|
1594
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
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:
|
|
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
|
|
1975
|
-
|
|
1976
|
-
|
|
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
|
|
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:
|
|
2389
|
+
output: payload,
|
|
2025
2390
|
evidencePaths: finalCitations.map((citation) => citation.path),
|
|
2026
2391
|
});
|
|
2027
2392
|
}
|