@neurcode-ai/cli 0.9.10 → 0.9.12
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/LICENSE +201 -0
- package/dist/commands/ask.d.ts +10 -0
- package/dist/commands/ask.d.ts.map +1 -0
- package/dist/commands/ask.js +778 -0
- package/dist/commands/ask.js.map +1 -0
- package/dist/commands/brain.d.ts.map +1 -1
- package/dist/commands/brain.js +33 -0
- package/dist/commands/brain.js.map +1 -1
- package/dist/commands/plan.d.ts +2 -0
- package/dist/commands/plan.d.ts.map +1 -1
- package/dist/commands/plan.js +4 -0
- package/dist/commands/plan.js.map +1 -1
- package/dist/index.js +33 -2
- package/dist/index.js.map +1 -1
- package/dist/utils/ask-cache.d.ts +87 -0
- package/dist/utils/ask-cache.d.ts.map +1 -0
- package/dist/utils/ask-cache.js +424 -0
- package/dist/utils/ask-cache.js.map +1 -0
- package/dist/utils/brain-context.d.ts +20 -1
- package/dist/utils/brain-context.d.ts.map +1 -1
- package/dist/utils/brain-context.js +56 -1
- package/dist/utils/brain-context.js.map +1 -1
- package/dist/utils/plan-cache.d.ts.map +1 -1
- package/dist/utils/plan-cache.js +35 -6
- package/dist/utils/plan-cache.js.map +1 -1
- package/package.json +7 -8
|
@@ -0,0 +1,778 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.askCommand = askCommand;
|
|
4
|
+
const fs_1 = require("fs");
|
|
5
|
+
const path_1 = require("path");
|
|
6
|
+
const config_1 = require("../config");
|
|
7
|
+
const project_root_1 = require("../utils/project-root");
|
|
8
|
+
const state_1 = require("../utils/state");
|
|
9
|
+
const neurcode_context_1 = require("../utils/neurcode-context");
|
|
10
|
+
const plan_cache_1 = require("../utils/plan-cache");
|
|
11
|
+
const brain_context_1 = require("../utils/brain-context");
|
|
12
|
+
const ask_cache_1 = require("../utils/ask-cache");
|
|
13
|
+
let chalk;
|
|
14
|
+
try {
|
|
15
|
+
chalk = require('chalk');
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
chalk = {
|
|
19
|
+
green: (str) => str,
|
|
20
|
+
yellow: (str) => str,
|
|
21
|
+
red: (str) => str,
|
|
22
|
+
bold: (str) => str,
|
|
23
|
+
dim: (str) => str,
|
|
24
|
+
cyan: (str) => str,
|
|
25
|
+
white: (str) => str,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const MAX_SCAN_FILES = 320;
|
|
29
|
+
const MAX_FILE_BYTES = 512 * 1024;
|
|
30
|
+
const MAX_RAW_CITATIONS = 120;
|
|
31
|
+
const PRIMARY_SOURCE_EXTENSIONS = new Set([
|
|
32
|
+
'ts', 'tsx', 'js', 'jsx', 'mjs', 'cjs',
|
|
33
|
+
'py', 'go', 'rs', 'java', 'kt', 'swift', 'rb', 'php', 'cs',
|
|
34
|
+
'json', 'yaml', 'yml', 'toml', 'md', 'sql', 'graphql', 'gql',
|
|
35
|
+
'sh', 'bash', 'zsh', 'ps1', 'env', 'html', 'css', 'scss', 'less', 'prisma',
|
|
36
|
+
]);
|
|
37
|
+
const STOP_WORDS = new Set([
|
|
38
|
+
'the', 'and', 'for', 'with', 'that', 'this', 'what', 'where', 'when', 'which', 'into',
|
|
39
|
+
'from', 'your', 'about', 'there', 'their', 'them', 'have', 'does', 'is', 'are', 'was',
|
|
40
|
+
'were', 'any', 'all', 'read', 'tell', 'me', 'its', 'it', 'instead', 'than', 'then',
|
|
41
|
+
'workflow', 'codebase', 'repo', 'repository', 'used', 'use', 'mentioned', 'mention', 'whether',
|
|
42
|
+
]);
|
|
43
|
+
const LOW_SIGNAL_TERMS = new Set([
|
|
44
|
+
'used', 'use', 'using', 'mentioned', 'mention', 'where', 'tell', 'read', 'check', 'find', 'search',
|
|
45
|
+
'workflow', 'repo', 'repository', 'codebase', 'anywhere',
|
|
46
|
+
]);
|
|
47
|
+
function scanFiles(dir, maxFiles = MAX_SCAN_FILES) {
|
|
48
|
+
const files = [];
|
|
49
|
+
const ignoreDirs = new Set(['node_modules', '.git', '.next', 'dist', 'build', '.turbo', '.cache', 'coverage']);
|
|
50
|
+
const ignoreExts = new Set(['map', 'log', 'lock', 'png', 'jpg', 'jpeg', 'gif', 'ico', 'svg', 'woff', 'woff2', 'ttf', 'eot', 'pdf']);
|
|
51
|
+
const walk = (current) => {
|
|
52
|
+
if (files.length >= maxFiles)
|
|
53
|
+
return;
|
|
54
|
+
let entries = [];
|
|
55
|
+
try {
|
|
56
|
+
entries = (0, fs_1.readdirSync)(current);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
for (const entry of entries) {
|
|
62
|
+
if (files.length >= maxFiles)
|
|
63
|
+
break;
|
|
64
|
+
if (entry.startsWith('.') && !entry.startsWith('.env')) {
|
|
65
|
+
if (ignoreDirs.has(entry))
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const fullPath = (0, path_1.join)(current, entry);
|
|
69
|
+
let st;
|
|
70
|
+
try {
|
|
71
|
+
st = (0, fs_1.statSync)(fullPath);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (st.isDirectory()) {
|
|
77
|
+
if (ignoreDirs.has(entry))
|
|
78
|
+
continue;
|
|
79
|
+
walk(fullPath);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (!st.isFile())
|
|
83
|
+
continue;
|
|
84
|
+
if (st.size > MAX_FILE_BYTES)
|
|
85
|
+
continue;
|
|
86
|
+
const ext = entry.includes('.') ? entry.split('.').pop()?.toLowerCase() || '' : '';
|
|
87
|
+
if (ext && ignoreExts.has(ext))
|
|
88
|
+
continue;
|
|
89
|
+
const rel = fullPath.slice(dir.length + 1).replace(/\\/g, '/');
|
|
90
|
+
files.push(rel);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
walk(dir);
|
|
94
|
+
return files.slice(0, maxFiles);
|
|
95
|
+
}
|
|
96
|
+
function tokenizeQuestion(question) {
|
|
97
|
+
return (0, plan_cache_1.normalizeIntent)(question)
|
|
98
|
+
.split(/\s+/)
|
|
99
|
+
.filter((token) => token.length >= 3 && !STOP_WORDS.has(token));
|
|
100
|
+
}
|
|
101
|
+
function escapeRegExp(input) {
|
|
102
|
+
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
103
|
+
}
|
|
104
|
+
function normalizeTerm(raw) {
|
|
105
|
+
return raw
|
|
106
|
+
.toLowerCase()
|
|
107
|
+
.replace(/['"`]/g, '')
|
|
108
|
+
.replace(/\s+/g, ' ')
|
|
109
|
+
.trim();
|
|
110
|
+
}
|
|
111
|
+
function extractQuotedTerms(question) {
|
|
112
|
+
const seen = new Set();
|
|
113
|
+
const terms = [];
|
|
114
|
+
const re = /["'`](.{2,80}?)["'`]/g;
|
|
115
|
+
for (const match of question.matchAll(re)) {
|
|
116
|
+
const term = normalizeTerm(match[1] || '');
|
|
117
|
+
if (!term || seen.has(term))
|
|
118
|
+
continue;
|
|
119
|
+
seen.add(term);
|
|
120
|
+
terms.push(term);
|
|
121
|
+
}
|
|
122
|
+
return terms;
|
|
123
|
+
}
|
|
124
|
+
function extractIdentityTerms(question) {
|
|
125
|
+
const normalized = (0, plan_cache_1.normalizeIntent)(question);
|
|
126
|
+
const matches = normalized.match(/(?:organization[\s_-]*id|org[\s_-]*id|orgid|organizationid|user[\s_-]*id|userid|userid)/g);
|
|
127
|
+
if (!matches)
|
|
128
|
+
return [];
|
|
129
|
+
const seen = new Set();
|
|
130
|
+
const ordered = [];
|
|
131
|
+
for (const raw of matches) {
|
|
132
|
+
const term = normalizeTerm(raw);
|
|
133
|
+
if (seen.has(term))
|
|
134
|
+
continue;
|
|
135
|
+
seen.add(term);
|
|
136
|
+
ordered.push(term);
|
|
137
|
+
}
|
|
138
|
+
return ordered;
|
|
139
|
+
}
|
|
140
|
+
function extractComparisonTerms(question) {
|
|
141
|
+
const quoted = extractQuotedTerms(question);
|
|
142
|
+
if (quoted.length >= 2)
|
|
143
|
+
return quoted.slice(0, 2);
|
|
144
|
+
const normalized = (0, plan_cache_1.normalizeIntent)(question);
|
|
145
|
+
const hasComparisonJoiner = normalized.includes('instead of') ||
|
|
146
|
+
normalized.includes(' versus ') ||
|
|
147
|
+
normalized.includes(' vs ') ||
|
|
148
|
+
normalized.includes(' compared to ');
|
|
149
|
+
if (!hasComparisonJoiner)
|
|
150
|
+
return [];
|
|
151
|
+
const identities = extractIdentityTerms(question);
|
|
152
|
+
if (identities.length >= 2)
|
|
153
|
+
return identities.slice(0, 2);
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
function buildTermMatchers(term, weight) {
|
|
157
|
+
const normalized = normalizeTerm(term);
|
|
158
|
+
if (!normalized)
|
|
159
|
+
return [];
|
|
160
|
+
const tokens = normalized.split(/\s+/).filter(Boolean);
|
|
161
|
+
const out = [];
|
|
162
|
+
const push = (label, regexSource) => {
|
|
163
|
+
out.push({
|
|
164
|
+
label,
|
|
165
|
+
regex: new RegExp(regexSource, 'i'),
|
|
166
|
+
weight,
|
|
167
|
+
});
|
|
168
|
+
};
|
|
169
|
+
if (tokens.length === 1) {
|
|
170
|
+
const token = escapeRegExp(tokens[0]);
|
|
171
|
+
push(normalized, `\\b${token}\\b`);
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
const tokenChain = tokens.map((t) => escapeRegExp(t)).join('[\\s_-]*');
|
|
175
|
+
push(normalized, `\\b${tokenChain}\\b`);
|
|
176
|
+
push(normalized, `\\b${escapeRegExp(tokens.join(''))}\\b`);
|
|
177
|
+
push(normalized, `\\b${escapeRegExp(tokens.join('_'))}\\b`);
|
|
178
|
+
push(normalized, `\\b${escapeRegExp(tokens.join('-'))}\\b`);
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
function buildMatchers(question) {
|
|
182
|
+
const comparisonTerms = extractComparisonTerms(question);
|
|
183
|
+
if (comparisonTerms.length >= 2) {
|
|
184
|
+
const matchers = [
|
|
185
|
+
...buildTermMatchers(comparisonTerms[0], 1.0),
|
|
186
|
+
...buildTermMatchers(comparisonTerms[1], 0.9),
|
|
187
|
+
];
|
|
188
|
+
return {
|
|
189
|
+
mode: 'comparison',
|
|
190
|
+
terms: comparisonTerms.slice(0, 2),
|
|
191
|
+
matchers,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
const quoted = extractQuotedTerms(question);
|
|
195
|
+
const keywords = tokenizeQuestion(question).slice(0, 8);
|
|
196
|
+
const quotedSet = new Set(quoted.map((term) => normalizeTerm(term)));
|
|
197
|
+
const terms = [...new Set([...quoted, ...keywords].map(normalizeTerm).filter(Boolean))]
|
|
198
|
+
.filter((term) => quotedSet.has(term) || !LOW_SIGNAL_TERMS.has(term));
|
|
199
|
+
const matchers = terms.flatMap((term) => buildTermMatchers(term, quoted.includes(term) ? 0.9 : 0.55));
|
|
200
|
+
return {
|
|
201
|
+
mode: 'search',
|
|
202
|
+
terms,
|
|
203
|
+
matchers,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function derivePathHints(question) {
|
|
207
|
+
const normalized = (0, plan_cache_1.normalizeIntent)(question);
|
|
208
|
+
const hints = [];
|
|
209
|
+
if (/\bcli|command|terminal\b/.test(normalized)) {
|
|
210
|
+
hints.push('packages/cli/src/commands/');
|
|
211
|
+
hints.push('packages/cli/src/');
|
|
212
|
+
hints.push('packages/cli/');
|
|
213
|
+
}
|
|
214
|
+
if (/\bapi|backend|server|route|middleware\b/.test(normalized)) {
|
|
215
|
+
hints.push('services/api/');
|
|
216
|
+
}
|
|
217
|
+
if (/\bdashboard|landing|docs|frontend|ui|pricing\b/.test(normalized)) {
|
|
218
|
+
hints.push('web/dashboard/');
|
|
219
|
+
}
|
|
220
|
+
if (/(github action|\bci\b)/.test(normalized)) {
|
|
221
|
+
hints.push('packages/action/');
|
|
222
|
+
hints.push('actions/');
|
|
223
|
+
}
|
|
224
|
+
return [...new Set(hints)];
|
|
225
|
+
}
|
|
226
|
+
function normalizeSnippet(line) {
|
|
227
|
+
return line
|
|
228
|
+
.replace(/\t/g, ' ')
|
|
229
|
+
.replace(/\s+/g, ' ')
|
|
230
|
+
.trim()
|
|
231
|
+
.slice(0, 220);
|
|
232
|
+
}
|
|
233
|
+
function isPrimarySourcePath(filePath) {
|
|
234
|
+
const normalized = filePath.trim().replace(/\\/g, '/').toLowerCase();
|
|
235
|
+
if (!normalized)
|
|
236
|
+
return false;
|
|
237
|
+
if (normalized.startsWith('.neurcode/') ||
|
|
238
|
+
normalized.startsWith('.git/') ||
|
|
239
|
+
normalized.startsWith('node_modules/') ||
|
|
240
|
+
normalized.startsWith('dist/') ||
|
|
241
|
+
normalized.startsWith('build/') ||
|
|
242
|
+
normalized.startsWith('coverage/')) {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
if (normalized.includes('/dist/') || normalized.includes('/build/') || normalized.includes('/coverage/')) {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
const ext = normalized.includes('.') ? normalized.split('.').pop() || '' : '';
|
|
249
|
+
if (!ext) {
|
|
250
|
+
// Allow extensionless source-like files (e.g. Dockerfile, Makefile).
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
return PRIMARY_SOURCE_EXTENSIONS.has(ext);
|
|
254
|
+
}
|
|
255
|
+
function isBroadQuestion(normalizedQuestion) {
|
|
256
|
+
return (/\banywhere\b/.test(normalizedQuestion) ||
|
|
257
|
+
/\bacross\b/.test(normalizedQuestion) ||
|
|
258
|
+
/\bworkflow\b/.test(normalizedQuestion) ||
|
|
259
|
+
/\bentire\b/.test(normalizedQuestion) ||
|
|
260
|
+
/\bwhole\b/.test(normalizedQuestion) ||
|
|
261
|
+
/\ball\b/.test(normalizedQuestion) ||
|
|
262
|
+
/\bin repo\b/.test(normalizedQuestion));
|
|
263
|
+
}
|
|
264
|
+
function calibrateConfidence(truth) {
|
|
265
|
+
if (truth.status === 'insufficient')
|
|
266
|
+
return 'low';
|
|
267
|
+
if (truth.score >= 0.78)
|
|
268
|
+
return 'high';
|
|
269
|
+
if (truth.score >= 0.52)
|
|
270
|
+
return 'medium';
|
|
271
|
+
return 'low';
|
|
272
|
+
}
|
|
273
|
+
function evaluateTruthAssessment(mode, normalizedQuestion, terms, sourceCitations, sourcePerFileCounts, sourceTermCounts) {
|
|
274
|
+
const broadQuestion = isBroadQuestion(normalizedQuestion);
|
|
275
|
+
const minCitationsRequired = mode === 'comparison' ? 2 : 2;
|
|
276
|
+
let minFilesRequired = mode === 'comparison' ? 2 : 1;
|
|
277
|
+
if (broadQuestion) {
|
|
278
|
+
minFilesRequired = Math.max(minFilesRequired, 2);
|
|
279
|
+
}
|
|
280
|
+
const reasons = [];
|
|
281
|
+
const sourceCitationCount = sourceCitations.length;
|
|
282
|
+
const sourceFileCount = sourcePerFileCounts.size;
|
|
283
|
+
if (sourceCitationCount === 0) {
|
|
284
|
+
reasons.push('No direct source-file evidence was found for the query terms.');
|
|
285
|
+
}
|
|
286
|
+
if (sourceCitationCount < minCitationsRequired) {
|
|
287
|
+
reasons.push(`Only ${sourceCitationCount} source citation(s) found (minimum ${minCitationsRequired} required).`);
|
|
288
|
+
}
|
|
289
|
+
if (sourceFileCount < minFilesRequired) {
|
|
290
|
+
reasons.push(`Evidence spans ${sourceFileCount} file(s) (minimum ${minFilesRequired} required for this question).`);
|
|
291
|
+
}
|
|
292
|
+
if (mode === 'comparison' && terms.length >= 2) {
|
|
293
|
+
const missingTerms = terms.filter((term) => (sourceTermCounts.get(term) || 0) === 0);
|
|
294
|
+
if (missingTerms.length > 0) {
|
|
295
|
+
reasons.push(`Missing direct source evidence for term(s): ${missingTerms.join(', ')}.`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (mode === 'search') {
|
|
299
|
+
const highSignalTerms = terms.filter((term) => !LOW_SIGNAL_TERMS.has(term));
|
|
300
|
+
if (highSignalTerms.length > 0) {
|
|
301
|
+
const matchedHighSignalTerms = highSignalTerms.filter((term) => (sourceTermCounts.get(term) || 0) > 0);
|
|
302
|
+
if (matchedHighSignalTerms.length === 0) {
|
|
303
|
+
reasons.push('No high-signal query terms were grounded in source citations.');
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
let dominantShare = 0;
|
|
308
|
+
if (sourceCitationCount > 0) {
|
|
309
|
+
const highest = Math.max(...sourcePerFileCounts.values());
|
|
310
|
+
dominantShare = highest / sourceCitationCount;
|
|
311
|
+
}
|
|
312
|
+
if (broadQuestion && dominantShare > 0.85 && sourceFileCount < 3) {
|
|
313
|
+
reasons.push('Evidence is highly concentrated in one file; broader coverage is required for this query.');
|
|
314
|
+
}
|
|
315
|
+
const citationScore = Math.min(1, sourceCitationCount / Math.max(minCitationsRequired, 4));
|
|
316
|
+
const fileScore = Math.min(1, sourceFileCount / Math.max(minFilesRequired, 3));
|
|
317
|
+
let termCoverageScore = 0;
|
|
318
|
+
if (mode === 'comparison' && terms.length >= 2) {
|
|
319
|
+
termCoverageScore = terms.filter((term) => (sourceTermCounts.get(term) || 0) > 0).length / terms.length;
|
|
320
|
+
}
|
|
321
|
+
else if (mode === 'search') {
|
|
322
|
+
const highSignalTerms = terms.filter((term) => !LOW_SIGNAL_TERMS.has(term));
|
|
323
|
+
if (highSignalTerms.length === 0) {
|
|
324
|
+
termCoverageScore = sourceCitationCount > 0 ? 1 : 0;
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
termCoverageScore = highSignalTerms.filter((term) => (sourceTermCounts.get(term) || 0) > 0).length / highSignalTerms.length;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const concentrationPenalty = dominantShare > 0.9 ? 0.08 : dominantShare > 0.75 ? 0.04 : 0;
|
|
331
|
+
let score = citationScore * 0.45 + fileScore * 0.35 + termCoverageScore * 0.2 - concentrationPenalty;
|
|
332
|
+
if (!Number.isFinite(score))
|
|
333
|
+
score = 0;
|
|
334
|
+
score = Math.max(0, Math.min(score, 1));
|
|
335
|
+
if (score < 0.42 && reasons.length === 0) {
|
|
336
|
+
reasons.push('Evidence quality score is below the confidence threshold.');
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
status: reasons.length > 0 ? 'insufficient' : 'grounded',
|
|
340
|
+
score,
|
|
341
|
+
reasons,
|
|
342
|
+
sourceCitations: sourceCitationCount,
|
|
343
|
+
sourceFiles: sourceFileCount,
|
|
344
|
+
minCitationsRequired,
|
|
345
|
+
minFilesRequired,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
function buildAnswer(mode, question, terms, citations, stats, termCounts, perFileCounts, truth) {
|
|
349
|
+
const confidence = calibrateConfidence(truth);
|
|
350
|
+
const normalizedQuestion = (0, plan_cache_1.normalizeIntent)(question);
|
|
351
|
+
const findings = [];
|
|
352
|
+
let answer = '';
|
|
353
|
+
if (truth.status === 'insufficient') {
|
|
354
|
+
answer = 'Insufficient grounded evidence to answer confidently from current source coverage.';
|
|
355
|
+
findings.push(...truth.reasons);
|
|
356
|
+
findings.push('Refine the query with specific file paths, modules, or identifiers for stronger grounding.');
|
|
357
|
+
}
|
|
358
|
+
else if (mode === 'comparison' && terms.length >= 2) {
|
|
359
|
+
const left = terms[0];
|
|
360
|
+
const right = terms[1];
|
|
361
|
+
const leftCount = termCounts.get(left) || 0;
|
|
362
|
+
const rightCount = termCounts.get(right) || 0;
|
|
363
|
+
if (leftCount > 0 && rightCount > 0) {
|
|
364
|
+
answer = `Found both "${left}" (${leftCount}) and "${right}" (${rightCount}) references in the scanned scope.`;
|
|
365
|
+
}
|
|
366
|
+
else if (leftCount > 0) {
|
|
367
|
+
answer = `Found "${left}" references (${leftCount}) and no direct "${right}" references in the scanned scope.`;
|
|
368
|
+
}
|
|
369
|
+
else if (rightCount > 0) {
|
|
370
|
+
answer = `No direct "${left}" references found; "${right}" appears ${rightCount} time(s) in the scanned scope.`;
|
|
371
|
+
}
|
|
372
|
+
else {
|
|
373
|
+
answer = `No direct references to "${left}" or "${right}" were found in the scanned scope.`;
|
|
374
|
+
}
|
|
375
|
+
findings.push(`Compared terms: "${left}" vs "${right}"`);
|
|
376
|
+
findings.push(`Matches by term: ${left}=${leftCount}, ${right}=${rightCount}`);
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
if (citations.length > 0) {
|
|
380
|
+
answer = `Found ${citations.length} relevant evidence line(s) across ${stats.matchedFiles} file(s) for your question.`;
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
answer = 'No direct evidence lines were found for this question in the scanned files.';
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
const topFiles = [...perFileCounts.entries()]
|
|
387
|
+
.sort((a, b) => b[1] - a[1])
|
|
388
|
+
.slice(0, 3)
|
|
389
|
+
.map(([file, count]) => `${file} (${count})`);
|
|
390
|
+
if (topFiles.length > 0) {
|
|
391
|
+
findings.push(`Most relevant files: ${topFiles.join(', ')}`);
|
|
392
|
+
}
|
|
393
|
+
findings.push(`Scanned ${stats.scannedFiles} file(s); matched ${stats.matchedFiles} file(s).`);
|
|
394
|
+
return {
|
|
395
|
+
question,
|
|
396
|
+
questionNormalized: normalizedQuestion,
|
|
397
|
+
mode,
|
|
398
|
+
answer,
|
|
399
|
+
findings,
|
|
400
|
+
confidence,
|
|
401
|
+
truth: {
|
|
402
|
+
status: truth.status,
|
|
403
|
+
score: Number(truth.score.toFixed(2)),
|
|
404
|
+
reasons: truth.reasons,
|
|
405
|
+
sourceCitations: truth.sourceCitations,
|
|
406
|
+
sourceFiles: truth.sourceFiles,
|
|
407
|
+
minCitationsRequired: truth.minCitationsRequired,
|
|
408
|
+
minFilesRequired: truth.minFilesRequired,
|
|
409
|
+
},
|
|
410
|
+
citations,
|
|
411
|
+
generatedAt: new Date().toISOString(),
|
|
412
|
+
stats,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
function emitAskResult(result, options) {
|
|
416
|
+
if (options.json) {
|
|
417
|
+
console.log(JSON.stringify({
|
|
418
|
+
...result,
|
|
419
|
+
citations: result.citations.slice(0, options.maxCitations),
|
|
420
|
+
cache: options.cacheLabel || 'miss',
|
|
421
|
+
}, null, 2));
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
if (options.cacheLabel) {
|
|
425
|
+
console.log(chalk.dim(`⚡ ${options.cacheLabel}\n`));
|
|
426
|
+
}
|
|
427
|
+
if (!options.fromPlan) {
|
|
428
|
+
console.log(chalk.bold.cyan('🧠 Neurcode Ask\n'));
|
|
429
|
+
}
|
|
430
|
+
console.log(chalk.bold.white('Question:'));
|
|
431
|
+
console.log(chalk.dim(result.question));
|
|
432
|
+
console.log('');
|
|
433
|
+
console.log(chalk.bold.white('Answer:'));
|
|
434
|
+
if (result.truth.status === 'grounded') {
|
|
435
|
+
console.log(chalk.green(result.answer));
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
console.log(chalk.yellow(result.answer));
|
|
439
|
+
}
|
|
440
|
+
if (result.findings.length > 0) {
|
|
441
|
+
console.log(chalk.bold.white('\nFindings:'));
|
|
442
|
+
for (const finding of result.findings) {
|
|
443
|
+
console.log(chalk.cyan(` • ${finding}`));
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
const citations = result.citations.slice(0, options.maxCitations);
|
|
447
|
+
if (citations.length > 0) {
|
|
448
|
+
console.log(chalk.bold.white('\nEvidence:'));
|
|
449
|
+
citations.forEach((citation, idx) => {
|
|
450
|
+
const prefix = citation.term ? `${citation.term} ` : '';
|
|
451
|
+
console.log(chalk.dim(` ${idx + 1}. ${citation.path}:${citation.line} ${prefix}${citation.snippet}`));
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
console.log(chalk.yellow('\nEvidence: no direct matching lines found.'));
|
|
456
|
+
}
|
|
457
|
+
const truthLabel = result.truth.status === 'grounded' ? chalk.green('GROUNDED') : chalk.yellow('INSUFFICIENT');
|
|
458
|
+
console.log(chalk.dim(`\nTruth Mode: ${truthLabel} (score=${result.truth.score.toFixed(2)}, source_citations=${result.truth.sourceCitations}, source_files=${result.truth.sourceFiles})`));
|
|
459
|
+
console.log(chalk.dim(`Confidence: ${result.confidence.toUpperCase()} | scanned=${result.stats.scannedFiles} matched=${result.stats.matchedFiles}`));
|
|
460
|
+
}
|
|
461
|
+
async function askCommand(question, options = {}) {
|
|
462
|
+
try {
|
|
463
|
+
if (!question || !question.trim()) {
|
|
464
|
+
console.error(chalk.red('❌ Error: Question cannot be empty.'));
|
|
465
|
+
console.log(chalk.dim('Usage: neurcode ask "<question>"'));
|
|
466
|
+
process.exit(1);
|
|
467
|
+
}
|
|
468
|
+
const cwd = (0, project_root_1.resolveNeurcodeProjectRoot)(process.cwd());
|
|
469
|
+
const config = (0, config_1.loadConfig)();
|
|
470
|
+
const orgId = (0, state_1.getOrgId)();
|
|
471
|
+
const stateProjectId = (0, state_1.getProjectId)();
|
|
472
|
+
const projectId = options.projectId || stateProjectId || config.projectId || null;
|
|
473
|
+
const scope = { orgId: orgId || null, projectId: projectId || null };
|
|
474
|
+
const maxCitations = Math.max(3, Math.min(options.maxCitations || 12, 30));
|
|
475
|
+
const shouldUseCache = options.cache !== false && process.env.NEURCODE_ASK_NO_CACHE !== '1';
|
|
476
|
+
const normalizedQuestion = (0, plan_cache_1.normalizeIntent)(question);
|
|
477
|
+
if (process.stdout.isTTY && !process.env.CI) {
|
|
478
|
+
(0, neurcode_context_1.ensureDefaultLocalContextFile)(cwd);
|
|
479
|
+
}
|
|
480
|
+
const fileTree = scanFiles(cwd, MAX_SCAN_FILES);
|
|
481
|
+
if (fileTree.length === 0) {
|
|
482
|
+
console.error(chalk.red('❌ No files found in the current project.'));
|
|
483
|
+
process.exit(1);
|
|
484
|
+
}
|
|
485
|
+
const staticContext = (0, neurcode_context_1.loadStaticNeurcodeContext)(cwd, orgId && projectId ? { orgId, projectId } : undefined);
|
|
486
|
+
const policyVersionHash = (0, plan_cache_1.computePolicyVersionHash)(cwd);
|
|
487
|
+
const neurcodeVersion = (0, plan_cache_1.getNeurcodeVersion)();
|
|
488
|
+
const gitFingerprint = (0, plan_cache_1.getGitRepoFingerprint)(cwd);
|
|
489
|
+
const repoFingerprint = gitFingerprint || (0, plan_cache_1.getFilesystemFingerprintFromTree)(fileTree, cwd);
|
|
490
|
+
if (orgId && projectId) {
|
|
491
|
+
try {
|
|
492
|
+
(0, brain_context_1.refreshBrainContextFromWorkspace)(cwd, scope, {
|
|
493
|
+
workingTreeHash: gitFingerprint?.kind === 'git' ? gitFingerprint.workingTreeHash : undefined,
|
|
494
|
+
maxFiles: 90,
|
|
495
|
+
recordEvent: false,
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
catch {
|
|
499
|
+
// non-blocking
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
if (shouldUseCache && orgId && projectId) {
|
|
503
|
+
const questionHash = (0, ask_cache_1.computeAskQuestionHash)({
|
|
504
|
+
question: normalizedQuestion,
|
|
505
|
+
contextHash: staticContext.hash,
|
|
506
|
+
});
|
|
507
|
+
const exactKey = (0, ask_cache_1.computeAskCacheKey)({
|
|
508
|
+
schemaVersion: 3,
|
|
509
|
+
orgId,
|
|
510
|
+
projectId,
|
|
511
|
+
repo: repoFingerprint,
|
|
512
|
+
questionHash,
|
|
513
|
+
policyVersionHash,
|
|
514
|
+
neurcodeVersion,
|
|
515
|
+
});
|
|
516
|
+
const exact = (0, ask_cache_1.readCachedAsk)(cwd, exactKey);
|
|
517
|
+
if (exact) {
|
|
518
|
+
const exactOutput = {
|
|
519
|
+
...exact.output,
|
|
520
|
+
question,
|
|
521
|
+
questionNormalized: normalizedQuestion,
|
|
522
|
+
};
|
|
523
|
+
emitAskResult(exactOutput, {
|
|
524
|
+
json: options.json,
|
|
525
|
+
maxCitations,
|
|
526
|
+
cacheLabel: `Using cached answer (created: ${new Date(exact.createdAt).toLocaleString()})`,
|
|
527
|
+
fromPlan: options.fromPlan,
|
|
528
|
+
});
|
|
529
|
+
(0, brain_context_1.recordBrainProgressEvent)(cwd, scope, {
|
|
530
|
+
type: 'ask',
|
|
531
|
+
note: 'cache_hit=exact',
|
|
532
|
+
});
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
const near = (0, ask_cache_1.findNearCachedAsk)(cwd, {
|
|
536
|
+
orgId,
|
|
537
|
+
projectId,
|
|
538
|
+
repo: repoFingerprint,
|
|
539
|
+
question: normalizedQuestion,
|
|
540
|
+
policyVersionHash,
|
|
541
|
+
neurcodeVersion,
|
|
542
|
+
contextHash: staticContext.hash,
|
|
543
|
+
changedPaths: (0, ask_cache_1.getChangedWorkingTreePaths)(cwd),
|
|
544
|
+
minSimilarity: 0.68,
|
|
545
|
+
});
|
|
546
|
+
if (near) {
|
|
547
|
+
const nearOutput = {
|
|
548
|
+
...near.entry.output,
|
|
549
|
+
question,
|
|
550
|
+
questionNormalized: normalizedQuestion,
|
|
551
|
+
};
|
|
552
|
+
const reasonText = near.reason === 'safe_repo_drift_similar_question'
|
|
553
|
+
? 'Using near-cached answer (safe repo drift)'
|
|
554
|
+
: 'Using near-cached answer';
|
|
555
|
+
emitAskResult(nearOutput, {
|
|
556
|
+
json: options.json,
|
|
557
|
+
maxCitations,
|
|
558
|
+
cacheLabel: `${reasonText}, similarity ${near.similarity.toFixed(2)}, created: ${new Date(near.entry.createdAt).toLocaleString()}`,
|
|
559
|
+
fromPlan: options.fromPlan,
|
|
560
|
+
});
|
|
561
|
+
(0, brain_context_1.recordBrainProgressEvent)(cwd, scope, {
|
|
562
|
+
type: 'ask',
|
|
563
|
+
note: `cache_hit=near;similarity=${near.similarity.toFixed(2)};reason=${near.reason}`,
|
|
564
|
+
});
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (!options.json) {
|
|
569
|
+
console.log(chalk.dim(`🧠 Asking repo context in ${cwd}...`));
|
|
570
|
+
}
|
|
571
|
+
const brainResults = orgId && projectId
|
|
572
|
+
? (0, brain_context_1.searchBrainContextEntries)(cwd, scope, normalizedQuestion, { limit: 48 })
|
|
573
|
+
: { entries: [], totalIndexedFiles: 0 };
|
|
574
|
+
if (!options.json && brainResults.entries.length > 0) {
|
|
575
|
+
const top = brainResults.entries.filter((entry) => entry.score > 0).length;
|
|
576
|
+
console.log(chalk.dim(`🧠 Brain retrieval: ${top} relevant file summaries from ${brainResults.totalIndexedFiles} indexed files`));
|
|
577
|
+
}
|
|
578
|
+
const { mode, terms, matchers } = buildMatchers(question);
|
|
579
|
+
const pathHints = derivePathHints(question);
|
|
580
|
+
const mentionsAskCommand = /\bask\b/.test(normalizedQuestion);
|
|
581
|
+
if (matchers.length === 0) {
|
|
582
|
+
console.error(chalk.red('❌ Could not derive useful search terms from the question.'));
|
|
583
|
+
process.exit(1);
|
|
584
|
+
}
|
|
585
|
+
const candidateSet = new Set();
|
|
586
|
+
const pathPriority = new Map();
|
|
587
|
+
for (const entry of brainResults.entries) {
|
|
588
|
+
candidateSet.add(entry.path);
|
|
589
|
+
pathPriority.set(entry.path, (pathPriority.get(entry.path) || 0) + entry.score);
|
|
590
|
+
}
|
|
591
|
+
const tokenHints = tokenizeQuestion(question);
|
|
592
|
+
if (candidateSet.size < 80) {
|
|
593
|
+
for (const filePath of fileTree) {
|
|
594
|
+
const normalizedPath = filePath.toLowerCase();
|
|
595
|
+
let score = pathPriority.get(filePath) || 0;
|
|
596
|
+
for (const token of tokenHints) {
|
|
597
|
+
if (normalizedPath.includes(token))
|
|
598
|
+
score += 0.2;
|
|
599
|
+
}
|
|
600
|
+
if (!mentionsAskCommand && (filePath.endsWith('/commands/ask.ts') || filePath.endsWith('/utils/ask-cache.ts'))) {
|
|
601
|
+
score -= 0.45;
|
|
602
|
+
}
|
|
603
|
+
if (pathHints.some((hint) => normalizedPath.startsWith(hint))) {
|
|
604
|
+
score += 0.45;
|
|
605
|
+
}
|
|
606
|
+
else if (pathHints.length > 0) {
|
|
607
|
+
score -= 0.08;
|
|
608
|
+
}
|
|
609
|
+
if (score > 0) {
|
|
610
|
+
candidateSet.add(filePath);
|
|
611
|
+
pathPriority.set(filePath, score);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
if (candidateSet.size < 20) {
|
|
616
|
+
for (const filePath of fileTree.slice(0, 160)) {
|
|
617
|
+
candidateSet.add(filePath);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
const selfReferenceFiles = new Set([
|
|
621
|
+
'packages/cli/src/commands/ask.ts',
|
|
622
|
+
'packages/cli/src/utils/ask-cache.ts',
|
|
623
|
+
]);
|
|
624
|
+
const prioritized = [...candidateSet]
|
|
625
|
+
.filter((filePath) => (0, fs_1.existsSync)((0, path_1.join)(cwd, filePath)))
|
|
626
|
+
.filter((filePath) => mentionsAskCommand || !selfReferenceFiles.has(filePath))
|
|
627
|
+
.sort((a, b) => (pathPriority.get(b) || 0) - (pathPriority.get(a) || 0));
|
|
628
|
+
const hinted = pathHints.length > 0
|
|
629
|
+
? prioritized.filter((filePath) => pathHints.some((hint) => filePath.startsWith(hint)))
|
|
630
|
+
: [];
|
|
631
|
+
const nonHinted = pathHints.length > 0
|
|
632
|
+
? prioritized.filter((filePath) => !pathHints.some((hint) => filePath.startsWith(hint)))
|
|
633
|
+
: prioritized;
|
|
634
|
+
const candidates = [...hinted, ...nonHinted].slice(0, MAX_SCAN_FILES);
|
|
635
|
+
const citations = [];
|
|
636
|
+
let scannedFiles = 0;
|
|
637
|
+
for (const filePath of candidates) {
|
|
638
|
+
if (citations.length >= MAX_RAW_CITATIONS)
|
|
639
|
+
break;
|
|
640
|
+
const fullPath = (0, path_1.join)(cwd, filePath);
|
|
641
|
+
let content = '';
|
|
642
|
+
try {
|
|
643
|
+
const st = (0, fs_1.statSync)(fullPath);
|
|
644
|
+
if (st.size > MAX_FILE_BYTES)
|
|
645
|
+
continue;
|
|
646
|
+
content = (0, fs_1.readFileSync)(fullPath, 'utf-8');
|
|
647
|
+
}
|
|
648
|
+
catch {
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
scannedFiles++;
|
|
652
|
+
const lines = content.split(/\r?\n/);
|
|
653
|
+
for (let idx = 0; idx < lines.length; idx++) {
|
|
654
|
+
if (citations.length >= MAX_RAW_CITATIONS)
|
|
655
|
+
break;
|
|
656
|
+
const line = lines[idx];
|
|
657
|
+
if (!line || line.trim().length === 0)
|
|
658
|
+
continue;
|
|
659
|
+
for (const matcher of matchers) {
|
|
660
|
+
if (!matcher.regex.test(line))
|
|
661
|
+
continue;
|
|
662
|
+
const snippet = normalizeSnippet(line);
|
|
663
|
+
if (!snippet)
|
|
664
|
+
continue;
|
|
665
|
+
const evidence = {
|
|
666
|
+
path: filePath,
|
|
667
|
+
line: idx + 1,
|
|
668
|
+
snippet,
|
|
669
|
+
term: matcher.label,
|
|
670
|
+
weight: matcher.weight +
|
|
671
|
+
(pathPriority.get(filePath) || 0) +
|
|
672
|
+
(pathHints.some((hint) => filePath.startsWith(hint)) ? 0.25 : 0),
|
|
673
|
+
};
|
|
674
|
+
const dedupeKey = `${evidence.path}:${evidence.line}:${evidence.term || ''}`;
|
|
675
|
+
if (citations.some((item) => `${item.path}:${item.line}:${item.term || ''}` === dedupeKey)) {
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
citations.push(evidence);
|
|
679
|
+
break;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
citations.sort((a, b) => b.weight - a.weight);
|
|
684
|
+
const sourceEvidence = citations.filter((citation) => isPrimarySourcePath(citation.path));
|
|
685
|
+
const sourcePerFileCounts = new Map();
|
|
686
|
+
const sourceTermCounts = new Map();
|
|
687
|
+
for (const citation of sourceEvidence) {
|
|
688
|
+
sourcePerFileCounts.set(citation.path, (sourcePerFileCounts.get(citation.path) || 0) + 1);
|
|
689
|
+
if (citation.term) {
|
|
690
|
+
sourceTermCounts.set(citation.term, (sourceTermCounts.get(citation.term) || 0) + 1);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
const selectedForOutput = [];
|
|
694
|
+
if (mode === 'comparison' && terms.length >= 2) {
|
|
695
|
+
for (const term of terms.slice(0, 2)) {
|
|
696
|
+
const firstForTerm = sourceEvidence.find((citation) => citation.term === term);
|
|
697
|
+
if (firstForTerm) {
|
|
698
|
+
selectedForOutput.push(firstForTerm);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
for (const citation of sourceEvidence) {
|
|
703
|
+
if (selectedForOutput.length >= maxCitations)
|
|
704
|
+
break;
|
|
705
|
+
if (selectedForOutput.some((existing) => existing.path === citation.path && existing.line === citation.line && existing.term === citation.term)) {
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
selectedForOutput.push(citation);
|
|
709
|
+
}
|
|
710
|
+
const finalCitations = selectedForOutput.map(({ path, line, snippet, term }) => ({
|
|
711
|
+
path,
|
|
712
|
+
line,
|
|
713
|
+
snippet,
|
|
714
|
+
term,
|
|
715
|
+
}));
|
|
716
|
+
const stats = {
|
|
717
|
+
scannedFiles,
|
|
718
|
+
matchedFiles: sourcePerFileCounts.size,
|
|
719
|
+
matchedLines: sourceEvidence.length,
|
|
720
|
+
brainCandidates: brainResults.entries.length,
|
|
721
|
+
};
|
|
722
|
+
const truth = evaluateTruthAssessment(mode, normalizedQuestion, terms, sourceEvidence, sourcePerFileCounts, sourceTermCounts);
|
|
723
|
+
const answer = buildAnswer(mode, question, terms, finalCitations, stats, sourceTermCounts, sourcePerFileCounts, truth);
|
|
724
|
+
emitAskResult(answer, {
|
|
725
|
+
json: options.json,
|
|
726
|
+
maxCitations,
|
|
727
|
+
fromPlan: options.fromPlan,
|
|
728
|
+
});
|
|
729
|
+
if (orgId && projectId) {
|
|
730
|
+
(0, brain_context_1.recordBrainProgressEvent)(cwd, scope, {
|
|
731
|
+
type: 'ask',
|
|
732
|
+
note: `mode=${mode};truth=${truth.status};score=${truth.score.toFixed(2)};matched_files=${stats.matchedFiles};matched_lines=${stats.matchedLines}`,
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
if (shouldUseCache && orgId && projectId) {
|
|
736
|
+
const questionHash = (0, ask_cache_1.computeAskQuestionHash)({
|
|
737
|
+
question: normalizedQuestion,
|
|
738
|
+
contextHash: staticContext.hash,
|
|
739
|
+
});
|
|
740
|
+
const key = (0, ask_cache_1.computeAskCacheKey)({
|
|
741
|
+
schemaVersion: 3,
|
|
742
|
+
orgId,
|
|
743
|
+
projectId,
|
|
744
|
+
repo: repoFingerprint,
|
|
745
|
+
questionHash,
|
|
746
|
+
policyVersionHash,
|
|
747
|
+
neurcodeVersion,
|
|
748
|
+
});
|
|
749
|
+
(0, ask_cache_1.writeCachedAsk)(cwd, {
|
|
750
|
+
key,
|
|
751
|
+
input: {
|
|
752
|
+
schemaVersion: 3,
|
|
753
|
+
orgId,
|
|
754
|
+
projectId,
|
|
755
|
+
repo: repoFingerprint,
|
|
756
|
+
questionHash,
|
|
757
|
+
policyVersionHash,
|
|
758
|
+
neurcodeVersion,
|
|
759
|
+
question: normalizedQuestion,
|
|
760
|
+
contextHash: staticContext.hash,
|
|
761
|
+
},
|
|
762
|
+
output: answer,
|
|
763
|
+
evidencePaths: finalCitations.map((citation) => citation.path),
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
catch (error) {
|
|
768
|
+
console.error(chalk.red('\n❌ Error answering question:'));
|
|
769
|
+
if (error instanceof Error) {
|
|
770
|
+
console.error(chalk.red(error.message));
|
|
771
|
+
}
|
|
772
|
+
else {
|
|
773
|
+
console.error(error);
|
|
774
|
+
}
|
|
775
|
+
process.exit(1);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
//# sourceMappingURL=ask.js.map
|