@lumy-pack/line-lore 0.0.6 → 0.0.8

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/dist/index.mjs CHANGED
@@ -368,26 +368,36 @@ var init_executor = __esm({
368
368
 
369
369
  // src/core/ancestry/ancestry.ts
370
370
  import { filter as filter4, isTruthy as isTruthy4 } from "@winglet/common-utils";
371
- async function findMergeCommit(commitSha, options) {
372
- const ref = options?.ref ?? "HEAD";
373
- const budget = options?.timeout ?? DEFAULT_ANCESTRY_TIMEOUT;
374
- const startTime = Date.now();
375
- const firstParentResult = await findMergeCommitWithArgs(
376
- commitSha,
377
- ref,
378
- ["--first-parent"],
379
- { ...options, timeout: budget }
380
- );
381
- if (firstParentResult) return firstParentResult;
382
- const elapsed = Date.now() - startTime;
383
- const remaining = budget - elapsed;
384
- if (remaining <= 0) return null;
385
- return findMergeCommitWithArgs(commitSha, ref, [], {
386
- ...options,
387
- timeout: remaining
388
- });
371
+ async function verifyMergeIntroducesCommit(targetSha, mergeResult, options) {
372
+ if (mergeResult.parentShas.length < 2) return true;
373
+ const firstParent = mergeResult.parentShas[0];
374
+ const branchParents = mergeResult.parentShas.slice(1);
375
+ const onMainline = await isAncestor(targetSha, firstParent, options);
376
+ if (onMainline === null) return false;
377
+ if (onMainline) return false;
378
+ for (const branchParent of branchParents) {
379
+ const onBranch = await isAncestor(targetSha, branchParent, options);
380
+ if (onBranch === null) return false;
381
+ if (onBranch) return true;
382
+ }
383
+ return false;
384
+ }
385
+ async function isAncestor(commitA, commitB, options) {
386
+ try {
387
+ const result = await gitExec(
388
+ ["merge-base", "--is-ancestor", commitA, commitB],
389
+ {
390
+ cwd: options?.cwd,
391
+ timeout: options?.timeout ?? 5e3,
392
+ allowExitCodes: [1]
393
+ }
394
+ );
395
+ return result.exitCode === 0;
396
+ } catch {
397
+ return null;
398
+ }
389
399
  }
390
- async function findMergeCommitWithArgs(commitSha, ref, extraArgs, options) {
400
+ async function findMergeCommitsWithArgs(commitSha, ref, extraArgs, options) {
391
401
  try {
392
402
  const result = await gitExec(
393
403
  [
@@ -403,10 +413,29 @@ async function findMergeCommitWithArgs(commitSha, ref, extraArgs, options) {
403
413
  { cwd: options?.cwd, timeout: options?.timeout }
404
414
  );
405
415
  const lines = filter4(result.stdout.trim().split("\n"), isTruthy4);
406
- if (lines.length === 0) return null;
407
- return parseMergeLogLine(lines[0]);
416
+ if (lines.length === 0) return [];
417
+ const verifiedCandidates = [];
418
+ const candidateCount = Math.min(lines.length, MAX_CANDIDATES);
419
+ let attemptedCount = 0;
420
+ for (let i = 0; i < candidateCount; i++) {
421
+ const candidate = parseMergeLogLine(lines[i]);
422
+ if (!candidate) continue;
423
+ attemptedCount++;
424
+ const verified = await verifyMergeIntroducesCommit(
425
+ commitSha,
426
+ candidate,
427
+ options
428
+ );
429
+ if (verified) verifiedCandidates.push(candidate);
430
+ }
431
+ if (attemptedCount > 0 && verifiedCandidates.length === 0 && options?.warnings) {
432
+ options.warnings.push(
433
+ `ancestry: all ${attemptedCount} merge candidate(s) failed verification for ${commitSha.slice(0, 8)}`
434
+ );
435
+ }
436
+ return verifiedCandidates;
408
437
  } catch {
409
- return null;
438
+ return [];
410
439
  }
411
440
  }
412
441
  function parseMergeLogLine(line) {
@@ -426,24 +455,71 @@ function parseMergeLogLine(line) {
426
455
  const subject = parts.slice(subjectStart).join(" ");
427
456
  return { mergeCommitSha, parentShas, subject };
428
457
  }
429
- function extractPRFromMergeMessage(subject) {
458
+ async function findMergeCommits(commitSha, options) {
459
+ const ref = options?.ref ?? "HEAD";
460
+ const budget = options?.timeout ?? DEFAULT_ANCESTRY_TIMEOUT;
461
+ const startTime = Date.now();
462
+ const results = [];
463
+ const seen = /* @__PURE__ */ new Set();
464
+ const pushUnique = (candidates) => {
465
+ for (const candidate of candidates) {
466
+ if (seen.has(candidate.mergeCommitSha)) continue;
467
+ seen.add(candidate.mergeCommitSha);
468
+ results.push(candidate);
469
+ if (results.length >= MAX_CANDIDATES) break;
470
+ }
471
+ };
472
+ const firstParent = await findMergeCommitsWithArgs(
473
+ commitSha,
474
+ ref,
475
+ ["--first-parent"],
476
+ { ...options, timeout: budget }
477
+ );
478
+ pushUnique(firstParent);
479
+ const elapsed = Date.now() - startTime;
480
+ const remaining = budget - elapsed;
481
+ if (remaining > 0 && results.length < MAX_CANDIDATES) {
482
+ const full = await findMergeCommitsWithArgs(commitSha, ref, [], {
483
+ ...options,
484
+ timeout: remaining
485
+ });
486
+ pushUnique(full);
487
+ }
488
+ return results;
489
+ }
490
+ async function getCommitSubject(sha, options) {
491
+ try {
492
+ const result = await gitExec(["log", "-1", "--format=%s", sha], {
493
+ cwd: options?.cwd,
494
+ timeout: options?.timeout ?? 5e3
495
+ });
496
+ const subject = result.stdout.trim();
497
+ return subject || null;
498
+ } catch {
499
+ return null;
500
+ }
501
+ }
502
+ function extractPRFromMergeMessage(subject, platform) {
430
503
  const ghMatch = /Merge pull request #(\d+)/.exec(subject);
431
504
  if (ghMatch) return parseInt(ghMatch[1], 10);
432
505
  const squashMatch = /\(#(\d+)\)\s*$/.exec(subject);
433
506
  if (squashMatch) return parseInt(squashMatch[1], 10);
434
- const glMatch = /!(\d+)\s*$/.exec(subject);
435
- if (glMatch) return parseInt(glMatch[1], 10);
507
+ if (!platform || platform === "gitlab" || platform === "gitlab-self-hosted") {
508
+ const glMatch = /See merge request\s+\S*!(\d+)\s*$/.exec(subject);
509
+ if (glMatch) return parseInt(glMatch[1], 10);
510
+ }
436
511
  const adoMatch = /Merged PR (\d+):/.exec(subject);
437
512
  if (adoMatch) return parseInt(adoMatch[1], 10);
438
513
  return null;
439
514
  }
440
- var DEFAULT_ANCESTRY_TIMEOUT;
515
+ var DEFAULT_ANCESTRY_TIMEOUT, MAX_CANDIDATES;
441
516
  var init_ancestry = __esm({
442
517
  "src/core/ancestry/ancestry.ts"() {
443
518
  "use strict";
444
519
  init_esm_shims();
445
520
  init_executor();
446
521
  DEFAULT_ANCESTRY_TIMEOUT = 3e4;
522
+ MAX_CANDIDATES = 10;
447
523
  }
448
524
  });
449
525
 
@@ -574,7 +650,8 @@ function toCachedPR(pr) {
574
650
  url: pr.url,
575
651
  mergeCommit: pr.mergeCommit,
576
652
  baseBranch: pr.baseBranch,
577
- mergedAt: pr.mergedAt ? new Date(pr.mergedAt).getTime() : void 0
653
+ mergedAt: pr.mergedAt ? new Date(pr.mergedAt).getTime() : void 0,
654
+ resolvedVia: pr.resolvedVia
578
655
  };
579
656
  }
580
657
  function fromCachedPR(cached) {
@@ -589,7 +666,9 @@ function fromCachedPR(cached) {
589
666
  url: cached.url,
590
667
  mergeCommit: cached.mergeCommit,
591
668
  baseBranch: cached.baseBranch,
592
- mergedAt
669
+ mergedAt,
670
+ // Preserve original resolvedVia; fallback to url heuristic for legacy cache entries
671
+ resolvedVia: cached.resolvedVia ?? (cached.url ? "api" : "message")
593
672
  };
594
673
  }
595
674
  async function lookupPR(commitSha, adapter, options, _recursionDepth = 0) {
@@ -600,45 +679,84 @@ async function lookupPR(commitSha, adapter, options, _recursionDepth = 0) {
600
679
  const cached = await cache.get(commitSha);
601
680
  if (cached) return fromCachedPR(cached);
602
681
  if (options?.cacheOnly) return null;
682
+ const prSelectOptions = options?.preferredBase ? { preferredBase: options.preferredBase } : void 0;
683
+ if (adapter) {
684
+ const directPR = await adapter.getPRForCommit(commitSha, prSelectOptions);
685
+ if (directPR?.mergedAt) {
686
+ const result = { ...directPR, resolvedVia: "api" };
687
+ await cache.set(commitSha, toCachedPR(result));
688
+ return result;
689
+ }
690
+ }
603
691
  let mergeBasedPR = null;
604
- const mergeResult = await findMergeCommit(commitSha, options);
605
- if (mergeResult) {
606
- const prNumber = extractPRFromMergeMessage(mergeResult.subject);
692
+ const mergeCandidates = await findMergeCommits(commitSha, options);
693
+ const hasAncestryMerges = mergeCandidates.length > 0;
694
+ for (const candidate of mergeCandidates) {
695
+ const prNumber = extractPRFromMergeMessage(
696
+ candidate.subject,
697
+ options?.platform
698
+ );
607
699
  if (prNumber) {
608
700
  if (adapter) {
609
- const prInfo = await adapter.getPRForCommit(mergeResult.mergeCommitSha);
701
+ const prInfo = await adapter.getPRForCommit(
702
+ candidate.mergeCommitSha,
703
+ prSelectOptions
704
+ );
610
705
  if (prInfo?.mergedAt) {
611
- mergeBasedPR = prInfo;
706
+ mergeBasedPR = { ...prInfo, resolvedVia: "ancestry" };
612
707
  }
613
708
  }
614
709
  if (!mergeBasedPR) {
615
710
  mergeBasedPR = {
616
711
  number: prNumber,
617
- title: mergeResult.subject,
712
+ title: candidate.subject,
618
713
  author: "",
619
714
  url: "",
620
- mergeCommit: mergeResult.mergeCommitSha,
621
- baseBranch: ""
715
+ mergeCommit: candidate.mergeCommitSha,
716
+ baseBranch: "",
717
+ resolvedVia: "ancestry"
622
718
  };
623
719
  }
624
- if (!options?.deep || mergeBasedPR.mergedAt) {
625
- await cache.set(commitSha, toCachedPR(mergeBasedPR));
626
- return mergeBasedPR;
720
+ break;
721
+ }
722
+ if (adapter) {
723
+ const mergeCommitPR = await adapter.getPRForCommit(
724
+ candidate.mergeCommitSha,
725
+ prSelectOptions
726
+ );
727
+ if (mergeCommitPR?.mergedAt) {
728
+ mergeBasedPR = { ...mergeCommitPR, resolvedVia: "ancestry" };
729
+ break;
627
730
  }
628
731
  }
629
732
  }
630
733
  if (mergeBasedPR) {
631
- await cache.set(commitSha, toCachedPR(mergeBasedPR));
632
- return mergeBasedPR;
734
+ if (!options?.deep || mergeBasedPR.mergedAt) {
735
+ await cache.set(commitSha, toCachedPR(mergeBasedPR));
736
+ return mergeBasedPR;
737
+ }
633
738
  }
634
- if (adapter) {
635
- const prInfo = await adapter.getPRForCommit(commitSha);
636
- if (prInfo?.mergedAt) {
637
- await cache.set(commitSha, toCachedPR(prInfo));
638
- return prInfo;
739
+ const commitSubject = await getCommitSubject(commitSha, options);
740
+ if (commitSubject) {
741
+ const directPrNumber = extractPRFromMergeMessage(
742
+ commitSubject,
743
+ options?.platform
744
+ );
745
+ if (directPrNumber) {
746
+ const subjectPR = {
747
+ number: directPrNumber,
748
+ title: commitSubject,
749
+ author: "",
750
+ url: "",
751
+ mergeCommit: commitSha,
752
+ baseBranch: "",
753
+ resolvedVia: "message"
754
+ };
755
+ await cache.set(commitSha, toCachedPR(subjectPR));
756
+ return subjectPR;
639
757
  }
640
758
  }
641
- if (!options?.skipPatchIdScan && _recursionDepth < MAX_RECURSION_DEPTH) {
759
+ if (!options?.skipPatchIdScan && _recursionDepth < MAX_RECURSION_DEPTH && (!hasAncestryMerges || options?.deep)) {
642
760
  const patchIdMatch = await findPatchIdMatch(commitSha, {
643
761
  ...options,
644
762
  scanDepth: options?.deep ? DEEP_SCAN_DEPTH : void 0
@@ -656,6 +774,10 @@ async function lookupPR(commitSha, adapter, options, _recursionDepth = 0) {
656
774
  }
657
775
  }
658
776
  }
777
+ if (mergeBasedPR) {
778
+ await cache.set(commitSha, toCachedPR(mergeBasedPR));
779
+ return mergeBasedPR;
780
+ }
659
781
  return null;
660
782
  }
661
783
  function resetPRCache() {
@@ -696,6 +818,7 @@ init_errors();
696
818
  // src/core/core.ts
697
819
  init_esm_shims();
698
820
  import { createHash as createHash2 } from "crypto";
821
+ import { dirname, isAbsolute, relative } from "path";
699
822
  import { map as map8 } from "@winglet/common-utils";
700
823
 
701
824
  // src/ast/index.ts
@@ -979,7 +1102,7 @@ async function checkGitHealth(options) {
979
1102
  const cloneStatus = await checkCloneStatus({ cwd: options?.cwd });
980
1103
  if (cloneStatus.partialClone) {
981
1104
  hints.push(
982
- "Partial clone detected. Patch-ID scan (Strategy 4) will be skipped to avoid blob downloads."
1105
+ "Partial clone detected. Patch-ID scan (Strategy 5) will be skipped to avoid blob downloads."
983
1106
  );
984
1107
  }
985
1108
  if (cloneStatus.shallow) {
@@ -1095,7 +1218,7 @@ var GitHubAdapter = class {
1095
1218
  return { authenticated: false, hostname: this.hostname };
1096
1219
  }
1097
1220
  }
1098
- async getPRForCommit(sha) {
1221
+ async getPRForCommit(sha, options) {
1099
1222
  if (this.scheduler.isRateLimited()) return null;
1100
1223
  try {
1101
1224
  const result = await shellExec(
@@ -1112,18 +1235,20 @@ var GitHubAdapter = class {
1112
1235
  );
1113
1236
  const prs = JSON.parse(result.stdout);
1114
1237
  if (!isArray(prs) || prs.length === 0) return null;
1115
- const defaultBranch = await this.detectDefaultBranch();
1116
- const defaultBranchPR = prs.find(
1117
- (pr) => pr.base === defaultBranch
1118
- );
1119
- const data = defaultBranchPR ?? prs[0];
1238
+ let data = prs[0];
1239
+ if (options?.preferredBase) {
1240
+ const preferred = prs.find(
1241
+ (pr) => pr.base === options.preferredBase
1242
+ );
1243
+ if (preferred) data = preferred;
1244
+ }
1120
1245
  return {
1121
1246
  number: data.number,
1122
1247
  title: data.title ?? "",
1123
1248
  author: data.user ?? "",
1124
1249
  url: data.html_url ?? "",
1125
1250
  mergeCommit: data.merge_commit_sha ?? sha,
1126
- baseBranch: data.base ?? defaultBranch,
1251
+ baseBranch: data.base ?? "",
1127
1252
  mergedAt: data.merged_at
1128
1253
  };
1129
1254
  } catch {
@@ -1305,7 +1430,7 @@ var GitLabAdapter = class {
1305
1430
  return { authenticated: false, hostname: this.hostname };
1306
1431
  }
1307
1432
  }
1308
- async getPRForCommit(sha) {
1433
+ async getPRForCommit(sha, options) {
1309
1434
  if (this.scheduler.isRateLimited()) return null;
1310
1435
  try {
1311
1436
  const result = await shellExec(
@@ -1329,18 +1454,20 @@ var GitLabAdapter = class {
1329
1454
  return aTime - bTime;
1330
1455
  });
1331
1456
  if (mergedMRs.length === 0) return null;
1332
- const defaultBranch = await this.detectDefaultBranch();
1333
- const defaultBranchMR = mergedMRs.find(
1334
- (mr2) => mr2.target_branch === defaultBranch
1335
- );
1336
- const mr = defaultBranchMR ?? mergedMRs[0];
1457
+ let mr = mergedMRs[0];
1458
+ if (options?.preferredBase) {
1459
+ const preferred = mergedMRs.find(
1460
+ (m) => m.target_branch === options.preferredBase
1461
+ );
1462
+ if (preferred) mr = preferred;
1463
+ }
1337
1464
  return {
1338
1465
  number: mr.iid,
1339
1466
  title: mr.title ?? "",
1340
1467
  author: mr.author?.username ?? "",
1341
1468
  url: mr.web_url ?? "",
1342
1469
  mergeCommit: mr.merge_commit_sha ?? sha,
1343
- baseBranch: mr.target_branch ?? defaultBranch,
1470
+ baseBranch: mr.target_branch ?? "",
1344
1471
  mergedAt: mr.merged_at
1345
1472
  };
1346
1473
  } catch {
@@ -1959,6 +2086,7 @@ function parsePorcelainOutput(output) {
1959
2086
  }
1960
2087
  let commitHash = headerMatch[1];
1961
2088
  const originalLine = parseInt(headerMatch[2], 10);
2089
+ const finalLine = parseInt(headerMatch[3], 10) || 0;
1962
2090
  const isBoundary = commitHash.startsWith("^");
1963
2091
  if (isBoundary) {
1964
2092
  commitHash = commitHash.slice(1).padStart(40, "0");
@@ -2002,6 +2130,7 @@ function parsePorcelainOutput(output) {
2002
2130
  authorEmail: cleanEmail,
2003
2131
  date,
2004
2132
  lineContent,
2133
+ finalLine,
2005
2134
  originalFile,
2006
2135
  originalLine: originalFile ? originalLine : void 0
2007
2136
  });
@@ -2012,10 +2141,8 @@ function parsePorcelainOutput(output) {
2012
2141
  // src/core/blame/blame.ts
2013
2142
  async function executeBlame(file, lineRange, options) {
2014
2143
  const lineSpec = `${lineRange.start},${lineRange.end}`;
2015
- const result = await gitExec(
2016
- ["blame", "-w", "-C", "-C", "-M", "--porcelain", "-L", lineSpec, file],
2017
- options
2018
- );
2144
+ const args = options?.mode === "change" ? ["blame", "-w", "--porcelain", "-L", lineSpec, file] : ["blame", "-w", "-C", "-C", "-M", "--porcelain", "-L", lineSpec, file];
2145
+ const result = await gitExec(args, options);
2019
2146
  return parsePorcelainOutput(result.stdout);
2020
2147
  }
2021
2148
  async function analyzeBlameResults(results, filePath, options) {
@@ -2148,6 +2275,28 @@ async function traverse(adapter, type, number, depth, maxDepth, nodes, edges, vi
2148
2275
 
2149
2276
  // src/core/core.ts
2150
2277
  init_pr_lookup2();
2278
+ function resolvedViaToTrackingMethod(resolvedVia) {
2279
+ switch (resolvedVia) {
2280
+ case "api":
2281
+ return "api";
2282
+ case "ancestry":
2283
+ return "ancestry-path";
2284
+ case "message":
2285
+ return "message-parse";
2286
+ case "patch-id":
2287
+ return "patch-id";
2288
+ }
2289
+ }
2290
+ function resolvedViaToConfidence(resolvedVia) {
2291
+ switch (resolvedVia) {
2292
+ case "api":
2293
+ case "ancestry":
2294
+ return "exact";
2295
+ case "message":
2296
+ case "patch-id":
2297
+ return "heuristic";
2298
+ }
2299
+ }
2151
2300
  function computeFeatureFlags(operatingLevel, options) {
2152
2301
  return {
2153
2302
  astDiff: isAstAvailable() && !options.noAst,
@@ -2165,6 +2314,22 @@ async function resolveRepoIdentity(cwd) {
2165
2314
  return { host: "_local", owner: "_", repo: "_unknown" };
2166
2315
  }
2167
2316
  }
2317
+ async function resolveFileContext(file, cwd) {
2318
+ if (cwd || !isAbsolute(file)) return { file, cwd };
2319
+ const fileDir = dirname(file);
2320
+ try {
2321
+ const result = await gitExec(["rev-parse", "--show-toplevel"], {
2322
+ cwd: fileDir
2323
+ });
2324
+ const repoRoot = result.stdout.trim();
2325
+ return {
2326
+ file: relative(repoRoot, file),
2327
+ cwd: repoRoot
2328
+ };
2329
+ } catch {
2330
+ return { file, cwd };
2331
+ }
2332
+ }
2168
2333
  async function detectPlatform2(options) {
2169
2334
  const warnings = [];
2170
2335
  let adapter = null;
@@ -2188,9 +2353,10 @@ async function runBlameAndAuth(adapter, options, execOptions) {
2188
2353
  const lineRange = parseLineRange(
2189
2354
  options.endLine ? `${options.line},${options.endLine}` : `${options.line}`
2190
2355
  );
2191
- const blameChain = executeBlame(options.file, lineRange, execOptions).then(
2192
- (results) => analyzeBlameResults(results, options.file, execOptions)
2193
- );
2356
+ const blameChain = executeBlame(options.file, lineRange, {
2357
+ ...execOptions,
2358
+ mode: options.mode
2359
+ }).then((results) => analyzeBlameResults(results, options.file, execOptions));
2194
2360
  const [authResult, blameResult] = await Promise.allSettled([
2195
2361
  adapter ? adapter.checkAuth() : Promise.resolve({ authenticated: false }),
2196
2362
  blameChain
@@ -2211,12 +2377,24 @@ async function runBlameAndAuth(adapter, options, execOptions) {
2211
2377
  }
2212
2378
  return { analyzed: blameResult.value, operatingLevel, warnings };
2213
2379
  }
2214
- async function processEntry(entry, featureFlags, adapter, options, execOptions, repoId, skipPatchIdScan) {
2380
+ function resolveTraceMode(mode) {
2381
+ return mode ?? "origin";
2382
+ }
2383
+ function deduplicatedLookupPR(sha, adapter, options, inflight) {
2384
+ const existing = inflight.get(sha);
2385
+ if (existing) return existing;
2386
+ const promise = lookupPR(sha, adapter, options);
2387
+ inflight.set(sha, promise);
2388
+ promise.finally(() => inflight.delete(sha));
2389
+ return promise;
2390
+ }
2391
+ async function processEntry(entry, featureFlags, adapter, options, execOptions, repoId, inflightPR, skipPatchIdScan, preferredBase) {
2215
2392
  const nodes = [];
2393
+ const traceMode = resolveTraceMode(options.mode);
2216
2394
  const commitNode = {
2217
2395
  type: entry.isCosmetic ? "cosmetic_commit" : "original_commit",
2218
2396
  sha: entry.blame.commitHash,
2219
- trackingMethod: "blame-CMw",
2397
+ trackingMethod: traceMode === "change" ? "blame" : "blame-CMw",
2220
2398
  confidence: "exact",
2221
2399
  note: entry.cosmeticReason ? `Cosmetic change: ${entry.cosmeticReason}` : void 0
2222
2400
  };
@@ -2238,21 +2416,24 @@ async function processEntry(entry, featureFlags, adapter, options, execOptions,
2238
2416
  }
2239
2417
  }
2240
2418
  const targetSha = nodes[nodes.length - 1].sha;
2419
+ const prLookupOptions = {
2420
+ ...execOptions,
2421
+ noCache: options.noCache,
2422
+ cacheOnly: options.cacheOnly,
2423
+ deep: featureFlags.deepTrace,
2424
+ repoId,
2425
+ skipPatchIdScan,
2426
+ preferredBase,
2427
+ platform: adapter?.platform
2428
+ };
2241
2429
  if (targetSha) {
2242
- const prInfo = await lookupPR(targetSha, adapter, {
2243
- ...execOptions,
2244
- noCache: options.noCache,
2245
- cacheOnly: options.cacheOnly,
2246
- deep: featureFlags.deepTrace,
2247
- repoId,
2248
- skipPatchIdScan
2249
- });
2430
+ const prInfo = await deduplicatedLookupPR(targetSha, adapter, prLookupOptions, inflightPR);
2250
2431
  if (prInfo) {
2251
2432
  nodes.push({
2252
2433
  type: "pull_request",
2253
2434
  sha: prInfo.mergeCommit,
2254
- trackingMethod: prInfo.url ? "api" : "message-parse",
2255
- confidence: prInfo.url ? "exact" : "heuristic",
2435
+ trackingMethod: resolvedViaToTrackingMethod(prInfo.resolvedVia),
2436
+ confidence: resolvedViaToConfidence(prInfo.resolvedVia),
2256
2437
  prNumber: prInfo.number,
2257
2438
  prUrl: prInfo.url || void 0,
2258
2439
  prTitle: prInfo.title || void 0,
@@ -2262,7 +2443,8 @@ async function processEntry(entry, featureFlags, adapter, options, execOptions,
2262
2443
  }
2263
2444
  return nodes;
2264
2445
  }
2265
- async function buildTraceNodes(analyzed, featureFlags, adapter, options, execOptions, repoId, skipPatchIdScan) {
2446
+ async function buildTraceNodes(analyzed, featureFlags, adapter, options, execOptions, repoId, skipPatchIdScan, preferredBase) {
2447
+ const inflightPR = /* @__PURE__ */ new Map();
2266
2448
  const results = await Promise.allSettled(
2267
2449
  map8(
2268
2450
  analyzed,
@@ -2273,7 +2455,9 @@ async function buildTraceNodes(analyzed, featureFlags, adapter, options, execOpt
2273
2455
  options,
2274
2456
  execOptions,
2275
2457
  repoId,
2276
- skipPatchIdScan
2458
+ inflightPR,
2459
+ skipPatchIdScan,
2460
+ preferredBase
2277
2461
  )
2278
2462
  )
2279
2463
  );
@@ -2281,13 +2465,16 @@ async function buildTraceNodes(analyzed, featureFlags, adapter, options, execOpt
2281
2465
  }
2282
2466
  var legacyCacheCleaned = false;
2283
2467
  async function trace(options) {
2284
- const execOptions = { cwd: options.cwd };
2468
+ const mode = resolveTraceMode(options.mode);
2469
+ const { file, cwd } = await resolveFileContext(options.file, options.cwd);
2470
+ const warnings = [];
2471
+ const execOptions = { cwd, warnings };
2285
2472
  if (!legacyCacheCleaned) {
2286
2473
  legacyCacheCleaned = true;
2287
2474
  cleanupLegacyCache().catch(() => {
2288
2475
  });
2289
2476
  }
2290
- const platform = await detectPlatform2(options);
2477
+ const platform = await detectPlatform2({ ...options, cwd });
2291
2478
  let repoId;
2292
2479
  if (platform.remote) {
2293
2480
  repoId = {
@@ -2296,15 +2483,15 @@ async function trace(options) {
2296
2483
  repo: platform.remote.repo
2297
2484
  };
2298
2485
  } else {
2299
- repoId = await resolveRepoIdentity(options.cwd);
2486
+ repoId = await resolveRepoIdentity(cwd);
2300
2487
  }
2301
2488
  const blameAuth = await runBlameAndAuth(
2302
2489
  platform.adapter,
2303
- options,
2490
+ { ...options, mode, file, cwd },
2304
2491
  execOptions
2305
2492
  );
2306
2493
  const operatingLevel = blameAuth.operatingLevel || platform.operatingLevel;
2307
- const warnings = [...platform.warnings, ...blameAuth.warnings];
2494
+ warnings.push(...platform.warnings, ...blameAuth.warnings);
2308
2495
  if (options.cacheOnly && options.noCache) {
2309
2496
  warnings.push(
2310
2497
  "Both cacheOnly and noCache are set. cacheOnly takes precedence \u2014 cache reads are enabled."
@@ -2313,13 +2500,13 @@ async function trace(options) {
2313
2500
  const featureFlags = computeFeatureFlags(operatingLevel, options);
2314
2501
  let cloneStatus = { partialClone: false, shallow: false };
2315
2502
  try {
2316
- const result = await checkCloneStatus({ cwd: options.cwd });
2503
+ const result = await checkCloneStatus({ cwd });
2317
2504
  if (result) cloneStatus = result;
2318
2505
  } catch {
2319
2506
  }
2320
2507
  if (cloneStatus.partialClone) {
2321
2508
  warnings.push(
2322
- "Partial clone detected. Patch-ID scan (Strategy 4) will be skipped to avoid blob downloads."
2509
+ "Partial clone detected. Patch-ID scan (Strategy 5) will be skipped to avoid blob downloads."
2323
2510
  );
2324
2511
  }
2325
2512
  if (cloneStatus.shallow) {
@@ -2327,14 +2514,27 @@ async function trace(options) {
2327
2514
  "Shallow repository detected. Ancestry-path results may be incomplete."
2328
2515
  );
2329
2516
  }
2517
+ let preferredBase;
2518
+ try {
2519
+ const branchResult = await gitExec(
2520
+ ["rev-parse", "--abbrev-ref", "HEAD"],
2521
+ execOptions
2522
+ );
2523
+ const branch = branchResult.stdout.trim();
2524
+ if (branch && branch !== "HEAD") {
2525
+ preferredBase = branch;
2526
+ }
2527
+ } catch {
2528
+ }
2330
2529
  const nodes = await buildTraceNodes(
2331
2530
  blameAuth.analyzed,
2332
2531
  featureFlags,
2333
2532
  platform.adapter,
2334
- options,
2533
+ { ...options, mode, file, cwd },
2335
2534
  execOptions,
2336
2535
  repoId,
2337
- cloneStatus.partialClone || void 0
2536
+ cloneStatus.partialClone || void 0,
2537
+ preferredBase
2338
2538
  );
2339
2539
  return { nodes, operatingLevel, featureFlags, warnings };
2340
2540
  }
@@ -14,7 +14,9 @@ export declare class GitHubAdapter implements PlatformAdapter {
14
14
  cwd?: string;
15
15
  });
16
16
  checkAuth(): Promise<AuthStatus>;
17
- getPRForCommit(sha: string): Promise<PRInfo | null>;
17
+ getPRForCommit(sha: string, options?: {
18
+ preferredBase?: string;
19
+ }): Promise<PRInfo | null>;
18
20
  private detectDefaultBranch;
19
21
  getPRCommits(prNumber: number): Promise<string[]>;
20
22
  getLinkedIssues(prNumber: number): Promise<IssueInfo[]>;
@@ -14,7 +14,9 @@ export declare class GitLabAdapter implements PlatformAdapter {
14
14
  cwd?: string;
15
15
  });
16
16
  checkAuth(): Promise<AuthStatus>;
17
- getPRForCommit(sha: string): Promise<PRInfo | null>;
17
+ getPRForCommit(sha: string, options?: {
18
+ preferredBase?: string;
19
+ }): Promise<PRInfo | null>;
18
20
  private detectDefaultBranch;
19
21
  getPRCommits(prNumber: number): Promise<string[]>;
20
22
  getLinkedIssues(prNumber: number): Promise<IssueInfo[]>;
@@ -12,6 +12,8 @@ export interface BlameResult {
12
12
  date: string;
13
13
  /** The actual content of the blamed line */
14
14
  lineContent: string;
15
+ /** Final line number in the current file */
16
+ finalLine: number;
15
17
  /** Original filename if the line was moved/renamed */
16
18
  originalFile?: string;
17
19
  /** Original line number before any moves/renames */
@@ -13,4 +13,6 @@ export interface CachedPRInfo {
13
13
  baseBranch: string;
14
14
  /** Unix timestamp in milliseconds, NOT ISO 8601 string */
15
15
  mergedAt?: number;
16
+ /** Which strategy resolved this PR — absent in legacy cache entries */
17
+ resolvedVia?: string;
16
18
  }