@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.
Files changed (109) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/bin/ukit +12 -4
  3. package/manifests/engineConformance.yaml +29 -0
  4. package/manifests/platform.full.yaml +13 -0
  5. package/package.json +2 -1
  6. package/scripts/audit/decision-coverage.mjs +295 -0
  7. package/scripts/bench/outline-savings.mjs +19 -4
  8. package/scripts/bench/parallel-agents.mjs +15 -4
  9. package/scripts/bench/runGold.mjs +22 -4
  10. package/scripts/bench/v3-ceremony.mjs +7 -1
  11. package/scripts/bug/triage.mjs +56 -17
  12. package/scripts/index/build-index.mjs +94 -28
  13. package/scripts/index/query-index.mjs +48 -14
  14. package/scripts/index/refresh-index.mjs +142 -62
  15. package/scripts/perf/audit-perf.mjs +8 -2
  16. package/scripts/skill/audit-skill.mjs +54 -25
  17. package/src/bug/triageBug.js +9 -6
  18. package/src/cli/adapters.js +6 -0
  19. package/src/cli/commands/code.js +7 -1
  20. package/src/cli/commands/indexArgs.js +4 -2
  21. package/src/cli/commands/indexTools.js +8 -1
  22. package/src/cli/commands/install.js +13 -0
  23. package/src/cli/commands/memory.js +13 -9
  24. package/src/cli/commands/status.js +17 -1
  25. package/src/cli/commands/update.js +7 -0
  26. package/src/context/detectProjectContext.js +3 -1
  27. package/src/core/codeintel/analogy.js +1 -1
  28. package/src/core/codeintel/diagnostics.js +9 -6
  29. package/src/core/codeintel/graph.js +14 -8
  30. package/src/core/codeintel/impact.js +0 -1
  31. package/src/core/codeintel/invalidation.js +5 -3
  32. package/src/core/codeintel/packet.js +11 -0
  33. package/src/core/codeintel/router.js +17 -3
  34. package/src/core/codeintel/semanticProvider.js +1 -1
  35. package/src/core/codeintel/summaries.js +11 -7
  36. package/src/core/compact/contextBudget.js +26 -12
  37. package/src/core/compact/index.js +15 -8
  38. package/src/core/docContracts.js +10 -2
  39. package/src/core/experiments/deliberation.js +321 -0
  40. package/src/core/experiments/dynamicWorkflow.js +492 -0
  41. package/src/core/fileOps.js +8 -1
  42. package/src/core/gatewayProbe.js +29 -1
  43. package/src/core/gatewayResilienceEnv.js +44 -3
  44. package/src/core/handoffDocValidator.js +3 -1
  45. package/src/core/hookChainDoctor.js +16 -1
  46. package/src/core/memory/deltaOverlays.js +448 -0
  47. package/src/core/memory/learningCandidates.js +302 -0
  48. package/src/core/memory/migrate.js +59 -32
  49. package/src/core/memory/recordStore.js +24 -1
  50. package/src/core/memory/store.js +44 -8
  51. package/src/core/memory/storeV2.js +48 -33
  52. package/src/core/memory/userMemory.js +26 -10
  53. package/src/core/output/index.js +15 -10
  54. package/src/core/permissionPolicy.js +8 -0
  55. package/src/core/runtimeConfig.js +224 -4
  56. package/src/core/sensitiveValueScanner.js +10 -2
  57. package/src/core/taskBudgetValidator.js +12 -17
  58. package/src/core/taskProgressGuard.js +59 -9
  59. package/src/core/unattendedDoctor.js +5 -2
  60. package/src/core/uninstall.js +37 -8
  61. package/src/decision/client.js +371 -0
  62. package/src/decision/lease.js +198 -0
  63. package/src/decision/preflight.js +492 -0
  64. package/src/decision/protocol.js +308 -0
  65. package/src/decision/registry.js +384 -0
  66. package/src/decision/shadow.js +281 -0
  67. package/src/decision/statePacket.js +165 -0
  68. package/src/diagnostics/failurePatterns.js +2 -1
  69. package/src/diagnostics/ledgerFiles.js +3 -1
  70. package/src/diagnostics/routeOutcomes.js +1 -29
  71. package/src/index/buildIndex.js +23 -11
  72. package/src/index/impactContext.js +21 -2
  73. package/src/index/importResolution.js +13 -7
  74. package/src/index/queryIndex.js +11 -5
  75. package/src/index/resolveContext.js +22 -9
  76. package/src/index/taskRouting.js +63 -2
  77. package/src/index/verificationPlan.js +12 -1
  78. package/src/learning/patternProposals.js +6 -0
  79. package/src/render/renderTemplate.js +1 -1
  80. package/src/skill/auditSkill.js +3 -1
  81. package/src/stack/detectStack.js +3 -1
  82. package/template_project/.claude/agents/handoff-planner.md +2 -5
  83. package/template_project/.claude/hooks/auto-allow-bash.sh +5 -0
  84. package/template_project/.claude/hooks/block-dangerous.mjs +11 -4
  85. package/template_project/.claude/hooks/context-hardcap-gate.sh +10 -1
  86. package/template_project/.claude/hooks/handoff-model-guard.sh +14 -4
  87. package/template_project/.claude/hooks/handoff-resume.sh +10 -1
  88. package/template_project/.claude/hooks/protect-files.sh +0 -1
  89. package/template_project/.claude/hooks/record-execution.mjs +13 -1
  90. package/template_project/.claude/hooks/sensitive-data-guard.mjs +80 -5
  91. package/template_project/.claude/hooks/session-episode.sh +9 -2
  92. package/template_project/.claude/skills/pdf-processing-pro/SKILL.md +1 -1
  93. package/template_project/.claude/ukit/index/handoff-doc-validator.mjs +3 -1
  94. package/template_project/.claude/ukit/index/lib/index-core.mjs +129 -42
  95. package/template_project/.claude/ukit/index/route-task.mjs +444 -0
  96. package/template_project/.claude/ukit/index/task-budget-validator.mjs +12 -16
  97. package/template_project/.claude/ukit/index/unic-decision.mjs +786 -0
  98. package/template_project/.claude/ukit/index/verify-context.mjs +11 -0
  99. package/template_project/.claude/ukit/runtime/execution-ledger.mjs +116 -9
  100. package/template_project/.claude/ukit/runtime/project-important.mjs +9 -7
  101. package/template_project/.claude/ukit/runtime/reinject-context.mjs +48 -0
  102. package/template_project/.claude/ukit/runtime/resumable-run.mjs +596 -0
  103. package/template_project/.claude/ukit/runtime/sensitive-value-scanner.mjs +4 -7
  104. package/template_project/.claude/ukit/runtime/stop-coordinator.mjs +1 -1
  105. package/template_project/docs/AI_HANDOFF/PLAN.md +7 -7
  106. package/template_project/docs/AI_HANDOFF/RULES.md +1 -1
  107. package/template_project/ukit/storage/config.json +34 -1
  108. package/template_project/.claude/ukit/skill-router-state.json +0 -1
  109. 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 2; kill "$UKIT_HEAD_PID" 2>/dev/null ) >/dev/null 2>&1 &
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: PDF Processing Pro
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
- if (!positions.has(token) && s.name.startsWith(token)) positions.set(token, s.pos);
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?.items?.length === codeFiles.length
390
- && (previousArchetypesArtifact.items ?? []).every((item) => currentCodePaths.has(item.filePath));
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?.items?.length === archetypes.length
966
- && (archetypesArtifact.items ?? []).every((item) => currentCodePaths.has(item?.filePath));
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
- if (!query || !query.trim()) return [];
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
- if (!filePath || !filePath.trim()) return [];
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
- return artifact && typeof artifact === 'object' ? artifact : { items: [] };
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', 'security', 'migration', 'uninstall', 'password', 'token', 'permission',
1349
- 'delete all', 'drop table', 'race', 'flaky', 'intermittent', 'timeout', 'deadlock',
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 (RISKY_SIGNALS.some((signal) => lower.includes(signal))) {
1537
+ if (containsSignalWord(lower, RISKY_SIGNALS)) {
1510
1538
  return 'non-trivial';
1511
1539
  }
1512
1540
 
1513
- if (TRIVIAL_SIGNALS.some((signal) => lower.includes(signal))) {
1514
- if (lower.includes('all ') || lower.includes('every ') || lower.includes('multiple')) {
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 EXPANSION_SIGNALS.some((signal) => normalizedIntent.includes(signal));
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+(?:[^'";]+?\s+from\s+)?['"]([^'"]+)['"]/g)) {
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+from\s+['"]([^'"]+)['"]/g)) {
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+from\s+['"]([^'"]+)['"]/g)) {
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 && (extendsValue.startsWith('.') || extendsValue.startsWith('/'))) {
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
- function resolveExtendedConfigPath(configDir, extendsValue) {
3578
- const withExtension = extendsValue.endsWith('.json')
3579
- ? extendsValue
3580
- : `${extendsValue}.json`;
3581
- return path.resolve(configDir, withExtension);
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)) && changedSymbolSet.has(record.symbol)) continue;
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 matched = resolveImpactImportTargets(from, imp.to)
4234
- .find((candidate) => changedFileSet.has(normalizeImpactPath(candidate)));
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,