@ngockhoale/ukit 3.0.2 → 3.0.4
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 +21 -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 +10 -3
- 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 +11 -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 +16 -6
- 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 +71 -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 +123 -39
- 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
|
@@ -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
|
|
|
@@ -957,13 +967,13 @@ async function tryIncrementalIndexUpdate({ absoluteRoot, indexDir, changedPaths,
|
|
|
957
967
|
const previousCodePaths = new Set(
|
|
958
968
|
filesArtifact.items.filter((item) => CODE_EXTENSIONS.has(item?.ext)).map((item) => item.filePath),
|
|
959
969
|
);
|
|
960
|
-
const currentCodePaths = new Set(codeFiles.map((file) => file.filePath));
|
|
961
970
|
const canReuseArchetypes = parsedFiles.length === 0
|
|
962
971
|
&& !recordsChanged
|
|
963
972
|
&& archetypesArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
973
|
+
&& Array.isArray(archetypesArtifact.items)
|
|
964
974
|
&& codeFiles.every((file) => previousCodePaths.has(file.filePath))
|
|
965
|
-
&& archetypesArtifact
|
|
966
|
-
&&
|
|
975
|
+
&& archetypesArtifact.items.length === archetypes.length
|
|
976
|
+
&& archetypesArtifact.items.every((item) => currentCodePaths.has(item?.filePath));
|
|
967
977
|
const archetypeItems = canReuseArchetypes ? archetypesArtifact.items : archetypes;
|
|
968
978
|
|
|
969
979
|
const needsAliasContext = importsNeedAliasContext(imports);
|
|
@@ -981,7 +991,8 @@ async function tryIncrementalIndexUpdate({ absoluteRoot, indexDir, changedPaths,
|
|
|
981
991
|
&& areStringArraysEqual(relationsArtifact?.sourceSnapshot?.styleFiles, styleFilePaths)
|
|
982
992
|
&& arePathSnapshotsEqual(relationsArtifact?.sourceSnapshot?.importAliasSnapshot, styleSnapshot.importAliasSnapshot);
|
|
983
993
|
const canReuseAnalogs = canReuseRelations
|
|
984
|
-
&& analogsArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
994
|
+
&& analogsArtifact?.schemaVersion === INDEX_SCHEMA_VERSION
|
|
995
|
+
&& Array.isArray(analogsArtifact.items);
|
|
985
996
|
|
|
986
997
|
let relations = relationsArtifact.items;
|
|
987
998
|
let analogs = analogsArtifact?.items ?? null;
|
|
@@ -1163,11 +1174,11 @@ export async function getFileOutline({ rootDir = process.cwd(), filePath, limit
|
|
|
1163
1174
|
}
|
|
1164
1175
|
|
|
1165
1176
|
export async function queryCodeIndex({ rootDir = process.cwd(), query, limit = 5 } = {}) {
|
|
1166
|
-
|
|
1177
|
+
const normalizedQuery = String(query ?? '').trim();
|
|
1178
|
+
if (!normalizedQuery) return [];
|
|
1167
1179
|
if (limit <= 0) return Object.freeze([]);
|
|
1168
1180
|
|
|
1169
1181
|
const absoluteRoot = path.resolve(rootDir);
|
|
1170
|
-
const normalizedQuery = String(query).trim();
|
|
1171
1182
|
const queryDescriptor = buildSearchDescriptor(normalizedQuery, { expandVietnameseAliases: true });
|
|
1172
1183
|
if (queryDescriptor.tokens.size === 0) return Object.freeze([]);
|
|
1173
1184
|
const queryCacheKey = JSON.stringify({
|
|
@@ -1247,12 +1258,11 @@ export async function queryCodeIndex({ rootDir = process.cwd(), query, limit = 5
|
|
|
1247
1258
|
}
|
|
1248
1259
|
|
|
1249
1260
|
// ── Analog Query ──
|
|
1250
|
-
|
|
1251
1261
|
export async function queryAnalog({ rootDir = process.cwd(), filePath, limit = 5 } = {}) {
|
|
1252
|
-
|
|
1262
|
+
const normalizedFilePath = String(filePath ?? '').trim();
|
|
1263
|
+
if (!normalizedFilePath) return [];
|
|
1253
1264
|
|
|
1254
1265
|
const absoluteRoot = path.resolve(rootDir);
|
|
1255
|
-
const normalizedFilePath = String(filePath).trim();
|
|
1256
1266
|
const analogCacheKey = JSON.stringify({
|
|
1257
1267
|
rootDir: absoluteRoot,
|
|
1258
1268
|
filePath: normalizedFilePath,
|
|
@@ -1325,9 +1335,14 @@ function freezeQueryResults(results) {
|
|
|
1325
1335
|
function freezeAnalogResults(results) {
|
|
1326
1336
|
return Object.freeze(results.map((item) => Object.freeze({ ...item })));
|
|
1327
1337
|
}
|
|
1328
|
-
|
|
1329
1338
|
function ensureArtifact(artifact) {
|
|
1330
|
-
|
|
1339
|
+
if (!artifact || typeof artifact !== 'object') {
|
|
1340
|
+
return { items: [] };
|
|
1341
|
+
}
|
|
1342
|
+
// A schema-current artifact whose `items` is not an array (truncated or
|
|
1343
|
+
// hand-corrupted JSON) must degrade to an empty item list — callers iterate
|
|
1344
|
+
// `items` unconditionally and a non-array value crashes the query path.
|
|
1345
|
+
return Array.isArray(artifact.items) ? artifact : { ...artifact, items: [] };
|
|
1331
1346
|
}
|
|
1332
1347
|
|
|
1333
1348
|
function escapeJsonString(value) {
|
|
@@ -1368,10 +1383,20 @@ 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
|
+
function escapeSignalRegExp(signal) {
|
|
1393
|
+
return signal.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
function containsSignalWord(lower, signals) {
|
|
1397
|
+
return signals.some((signal) => new RegExp(`\\b${escapeSignalRegExp(signal)}\\b`).test(lower));
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1375
1400
|
export async function resolveContext({
|
|
1376
1401
|
rootDir = process.cwd(),
|
|
1377
1402
|
intent = '',
|
|
@@ -1506,12 +1531,12 @@ export async function resolveContext({
|
|
|
1506
1531
|
export function classifyTask(intent) {
|
|
1507
1532
|
const lower = intent.toLowerCase();
|
|
1508
1533
|
|
|
1509
|
-
if (
|
|
1534
|
+
if (containsSignalWord(lower, RISKY_SIGNALS)) {
|
|
1510
1535
|
return 'non-trivial';
|
|
1511
1536
|
}
|
|
1512
1537
|
|
|
1513
|
-
if (
|
|
1514
|
-
if (lower
|
|
1538
|
+
if (containsSignalWord(lower, TRIVIAL_SIGNALS)) {
|
|
1539
|
+
if (/\ball\b/.test(lower) || /\bevery\b/.test(lower) || /\bmultiple\b/.test(lower)) {
|
|
1515
1540
|
return 'simple';
|
|
1516
1541
|
}
|
|
1517
1542
|
return 'trivial';
|
|
@@ -1535,7 +1560,7 @@ function shouldQueryIndex({
|
|
|
1535
1560
|
if (taskType === 'non-trivial') {
|
|
1536
1561
|
return true;
|
|
1537
1562
|
}
|
|
1538
|
-
return
|
|
1563
|
+
return containsSignalWord(normalizedIntent, EXPANSION_SIGNALS);
|
|
1539
1564
|
}
|
|
1540
1565
|
|
|
1541
1566
|
function enforceContextBudget(result, maxFiles) {
|
|
@@ -2628,7 +2653,7 @@ function extractImports(filePath, content) {
|
|
|
2628
2653
|
const stripped = content
|
|
2629
2654
|
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
2630
2655
|
.replace(/\/\/.*$/gm, '');
|
|
2631
|
-
for (const match of stripped.matchAll(/import\s
|
|
2656
|
+
for (const match of stripped.matchAll(/(?:^|[;\n])\s*import\s*(?:[^'";]+?\s*from\s*)?['"]([^'"]+)['"]/g)) {
|
|
2632
2657
|
out.push({ from: filePath, to: match[1], kind: 'import' });
|
|
2633
2658
|
}
|
|
2634
2659
|
for (const match of stripped.matchAll(/import\(\s*['"]([^'"]+)['"]\s*\)/g)) {
|
|
@@ -2637,10 +2662,10 @@ function extractImports(filePath, content) {
|
|
|
2637
2662
|
for (const match of stripped.matchAll(/require\(\s*['"]([^'"]+)['"]\s*\)/g)) {
|
|
2638
2663
|
out.push({ from: filePath, to: match[1], kind: 'require' });
|
|
2639
2664
|
}
|
|
2640
|
-
for (const match of stripped.matchAll(/export\s+(?:type\s+)?\{[^}]+\}\s
|
|
2665
|
+
for (const match of stripped.matchAll(/(?:^|[;\n])\s*export\s+(?:type\s+)?\{[^}]+\}\s*from\s*['"]([^'"]+)['"]/g)) {
|
|
2641
2666
|
out.push({ from: filePath, to: match[1], kind: 're-export' });
|
|
2642
2667
|
}
|
|
2643
|
-
for (const match of stripped.matchAll(/export\s+\*(?:\s+as\s+[A-Za-z_$][A-Za-z0-9_$]*)?\s
|
|
2668
|
+
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
2669
|
out.push({ from: filePath, to: match[1], kind: 're-export-all' });
|
|
2645
2670
|
}
|
|
2646
2671
|
return out;
|
|
@@ -3533,9 +3558,9 @@ async function loadAliasConfig(configPath, visited = new Set(), tracker = null)
|
|
|
3533
3558
|
|
|
3534
3559
|
let inherited = null;
|
|
3535
3560
|
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);
|
|
3561
|
+
if (extendsValue) {
|
|
3562
|
+
const inheritedPath = await resolveExtendedConfigPath(configDir, extendsValue);
|
|
3563
|
+
inherited = inheritedPath ? await loadAliasConfig(inheritedPath, visited, tracker) : null;
|
|
3539
3564
|
}
|
|
3540
3565
|
|
|
3541
3566
|
const inheritedRules = inherited?.pathRules ?? [];
|
|
@@ -3573,12 +3598,54 @@ function buildBaseUrlDirs({ rootDir, compilerOptions }) {
|
|
|
3573
3598
|
if (!baseUrl) return [];
|
|
3574
3599
|
return [path.resolve(rootDir, baseUrl)];
|
|
3575
3600
|
}
|
|
3601
|
+
async function resolveExtendedConfigPath(configDir, extendsValue) {
|
|
3602
|
+
if (extendsValue.startsWith('.') || extendsValue.startsWith('/')) {
|
|
3603
|
+
const withExtension = extendsValue.endsWith('.json')
|
|
3604
|
+
? extendsValue
|
|
3605
|
+
: `${extendsValue}.json`;
|
|
3606
|
+
return path.resolve(configDir, withExtension);
|
|
3607
|
+
}
|
|
3608
|
+
|
|
3609
|
+
// Package specifiers split on the RAW value: appending `.json` first would
|
|
3610
|
+
// turn a bare `extends: "acme-base"` into the package name `acme-base.json`,
|
|
3611
|
+
// which never exists under node_modules.
|
|
3612
|
+
const parts = extendsValue.split('/').filter(Boolean);
|
|
3613
|
+
if (parts.length === 0) {
|
|
3614
|
+
return null;
|
|
3615
|
+
}
|
|
3576
3616
|
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3617
|
+
const packageName = extendsValue.startsWith('@')
|
|
3618
|
+
? parts.slice(0, 2).join('/')
|
|
3619
|
+
: parts[0];
|
|
3620
|
+
const packageRest = parts.slice(packageName.startsWith('@') ? 2 : 1).join('/');
|
|
3621
|
+
const packageRoot = await findNodeModulePackageRoot(configDir, packageName);
|
|
3622
|
+
if (!packageRoot) {
|
|
3623
|
+
return null;
|
|
3624
|
+
}
|
|
3625
|
+
|
|
3626
|
+
const rest = packageRest && !packageRest.endsWith('.json')
|
|
3627
|
+
? `${packageRest}.json`
|
|
3628
|
+
: packageRest;
|
|
3629
|
+
return path.join(packageRoot, rest || 'tsconfig.json');
|
|
3630
|
+
}
|
|
3631
|
+
|
|
3632
|
+
async function findNodeModulePackageRoot(startDir, packageName) {
|
|
3633
|
+
let currentDir = path.resolve(startDir);
|
|
3634
|
+
while (true) {
|
|
3635
|
+
const candidate = path.join(currentDir, 'node_modules', packageName);
|
|
3636
|
+
try {
|
|
3637
|
+
const stat = await fs.stat(candidate);
|
|
3638
|
+
if (stat.isDirectory()) {
|
|
3639
|
+
return candidate;
|
|
3640
|
+
}
|
|
3641
|
+
} catch {}
|
|
3642
|
+
|
|
3643
|
+
const parentDir = path.dirname(currentDir);
|
|
3644
|
+
if (parentDir === currentDir) {
|
|
3645
|
+
return null;
|
|
3646
|
+
}
|
|
3647
|
+
currentDir = parentDir;
|
|
3648
|
+
}
|
|
3582
3649
|
}
|
|
3583
3650
|
|
|
3584
3651
|
async function isAliasSnapshotValid(snapshot = []) {
|
|
@@ -4187,9 +4254,10 @@ export async function resolveImpactContext({
|
|
|
4187
4254
|
const normalizedChangedFiles = uniqueImpact(changedFiles.map(normalizeImpactPath));
|
|
4188
4255
|
const normalizedChangedSymbols = uniqueImpact(changedSymbols.map((symbol) => String(symbol ?? '').trim()));
|
|
4189
4256
|
|
|
4190
|
-
const [callsArtifact, importsArtifact, relatedArtifacts] = await Promise.all([
|
|
4257
|
+
const [callsArtifact, importsArtifact, filesArtifact, relatedArtifacts] = await Promise.all([
|
|
4191
4258
|
readArtifact(absoluteRoot, INDEX_ARTIFACTS.calls),
|
|
4192
4259
|
readArtifact(absoluteRoot, INDEX_ARTIFACTS.imports),
|
|
4260
|
+
readArtifact(absoluteRoot, INDEX_ARTIFACTS.files),
|
|
4193
4261
|
loadRelatedTestArtifacts({ rootDir: absoluteRoot }),
|
|
4194
4262
|
]);
|
|
4195
4263
|
|
|
@@ -4221,23 +4289,39 @@ export async function resolveImpactContext({
|
|
|
4221
4289
|
for (const record of callItems) {
|
|
4222
4290
|
const matchedSymbol = (record.calls ?? []).find((name) => callerTargets.has(name));
|
|
4223
4291
|
if (!matchedSymbol) continue;
|
|
4224
|
-
if (changedFileSet.has(normalizeImpactPath(record.filePath))
|
|
4292
|
+
if (changedFileSet.has(normalizeImpactPath(record.filePath))) continue;
|
|
4225
4293
|
callers.push({ filePath: record.filePath, symbol: record.symbol, reason: `calls ${matchedSymbol}` });
|
|
4226
4294
|
}
|
|
4227
4295
|
}
|
|
4228
4296
|
|
|
4229
4297
|
const importers = [];
|
|
4230
4298
|
const dependencies = [];
|
|
4299
|
+
// Non-relative specifiers (aliases like `@/x`, `~/x`, tsconfig paths) resolve
|
|
4300
|
+
// through the same machinery the index builder uses; relative specifiers keep
|
|
4301
|
+
// the candidate-list match so a changed file absent from files.json (e.g. a
|
|
4302
|
+
// brand-new file) still counts.
|
|
4303
|
+
const indexedFileSet = new Set((filesArtifact.items ?? []).map((item) => item.filePath));
|
|
4304
|
+
const importAliasContext = importsNeedAliasContext(importItems)
|
|
4305
|
+
? await loadImportAliasContext({ rootDir: absoluteRoot }).catch(() => null)
|
|
4306
|
+
: null;
|
|
4231
4307
|
for (const imp of importItems) {
|
|
4232
4308
|
const from = normalizeImpactPath(imp.from);
|
|
4233
|
-
const
|
|
4234
|
-
|
|
4309
|
+
const specifier = String(imp.to ?? '').trim();
|
|
4310
|
+
const targets = specifier.startsWith('.')
|
|
4311
|
+
? resolveImpactImportTargets(from, specifier)
|
|
4312
|
+
: [resolveImportSpecifier({
|
|
4313
|
+
rootDir: absoluteRoot,
|
|
4314
|
+
fromFilePath: from,
|
|
4315
|
+
specifier,
|
|
4316
|
+
indexedFileSet,
|
|
4317
|
+
aliasContext: importAliasContext,
|
|
4318
|
+
})].filter(Boolean);
|
|
4319
|
+
const matched = targets.find((candidate) => changedFileSet.has(normalizeImpactPath(candidate)));
|
|
4235
4320
|
if (matched) {
|
|
4236
4321
|
importers.push(from);
|
|
4237
4322
|
dependencies.push(matched);
|
|
4238
4323
|
}
|
|
4239
4324
|
}
|
|
4240
|
-
|
|
4241
4325
|
const candidateFiles = uniqueImpact([
|
|
4242
4326
|
...normalizedChangedFiles,
|
|
4243
4327
|
...importers,
|