@ngockhoale/ukit 3.0.3 → 3.0.5
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/CHANGELOG.md +26 -0
- package/bin/ukit +12 -4
- package/manifests/engineConformance.yaml +29 -0
- package/manifests/platform.full.yaml +13 -0
- package/package.json +2 -1
- package/scripts/audit/decision-coverage.mjs +295 -0
- package/scripts/bench/outline-savings.mjs +19 -4
- package/scripts/bench/parallel-agents.mjs +15 -4
- package/scripts/bench/runGold.mjs +22 -4
- package/scripts/bench/v3-ceremony.mjs +7 -1
- package/scripts/bug/triage.mjs +56 -17
- package/scripts/index/build-index.mjs +94 -28
- package/scripts/index/query-index.mjs +48 -14
- package/scripts/index/refresh-index.mjs +142 -62
- package/scripts/perf/audit-perf.mjs +8 -2
- package/scripts/skill/audit-skill.mjs +54 -25
- package/src/bug/triageBug.js +9 -6
- package/src/cli/adapters.js +6 -0
- package/src/cli/commands/code.js +7 -1
- package/src/cli/commands/indexArgs.js +4 -2
- package/src/cli/commands/indexTools.js +8 -1
- package/src/cli/commands/install.js +13 -0
- package/src/cli/commands/memory.js +13 -9
- package/src/cli/commands/status.js +17 -1
- package/src/cli/commands/update.js +7 -0
- package/src/context/detectProjectContext.js +3 -1
- package/src/core/codeintel/analogy.js +1 -1
- package/src/core/codeintel/diagnostics.js +9 -6
- package/src/core/codeintel/graph.js +14 -8
- package/src/core/codeintel/impact.js +0 -1
- package/src/core/codeintel/invalidation.js +5 -3
- package/src/core/codeintel/packet.js +11 -0
- package/src/core/codeintel/router.js +17 -3
- package/src/core/codeintel/semanticProvider.js +1 -1
- package/src/core/codeintel/summaries.js +11 -7
- package/src/core/compact/contextBudget.js +26 -12
- package/src/core/compact/index.js +15 -8
- package/src/core/docContracts.js +10 -2
- package/src/core/experiments/deliberation.js +321 -0
- package/src/core/experiments/dynamicWorkflow.js +492 -0
- package/src/core/fileOps.js +8 -1
- package/src/core/gatewayProbe.js +29 -1
- package/src/core/gatewayResilienceEnv.js +44 -3
- package/src/core/handoffDocValidator.js +3 -1
- package/src/core/hookChainDoctor.js +16 -1
- package/src/core/memory/deltaOverlays.js +448 -0
- package/src/core/memory/learningCandidates.js +302 -0
- package/src/core/memory/migrate.js +59 -32
- package/src/core/memory/recordStore.js +24 -1
- package/src/core/memory/store.js +44 -8
- package/src/core/memory/storeV2.js +48 -33
- package/src/core/memory/userMemory.js +26 -10
- package/src/core/output/index.js +15 -10
- package/src/core/permissionPolicy.js +8 -0
- package/src/core/runtimeConfig.js +224 -4
- package/src/core/sensitiveValueScanner.js +10 -2
- package/src/core/taskBudgetValidator.js +12 -17
- package/src/core/taskProgressGuard.js +59 -9
- package/src/core/unattendedDoctor.js +5 -2
- package/src/core/uninstall.js +37 -8
- package/src/decision/client.js +371 -0
- package/src/decision/lease.js +198 -0
- package/src/decision/preflight.js +492 -0
- package/src/decision/protocol.js +308 -0
- package/src/decision/registry.js +384 -0
- package/src/decision/shadow.js +281 -0
- package/src/decision/statePacket.js +165 -0
- package/src/diagnostics/failurePatterns.js +2 -1
- package/src/diagnostics/ledgerFiles.js +3 -1
- package/src/diagnostics/routeOutcomes.js +1 -29
- package/src/index/buildIndex.js +23 -11
- package/src/index/impactContext.js +21 -2
- package/src/index/importResolution.js +13 -7
- package/src/index/queryIndex.js +11 -5
- package/src/index/resolveContext.js +22 -9
- package/src/index/taskRouting.js +63 -2
- package/src/index/verificationPlan.js +12 -1
- package/src/learning/patternProposals.js +6 -0
- package/src/render/renderTemplate.js +1 -1
- package/src/skill/auditSkill.js +3 -1
- package/src/stack/detectStack.js +3 -1
- package/template_project/.claude/agents/handoff-planner.md +2 -5
- package/template_project/.claude/hooks/auto-allow-bash.sh +5 -0
- package/template_project/.claude/hooks/block-dangerous.mjs +11 -4
- package/template_project/.claude/hooks/context-hardcap-gate.sh +10 -1
- package/template_project/.claude/hooks/handoff-model-guard.sh +14 -4
- package/template_project/.claude/hooks/handoff-resume.sh +10 -1
- package/template_project/.claude/hooks/protect-files.sh +0 -1
- package/template_project/.claude/hooks/record-execution.mjs +13 -1
- package/template_project/.claude/hooks/sensitive-data-guard.mjs +80 -5
- package/template_project/.claude/hooks/session-episode.sh +9 -2
- package/template_project/.claude/skills/pdf-processing-pro/SKILL.md +1 -1
- package/template_project/.claude/ukit/index/handoff-doc-validator.mjs +3 -1
- package/template_project/.claude/ukit/index/lib/index-core.mjs +129 -42
- package/template_project/.claude/ukit/index/route-task.mjs +444 -0
- package/template_project/.claude/ukit/index/task-budget-validator.mjs +12 -16
- package/template_project/.claude/ukit/index/unic-decision.mjs +786 -0
- package/template_project/.claude/ukit/index/verify-context.mjs +11 -0
- package/template_project/.claude/ukit/runtime/execution-ledger.mjs +116 -9
- package/template_project/.claude/ukit/runtime/project-important.mjs +9 -7
- package/template_project/.claude/ukit/runtime/reinject-context.mjs +48 -0
- package/template_project/.claude/ukit/runtime/resumable-run.mjs +596 -0
- package/template_project/.claude/ukit/runtime/sensitive-value-scanner.mjs +4 -7
- package/template_project/.claude/ukit/runtime/stop-coordinator.mjs +1 -1
- package/template_project/docs/AI_HANDOFF/PLAN.md +7 -7
- package/template_project/docs/AI_HANDOFF/RULES.md +1 -1
- package/template_project/ukit/storage/config.json +34 -1
- package/template_project/.claude/ukit/skill-router-state.json +0 -1
- package/template_project/.ukit/storage/cache/hook-latency/unknown.jsonl +0 -4
|
@@ -64,11 +64,18 @@ if [ -e /dev/fd/0 ]; then
|
|
|
64
64
|
exec 8<&0
|
|
65
65
|
head -c 65537 <&8 > "$UKIT_INPUT_FILE" 2>/dev/null &
|
|
66
66
|
UKIT_HEAD_PID=$!
|
|
67
|
-
# UKIT_HOOK_STAGE_MS:-2000 — bounded wait for the staged payload
|
|
67
|
+
# UKIT_HOOK_STAGE_MS:-2000 — bounded wait for the staged payload (same derived
|
|
68
|
+
# budget every other hook uses; a hardcoded sleep ignored the operator knob).
|
|
69
|
+
__ukit_ep_stage_ms="${UKIT_HOOK_STAGE_MS:-2000}"
|
|
70
|
+
case "$__ukit_ep_stage_ms" in
|
|
71
|
+
''|*[!0-9]*) __ukit_ep_stage_ms=2000 ;;
|
|
72
|
+
esac
|
|
73
|
+
[ "$__ukit_ep_stage_ms" -gt 0 ] 2>/dev/null || __ukit_ep_stage_ms=2000
|
|
74
|
+
printf -v __ukit_ep_stage_s '%d.%03d' $((__ukit_ep_stage_ms / 1000)) $((__ukit_ep_stage_ms % 1000))
|
|
68
75
|
# TASK-002: detach the watchdog's inherited stdout/stderr — otherwise a
|
|
69
76
|
# captured-stdio caller (hook-chain-runner) sees the pipe held open ~2s
|
|
70
77
|
# after the script exits, inflating the SessionEnd residual to ~2000ms.
|
|
71
|
-
( sleep
|
|
78
|
+
( sleep "$__ukit_ep_stage_s"; kill "$UKIT_HEAD_PID" 2>/dev/null ) >/dev/null 2>&1 &
|
|
72
79
|
UKIT_WATCH_PID=$!
|
|
73
80
|
wait "$UKIT_HEAD_PID" 2>/dev/null
|
|
74
81
|
kill "$UKIT_WATCH_PID" 2>/dev/null
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: pdf-processing-pro
|
|
3
3
|
description: Production-ready PDF processing with forms, tables, OCR, validation, and batch operations. Use when working with complex PDF workflows in production environments, processing large volumes of PDFs, or requiring robust error handling and validation.
|
|
4
4
|
---
|
|
5
5
|
|
|
@@ -63,7 +63,9 @@ function planSectionPositions(sections) {
|
|
|
63
63
|
const positions = new Map();
|
|
64
64
|
for (const s of sections) {
|
|
65
65
|
for (const token of PLAN_SECTIONS) {
|
|
66
|
-
|
|
66
|
+
// `## §10`/`## §12` must not satisfy `§1` — the token only matches when it
|
|
67
|
+
// is the whole heading or is followed by a non-digit boundary (space, colon…).
|
|
68
|
+
if (!positions.has(token) && new RegExp(`^${token}(?!\\d)`).test(s.name)) positions.set(token, s.pos);
|
|
67
69
|
}
|
|
68
70
|
}
|
|
69
71
|
return positions;
|
|
@@ -308,7 +308,10 @@ export async function buildCodeIndex({ rootDir = process.cwd(), changedFiles = n
|
|
|
308
308
|
]);
|
|
309
309
|
canReuseParsedArtifacts = previousSymbolsArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
310
310
|
&& previousImportsArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
311
|
-
&& previousCallsArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
311
|
+
&& previousCallsArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
312
|
+
&& Array.isArray(previousSymbolsArtifact.items)
|
|
313
|
+
&& Array.isArray(previousImportsArtifact.items)
|
|
314
|
+
&& Array.isArray(previousCallsArtifact.items);
|
|
312
315
|
|
|
313
316
|
if (canReuseParsedArtifacts) {
|
|
314
317
|
const previousSymbolsByPath = groupBy(previousSymbolsArtifact.items ?? [], (item) => item.filePath);
|
|
@@ -363,7 +366,8 @@ export async function buildCodeIndex({ rootDir = process.cwd(), changedFiles = n
|
|
|
363
366
|
? await readArtifactIfExists(absoluteRoot, INDEX_ARTIFACTS.testsMap)
|
|
364
367
|
: null;
|
|
365
368
|
const canReuseTestsMap = canReuseTestsMapCandidate
|
|
366
|
-
&& previousTestsMapArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
369
|
+
&& previousTestsMapArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
370
|
+
&& Array.isArray(previousTestsMapArtifact.items);
|
|
367
371
|
const testsMap = canReuseTestsMap
|
|
368
372
|
? previousTestsMapArtifact.items
|
|
369
373
|
: buildTestsMap(fileRecords);
|
|
@@ -372,6 +376,7 @@ export async function buildCodeIndex({ rootDir = process.cwd(), changedFiles = n
|
|
|
372
376
|
? await readArtifactIfExists(absoluteRoot, INDEX_ARTIFACTS.hotspots)
|
|
373
377
|
: null;
|
|
374
378
|
const canReuseHotspots = previousHotspotsArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
379
|
+
&& Array.isArray(previousHotspotsArtifact.items)
|
|
375
380
|
&& areBugIndexSnapshotsEqual(previousHotspotsArtifact?.sourceSnapshot, bugIndexSnapshot);
|
|
376
381
|
const hotspots = canReuseHotspots
|
|
377
382
|
? previousHotspotsArtifact.items
|
|
@@ -386,8 +391,9 @@ export async function buildCodeIndex({ rootDir = process.cwd(), changedFiles = n
|
|
|
386
391
|
: null;
|
|
387
392
|
const canReuseArchetypes = canReuseArchetypesCandidate
|
|
388
393
|
&& previousArchetypesArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
389
|
-
&& previousArchetypesArtifact
|
|
390
|
-
&&
|
|
394
|
+
&& Array.isArray(previousArchetypesArtifact.items)
|
|
395
|
+
&& previousArchetypesArtifact.items.length === codeFiles.length
|
|
396
|
+
&& previousArchetypesArtifact.items.every((item) => currentCodePaths.has(item.filePath));
|
|
391
397
|
|
|
392
398
|
// Reuse cached archetypes only when every indexed code file is unchanged
|
|
393
399
|
const archetypes = canReuseArchetypes
|
|
@@ -413,6 +419,7 @@ export async function buildCodeIndex({ rootDir = process.cwd(), changedFiles = n
|
|
|
413
419
|
};
|
|
414
420
|
const canReuseRelations = canReuseGraphArtifactsCandidate
|
|
415
421
|
&& previousRelationsArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
422
|
+
&& Array.isArray(previousRelationsArtifact.items)
|
|
416
423
|
&& areStringArraysEqual(previousRelationsArtifact?.sourceSnapshot?.styleFiles, styleFilePaths)
|
|
417
424
|
&& arePathSnapshotsEqual(
|
|
418
425
|
previousRelationsArtifact?.sourceSnapshot?.importAliasSnapshot,
|
|
@@ -420,6 +427,7 @@ export async function buildCodeIndex({ rootDir = process.cwd(), changedFiles = n
|
|
|
420
427
|
);
|
|
421
428
|
const canReuseAnalogs = canReuseGraphArtifactsCandidate
|
|
422
429
|
&& previousAnalogsArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
430
|
+
&& Array.isArray(previousAnalogsArtifact.items)
|
|
423
431
|
&& areStringArraysEqual(previousAnalogsArtifact?.sourceSnapshot?.styleFiles, styleFilePaths)
|
|
424
432
|
&& arePathSnapshotsEqual(
|
|
425
433
|
previousAnalogsArtifact?.sourceSnapshot?.importAliasSnapshot,
|
|
@@ -943,13 +951,15 @@ async function tryIncrementalIndexUpdate({ absoluteRoot, indexDir, changedPaths,
|
|
|
943
951
|
|
|
944
952
|
// Derived artifacts are recomputed in memory from the merged inputs — the
|
|
945
953
|
// expensive work being avoided is discovery and re-parsing, not these pure
|
|
946
|
-
// transformations, so their output stays identical to a clean rebuild.
|
|
947
954
|
const canReuseTestsMap = haveStableTrackedFileIdentities(filesArtifact.items, mergedFileRecords)
|
|
948
|
-
&& testsMapArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
955
|
+
&& testsMapArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
956
|
+
&& Array.isArray(testsMapArtifact.items);
|
|
949
957
|
const testsMap = canReuseTestsMap ? testsMapArtifact.items : buildTestsMap(mergedFileRecords);
|
|
950
958
|
|
|
951
959
|
const bugIndexSnapshot = await readBugIndexSnapshot(absoluteRoot);
|
|
960
|
+
|
|
952
961
|
const canReuseHotspots = hotspotsArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
962
|
+
&& Array.isArray(hotspotsArtifact.items)
|
|
953
963
|
&& areBugIndexSnapshotsEqual(hotspotsArtifact?.sourceSnapshot, bugIndexSnapshot);
|
|
954
964
|
const hotspots = canReuseHotspots ? hotspotsArtifact.items : await buildHotspots(absoluteRoot, bugIndexSnapshot);
|
|
955
965
|
|
|
@@ -961,9 +971,10 @@ async function tryIncrementalIndexUpdate({ absoluteRoot, indexDir, changedPaths,
|
|
|
961
971
|
const canReuseArchetypes = parsedFiles.length === 0
|
|
962
972
|
&& !recordsChanged
|
|
963
973
|
&& archetypesArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
974
|
+
&& Array.isArray(archetypesArtifact.items)
|
|
964
975
|
&& codeFiles.every((file) => previousCodePaths.has(file.filePath))
|
|
965
|
-
&& archetypesArtifact
|
|
966
|
-
&&
|
|
976
|
+
&& archetypesArtifact.items.length === archetypes.length
|
|
977
|
+
&& archetypesArtifact.items.every((item) => currentCodePaths.has(item?.filePath));
|
|
967
978
|
const archetypeItems = canReuseArchetypes ? archetypesArtifact.items : archetypes;
|
|
968
979
|
|
|
969
980
|
const needsAliasContext = importsNeedAliasContext(imports);
|
|
@@ -981,7 +992,8 @@ async function tryIncrementalIndexUpdate({ absoluteRoot, indexDir, changedPaths,
|
|
|
981
992
|
&& areStringArraysEqual(relationsArtifact?.sourceSnapshot?.styleFiles, styleFilePaths)
|
|
982
993
|
&& arePathSnapshotsEqual(relationsArtifact?.sourceSnapshot?.importAliasSnapshot, styleSnapshot.importAliasSnapshot);
|
|
983
994
|
const canReuseAnalogs = canReuseRelations
|
|
984
|
-
&& analogsArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
995
|
+
&& analogsArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
996
|
+
&& Array.isArray(analogsArtifact.items);
|
|
985
997
|
|
|
986
998
|
let relations = relationsArtifact.items;
|
|
987
999
|
let analogs = analogsArtifact?.items ?? null;
|
|
@@ -1163,11 +1175,11 @@ export async function getFileOutline({ rootDir = process.cwd(), filePath, limit
|
|
|
1163
1175
|
}
|
|
1164
1176
|
|
|
1165
1177
|
export async function queryCodeIndex({ rootDir = process.cwd(), query, limit = 5 } = {}) {
|
|
1166
|
-
|
|
1178
|
+
const normalizedQuery = String(query ?? '').trim();
|
|
1179
|
+
if (!normalizedQuery) return [];
|
|
1167
1180
|
if (limit <= 0) return Object.freeze([]);
|
|
1168
1181
|
|
|
1169
1182
|
const absoluteRoot = path.resolve(rootDir);
|
|
1170
|
-
const normalizedQuery = String(query).trim();
|
|
1171
1183
|
const queryDescriptor = buildSearchDescriptor(normalizedQuery, { expandVietnameseAliases: true });
|
|
1172
1184
|
if (queryDescriptor.tokens.size === 0) return Object.freeze([]);
|
|
1173
1185
|
const queryCacheKey = JSON.stringify({
|
|
@@ -1247,12 +1259,11 @@ export async function queryCodeIndex({ rootDir = process.cwd(), query, limit = 5
|
|
|
1247
1259
|
}
|
|
1248
1260
|
|
|
1249
1261
|
// ── Analog Query ──
|
|
1250
|
-
|
|
1251
1262
|
export async function queryAnalog({ rootDir = process.cwd(), filePath, limit = 5 } = {}) {
|
|
1252
|
-
|
|
1263
|
+
const normalizedFilePath = String(filePath ?? '').trim();
|
|
1264
|
+
if (!normalizedFilePath) return [];
|
|
1253
1265
|
|
|
1254
1266
|
const absoluteRoot = path.resolve(rootDir);
|
|
1255
|
-
const normalizedFilePath = String(filePath).trim();
|
|
1256
1267
|
const analogCacheKey = JSON.stringify({
|
|
1257
1268
|
rootDir: absoluteRoot,
|
|
1258
1269
|
filePath: normalizedFilePath,
|
|
@@ -1325,9 +1336,14 @@ function freezeQueryResults(results) {
|
|
|
1325
1336
|
function freezeAnalogResults(results) {
|
|
1326
1337
|
return Object.freeze(results.map((item) => Object.freeze({ ...item })));
|
|
1327
1338
|
}
|
|
1328
|
-
|
|
1329
1339
|
function ensureArtifact(artifact) {
|
|
1330
|
-
|
|
1340
|
+
if (!artifact || typeof artifact !== 'object') {
|
|
1341
|
+
return { items: [] };
|
|
1342
|
+
}
|
|
1343
|
+
// A schema-current artifact whose `items` is not an array (truncated or
|
|
1344
|
+
// hand-corrupted JSON) must degrade to an empty item list — callers iterate
|
|
1345
|
+
// `items` unconditionally and a non-array value crashes the query path.
|
|
1346
|
+
return Array.isArray(artifact.items) ? artifact : { ...artifact, items: [] };
|
|
1331
1347
|
}
|
|
1332
1348
|
|
|
1333
1349
|
function escapeJsonString(value) {
|
|
@@ -1343,11 +1359,10 @@ const TASK_TYPE_BUDGETS = {
|
|
|
1343
1359
|
'non-trivial': { minFiles: 4, maxFiles: 8 },
|
|
1344
1360
|
};
|
|
1345
1361
|
|
|
1346
|
-
const TRIVIAL_SIGNALS = ['typo', 'label', 'text', 'rename', 'color', 'spacing', 'toggle', 'config', 'comment'];
|
|
1347
1362
|
const RISKY_SIGNALS = [
|
|
1348
|
-
'auth', '
|
|
1349
|
-
'
|
|
1350
|
-
'core', 'shared', 'runtime',
|
|
1363
|
+
'auth', 'authentication', 'authorization', 'security', 'migration', 'uninstall',
|
|
1364
|
+
'password', 'token', 'permission', 'delete all', 'drop table', 'race', 'flaky',
|
|
1365
|
+
'flakiness', 'intermittent', 'timeout', 'deadlock', 'core', 'shared', 'runtime',
|
|
1351
1366
|
];
|
|
1352
1367
|
const EXPANSION_SIGNALS = [
|
|
1353
1368
|
'similar',
|
|
@@ -1368,10 +1383,23 @@ const EXPANSION_SIGNALS = [
|
|
|
1368
1383
|
'project',
|
|
1369
1384
|
'repo',
|
|
1370
1385
|
'workspace',
|
|
1371
|
-
'all
|
|
1372
|
-
'every
|
|
1386
|
+
'all',
|
|
1387
|
+
'every',
|
|
1373
1388
|
];
|
|
1374
1389
|
|
|
1390
|
+
// Signals must match whole words: bare `includes` let 'score' fire 'core',
|
|
1391
|
+
// 'author' fire 'auth', 'context' fire 'text', and 'install ' fire 'all '.
|
|
1392
|
+
// Common inflections (plural/past/-ing) still count — 'timeouts', 'migrations',
|
|
1393
|
+
// 'deadlocked' — but 'authentication'/'authorization'/'flakiness' need explicit
|
|
1394
|
+
// entries since their stems diverge from the base signal.
|
|
1395
|
+
function escapeSignalRegExp(signal) {
|
|
1396
|
+
return signal.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
function containsSignalWord(lower, signals) {
|
|
1400
|
+
return signals.some((signal) => new RegExp(`\\b${escapeSignalRegExp(signal)}(?:s|es|ed|d|ing)?\\b`).test(lower));
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1375
1403
|
export async function resolveContext({
|
|
1376
1404
|
rootDir = process.cwd(),
|
|
1377
1405
|
intent = '',
|
|
@@ -1506,12 +1534,12 @@ export async function resolveContext({
|
|
|
1506
1534
|
export function classifyTask(intent) {
|
|
1507
1535
|
const lower = intent.toLowerCase();
|
|
1508
1536
|
|
|
1509
|
-
if (
|
|
1537
|
+
if (containsSignalWord(lower, RISKY_SIGNALS)) {
|
|
1510
1538
|
return 'non-trivial';
|
|
1511
1539
|
}
|
|
1512
1540
|
|
|
1513
|
-
if (
|
|
1514
|
-
if (lower
|
|
1541
|
+
if (containsSignalWord(lower, TRIVIAL_SIGNALS)) {
|
|
1542
|
+
if (/\ball\b/.test(lower) || /\bevery\b/.test(lower) || /\bmultiple\b/.test(lower)) {
|
|
1515
1543
|
return 'simple';
|
|
1516
1544
|
}
|
|
1517
1545
|
return 'trivial';
|
|
@@ -1535,7 +1563,7 @@ function shouldQueryIndex({
|
|
|
1535
1563
|
if (taskType === 'non-trivial') {
|
|
1536
1564
|
return true;
|
|
1537
1565
|
}
|
|
1538
|
-
return
|
|
1566
|
+
return containsSignalWord(normalizedIntent, EXPANSION_SIGNALS);
|
|
1539
1567
|
}
|
|
1540
1568
|
|
|
1541
1569
|
function enforceContextBudget(result, maxFiles) {
|
|
@@ -2628,7 +2656,7 @@ function extractImports(filePath, content) {
|
|
|
2628
2656
|
const stripped = content
|
|
2629
2657
|
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
2630
2658
|
.replace(/\/\/.*$/gm, '');
|
|
2631
|
-
for (const match of stripped.matchAll(/import\s
|
|
2659
|
+
for (const match of stripped.matchAll(/(?:^|[;\n])\s*import\s*(?:[^'";]+?\s*from\s*)?['"]([^'"]+)['"]/g)) {
|
|
2632
2660
|
out.push({ from: filePath, to: match[1], kind: 'import' });
|
|
2633
2661
|
}
|
|
2634
2662
|
for (const match of stripped.matchAll(/import\(\s*['"]([^'"]+)['"]\s*\)/g)) {
|
|
@@ -2637,10 +2665,10 @@ function extractImports(filePath, content) {
|
|
|
2637
2665
|
for (const match of stripped.matchAll(/require\(\s*['"]([^'"]+)['"]\s*\)/g)) {
|
|
2638
2666
|
out.push({ from: filePath, to: match[1], kind: 'require' });
|
|
2639
2667
|
}
|
|
2640
|
-
for (const match of stripped.matchAll(/export\s+(?:type\s+)?\{[^}]+\}\s
|
|
2668
|
+
for (const match of stripped.matchAll(/(?:^|[;\n])\s*export\s+(?:type\s+)?\{[^}]+\}\s*from\s*['"]([^'"]+)['"]/g)) {
|
|
2641
2669
|
out.push({ from: filePath, to: match[1], kind: 're-export' });
|
|
2642
2670
|
}
|
|
2643
|
-
for (const match of stripped.matchAll(/export\s+\*(?:\s+as\s+[A-Za-z_$][A-Za-z0-9_$]*)?\s
|
|
2671
|
+
for (const match of stripped.matchAll(/(?:^|[;\n])\s*export\s+\*(?:\s+as\s+[A-Za-z_$][A-Za-z0-9_$]*)?\s*from\s*['"]([^'"]+)['"]/g)) {
|
|
2644
2672
|
out.push({ from: filePath, to: match[1], kind: 're-export-all' });
|
|
2645
2673
|
}
|
|
2646
2674
|
return out;
|
|
@@ -3533,9 +3561,9 @@ async function loadAliasConfig(configPath, visited = new Set(), tracker = null)
|
|
|
3533
3561
|
|
|
3534
3562
|
let inherited = null;
|
|
3535
3563
|
const extendsValue = typeof parsed?.extends === 'string' ? parsed.extends.trim() : '';
|
|
3536
|
-
if (extendsValue
|
|
3537
|
-
const inheritedPath = resolveExtendedConfigPath(configDir, extendsValue);
|
|
3538
|
-
inherited = await loadAliasConfig(inheritedPath, visited, tracker);
|
|
3564
|
+
if (extendsValue) {
|
|
3565
|
+
const inheritedPath = await resolveExtendedConfigPath(configDir, extendsValue);
|
|
3566
|
+
inherited = inheritedPath ? await loadAliasConfig(inheritedPath, visited, tracker) : null;
|
|
3539
3567
|
}
|
|
3540
3568
|
|
|
3541
3569
|
const inheritedRules = inherited?.pathRules ?? [];
|
|
@@ -3573,12 +3601,54 @@ function buildBaseUrlDirs({ rootDir, compilerOptions }) {
|
|
|
3573
3601
|
if (!baseUrl) return [];
|
|
3574
3602
|
return [path.resolve(rootDir, baseUrl)];
|
|
3575
3603
|
}
|
|
3604
|
+
async function resolveExtendedConfigPath(configDir, extendsValue) {
|
|
3605
|
+
if (extendsValue.startsWith('.') || extendsValue.startsWith('/')) {
|
|
3606
|
+
const withExtension = extendsValue.endsWith('.json')
|
|
3607
|
+
? extendsValue
|
|
3608
|
+
: `${extendsValue}.json`;
|
|
3609
|
+
return path.resolve(configDir, withExtension);
|
|
3610
|
+
}
|
|
3611
|
+
|
|
3612
|
+
// Package specifiers split on the RAW value: appending `.json` first would
|
|
3613
|
+
// turn a bare `extends: "acme-base"` into the package name `acme-base.json`,
|
|
3614
|
+
// which never exists under node_modules.
|
|
3615
|
+
const parts = extendsValue.split('/').filter(Boolean);
|
|
3616
|
+
if (parts.length === 0) {
|
|
3617
|
+
return null;
|
|
3618
|
+
}
|
|
3576
3619
|
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3620
|
+
const packageName = extendsValue.startsWith('@')
|
|
3621
|
+
? parts.slice(0, 2).join('/')
|
|
3622
|
+
: parts[0];
|
|
3623
|
+
const packageRest = parts.slice(packageName.startsWith('@') ? 2 : 1).join('/');
|
|
3624
|
+
const packageRoot = await findNodeModulePackageRoot(configDir, packageName);
|
|
3625
|
+
if (!packageRoot) {
|
|
3626
|
+
return null;
|
|
3627
|
+
}
|
|
3628
|
+
|
|
3629
|
+
const rest = packageRest && !packageRest.endsWith('.json')
|
|
3630
|
+
? `${packageRest}.json`
|
|
3631
|
+
: packageRest;
|
|
3632
|
+
return path.join(packageRoot, rest || 'tsconfig.json');
|
|
3633
|
+
}
|
|
3634
|
+
|
|
3635
|
+
async function findNodeModulePackageRoot(startDir, packageName) {
|
|
3636
|
+
let currentDir = path.resolve(startDir);
|
|
3637
|
+
while (true) {
|
|
3638
|
+
const candidate = path.join(currentDir, 'node_modules', packageName);
|
|
3639
|
+
try {
|
|
3640
|
+
const stat = await fs.stat(candidate);
|
|
3641
|
+
if (stat.isDirectory()) {
|
|
3642
|
+
return candidate;
|
|
3643
|
+
}
|
|
3644
|
+
} catch {}
|
|
3645
|
+
|
|
3646
|
+
const parentDir = path.dirname(currentDir);
|
|
3647
|
+
if (parentDir === currentDir) {
|
|
3648
|
+
return null;
|
|
3649
|
+
}
|
|
3650
|
+
currentDir = parentDir;
|
|
3651
|
+
}
|
|
3582
3652
|
}
|
|
3583
3653
|
|
|
3584
3654
|
async function isAliasSnapshotValid(snapshot = []) {
|
|
@@ -4187,9 +4257,10 @@ export async function resolveImpactContext({
|
|
|
4187
4257
|
const normalizedChangedFiles = uniqueImpact(changedFiles.map(normalizeImpactPath));
|
|
4188
4258
|
const normalizedChangedSymbols = uniqueImpact(changedSymbols.map((symbol) => String(symbol ?? '').trim()));
|
|
4189
4259
|
|
|
4190
|
-
const [callsArtifact, importsArtifact, relatedArtifacts] = await Promise.all([
|
|
4260
|
+
const [callsArtifact, importsArtifact, filesArtifact, relatedArtifacts] = await Promise.all([
|
|
4191
4261
|
readArtifact(absoluteRoot, INDEX_ARTIFACTS.calls),
|
|
4192
4262
|
readArtifact(absoluteRoot, INDEX_ARTIFACTS.imports),
|
|
4263
|
+
readArtifact(absoluteRoot, INDEX_ARTIFACTS.files),
|
|
4193
4264
|
loadRelatedTestArtifacts({ rootDir: absoluteRoot }),
|
|
4194
4265
|
]);
|
|
4195
4266
|
|
|
@@ -4221,23 +4292,39 @@ export async function resolveImpactContext({
|
|
|
4221
4292
|
for (const record of callItems) {
|
|
4222
4293
|
const matchedSymbol = (record.calls ?? []).find((name) => callerTargets.has(name));
|
|
4223
4294
|
if (!matchedSymbol) continue;
|
|
4224
|
-
if (changedFileSet.has(normalizeImpactPath(record.filePath))
|
|
4295
|
+
if (changedFileSet.has(normalizeImpactPath(record.filePath))) continue;
|
|
4225
4296
|
callers.push({ filePath: record.filePath, symbol: record.symbol, reason: `calls ${matchedSymbol}` });
|
|
4226
4297
|
}
|
|
4227
4298
|
}
|
|
4228
4299
|
|
|
4229
4300
|
const importers = [];
|
|
4230
4301
|
const dependencies = [];
|
|
4302
|
+
// Non-relative specifiers (aliases like `@/x`, `~/x`, tsconfig paths) resolve
|
|
4303
|
+
// through the same machinery the index builder uses; relative specifiers keep
|
|
4304
|
+
// the candidate-list match so a changed file absent from files.json (e.g. a
|
|
4305
|
+
// brand-new file) still counts.
|
|
4306
|
+
const indexedFileSet = new Set((filesArtifact.items ?? []).map((item) => item.filePath));
|
|
4307
|
+
const importAliasContext = importsNeedAliasContext(importItems)
|
|
4308
|
+
? await loadImportAliasContext({ rootDir: absoluteRoot }).catch(() => null)
|
|
4309
|
+
: null;
|
|
4231
4310
|
for (const imp of importItems) {
|
|
4232
4311
|
const from = normalizeImpactPath(imp.from);
|
|
4233
|
-
const
|
|
4234
|
-
|
|
4312
|
+
const specifier = String(imp.to ?? '').trim();
|
|
4313
|
+
const targets = specifier.startsWith('.')
|
|
4314
|
+
? resolveImpactImportTargets(from, specifier)
|
|
4315
|
+
: [resolveImportSpecifier({
|
|
4316
|
+
rootDir: absoluteRoot,
|
|
4317
|
+
fromFilePath: from,
|
|
4318
|
+
specifier,
|
|
4319
|
+
indexedFileSet,
|
|
4320
|
+
aliasContext: importAliasContext,
|
|
4321
|
+
})].filter(Boolean);
|
|
4322
|
+
const matched = targets.find((candidate) => changedFileSet.has(normalizeImpactPath(candidate)));
|
|
4235
4323
|
if (matched) {
|
|
4236
4324
|
importers.push(from);
|
|
4237
4325
|
dependencies.push(matched);
|
|
4238
4326
|
}
|
|
4239
4327
|
}
|
|
4240
|
-
|
|
4241
4328
|
const candidateFiles = uniqueImpact([
|
|
4242
4329
|
...normalizedChangedFiles,
|
|
4243
4330
|
...importers,
|