@neurcode-ai/cli 0.9.31 → 0.9.32
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/README.md +22 -0
- package/dist/commands/apply.d.ts.map +1 -1
- package/dist/commands/apply.js +45 -3
- package/dist/commands/apply.js.map +1 -1
- package/dist/commands/map.d.ts.map +1 -1
- package/dist/commands/map.js +78 -1
- package/dist/commands/map.js.map +1 -1
- package/dist/commands/plan-slo.d.ts +7 -0
- package/dist/commands/plan-slo.d.ts.map +1 -0
- package/dist/commands/plan-slo.js +205 -0
- package/dist/commands/plan-slo.js.map +1 -0
- package/dist/commands/plan.d.ts.map +1 -1
- package/dist/commands/plan.js +665 -29
- package/dist/commands/plan.js.map +1 -1
- package/dist/commands/repo.d.ts +3 -0
- package/dist/commands/repo.d.ts.map +1 -0
- package/dist/commands/repo.js +166 -0
- package/dist/commands/repo.js.map +1 -0
- package/dist/commands/ship.d.ts.map +1 -1
- package/dist/commands/ship.js +29 -0
- package/dist/commands/ship.js.map +1 -1
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +261 -9
- package/dist/commands/verify.js.map +1 -1
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -1
- package/dist/services/mapper/ProjectScanner.d.ts +76 -2
- package/dist/services/mapper/ProjectScanner.d.ts.map +1 -1
- package/dist/services/mapper/ProjectScanner.js +545 -40
- package/dist/services/mapper/ProjectScanner.js.map +1 -1
- package/dist/services/security/SecurityGuard.d.ts +21 -2
- package/dist/services/security/SecurityGuard.d.ts.map +1 -1
- package/dist/services/security/SecurityGuard.js +130 -27
- package/dist/services/security/SecurityGuard.js.map +1 -1
- package/dist/utils/governance.d.ts +2 -0
- package/dist/utils/governance.d.ts.map +1 -1
- package/dist/utils/governance.js +2 -0
- package/dist/utils/governance.js.map +1 -1
- package/dist/utils/plan-slo.d.ts +73 -0
- package/dist/utils/plan-slo.d.ts.map +1 -0
- package/dist/utils/plan-slo.js +271 -0
- package/dist/utils/plan-slo.js.map +1 -0
- package/dist/utils/project-root.d.ts +5 -4
- package/dist/utils/project-root.d.ts.map +1 -1
- package/dist/utils/project-root.js +82 -7
- package/dist/utils/project-root.js.map +1 -1
- package/dist/utils/repo-links.d.ts +17 -0
- package/dist/utils/repo-links.d.ts.map +1 -0
- package/dist/utils/repo-links.js +136 -0
- package/dist/utils/repo-links.js.map +1 -0
- package/package.json +3 -3
package/dist/commands/plan.js
CHANGED
|
@@ -49,6 +49,7 @@ const plan_cache_1 = require("../utils/plan-cache");
|
|
|
49
49
|
const neurcode_context_1 = require("../utils/neurcode-context");
|
|
50
50
|
const brain_context_1 = require("../utils/brain-context");
|
|
51
51
|
const project_root_1 = require("../utils/project-root");
|
|
52
|
+
const plan_slo_1 = require("../utils/plan-slo");
|
|
52
53
|
// Import chalk with fallback for plain strings if not available
|
|
53
54
|
let chalk;
|
|
54
55
|
try {
|
|
@@ -84,6 +85,12 @@ function scanFiles(dir, baseDir, maxFiles = 200) {
|
|
|
84
85
|
break;
|
|
85
86
|
const fullPath = (0, path_1.join)(currentDir, entry);
|
|
86
87
|
const relativePath = (0, path_1.relative)(baseDir, fullPath);
|
|
88
|
+
const normalizedRelativePath = toUnixPath(relativePath);
|
|
89
|
+
if (normalizedRelativePath === '' ||
|
|
90
|
+
normalizedRelativePath === '.' ||
|
|
91
|
+
normalizedRelativePath.startsWith('..')) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
87
94
|
// Skip hidden files and directories
|
|
88
95
|
if (entry.startsWith('.')) {
|
|
89
96
|
// Allow .env, .gitignore, etc. but skip .git, .next, etc.
|
|
@@ -97,7 +104,11 @@ function scanFiles(dir, baseDir, maxFiles = 200) {
|
|
|
97
104
|
if (ignoreDirs.has(entry))
|
|
98
105
|
continue;
|
|
99
106
|
try {
|
|
100
|
-
const stat = (0, fs_1.
|
|
107
|
+
const stat = (0, fs_1.lstatSync)(fullPath);
|
|
108
|
+
if (stat.isSymbolicLink()) {
|
|
109
|
+
// Skip symlinked entries to avoid escaping repository boundaries.
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
101
112
|
if (stat.isDirectory()) {
|
|
102
113
|
scan(fullPath);
|
|
103
114
|
}
|
|
@@ -190,35 +201,316 @@ function displayPlan(plan) {
|
|
|
190
201
|
}
|
|
191
202
|
console.log('');
|
|
192
203
|
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
204
|
+
function parseRatio(raw) {
|
|
205
|
+
if (!raw)
|
|
206
|
+
return null;
|
|
207
|
+
const parsed = Number(raw);
|
|
208
|
+
if (!Number.isFinite(parsed) || parsed < 0 || parsed > 1)
|
|
209
|
+
return null;
|
|
210
|
+
return parsed;
|
|
211
|
+
}
|
|
212
|
+
function parsePercent(raw) {
|
|
213
|
+
if (!raw)
|
|
214
|
+
return null;
|
|
215
|
+
const parsed = Number(raw);
|
|
216
|
+
if (!Number.isFinite(parsed) || parsed < 0 || parsed > 100)
|
|
217
|
+
return null;
|
|
218
|
+
return Math.floor(parsed);
|
|
219
|
+
}
|
|
220
|
+
function getEscalationGuardPath(cwd) {
|
|
221
|
+
return (0, path_1.join)(cwd, '.neurcode', 'asset-map-escalation-guard.json');
|
|
222
|
+
}
|
|
223
|
+
function loadEscalationGuardState(cwd) {
|
|
224
|
+
const pathValue = getEscalationGuardPath(cwd);
|
|
225
|
+
if (!(0, fs_1.existsSync)(pathValue)) {
|
|
226
|
+
return {
|
|
227
|
+
version: 1,
|
|
228
|
+
updatedAt: new Date(0).toISOString(),
|
|
229
|
+
consecutiveBreaches: 0,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
const raw = JSON.parse((0, fs_1.readFileSync)(pathValue, 'utf-8'));
|
|
234
|
+
return {
|
|
235
|
+
version: 1,
|
|
236
|
+
updatedAt: typeof raw.updatedAt === 'string' ? raw.updatedAt : new Date(0).toISOString(),
|
|
237
|
+
consecutiveBreaches: typeof raw.consecutiveBreaches === 'number' && Number.isFinite(raw.consecutiveBreaches) && raw.consecutiveBreaches > 0
|
|
238
|
+
? Math.floor(raw.consecutiveBreaches)
|
|
239
|
+
: 0,
|
|
240
|
+
lastBreachAt: typeof raw.lastBreachAt === 'string' ? raw.lastBreachAt : undefined,
|
|
241
|
+
lastReason: typeof raw.lastReason === 'string' ? raw.lastReason : undefined,
|
|
242
|
+
cooldownUntil: typeof raw.cooldownUntil === 'string' ? raw.cooldownUntil : undefined,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
return {
|
|
247
|
+
version: 1,
|
|
248
|
+
updatedAt: new Date(0).toISOString(),
|
|
249
|
+
consecutiveBreaches: 0,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function saveEscalationGuardState(cwd, state) {
|
|
254
|
+
const pathValue = getEscalationGuardPath(cwd);
|
|
255
|
+
const dir = (0, path_1.join)(cwd, '.neurcode');
|
|
256
|
+
if (!(0, fs_1.existsSync)(dir)) {
|
|
257
|
+
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
258
|
+
}
|
|
259
|
+
(0, fs_1.writeFileSync)(pathValue, JSON.stringify(state, null, 2) + '\n', 'utf-8');
|
|
260
|
+
}
|
|
261
|
+
function computeEscalationCanaryBucket(cwd) {
|
|
262
|
+
const seed = (process.env.NEURCODE_ASSET_MAP_ESCALATE_CANARY_SEED || 'neurcode-escalation-v1').trim();
|
|
263
|
+
const digest = (0, crypto_1.createHash)('sha1')
|
|
264
|
+
.update(`${(0, path_1.resolve)(cwd)}|${seed}`, 'utf-8')
|
|
265
|
+
.digest('hex');
|
|
266
|
+
const bucketRaw = parseInt(digest.slice(0, 8), 16);
|
|
267
|
+
if (!Number.isFinite(bucketRaw))
|
|
268
|
+
return 0;
|
|
269
|
+
return Math.abs(bucketRaw % 100);
|
|
270
|
+
}
|
|
271
|
+
function resolveEscalationPolicy(cwd) {
|
|
272
|
+
const enabledByEnv = parseBooleanFlag(process.env.NEURCODE_ASSET_MAP_ESCALATE_DEEPEN, true);
|
|
273
|
+
const canaryPercent = parsePercent(process.env.NEURCODE_ASSET_MAP_ESCALATE_CANARY_PERCENT) ?? 100;
|
|
274
|
+
const canaryBucket = computeEscalationCanaryBucket(cwd);
|
|
275
|
+
if (!enabledByEnv) {
|
|
276
|
+
return {
|
|
277
|
+
enabled: false,
|
|
278
|
+
reason: 'env_disabled',
|
|
279
|
+
canaryPercent,
|
|
280
|
+
canaryBucket,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
const state = loadEscalationGuardState(cwd);
|
|
284
|
+
const nowMs = Date.now();
|
|
285
|
+
const cooldownMs = state.cooldownUntil ? Date.parse(state.cooldownUntil) : NaN;
|
|
286
|
+
if (Number.isFinite(cooldownMs) && cooldownMs > nowMs) {
|
|
287
|
+
return {
|
|
288
|
+
enabled: false,
|
|
289
|
+
reason: 'kill_switch_cooldown',
|
|
290
|
+
canaryPercent,
|
|
291
|
+
canaryBucket,
|
|
292
|
+
cooldownUntil: state.cooldownUntil,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
if (canaryBucket >= canaryPercent) {
|
|
296
|
+
return {
|
|
297
|
+
enabled: false,
|
|
298
|
+
reason: 'canary_excluded',
|
|
299
|
+
canaryPercent,
|
|
300
|
+
canaryBucket,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
enabled: true,
|
|
305
|
+
reason: 'enabled',
|
|
306
|
+
canaryPercent,
|
|
307
|
+
canaryBucket,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
function updateEscalationGuardForBuild(cwd, escalationEnabled, buildStartedAtMs) {
|
|
311
|
+
if (!escalationEnabled) {
|
|
312
|
+
return { killSwitchTripped: false };
|
|
313
|
+
}
|
|
314
|
+
const maxBuildMs = parsePositiveInt(process.env.NEURCODE_ASSET_MAP_ESCALATE_KILL_MAX_BUILD_MS) ?? 5000;
|
|
315
|
+
const maxRssKb = parsePositiveInt(process.env.NEURCODE_ASSET_MAP_ESCALATE_KILL_MAX_RSS_KB) ?? (512 * 1024);
|
|
316
|
+
const minBreaches = parsePositiveInt(process.env.NEURCODE_ASSET_MAP_ESCALATE_KILL_BREACHES) ?? 3;
|
|
317
|
+
const cooldownMinutes = parsePositiveInt(process.env.NEURCODE_ASSET_MAP_ESCALATE_KILL_COOLDOWN_MINUTES) ?? 60;
|
|
318
|
+
const elapsedMs = Math.max(0, Date.now() - buildStartedAtMs);
|
|
319
|
+
const rssKb = Math.max(0, Math.floor(process.memoryUsage().rss / 1024));
|
|
320
|
+
const breachReasons = [];
|
|
321
|
+
if (elapsedMs > maxBuildMs) {
|
|
322
|
+
breachReasons.push(`build_ms>${maxBuildMs}`);
|
|
323
|
+
}
|
|
324
|
+
if (rssKb > maxRssKb) {
|
|
325
|
+
breachReasons.push(`rss_kb>${maxRssKb}`);
|
|
326
|
+
}
|
|
327
|
+
const state = loadEscalationGuardState(cwd);
|
|
328
|
+
const nowIso = new Date().toISOString();
|
|
329
|
+
let killSwitchTripped = false;
|
|
330
|
+
let cooldownUntil;
|
|
331
|
+
if (breachReasons.length > 0) {
|
|
332
|
+
const nextBreaches = state.consecutiveBreaches + 1;
|
|
333
|
+
state.consecutiveBreaches = nextBreaches;
|
|
334
|
+
state.lastBreachAt = nowIso;
|
|
335
|
+
state.lastReason = breachReasons.join(',');
|
|
336
|
+
if (nextBreaches >= minBreaches) {
|
|
337
|
+
killSwitchTripped = true;
|
|
338
|
+
const cooldownMs = Date.now() + cooldownMinutes * 60 * 1000;
|
|
339
|
+
cooldownUntil = new Date(cooldownMs).toISOString();
|
|
340
|
+
state.cooldownUntil = cooldownUntil;
|
|
341
|
+
state.consecutiveBreaches = 0;
|
|
212
342
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
state.consecutiveBreaches = 0;
|
|
346
|
+
state.lastReason = undefined;
|
|
347
|
+
state.lastBreachAt = undefined;
|
|
348
|
+
}
|
|
349
|
+
state.updatedAt = nowIso;
|
|
350
|
+
saveEscalationGuardState(cwd, state);
|
|
351
|
+
return {
|
|
352
|
+
killSwitchTripped,
|
|
353
|
+
cooldownUntil,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
function computeAssetMapIntentFingerprint(intent) {
|
|
357
|
+
const tokens = (intent || '')
|
|
358
|
+
.toLowerCase()
|
|
359
|
+
.match(/[a-z0-9_]{3,}/g);
|
|
360
|
+
if (!tokens || tokens.length === 0)
|
|
361
|
+
return null;
|
|
362
|
+
const filtered = Array.from(new Set(tokens))
|
|
363
|
+
.filter((token) => token.length >= 3)
|
|
364
|
+
.slice(0, 24)
|
|
365
|
+
.sort();
|
|
366
|
+
if (filtered.length === 0)
|
|
367
|
+
return null;
|
|
368
|
+
return (0, crypto_1.createHash)('sha1').update(filtered.join('|'), 'utf-8').digest('hex');
|
|
369
|
+
}
|
|
370
|
+
function decideAssetMapRefresh(map, intentForAdaptiveDeepen) {
|
|
371
|
+
if (process.env.NEURCODE_ASSET_MAP_FORCE_REFRESH === '1') {
|
|
372
|
+
return { refresh: true, reason: 'forced_refresh' };
|
|
373
|
+
}
|
|
374
|
+
const nowMs = Date.now();
|
|
375
|
+
const scannedAtMs = Date.parse(map.scannedAt || '');
|
|
376
|
+
const ageMs = Number.isFinite(scannedAtMs) ? Math.max(0, nowMs - scannedAtMs) : Number.POSITIVE_INFINITY;
|
|
377
|
+
const maxAgeMinutes = parsePositiveInt(process.env.NEURCODE_ASSET_MAP_MAX_AGE_MINUTES) ?? 360;
|
|
378
|
+
const minRefreshIntervalMinutes = parsePositiveInt(process.env.NEURCODE_ASSET_MAP_MIN_REFRESH_INTERVAL_MINUTES) ?? 20;
|
|
379
|
+
const minRefreshIntervalMs = minRefreshIntervalMinutes * 60 * 1000;
|
|
380
|
+
const staleByAge = ageMs > maxAgeMinutes * 60 * 1000;
|
|
381
|
+
if (staleByAge) {
|
|
382
|
+
return { refresh: true, reason: 'stale_age' };
|
|
383
|
+
}
|
|
384
|
+
const withinMinRefreshInterval = ageMs < minRefreshIntervalMs;
|
|
385
|
+
const stats = map.scanStats;
|
|
386
|
+
const indexedSourceFiles = Math.max(1, stats?.indexedSourceFiles || Object.keys(map.files || {}).length || 1);
|
|
387
|
+
const shallowIndexedSourceFiles = stats?.shallowIndexedSourceFiles || 0;
|
|
388
|
+
const shallowRatio = shallowIndexedSourceFiles / indexedSourceFiles;
|
|
389
|
+
const shallowRatioThreshold = parseRatio(process.env.NEURCODE_ASSET_MAP_SHALLOW_RATIO_REFRESH_THRESHOLD) ?? 0.3;
|
|
390
|
+
const shallowPressure = shallowIndexedSourceFiles > 0 && shallowRatio >= shallowRatioThreshold;
|
|
391
|
+
const shallowFailures = (stats?.shallowIndexFailures || 0) > 0;
|
|
392
|
+
const cappedCoverage = Boolean(stats?.cappedByMaxSourceFiles);
|
|
393
|
+
const refreshOnCapped = process.env.NEURCODE_ASSET_MAP_REFRESH_ON_CAPPED !== '0';
|
|
394
|
+
if (shallowFailures && !withinMinRefreshInterval) {
|
|
395
|
+
return { refresh: true, reason: 'shallow_index_failures' };
|
|
396
|
+
}
|
|
397
|
+
if (cappedCoverage && refreshOnCapped && !withinMinRefreshInterval) {
|
|
398
|
+
return { refresh: true, reason: 'capped_coverage' };
|
|
399
|
+
}
|
|
400
|
+
if (shallowPressure && !withinMinRefreshInterval) {
|
|
401
|
+
const currentIntentFingerprint = computeAssetMapIntentFingerprint(intentForAdaptiveDeepen);
|
|
402
|
+
const previousIntentFingerprint = map.scanContext?.adaptiveIntentFingerprint || null;
|
|
403
|
+
if (currentIntentFingerprint && previousIntentFingerprint && currentIntentFingerprint !== previousIntentFingerprint) {
|
|
404
|
+
return { refresh: true, reason: 'intent_shift_on_shallow_map' };
|
|
405
|
+
}
|
|
406
|
+
if ((stats?.adaptiveDeepenedFiles || 0) === 0) {
|
|
407
|
+
return { refresh: true, reason: 'shallow_pressure_without_deepening' };
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return { refresh: false };
|
|
411
|
+
}
|
|
412
|
+
async function buildAssetMap(cwd, intentForAdaptiveDeepen) {
|
|
413
|
+
const escalationPolicy = resolveEscalationPolicy(cwd);
|
|
414
|
+
const startedAtMs = Date.now();
|
|
415
|
+
let escalationKillSwitchTripped = false;
|
|
416
|
+
let escalationKillSwitchCooldownUntil;
|
|
417
|
+
try {
|
|
418
|
+
const { ProjectScanner } = await Promise.resolve().then(() => __importStar(require('../services/mapper/ProjectScanner')));
|
|
419
|
+
const scanner = new ProjectScanner(cwd, {
|
|
420
|
+
maxSourceFiles: parsePositiveInt(process.env.NEURCODE_ASSET_MAP_MAX_FILES) ?? 800,
|
|
421
|
+
maxFileBytes: parsePositiveInt(process.env.NEURCODE_ASSET_MAP_MAX_BYTES) ?? (512 * 1024),
|
|
422
|
+
shallowScanBytes: parsePositiveInt(process.env.NEURCODE_ASSET_MAP_SHALLOW_SCAN_BYTES) ?? (256 * 1024),
|
|
423
|
+
shallowScanWindows: parsePositiveInt(process.env.NEURCODE_ASSET_MAP_SHALLOW_SCAN_WINDOWS) ?? 5,
|
|
424
|
+
adaptiveDeepenIntent: intentForAdaptiveDeepen || '',
|
|
425
|
+
maxAdaptiveDeepenFiles: parsePositiveInt(process.env.NEURCODE_ASSET_MAP_ADAPTIVE_DEEPEN_FILES) ?? 3,
|
|
426
|
+
maxAdaptiveDeepenTotalBytes: parsePositiveInt(process.env.NEURCODE_ASSET_MAP_ADAPTIVE_DEEPEN_TOTAL_BYTES) ?? (2 * 1024 * 1024),
|
|
427
|
+
enableAdaptiveEscalation: escalationPolicy.enabled,
|
|
428
|
+
adaptiveEscalationShallowRatioThreshold: parseRatio(process.env.NEURCODE_ASSET_MAP_ESCALATE_SHALLOW_RATIO) ?? 0.35,
|
|
429
|
+
adaptiveEscalationMinCandidates: parsePositiveInt(process.env.NEURCODE_ASSET_MAP_ESCALATE_MIN_CANDIDATES) ?? 3,
|
|
430
|
+
maxAdaptiveEscalationFiles: parsePositiveInt(process.env.NEURCODE_ASSET_MAP_ESCALATE_MAX_FILES) ?? 2,
|
|
431
|
+
maxAdaptiveEscalationTotalBytes: parsePositiveInt(process.env.NEURCODE_ASSET_MAP_ESCALATE_MAX_BYTES) ?? (1024 * 1024),
|
|
432
|
+
});
|
|
433
|
+
const map = await scanner.scan();
|
|
434
|
+
const killSwitchUpdate = updateEscalationGuardForBuild(cwd, escalationPolicy.enabled, startedAtMs);
|
|
435
|
+
escalationKillSwitchTripped = killSwitchUpdate.killSwitchTripped;
|
|
436
|
+
escalationKillSwitchCooldownUntil = killSwitchUpdate.cooldownUntil;
|
|
437
|
+
const { writeFileSync, mkdirSync } = await Promise.resolve().then(() => __importStar(require('fs')));
|
|
438
|
+
const neurcodeDir = (0, path_1.join)(cwd, '.neurcode');
|
|
439
|
+
if (!(0, fs_1.existsSync)(neurcodeDir)) {
|
|
440
|
+
mkdirSync(neurcodeDir, { recursive: true });
|
|
441
|
+
}
|
|
442
|
+
const mapPath = (0, path_1.join)(neurcodeDir, 'asset-map.json');
|
|
443
|
+
writeFileSync(mapPath, JSON.stringify(map, null, 2) + '\n', 'utf-8');
|
|
444
|
+
return {
|
|
445
|
+
map,
|
|
446
|
+
escalationPolicy,
|
|
447
|
+
escalationKillSwitchTripped,
|
|
448
|
+
escalationKillSwitchCooldownUntil,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
catch (error) {
|
|
452
|
+
if (process.env.DEBUG) {
|
|
453
|
+
console.warn(chalk.yellow(`⚠️ Could not generate asset map: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
219
454
|
}
|
|
455
|
+
return {
|
|
456
|
+
map: null,
|
|
457
|
+
escalationPolicy,
|
|
458
|
+
escalationKillSwitchTripped: false,
|
|
459
|
+
escalationKillSwitchCooldownUntil: undefined,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
async function ensureAssetMap(cwd, intentForAdaptiveDeepen) {
|
|
464
|
+
if (process.env.NEURCODE_DISABLE_ASSET_MAP === '1') {
|
|
465
|
+
return {
|
|
466
|
+
map: null,
|
|
467
|
+
generated: false,
|
|
468
|
+
refreshed: false,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
const existingMap = (0, map_1.loadAssetMap)(cwd);
|
|
472
|
+
if (!existingMap) {
|
|
473
|
+
const generated = await buildAssetMap(cwd, intentForAdaptiveDeepen);
|
|
474
|
+
return {
|
|
475
|
+
map: generated.map,
|
|
476
|
+
generated: Boolean(generated.map),
|
|
477
|
+
refreshed: false,
|
|
478
|
+
refreshReason: generated.map ? 'missing' : undefined,
|
|
479
|
+
escalationPolicy: generated.escalationPolicy,
|
|
480
|
+
escalationKillSwitchTripped: generated.escalationKillSwitchTripped,
|
|
481
|
+
escalationKillSwitchCooldownUntil: generated.escalationKillSwitchCooldownUntil,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
const refreshDecision = decideAssetMapRefresh(existingMap, intentForAdaptiveDeepen);
|
|
485
|
+
if (!refreshDecision.refresh) {
|
|
486
|
+
return {
|
|
487
|
+
map: existingMap,
|
|
488
|
+
generated: false,
|
|
489
|
+
refreshed: false,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
const refreshed = await buildAssetMap(cwd, intentForAdaptiveDeepen);
|
|
493
|
+
if (!refreshed.map) {
|
|
494
|
+
return {
|
|
495
|
+
map: existingMap,
|
|
496
|
+
generated: false,
|
|
497
|
+
refreshed: false,
|
|
498
|
+
refreshReason: refreshDecision.reason,
|
|
499
|
+
refreshFailed: true,
|
|
500
|
+
escalationPolicy: refreshed.escalationPolicy,
|
|
501
|
+
escalationKillSwitchTripped: refreshed.escalationKillSwitchTripped,
|
|
502
|
+
escalationKillSwitchCooldownUntil: refreshed.escalationKillSwitchCooldownUntil,
|
|
503
|
+
};
|
|
220
504
|
}
|
|
221
|
-
return
|
|
505
|
+
return {
|
|
506
|
+
map: refreshed.map,
|
|
507
|
+
generated: false,
|
|
508
|
+
refreshed: true,
|
|
509
|
+
refreshReason: refreshDecision.reason,
|
|
510
|
+
escalationPolicy: refreshed.escalationPolicy,
|
|
511
|
+
escalationKillSwitchTripped: refreshed.escalationKillSwitchTripped,
|
|
512
|
+
escalationKillSwitchCooldownUntil: refreshed.escalationKillSwitchCooldownUntil,
|
|
513
|
+
};
|
|
222
514
|
}
|
|
223
515
|
function detectIntentMode(intent) {
|
|
224
516
|
const normalized = (0, plan_cache_1.normalizeIntent)(intent);
|
|
@@ -282,6 +574,141 @@ function parsePositiveInt(raw) {
|
|
|
282
574
|
return null;
|
|
283
575
|
return Math.floor(parsed);
|
|
284
576
|
}
|
|
577
|
+
function parseBooleanFlag(raw, fallback) {
|
|
578
|
+
if (!raw || !raw.trim())
|
|
579
|
+
return fallback;
|
|
580
|
+
const normalized = raw.trim().toLowerCase();
|
|
581
|
+
if (normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on') {
|
|
582
|
+
return true;
|
|
583
|
+
}
|
|
584
|
+
if (normalized === '0' || normalized === 'false' || normalized === 'no' || normalized === 'off') {
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
return fallback;
|
|
588
|
+
}
|
|
589
|
+
function parseConfidenceScoreThreshold(raw) {
|
|
590
|
+
if (!raw)
|
|
591
|
+
return null;
|
|
592
|
+
const parsed = Number(raw);
|
|
593
|
+
if (!Number.isFinite(parsed))
|
|
594
|
+
return null;
|
|
595
|
+
if (parsed < 0 || parsed > 100)
|
|
596
|
+
return null;
|
|
597
|
+
return Math.floor(parsed);
|
|
598
|
+
}
|
|
599
|
+
function buildPlanCoverageConfidence(input) {
|
|
600
|
+
const fileTreeCountSafe = Math.max(1, input.fileTreeCount);
|
|
601
|
+
const selectionCoverageRatio = input.filesUsedForGeneration / fileTreeCountSafe;
|
|
602
|
+
const readableSelectionRatio = input.selectedByScout > 0 ? input.readableSelected / input.selectedByScout : 1;
|
|
603
|
+
const mapStats = input.assetMap?.scanStats;
|
|
604
|
+
const metrics = {
|
|
605
|
+
fileTreeCount: input.fileTreeCount,
|
|
606
|
+
fileTreeCapped: input.fileTreeCapped,
|
|
607
|
+
selectedByScout: input.selectedByScout,
|
|
608
|
+
readableSelected: input.readableSelected,
|
|
609
|
+
filesUsedForGeneration: input.filesUsedForGeneration,
|
|
610
|
+
selectionCoverageRatio,
|
|
611
|
+
readableSelectionRatio,
|
|
612
|
+
usedFallbackSelection: input.usedFallbackSelection,
|
|
613
|
+
assetMapAvailable: Boolean(input.assetMap),
|
|
614
|
+
assetMapExports: input.assetMap?.globalExports.length || 0,
|
|
615
|
+
assetMapCapped: Boolean(mapStats?.cappedByMaxSourceFiles),
|
|
616
|
+
shallowIndexedSourceFiles: mapStats?.shallowIndexedSourceFiles || 0,
|
|
617
|
+
shallowIndexFailures: mapStats?.shallowIndexFailures || 0,
|
|
618
|
+
adaptiveDeepenedFiles: mapStats?.adaptiveDeepenedFiles || 0,
|
|
619
|
+
adaptiveDeepenSkippedBudget: mapStats?.adaptiveDeepenSkippedBudget || 0,
|
|
620
|
+
adaptiveEscalationTriggered: Boolean(mapStats?.adaptiveEscalationTriggered),
|
|
621
|
+
adaptiveEscalationReason: mapStats?.adaptiveEscalationReason || null,
|
|
622
|
+
adaptiveEscalationDeepenedFiles: mapStats?.adaptiveEscalationDeepenedFiles || 0,
|
|
623
|
+
adaptiveEscalationSkippedBudget: mapStats?.adaptiveEscalationSkippedBudget || 0,
|
|
624
|
+
};
|
|
625
|
+
let score = 100;
|
|
626
|
+
const reasons = [];
|
|
627
|
+
if (metrics.fileTreeCapped) {
|
|
628
|
+
score -= 12;
|
|
629
|
+
reasons.push('File tree context hit the cap and may miss relevant paths.');
|
|
630
|
+
}
|
|
631
|
+
if (metrics.usedFallbackSelection) {
|
|
632
|
+
score -= 14;
|
|
633
|
+
reasons.push('Semantic Scout fallback was used instead of ranked file selection.');
|
|
634
|
+
}
|
|
635
|
+
if (metrics.selectionCoverageRatio < 0.04) {
|
|
636
|
+
score -= 22;
|
|
637
|
+
reasons.push('Very low file coverage for this intent.');
|
|
638
|
+
}
|
|
639
|
+
else if (metrics.selectionCoverageRatio < 0.1) {
|
|
640
|
+
score -= 14;
|
|
641
|
+
reasons.push('Low file coverage for this intent.');
|
|
642
|
+
}
|
|
643
|
+
else if (metrics.selectionCoverageRatio < 0.2) {
|
|
644
|
+
score -= 8;
|
|
645
|
+
reasons.push('Moderate file coverage; consider expanding selected files.');
|
|
646
|
+
}
|
|
647
|
+
if (metrics.readableSelectionRatio < 0.7) {
|
|
648
|
+
score -= 10;
|
|
649
|
+
reasons.push('Many selected files were unreadable and dropped from planning.');
|
|
650
|
+
}
|
|
651
|
+
else if (metrics.readableSelectionRatio < 0.9) {
|
|
652
|
+
score -= 4;
|
|
653
|
+
reasons.push('Some selected files were unreadable and excluded.');
|
|
654
|
+
}
|
|
655
|
+
if (metrics.fileTreeCount >= 30 && metrics.filesUsedForGeneration < 5) {
|
|
656
|
+
score -= 8;
|
|
657
|
+
reasons.push('Plan uses a small file set relative to repository size.');
|
|
658
|
+
}
|
|
659
|
+
if (!metrics.assetMapAvailable) {
|
|
660
|
+
score -= 10;
|
|
661
|
+
reasons.push('Asset map unavailable; toolbox context was not injected.');
|
|
662
|
+
}
|
|
663
|
+
else {
|
|
664
|
+
if (metrics.assetMapCapped) {
|
|
665
|
+
score -= 8;
|
|
666
|
+
reasons.push('Asset map indexing was capped before full repository coverage.');
|
|
667
|
+
}
|
|
668
|
+
if (metrics.shallowIndexFailures > 0) {
|
|
669
|
+
score -= 10;
|
|
670
|
+
reasons.push('Some oversized files could not be indexed, reducing context completeness.');
|
|
671
|
+
}
|
|
672
|
+
if (metrics.shallowIndexedSourceFiles > 0 && metrics.adaptiveDeepenedFiles === 0) {
|
|
673
|
+
score -= 4;
|
|
674
|
+
reasons.push('Oversized files remained shallow-indexed with no adaptive deepening.');
|
|
675
|
+
}
|
|
676
|
+
if (metrics.adaptiveDeepenSkippedBudget > 0) {
|
|
677
|
+
score -= 4;
|
|
678
|
+
reasons.push('Adaptive deepening skipped candidates due to budget constraints.');
|
|
679
|
+
}
|
|
680
|
+
if (metrics.adaptiveEscalationTriggered && metrics.adaptiveEscalationDeepenedFiles === 0) {
|
|
681
|
+
score -= 3;
|
|
682
|
+
reasons.push('Escalation pass triggered but could not fully parse additional oversized files.');
|
|
683
|
+
}
|
|
684
|
+
if (metrics.adaptiveEscalationSkippedBudget > 0) {
|
|
685
|
+
score -= 2;
|
|
686
|
+
reasons.push('Escalation deepening skipped candidates due to strict byte/file limits.');
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
score = Math.max(0, Math.min(100, score));
|
|
690
|
+
const level = score >= 80 ? 'high' : score >= 60 ? 'medium' : 'low';
|
|
691
|
+
const status = score >= 70 ? 'sufficient' : score >= 50 ? 'warning' : 'insufficient';
|
|
692
|
+
if (reasons.length === 0) {
|
|
693
|
+
reasons.push('Context coverage is strong for this plan run.');
|
|
694
|
+
}
|
|
695
|
+
return {
|
|
696
|
+
score,
|
|
697
|
+
level,
|
|
698
|
+
status,
|
|
699
|
+
reasons,
|
|
700
|
+
metrics,
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
function displayPlanCoverageConfidence(coverage) {
|
|
704
|
+
const scoreColor = coverage.level === 'high' ? chalk.green : coverage.level === 'medium' ? chalk.yellow : chalk.red;
|
|
705
|
+
console.log(chalk.bold.white('Context Confidence:'), scoreColor(`${coverage.score}/100 (${coverage.level.toUpperCase()})`));
|
|
706
|
+
if (coverage.level !== 'high') {
|
|
707
|
+
for (const reason of coverage.reasons.slice(0, 3)) {
|
|
708
|
+
console.log(chalk.dim(` • ${reason}`));
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
285
712
|
function parseSnapshotMode(raw) {
|
|
286
713
|
if (!raw)
|
|
287
714
|
return null;
|
|
@@ -453,6 +880,62 @@ function emitCachedPlanHit(input) {
|
|
|
453
880
|
return persistedPlan;
|
|
454
881
|
}
|
|
455
882
|
async function planCommand(intent, options) {
|
|
883
|
+
const planStartedAtMs = Date.now();
|
|
884
|
+
let planRootForSlo = (0, path_1.resolve)(process.cwd());
|
|
885
|
+
try {
|
|
886
|
+
planRootForSlo = (0, project_root_1.resolveNeurcodeProjectRoot)(process.cwd());
|
|
887
|
+
}
|
|
888
|
+
catch {
|
|
889
|
+
// Fallback to current working directory when root resolution fails.
|
|
890
|
+
}
|
|
891
|
+
let planIntentModeForSlo = 'implementation';
|
|
892
|
+
let planCachedForSlo = false;
|
|
893
|
+
let planSuccessForSlo = false;
|
|
894
|
+
let planEscalationPolicyForSlo = null;
|
|
895
|
+
let planEscalationKillSwitchTrippedForSlo = false;
|
|
896
|
+
let planEscalationKillSwitchCooldownUntilForSlo = null;
|
|
897
|
+
let planSloEventFlushed = false;
|
|
898
|
+
const flushPlanSloEvent = (exitCode) => {
|
|
899
|
+
if (planSloEventFlushed) {
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
planSloEventFlushed = true;
|
|
903
|
+
const coverageMetrics = coverageConfidence?.metrics;
|
|
904
|
+
const rssKb = Math.max(0, Math.floor(process.memoryUsage().rss / 1024));
|
|
905
|
+
try {
|
|
906
|
+
(0, plan_slo_1.appendPlanSloEvent)(planRootForSlo, {
|
|
907
|
+
timestamp: new Date().toISOString(),
|
|
908
|
+
intentMode: planIntentModeForSlo,
|
|
909
|
+
cached: planCachedForSlo,
|
|
910
|
+
success: planSuccessForSlo && exitCode === 0,
|
|
911
|
+
exitCode,
|
|
912
|
+
elapsedMs: Math.max(0, Date.now() - planStartedAtMs),
|
|
913
|
+
rssKb,
|
|
914
|
+
coverageScore: coverageConfidence?.score ?? null,
|
|
915
|
+
coverageLevel: coverageConfidence?.level ?? null,
|
|
916
|
+
coverageStatus: coverageConfidence?.status ?? null,
|
|
917
|
+
adaptiveEscalationTriggered: coverageMetrics?.adaptiveEscalationTriggered === true,
|
|
918
|
+
adaptiveEscalationReason: coverageMetrics?.adaptiveEscalationReason || null,
|
|
919
|
+
adaptiveEscalationDeepenedFiles: coverageMetrics?.adaptiveEscalationDeepenedFiles || 0,
|
|
920
|
+
escalationPolicyEnabled: planEscalationPolicyForSlo?.enabled ?? null,
|
|
921
|
+
escalationPolicyReason: planEscalationPolicyForSlo?.reason ?? null,
|
|
922
|
+
escalationCanaryPercent: planEscalationPolicyForSlo?.canaryPercent ?? null,
|
|
923
|
+
escalationCanaryBucket: planEscalationPolicyForSlo?.canaryBucket ?? null,
|
|
924
|
+
escalationKillSwitchTripped: planEscalationKillSwitchTrippedForSlo,
|
|
925
|
+
escalationKillSwitchCooldownUntil: planEscalationKillSwitchCooldownUntilForSlo || planEscalationPolicyForSlo?.cooldownUntil || null,
|
|
926
|
+
fileTreeCount: coverageMetrics?.fileTreeCount ?? null,
|
|
927
|
+
filesUsedForGeneration: coverageMetrics?.filesUsedForGeneration ?? null,
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
catch {
|
|
931
|
+
// SLO logging is best-effort and should never block plan command.
|
|
932
|
+
}
|
|
933
|
+
};
|
|
934
|
+
const onPlanProcessExit = (code) => {
|
|
935
|
+
flushPlanSloEvent(code);
|
|
936
|
+
};
|
|
937
|
+
process.once('exit', onPlanProcessExit);
|
|
938
|
+
let coverageConfidence;
|
|
456
939
|
try {
|
|
457
940
|
if (!intent || !intent.trim()) {
|
|
458
941
|
if (options.json) {
|
|
@@ -505,12 +988,38 @@ async function planCommand(intent, options) {
|
|
|
505
988
|
// If the same user/org/project runs the same intent against the same repo snapshot,
|
|
506
989
|
// return the cached plan immediately (no network, no file scanning).
|
|
507
990
|
const cwd = (0, project_root_1.resolveNeurcodeProjectRoot)(process.cwd());
|
|
991
|
+
const homeDir = (0, path_1.resolve)(process.env.HOME || process.env.USERPROFILE || '');
|
|
992
|
+
if (homeDir &&
|
|
993
|
+
cwd === homeDir &&
|
|
994
|
+
process.cwd() !== homeDir &&
|
|
995
|
+
process.env.NEURCODE_ALLOW_HOME_ROOT !== '1') {
|
|
996
|
+
const message = [
|
|
997
|
+
'Resolved project root points to your home directory, which can mix unrelated repositories.',
|
|
998
|
+
'Run `neurcode init` from this repository root to isolate Neurcode state.',
|
|
999
|
+
'Set NEURCODE_ALLOW_HOME_ROOT=1 only if you intentionally want home-directory scope.',
|
|
1000
|
+
].join(' ');
|
|
1001
|
+
if (options.json) {
|
|
1002
|
+
emitPlanJson({
|
|
1003
|
+
success: false,
|
|
1004
|
+
cached: false,
|
|
1005
|
+
mode: 'implementation',
|
|
1006
|
+
planId: null,
|
|
1007
|
+
sessionId: null,
|
|
1008
|
+
projectId: options.projectId || null,
|
|
1009
|
+
timestamp: new Date().toISOString(),
|
|
1010
|
+
message,
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
console.error(chalk.red(`❌ ${message}`));
|
|
1014
|
+
process.exit(1);
|
|
1015
|
+
}
|
|
508
1016
|
const orgId = (0, state_1.getOrgId)();
|
|
509
1017
|
const stateProjectId = (0, state_1.getProjectId)();
|
|
510
1018
|
const finalProjectIdEarly = options.projectId || stateProjectId || config.projectId;
|
|
511
1019
|
const shouldUseCache = options.cache !== false && process.env.NEURCODE_PLAN_NO_CACHE !== '1';
|
|
512
1020
|
const normalizedIntent = (0, plan_cache_1.normalizeIntent)(intent);
|
|
513
1021
|
const intentMode = detectIntentMode(intent);
|
|
1022
|
+
planIntentModeForSlo = intentMode;
|
|
514
1023
|
const isReadOnlyAnalysis = intentMode === 'analysis';
|
|
515
1024
|
const nearIntentSimilarityFloor = isReadOnlyAnalysis ? 0.66 : undefined;
|
|
516
1025
|
const policyVersionHash = (0, plan_cache_1.computePolicyVersionHash)(cwd);
|
|
@@ -557,8 +1066,12 @@ async function planCommand(intent, options) {
|
|
|
557
1066
|
jsonMode: options.json === true,
|
|
558
1067
|
intentMode,
|
|
559
1068
|
});
|
|
1069
|
+
planCachedForSlo = true;
|
|
1070
|
+
planSuccessForSlo = persisted;
|
|
560
1071
|
if (!persisted)
|
|
561
1072
|
process.exit(2);
|
|
1073
|
+
flushPlanSloEvent(0);
|
|
1074
|
+
process.removeListener('exit', onPlanProcessExit);
|
|
562
1075
|
return;
|
|
563
1076
|
}
|
|
564
1077
|
const near = (0, plan_cache_1.findNearCachedPlan)(cwd, {
|
|
@@ -585,8 +1098,12 @@ async function planCommand(intent, options) {
|
|
|
585
1098
|
jsonMode: options.json === true,
|
|
586
1099
|
intentMode,
|
|
587
1100
|
});
|
|
1101
|
+
planCachedForSlo = true;
|
|
1102
|
+
planSuccessForSlo = persisted;
|
|
588
1103
|
if (!persisted)
|
|
589
1104
|
process.exit(2);
|
|
1105
|
+
flushPlanSloEvent(0);
|
|
1106
|
+
process.removeListener('exit', onPlanProcessExit);
|
|
590
1107
|
return;
|
|
591
1108
|
}
|
|
592
1109
|
const miss = (0, plan_cache_1.diagnosePlanCacheMiss)(cwd, {
|
|
@@ -719,12 +1236,16 @@ async function planCommand(intent, options) {
|
|
|
719
1236
|
};
|
|
720
1237
|
// Step B: Scan file tree (paths only, no content)
|
|
721
1238
|
console.log(chalk.dim(`📂 Scanning file tree in ${cwd}...`));
|
|
722
|
-
const
|
|
1239
|
+
const planFileTreeMaxFiles = Math.min(parsePositiveInt(process.env.NEURCODE_PLAN_FILE_TREE_MAX_FILES) ?? 200, 5000);
|
|
1240
|
+
const fileTree = scanFiles(cwd, cwd, planFileTreeMaxFiles);
|
|
723
1241
|
if (fileTree.length === 0) {
|
|
724
1242
|
console.warn(chalk.yellow('⚠️ No files found in current directory'));
|
|
725
1243
|
process.exit(1);
|
|
726
1244
|
}
|
|
727
1245
|
console.log(chalk.dim(`Found ${fileTree.length} files in project`));
|
|
1246
|
+
if (fileTree.length >= planFileTreeMaxFiles) {
|
|
1247
|
+
console.log(chalk.dim(`📎 File tree context capped at ${planFileTreeMaxFiles} files. Set NEURCODE_PLAN_FILE_TREE_MAX_FILES to increase.`));
|
|
1248
|
+
}
|
|
728
1249
|
// Load Neurcode static context (repo + local + org/project) early so it can:
|
|
729
1250
|
// - influence plan generation
|
|
730
1251
|
// - participate in the cache key (avoid stale cached plans after context edits)
|
|
@@ -775,8 +1296,12 @@ async function planCommand(intent, options) {
|
|
|
775
1296
|
jsonMode: options.json === true,
|
|
776
1297
|
intentMode,
|
|
777
1298
|
});
|
|
1299
|
+
planCachedForSlo = true;
|
|
1300
|
+
planSuccessForSlo = persisted;
|
|
778
1301
|
if (!persisted)
|
|
779
1302
|
process.exit(2);
|
|
1303
|
+
flushPlanSloEvent(0);
|
|
1304
|
+
process.removeListener('exit', onPlanProcessExit);
|
|
780
1305
|
return;
|
|
781
1306
|
}
|
|
782
1307
|
const near = (0, plan_cache_1.findNearCachedPlan)(cwd, {
|
|
@@ -803,8 +1328,12 @@ async function planCommand(intent, options) {
|
|
|
803
1328
|
jsonMode: options.json === true,
|
|
804
1329
|
intentMode,
|
|
805
1330
|
});
|
|
1331
|
+
planCachedForSlo = true;
|
|
1332
|
+
planSuccessForSlo = persisted;
|
|
806
1333
|
if (!persisted)
|
|
807
1334
|
process.exit(2);
|
|
1335
|
+
flushPlanSloEvent(0);
|
|
1336
|
+
process.removeListener('exit', onPlanProcessExit);
|
|
808
1337
|
return;
|
|
809
1338
|
}
|
|
810
1339
|
const miss = (0, plan_cache_1.diagnosePlanCacheMiss)(cwd, {
|
|
@@ -850,9 +1379,75 @@ async function planCommand(intent, options) {
|
|
|
850
1379
|
}
|
|
851
1380
|
}
|
|
852
1381
|
// Step 3: Load or create asset map for context injection (toolbox summary)
|
|
1382
|
+
let assetMapForCoverage = null;
|
|
853
1383
|
try {
|
|
854
|
-
const
|
|
1384
|
+
const assetMapResolution = await ensureAssetMap(cwd, enrichedIntent);
|
|
1385
|
+
const map = assetMapResolution.map;
|
|
1386
|
+
assetMapForCoverage = map;
|
|
1387
|
+
if (assetMapResolution.escalationPolicy) {
|
|
1388
|
+
planEscalationPolicyForSlo = assetMapResolution.escalationPolicy;
|
|
1389
|
+
}
|
|
1390
|
+
if (assetMapResolution.escalationKillSwitchTripped) {
|
|
1391
|
+
planEscalationKillSwitchTrippedForSlo = true;
|
|
1392
|
+
}
|
|
1393
|
+
if (assetMapResolution.escalationKillSwitchCooldownUntil) {
|
|
1394
|
+
planEscalationKillSwitchCooldownUntilForSlo = assetMapResolution.escalationKillSwitchCooldownUntil;
|
|
1395
|
+
}
|
|
1396
|
+
if (assetMapResolution.generated) {
|
|
1397
|
+
console.log(chalk.dim('♻️ Generated fresh asset map for this repository.'));
|
|
1398
|
+
}
|
|
1399
|
+
else if (assetMapResolution.refreshed) {
|
|
1400
|
+
console.log(chalk.dim(`♻️ Refreshed asset map (${assetMapResolution.refreshReason || 'policy_trigger'}).`));
|
|
1401
|
+
}
|
|
1402
|
+
else if (assetMapResolution.refreshFailed) {
|
|
1403
|
+
console.log(chalk.yellow(`⚠️ Asset map refresh failed (${assetMapResolution.refreshReason || 'unknown'}), using previous cached map.`));
|
|
1404
|
+
}
|
|
1405
|
+
if (assetMapResolution.escalationPolicy && (assetMapResolution.generated || assetMapResolution.refreshed)) {
|
|
1406
|
+
const escalationPolicy = assetMapResolution.escalationPolicy;
|
|
1407
|
+
if (escalationPolicy.enabled) {
|
|
1408
|
+
console.log(chalk.dim(`🧪 Escalation policy: enabled (canary=${escalationPolicy.canaryBucket}/${escalationPolicy.canaryPercent}).`));
|
|
1409
|
+
}
|
|
1410
|
+
else {
|
|
1411
|
+
let detail = `reason=${escalationPolicy.reason}`;
|
|
1412
|
+
if (escalationPolicy.reason === 'canary_excluded') {
|
|
1413
|
+
detail += `, canary=${escalationPolicy.canaryBucket}/${escalationPolicy.canaryPercent}`;
|
|
1414
|
+
}
|
|
1415
|
+
if (escalationPolicy.reason === 'kill_switch_cooldown' && escalationPolicy.cooldownUntil) {
|
|
1416
|
+
detail += `, cooldownUntil=${escalationPolicy.cooldownUntil}`;
|
|
1417
|
+
}
|
|
1418
|
+
console.log(chalk.yellow(`⚠️ Adaptive escalation disabled (${detail}).`));
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
if (assetMapResolution.escalationKillSwitchTripped) {
|
|
1422
|
+
console.log(chalk.yellow(`⚠️ Escalation kill switch tripped; cooldown active until ${assetMapResolution.escalationKillSwitchCooldownUntil || 'later'}.`));
|
|
1423
|
+
}
|
|
855
1424
|
if (map && map.globalExports.length > 0) {
|
|
1425
|
+
if (map.scanStats?.cappedByMaxSourceFiles) {
|
|
1426
|
+
console.log(chalk.yellow(`⚠️ Asset map coverage capped at ${map.scanStats.indexedSourceFiles} files (limit ${map.scanStats.maxSourceFiles}).`));
|
|
1427
|
+
console.log(chalk.dim(' Increase NEURCODE_ASSET_MAP_MAX_FILES for broader coverage.'));
|
|
1428
|
+
}
|
|
1429
|
+
if ((map.scanStats?.skippedBySize || 0) > 0) {
|
|
1430
|
+
console.log(chalk.dim(`📦 Asset map shallow-indexed ${map.scanStats?.skippedBySize} oversized source files (> ${map.scanStats?.maxFileBytes} bytes).`));
|
|
1431
|
+
}
|
|
1432
|
+
if ((map.scanStats?.shallowIndexFailures || 0) > 0) {
|
|
1433
|
+
console.log(chalk.yellow(`⚠️ Asset map could not shallow-index ${map.scanStats?.shallowIndexFailures} oversized file(s).`));
|
|
1434
|
+
}
|
|
1435
|
+
if ((map.scanStats?.adaptiveDeepenedFiles || 0) > 0) {
|
|
1436
|
+
console.log(chalk.dim(`🧠 Adaptive deepened ${map.scanStats?.adaptiveDeepenedFiles} oversized file(s) for this intent.`));
|
|
1437
|
+
}
|
|
1438
|
+
if ((map.scanStats?.adaptiveDeepenSkippedBudget || 0) > 0) {
|
|
1439
|
+
console.log(chalk.dim(`📎 Adaptive deepening skipped ${map.scanStats?.adaptiveDeepenSkippedBudget} candidate(s) due to budget.`));
|
|
1440
|
+
}
|
|
1441
|
+
if (map.scanStats?.adaptiveEscalationTriggered) {
|
|
1442
|
+
const escalationReason = map.scanStats?.adaptiveEscalationReason || 'policy_trigger';
|
|
1443
|
+
console.log(chalk.dim(`🎯 Adaptive escalation pass triggered (${escalationReason}).`));
|
|
1444
|
+
}
|
|
1445
|
+
if ((map.scanStats?.adaptiveEscalationDeepenedFiles || 0) > 0) {
|
|
1446
|
+
console.log(chalk.dim(`🚀 Escalation deepened ${map.scanStats?.adaptiveEscalationDeepenedFiles} additional oversized file(s).`));
|
|
1447
|
+
}
|
|
1448
|
+
if ((map.scanStats?.adaptiveEscalationSkippedBudget || 0) > 0) {
|
|
1449
|
+
console.log(chalk.dim(`📎 Escalation skipped ${map.scanStats?.adaptiveEscalationSkippedBudget} candidate(s) due to escalation budget limits.`));
|
|
1450
|
+
}
|
|
856
1451
|
// Pass intent to generateToolboxSummary for relevance filtering
|
|
857
1452
|
const toolboxSummary = (0, toolbox_service_1.generateToolboxSummary)(map, enrichedIntent);
|
|
858
1453
|
if (toolboxSummary) {
|
|
@@ -998,12 +1593,14 @@ async function planCommand(intent, options) {
|
|
|
998
1593
|
// Step C: Pass 1 - The Semantic Scout (select relevant files)
|
|
999
1594
|
console.log(chalk.dim('🔍 Semantic Scout: Selecting relevant files...'));
|
|
1000
1595
|
let selectedFiles = [];
|
|
1596
|
+
let usedFallbackSelection = false;
|
|
1001
1597
|
try {
|
|
1002
1598
|
selectedFiles = await client.selectFiles(enhancedIntent, fileTree, projectSummary);
|
|
1003
1599
|
// Handle empty selection (fallback to top 10 files)
|
|
1004
1600
|
if (selectedFiles.length === 0) {
|
|
1005
1601
|
console.log(chalk.yellow('⚠️ No files selected by Semantic Scout, using fallback (top 10 files)'));
|
|
1006
1602
|
selectedFiles = fileTree.slice(0, 10);
|
|
1603
|
+
usedFallbackSelection = true;
|
|
1007
1604
|
}
|
|
1008
1605
|
console.log(chalk.green(`✅ Semantic Scout selected ${selectedFiles.length} file(s) from ${fileTree.length} total`));
|
|
1009
1606
|
if (process.env.DEBUG) {
|
|
@@ -1015,6 +1612,7 @@ async function planCommand(intent, options) {
|
|
|
1015
1612
|
console.warn(chalk.yellow(`⚠️ File selection failed: ${error instanceof Error ? error.message : 'Unknown error'}`));
|
|
1016
1613
|
console.log(chalk.yellow(' Using fallback: top 10 files from tree'));
|
|
1017
1614
|
selectedFiles = fileTree.slice(0, 10);
|
|
1615
|
+
usedFallbackSelection = true;
|
|
1018
1616
|
}
|
|
1019
1617
|
// Step D: Content Load - Verify selected files exist and are readable
|
|
1020
1618
|
const validFiles = [];
|
|
@@ -1047,6 +1645,38 @@ async function planCommand(intent, options) {
|
|
|
1047
1645
|
filesToUse = filesToUse.slice(0, 8);
|
|
1048
1646
|
console.log(chalk.dim(`🔎 Analysis mode: narrowed to ${filesToUse.length} top file(s)`));
|
|
1049
1647
|
}
|
|
1648
|
+
coverageConfidence = buildPlanCoverageConfidence({
|
|
1649
|
+
fileTreeCount: fileTree.length,
|
|
1650
|
+
fileTreeCapped: fileTree.length >= planFileTreeMaxFiles,
|
|
1651
|
+
selectedByScout: selectedFiles.length,
|
|
1652
|
+
readableSelected: validFiles.length,
|
|
1653
|
+
filesUsedForGeneration: filesToUse.length,
|
|
1654
|
+
usedFallbackSelection,
|
|
1655
|
+
assetMap: assetMapForCoverage,
|
|
1656
|
+
});
|
|
1657
|
+
displayPlanCoverageConfidence(coverageConfidence);
|
|
1658
|
+
const minConfidenceScore = parseConfidenceScoreThreshold(process.env.NEURCODE_PLAN_MIN_CONFIDENCE_SCORE);
|
|
1659
|
+
if (minConfidenceScore !== null && coverageConfidence.score < minConfidenceScore) {
|
|
1660
|
+
const message = [
|
|
1661
|
+
`Context confidence score ${coverageConfidence.score}/100 is below required threshold ${minConfidenceScore}.`,
|
|
1662
|
+
'Increase file/asset-map coverage or relax NEURCODE_PLAN_MIN_CONFIDENCE_SCORE.',
|
|
1663
|
+
].join(' ');
|
|
1664
|
+
if (options.json) {
|
|
1665
|
+
emitPlanJson({
|
|
1666
|
+
success: false,
|
|
1667
|
+
cached: false,
|
|
1668
|
+
mode: intentMode,
|
|
1669
|
+
planId: null,
|
|
1670
|
+
sessionId: null,
|
|
1671
|
+
projectId: finalProjectIdForGuard || null,
|
|
1672
|
+
timestamp: new Date().toISOString(),
|
|
1673
|
+
coverage: coverageConfidence,
|
|
1674
|
+
message,
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
console.error(chalk.red(`❌ ${message}`));
|
|
1678
|
+
process.exit(1);
|
|
1679
|
+
}
|
|
1050
1680
|
// Step E: Pass 2 - The Architect (generate plan with selected files)
|
|
1051
1681
|
console.log(chalk.dim('🤖 Generating plan with selected files...\n'));
|
|
1052
1682
|
const response = await client.generatePlan(enhancedIntent, filesToUse, finalProjectId, ticketMetadata, projectSummary);
|
|
@@ -1547,6 +2177,7 @@ async function planCommand(intent, options) {
|
|
|
1547
2177
|
timestamp: response.timestamp,
|
|
1548
2178
|
telemetry: response.telemetry,
|
|
1549
2179
|
snapshot: snapshotSummary,
|
|
2180
|
+
coverage: coverageConfidence,
|
|
1550
2181
|
message: missingPlanMessage,
|
|
1551
2182
|
plan: response.plan,
|
|
1552
2183
|
});
|
|
@@ -1595,10 +2226,14 @@ async function planCommand(intent, options) {
|
|
|
1595
2226
|
timestamp: response.timestamp,
|
|
1596
2227
|
telemetry: response.telemetry,
|
|
1597
2228
|
snapshot: snapshotSummary,
|
|
2229
|
+
coverage: coverageConfidence,
|
|
1598
2230
|
message: 'Plan generated and persisted',
|
|
1599
2231
|
plan: response.plan,
|
|
1600
2232
|
});
|
|
1601
2233
|
}
|
|
2234
|
+
planSuccessForSlo = true;
|
|
2235
|
+
flushPlanSloEvent(0);
|
|
2236
|
+
process.removeListener('exit', onPlanProcessExit);
|
|
1602
2237
|
}
|
|
1603
2238
|
catch (error) {
|
|
1604
2239
|
if (options.json) {
|
|
@@ -1610,6 +2245,7 @@ async function planCommand(intent, options) {
|
|
|
1610
2245
|
sessionId: null,
|
|
1611
2246
|
projectId: options.projectId || null,
|
|
1612
2247
|
timestamp: new Date().toISOString(),
|
|
2248
|
+
coverage: coverageConfidence,
|
|
1613
2249
|
message: error instanceof Error ? error.message : 'Unknown error',
|
|
1614
2250
|
});
|
|
1615
2251
|
}
|