@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.cjs CHANGED
@@ -381,26 +381,36 @@ var init_executor = __esm({
381
381
  });
382
382
 
383
383
  // src/core/ancestry/ancestry.ts
384
- async function findMergeCommit(commitSha, options) {
385
- const ref = options?.ref ?? "HEAD";
386
- const budget = options?.timeout ?? DEFAULT_ANCESTRY_TIMEOUT;
387
- const startTime = Date.now();
388
- const firstParentResult = await findMergeCommitWithArgs(
389
- commitSha,
390
- ref,
391
- ["--first-parent"],
392
- { ...options, timeout: budget }
393
- );
394
- if (firstParentResult) return firstParentResult;
395
- const elapsed = Date.now() - startTime;
396
- const remaining = budget - elapsed;
397
- if (remaining <= 0) return null;
398
- return findMergeCommitWithArgs(commitSha, ref, [], {
399
- ...options,
400
- timeout: remaining
401
- });
384
+ async function verifyMergeIntroducesCommit(targetSha, mergeResult, options) {
385
+ if (mergeResult.parentShas.length < 2) return true;
386
+ const firstParent = mergeResult.parentShas[0];
387
+ const branchParents = mergeResult.parentShas.slice(1);
388
+ const onMainline = await isAncestor(targetSha, firstParent, options);
389
+ if (onMainline === null) return false;
390
+ if (onMainline) return false;
391
+ for (const branchParent of branchParents) {
392
+ const onBranch = await isAncestor(targetSha, branchParent, options);
393
+ if (onBranch === null) return false;
394
+ if (onBranch) return true;
395
+ }
396
+ return false;
397
+ }
398
+ async function isAncestor(commitA, commitB, options) {
399
+ try {
400
+ const result = await gitExec(
401
+ ["merge-base", "--is-ancestor", commitA, commitB],
402
+ {
403
+ cwd: options?.cwd,
404
+ timeout: options?.timeout ?? 5e3,
405
+ allowExitCodes: [1]
406
+ }
407
+ );
408
+ return result.exitCode === 0;
409
+ } catch {
410
+ return null;
411
+ }
402
412
  }
403
- async function findMergeCommitWithArgs(commitSha, ref, extraArgs, options) {
413
+ async function findMergeCommitsWithArgs(commitSha, ref, extraArgs, options) {
404
414
  try {
405
415
  const result = await gitExec(
406
416
  [
@@ -416,10 +426,29 @@ async function findMergeCommitWithArgs(commitSha, ref, extraArgs, options) {
416
426
  { cwd: options?.cwd, timeout: options?.timeout }
417
427
  );
418
428
  const lines = (0, import_common_utils9.filter)(result.stdout.trim().split("\n"), import_common_utils9.isTruthy);
419
- if (lines.length === 0) return null;
420
- return parseMergeLogLine(lines[0]);
429
+ if (lines.length === 0) return [];
430
+ const verifiedCandidates = [];
431
+ const candidateCount = Math.min(lines.length, MAX_CANDIDATES);
432
+ let attemptedCount = 0;
433
+ for (let i = 0; i < candidateCount; i++) {
434
+ const candidate = parseMergeLogLine(lines[i]);
435
+ if (!candidate) continue;
436
+ attemptedCount++;
437
+ const verified = await verifyMergeIntroducesCommit(
438
+ commitSha,
439
+ candidate,
440
+ options
441
+ );
442
+ if (verified) verifiedCandidates.push(candidate);
443
+ }
444
+ if (attemptedCount > 0 && verifiedCandidates.length === 0 && options?.warnings) {
445
+ options.warnings.push(
446
+ `ancestry: all ${attemptedCount} merge candidate(s) failed verification for ${commitSha.slice(0, 8)}`
447
+ );
448
+ }
449
+ return verifiedCandidates;
421
450
  } catch {
422
- return null;
451
+ return [];
423
452
  }
424
453
  }
425
454
  function parseMergeLogLine(line) {
@@ -439,18 +468,64 @@ function parseMergeLogLine(line) {
439
468
  const subject = parts.slice(subjectStart).join(" ");
440
469
  return { mergeCommitSha, parentShas, subject };
441
470
  }
442
- function extractPRFromMergeMessage(subject) {
471
+ async function findMergeCommits(commitSha, options) {
472
+ const ref = options?.ref ?? "HEAD";
473
+ const budget = options?.timeout ?? DEFAULT_ANCESTRY_TIMEOUT;
474
+ const startTime = Date.now();
475
+ const results = [];
476
+ const seen = /* @__PURE__ */ new Set();
477
+ const pushUnique = (candidates) => {
478
+ for (const candidate of candidates) {
479
+ if (seen.has(candidate.mergeCommitSha)) continue;
480
+ seen.add(candidate.mergeCommitSha);
481
+ results.push(candidate);
482
+ if (results.length >= MAX_CANDIDATES) break;
483
+ }
484
+ };
485
+ const firstParent = await findMergeCommitsWithArgs(
486
+ commitSha,
487
+ ref,
488
+ ["--first-parent"],
489
+ { ...options, timeout: budget }
490
+ );
491
+ pushUnique(firstParent);
492
+ const elapsed = Date.now() - startTime;
493
+ const remaining = budget - elapsed;
494
+ if (remaining > 0 && results.length < MAX_CANDIDATES) {
495
+ const full = await findMergeCommitsWithArgs(commitSha, ref, [], {
496
+ ...options,
497
+ timeout: remaining
498
+ });
499
+ pushUnique(full);
500
+ }
501
+ return results;
502
+ }
503
+ async function getCommitSubject(sha, options) {
504
+ try {
505
+ const result = await gitExec(["log", "-1", "--format=%s", sha], {
506
+ cwd: options?.cwd,
507
+ timeout: options?.timeout ?? 5e3
508
+ });
509
+ const subject = result.stdout.trim();
510
+ return subject || null;
511
+ } catch {
512
+ return null;
513
+ }
514
+ }
515
+ function extractPRFromMergeMessage(subject, platform) {
443
516
  const ghMatch = /Merge pull request #(\d+)/.exec(subject);
444
517
  if (ghMatch) return parseInt(ghMatch[1], 10);
445
518
  const squashMatch = /\(#(\d+)\)\s*$/.exec(subject);
446
519
  if (squashMatch) return parseInt(squashMatch[1], 10);
447
- const glMatch = /!(\d+)\s*$/.exec(subject);
448
- if (glMatch) return parseInt(glMatch[1], 10);
520
+ if (!platform || platform === "gitlab" || platform === "gitlab-self-hosted") {
521
+ const glMatch = /See merge request\s+\S*!(\d+)\s*$/.exec(subject);
522
+ if (glMatch) return parseInt(glMatch[1], 10);
523
+ }
449
524
  const adoMatch = /Merged PR (\d+):/.exec(subject);
450
525
  if (adoMatch) return parseInt(adoMatch[1], 10);
451
526
  return null;
452
527
  }
453
- var import_common_utils9, DEFAULT_ANCESTRY_TIMEOUT;
528
+ var import_common_utils9, DEFAULT_ANCESTRY_TIMEOUT, MAX_CANDIDATES;
454
529
  var init_ancestry = __esm({
455
530
  "src/core/ancestry/ancestry.ts"() {
456
531
  "use strict";
@@ -458,6 +533,7 @@ var init_ancestry = __esm({
458
533
  import_common_utils9 = require("@winglet/common-utils");
459
534
  init_executor();
460
535
  DEFAULT_ANCESTRY_TIMEOUT = 3e4;
536
+ MAX_CANDIDATES = 10;
461
537
  }
462
538
  });
463
539
 
@@ -588,7 +664,8 @@ function toCachedPR(pr) {
588
664
  url: pr.url,
589
665
  mergeCommit: pr.mergeCommit,
590
666
  baseBranch: pr.baseBranch,
591
- mergedAt: pr.mergedAt ? new Date(pr.mergedAt).getTime() : void 0
667
+ mergedAt: pr.mergedAt ? new Date(pr.mergedAt).getTime() : void 0,
668
+ resolvedVia: pr.resolvedVia
592
669
  };
593
670
  }
594
671
  function fromCachedPR(cached) {
@@ -603,7 +680,9 @@ function fromCachedPR(cached) {
603
680
  url: cached.url,
604
681
  mergeCommit: cached.mergeCommit,
605
682
  baseBranch: cached.baseBranch,
606
- mergedAt
683
+ mergedAt,
684
+ // Preserve original resolvedVia; fallback to url heuristic for legacy cache entries
685
+ resolvedVia: cached.resolvedVia ?? (cached.url ? "api" : "message")
607
686
  };
608
687
  }
609
688
  async function lookupPR(commitSha, adapter, options, _recursionDepth = 0) {
@@ -614,45 +693,84 @@ async function lookupPR(commitSha, adapter, options, _recursionDepth = 0) {
614
693
  const cached = await cache.get(commitSha);
615
694
  if (cached) return fromCachedPR(cached);
616
695
  if (options?.cacheOnly) return null;
696
+ const prSelectOptions = options?.preferredBase ? { preferredBase: options.preferredBase } : void 0;
697
+ if (adapter) {
698
+ const directPR = await adapter.getPRForCommit(commitSha, prSelectOptions);
699
+ if (directPR?.mergedAt) {
700
+ const result = { ...directPR, resolvedVia: "api" };
701
+ await cache.set(commitSha, toCachedPR(result));
702
+ return result;
703
+ }
704
+ }
617
705
  let mergeBasedPR = null;
618
- const mergeResult = await findMergeCommit(commitSha, options);
619
- if (mergeResult) {
620
- const prNumber = extractPRFromMergeMessage(mergeResult.subject);
706
+ const mergeCandidates = await findMergeCommits(commitSha, options);
707
+ const hasAncestryMerges = mergeCandidates.length > 0;
708
+ for (const candidate of mergeCandidates) {
709
+ const prNumber = extractPRFromMergeMessage(
710
+ candidate.subject,
711
+ options?.platform
712
+ );
621
713
  if (prNumber) {
622
714
  if (adapter) {
623
- const prInfo = await adapter.getPRForCommit(mergeResult.mergeCommitSha);
715
+ const prInfo = await adapter.getPRForCommit(
716
+ candidate.mergeCommitSha,
717
+ prSelectOptions
718
+ );
624
719
  if (prInfo?.mergedAt) {
625
- mergeBasedPR = prInfo;
720
+ mergeBasedPR = { ...prInfo, resolvedVia: "ancestry" };
626
721
  }
627
722
  }
628
723
  if (!mergeBasedPR) {
629
724
  mergeBasedPR = {
630
725
  number: prNumber,
631
- title: mergeResult.subject,
726
+ title: candidate.subject,
632
727
  author: "",
633
728
  url: "",
634
- mergeCommit: mergeResult.mergeCommitSha,
635
- baseBranch: ""
729
+ mergeCommit: candidate.mergeCommitSha,
730
+ baseBranch: "",
731
+ resolvedVia: "ancestry"
636
732
  };
637
733
  }
638
- if (!options?.deep || mergeBasedPR.mergedAt) {
639
- await cache.set(commitSha, toCachedPR(mergeBasedPR));
640
- return mergeBasedPR;
734
+ break;
735
+ }
736
+ if (adapter) {
737
+ const mergeCommitPR = await adapter.getPRForCommit(
738
+ candidate.mergeCommitSha,
739
+ prSelectOptions
740
+ );
741
+ if (mergeCommitPR?.mergedAt) {
742
+ mergeBasedPR = { ...mergeCommitPR, resolvedVia: "ancestry" };
743
+ break;
641
744
  }
642
745
  }
643
746
  }
644
747
  if (mergeBasedPR) {
645
- await cache.set(commitSha, toCachedPR(mergeBasedPR));
646
- return mergeBasedPR;
748
+ if (!options?.deep || mergeBasedPR.mergedAt) {
749
+ await cache.set(commitSha, toCachedPR(mergeBasedPR));
750
+ return mergeBasedPR;
751
+ }
647
752
  }
648
- if (adapter) {
649
- const prInfo = await adapter.getPRForCommit(commitSha);
650
- if (prInfo?.mergedAt) {
651
- await cache.set(commitSha, toCachedPR(prInfo));
652
- return prInfo;
753
+ const commitSubject = await getCommitSubject(commitSha, options);
754
+ if (commitSubject) {
755
+ const directPrNumber = extractPRFromMergeMessage(
756
+ commitSubject,
757
+ options?.platform
758
+ );
759
+ if (directPrNumber) {
760
+ const subjectPR = {
761
+ number: directPrNumber,
762
+ title: commitSubject,
763
+ author: "",
764
+ url: "",
765
+ mergeCommit: commitSha,
766
+ baseBranch: "",
767
+ resolvedVia: "message"
768
+ };
769
+ await cache.set(commitSha, toCachedPR(subjectPR));
770
+ return subjectPR;
653
771
  }
654
772
  }
655
- if (!options?.skipPatchIdScan && _recursionDepth < MAX_RECURSION_DEPTH) {
773
+ if (!options?.skipPatchIdScan && _recursionDepth < MAX_RECURSION_DEPTH && (!hasAncestryMerges || options?.deep)) {
656
774
  const patchIdMatch = await findPatchIdMatch(commitSha, {
657
775
  ...options,
658
776
  scanDepth: options?.deep ? DEEP_SCAN_DEPTH : void 0
@@ -670,6 +788,10 @@ async function lookupPR(commitSha, adapter, options, _recursionDepth = 0) {
670
788
  }
671
789
  }
672
790
  }
791
+ if (mergeBasedPR) {
792
+ await cache.set(commitSha, toCachedPR(mergeBasedPR));
793
+ return mergeBasedPR;
794
+ }
673
795
  return null;
674
796
  }
675
797
  function resetPRCache() {
@@ -721,6 +843,7 @@ init_errors();
721
843
  // src/core/core.ts
722
844
  init_cjs_shims();
723
845
  var import_node_crypto2 = require("crypto");
846
+ var import_node_path2 = require("path");
724
847
  var import_common_utils11 = require("@winglet/common-utils");
725
848
 
726
849
  // src/ast/index.ts
@@ -1004,7 +1127,7 @@ async function checkGitHealth(options) {
1004
1127
  const cloneStatus = await checkCloneStatus({ cwd: options?.cwd });
1005
1128
  if (cloneStatus.partialClone) {
1006
1129
  hints.push(
1007
- "Partial clone detected. Patch-ID scan (Strategy 4) will be skipped to avoid blob downloads."
1130
+ "Partial clone detected. Patch-ID scan (Strategy 5) will be skipped to avoid blob downloads."
1008
1131
  );
1009
1132
  }
1010
1133
  if (cloneStatus.shallow) {
@@ -1120,7 +1243,7 @@ var GitHubAdapter = class {
1120
1243
  return { authenticated: false, hostname: this.hostname };
1121
1244
  }
1122
1245
  }
1123
- async getPRForCommit(sha) {
1246
+ async getPRForCommit(sha, options) {
1124
1247
  if (this.scheduler.isRateLimited()) return null;
1125
1248
  try {
1126
1249
  const result = await shellExec(
@@ -1137,18 +1260,20 @@ var GitHubAdapter = class {
1137
1260
  );
1138
1261
  const prs = JSON.parse(result.stdout);
1139
1262
  if (!(0, import_common_utils3.isArray)(prs) || prs.length === 0) return null;
1140
- const defaultBranch = await this.detectDefaultBranch();
1141
- const defaultBranchPR = prs.find(
1142
- (pr) => pr.base === defaultBranch
1143
- );
1144
- const data = defaultBranchPR ?? prs[0];
1263
+ let data = prs[0];
1264
+ if (options?.preferredBase) {
1265
+ const preferred = prs.find(
1266
+ (pr) => pr.base === options.preferredBase
1267
+ );
1268
+ if (preferred) data = preferred;
1269
+ }
1145
1270
  return {
1146
1271
  number: data.number,
1147
1272
  title: data.title ?? "",
1148
1273
  author: data.user ?? "",
1149
1274
  url: data.html_url ?? "",
1150
1275
  mergeCommit: data.merge_commit_sha ?? sha,
1151
- baseBranch: data.base ?? defaultBranch,
1276
+ baseBranch: data.base ?? "",
1152
1277
  mergedAt: data.merged_at
1153
1278
  };
1154
1279
  } catch {
@@ -1330,7 +1455,7 @@ var GitLabAdapter = class {
1330
1455
  return { authenticated: false, hostname: this.hostname };
1331
1456
  }
1332
1457
  }
1333
- async getPRForCommit(sha) {
1458
+ async getPRForCommit(sha, options) {
1334
1459
  if (this.scheduler.isRateLimited()) return null;
1335
1460
  try {
1336
1461
  const result = await shellExec(
@@ -1354,18 +1479,20 @@ var GitLabAdapter = class {
1354
1479
  return aTime - bTime;
1355
1480
  });
1356
1481
  if (mergedMRs.length === 0) return null;
1357
- const defaultBranch = await this.detectDefaultBranch();
1358
- const defaultBranchMR = mergedMRs.find(
1359
- (mr2) => mr2.target_branch === defaultBranch
1360
- );
1361
- const mr = defaultBranchMR ?? mergedMRs[0];
1482
+ let mr = mergedMRs[0];
1483
+ if (options?.preferredBase) {
1484
+ const preferred = mergedMRs.find(
1485
+ (m) => m.target_branch === options.preferredBase
1486
+ );
1487
+ if (preferred) mr = preferred;
1488
+ }
1362
1489
  return {
1363
1490
  number: mr.iid,
1364
1491
  title: mr.title ?? "",
1365
1492
  author: mr.author?.username ?? "",
1366
1493
  url: mr.web_url ?? "",
1367
1494
  mergeCommit: mr.merge_commit_sha ?? sha,
1368
- baseBranch: mr.target_branch ?? defaultBranch,
1495
+ baseBranch: mr.target_branch ?? "",
1369
1496
  mergedAt: mr.merged_at
1370
1497
  };
1371
1498
  } catch {
@@ -1984,6 +2111,7 @@ function parsePorcelainOutput(output) {
1984
2111
  }
1985
2112
  let commitHash = headerMatch[1];
1986
2113
  const originalLine = parseInt(headerMatch[2], 10);
2114
+ const finalLine = parseInt(headerMatch[3], 10) || 0;
1987
2115
  const isBoundary = commitHash.startsWith("^");
1988
2116
  if (isBoundary) {
1989
2117
  commitHash = commitHash.slice(1).padStart(40, "0");
@@ -2027,6 +2155,7 @@ function parsePorcelainOutput(output) {
2027
2155
  authorEmail: cleanEmail,
2028
2156
  date,
2029
2157
  lineContent,
2158
+ finalLine,
2030
2159
  originalFile,
2031
2160
  originalLine: originalFile ? originalLine : void 0
2032
2161
  });
@@ -2037,10 +2166,8 @@ function parsePorcelainOutput(output) {
2037
2166
  // src/core/blame/blame.ts
2038
2167
  async function executeBlame(file, lineRange, options) {
2039
2168
  const lineSpec = `${lineRange.start},${lineRange.end}`;
2040
- const result = await gitExec(
2041
- ["blame", "-w", "-C", "-C", "-M", "--porcelain", "-L", lineSpec, file],
2042
- options
2043
- );
2169
+ const args = options?.mode === "change" ? ["blame", "-w", "--porcelain", "-L", lineSpec, file] : ["blame", "-w", "-C", "-C", "-M", "--porcelain", "-L", lineSpec, file];
2170
+ const result = await gitExec(args, options);
2044
2171
  return parsePorcelainOutput(result.stdout);
2045
2172
  }
2046
2173
  async function analyzeBlameResults(results, filePath, options) {
@@ -2173,6 +2300,28 @@ async function traverse(adapter, type, number, depth, maxDepth, nodes, edges, vi
2173
2300
 
2174
2301
  // src/core/core.ts
2175
2302
  init_pr_lookup2();
2303
+ function resolvedViaToTrackingMethod(resolvedVia) {
2304
+ switch (resolvedVia) {
2305
+ case "api":
2306
+ return "api";
2307
+ case "ancestry":
2308
+ return "ancestry-path";
2309
+ case "message":
2310
+ return "message-parse";
2311
+ case "patch-id":
2312
+ return "patch-id";
2313
+ }
2314
+ }
2315
+ function resolvedViaToConfidence(resolvedVia) {
2316
+ switch (resolvedVia) {
2317
+ case "api":
2318
+ case "ancestry":
2319
+ return "exact";
2320
+ case "message":
2321
+ case "patch-id":
2322
+ return "heuristic";
2323
+ }
2324
+ }
2176
2325
  function computeFeatureFlags(operatingLevel, options) {
2177
2326
  return {
2178
2327
  astDiff: isAstAvailable() && !options.noAst,
@@ -2190,6 +2339,22 @@ async function resolveRepoIdentity(cwd) {
2190
2339
  return { host: "_local", owner: "_", repo: "_unknown" };
2191
2340
  }
2192
2341
  }
2342
+ async function resolveFileContext(file, cwd) {
2343
+ if (cwd || !(0, import_node_path2.isAbsolute)(file)) return { file, cwd };
2344
+ const fileDir = (0, import_node_path2.dirname)(file);
2345
+ try {
2346
+ const result = await gitExec(["rev-parse", "--show-toplevel"], {
2347
+ cwd: fileDir
2348
+ });
2349
+ const repoRoot = result.stdout.trim();
2350
+ return {
2351
+ file: (0, import_node_path2.relative)(repoRoot, file),
2352
+ cwd: repoRoot
2353
+ };
2354
+ } catch {
2355
+ return { file, cwd };
2356
+ }
2357
+ }
2193
2358
  async function detectPlatform2(options) {
2194
2359
  const warnings = [];
2195
2360
  let adapter = null;
@@ -2213,9 +2378,10 @@ async function runBlameAndAuth(adapter, options, execOptions) {
2213
2378
  const lineRange = parseLineRange(
2214
2379
  options.endLine ? `${options.line},${options.endLine}` : `${options.line}`
2215
2380
  );
2216
- const blameChain = executeBlame(options.file, lineRange, execOptions).then(
2217
- (results) => analyzeBlameResults(results, options.file, execOptions)
2218
- );
2381
+ const blameChain = executeBlame(options.file, lineRange, {
2382
+ ...execOptions,
2383
+ mode: options.mode
2384
+ }).then((results) => analyzeBlameResults(results, options.file, execOptions));
2219
2385
  const [authResult, blameResult] = await Promise.allSettled([
2220
2386
  adapter ? adapter.checkAuth() : Promise.resolve({ authenticated: false }),
2221
2387
  blameChain
@@ -2236,12 +2402,24 @@ async function runBlameAndAuth(adapter, options, execOptions) {
2236
2402
  }
2237
2403
  return { analyzed: blameResult.value, operatingLevel, warnings };
2238
2404
  }
2239
- async function processEntry(entry, featureFlags, adapter, options, execOptions, repoId, skipPatchIdScan) {
2405
+ function resolveTraceMode(mode) {
2406
+ return mode ?? "origin";
2407
+ }
2408
+ function deduplicatedLookupPR(sha, adapter, options, inflight) {
2409
+ const existing = inflight.get(sha);
2410
+ if (existing) return existing;
2411
+ const promise = lookupPR(sha, adapter, options);
2412
+ inflight.set(sha, promise);
2413
+ promise.finally(() => inflight.delete(sha));
2414
+ return promise;
2415
+ }
2416
+ async function processEntry(entry, featureFlags, adapter, options, execOptions, repoId, inflightPR, skipPatchIdScan, preferredBase) {
2240
2417
  const nodes = [];
2418
+ const traceMode = resolveTraceMode(options.mode);
2241
2419
  const commitNode = {
2242
2420
  type: entry.isCosmetic ? "cosmetic_commit" : "original_commit",
2243
2421
  sha: entry.blame.commitHash,
2244
- trackingMethod: "blame-CMw",
2422
+ trackingMethod: traceMode === "change" ? "blame" : "blame-CMw",
2245
2423
  confidence: "exact",
2246
2424
  note: entry.cosmeticReason ? `Cosmetic change: ${entry.cosmeticReason}` : void 0
2247
2425
  };
@@ -2263,21 +2441,24 @@ async function processEntry(entry, featureFlags, adapter, options, execOptions,
2263
2441
  }
2264
2442
  }
2265
2443
  const targetSha = nodes[nodes.length - 1].sha;
2444
+ const prLookupOptions = {
2445
+ ...execOptions,
2446
+ noCache: options.noCache,
2447
+ cacheOnly: options.cacheOnly,
2448
+ deep: featureFlags.deepTrace,
2449
+ repoId,
2450
+ skipPatchIdScan,
2451
+ preferredBase,
2452
+ platform: adapter?.platform
2453
+ };
2266
2454
  if (targetSha) {
2267
- const prInfo = await lookupPR(targetSha, adapter, {
2268
- ...execOptions,
2269
- noCache: options.noCache,
2270
- cacheOnly: options.cacheOnly,
2271
- deep: featureFlags.deepTrace,
2272
- repoId,
2273
- skipPatchIdScan
2274
- });
2455
+ const prInfo = await deduplicatedLookupPR(targetSha, adapter, prLookupOptions, inflightPR);
2275
2456
  if (prInfo) {
2276
2457
  nodes.push({
2277
2458
  type: "pull_request",
2278
2459
  sha: prInfo.mergeCommit,
2279
- trackingMethod: prInfo.url ? "api" : "message-parse",
2280
- confidence: prInfo.url ? "exact" : "heuristic",
2460
+ trackingMethod: resolvedViaToTrackingMethod(prInfo.resolvedVia),
2461
+ confidence: resolvedViaToConfidence(prInfo.resolvedVia),
2281
2462
  prNumber: prInfo.number,
2282
2463
  prUrl: prInfo.url || void 0,
2283
2464
  prTitle: prInfo.title || void 0,
@@ -2287,7 +2468,8 @@ async function processEntry(entry, featureFlags, adapter, options, execOptions,
2287
2468
  }
2288
2469
  return nodes;
2289
2470
  }
2290
- async function buildTraceNodes(analyzed, featureFlags, adapter, options, execOptions, repoId, skipPatchIdScan) {
2471
+ async function buildTraceNodes(analyzed, featureFlags, adapter, options, execOptions, repoId, skipPatchIdScan, preferredBase) {
2472
+ const inflightPR = /* @__PURE__ */ new Map();
2291
2473
  const results = await Promise.allSettled(
2292
2474
  (0, import_common_utils11.map)(
2293
2475
  analyzed,
@@ -2298,7 +2480,9 @@ async function buildTraceNodes(analyzed, featureFlags, adapter, options, execOpt
2298
2480
  options,
2299
2481
  execOptions,
2300
2482
  repoId,
2301
- skipPatchIdScan
2483
+ inflightPR,
2484
+ skipPatchIdScan,
2485
+ preferredBase
2302
2486
  )
2303
2487
  )
2304
2488
  );
@@ -2306,13 +2490,16 @@ async function buildTraceNodes(analyzed, featureFlags, adapter, options, execOpt
2306
2490
  }
2307
2491
  var legacyCacheCleaned = false;
2308
2492
  async function trace(options) {
2309
- const execOptions = { cwd: options.cwd };
2493
+ const mode = resolveTraceMode(options.mode);
2494
+ const { file, cwd } = await resolveFileContext(options.file, options.cwd);
2495
+ const warnings = [];
2496
+ const execOptions = { cwd, warnings };
2310
2497
  if (!legacyCacheCleaned) {
2311
2498
  legacyCacheCleaned = true;
2312
2499
  cleanupLegacyCache().catch(() => {
2313
2500
  });
2314
2501
  }
2315
- const platform = await detectPlatform2(options);
2502
+ const platform = await detectPlatform2({ ...options, cwd });
2316
2503
  let repoId;
2317
2504
  if (platform.remote) {
2318
2505
  repoId = {
@@ -2321,15 +2508,15 @@ async function trace(options) {
2321
2508
  repo: platform.remote.repo
2322
2509
  };
2323
2510
  } else {
2324
- repoId = await resolveRepoIdentity(options.cwd);
2511
+ repoId = await resolveRepoIdentity(cwd);
2325
2512
  }
2326
2513
  const blameAuth = await runBlameAndAuth(
2327
2514
  platform.adapter,
2328
- options,
2515
+ { ...options, mode, file, cwd },
2329
2516
  execOptions
2330
2517
  );
2331
2518
  const operatingLevel = blameAuth.operatingLevel || platform.operatingLevel;
2332
- const warnings = [...platform.warnings, ...blameAuth.warnings];
2519
+ warnings.push(...platform.warnings, ...blameAuth.warnings);
2333
2520
  if (options.cacheOnly && options.noCache) {
2334
2521
  warnings.push(
2335
2522
  "Both cacheOnly and noCache are set. cacheOnly takes precedence \u2014 cache reads are enabled."
@@ -2338,13 +2525,13 @@ async function trace(options) {
2338
2525
  const featureFlags = computeFeatureFlags(operatingLevel, options);
2339
2526
  let cloneStatus = { partialClone: false, shallow: false };
2340
2527
  try {
2341
- const result = await checkCloneStatus({ cwd: options.cwd });
2528
+ const result = await checkCloneStatus({ cwd });
2342
2529
  if (result) cloneStatus = result;
2343
2530
  } catch {
2344
2531
  }
2345
2532
  if (cloneStatus.partialClone) {
2346
2533
  warnings.push(
2347
- "Partial clone detected. Patch-ID scan (Strategy 4) will be skipped to avoid blob downloads."
2534
+ "Partial clone detected. Patch-ID scan (Strategy 5) will be skipped to avoid blob downloads."
2348
2535
  );
2349
2536
  }
2350
2537
  if (cloneStatus.shallow) {
@@ -2352,14 +2539,27 @@ async function trace(options) {
2352
2539
  "Shallow repository detected. Ancestry-path results may be incomplete."
2353
2540
  );
2354
2541
  }
2542
+ let preferredBase;
2543
+ try {
2544
+ const branchResult = await gitExec(
2545
+ ["rev-parse", "--abbrev-ref", "HEAD"],
2546
+ execOptions
2547
+ );
2548
+ const branch = branchResult.stdout.trim();
2549
+ if (branch && branch !== "HEAD") {
2550
+ preferredBase = branch;
2551
+ }
2552
+ } catch {
2553
+ }
2355
2554
  const nodes = await buildTraceNodes(
2356
2555
  blameAuth.analyzed,
2357
2556
  featureFlags,
2358
2557
  platform.adapter,
2359
- options,
2558
+ { ...options, mode, file, cwd },
2360
2559
  execOptions,
2361
2560
  repoId,
2362
- cloneStatus.partialClone || void 0
2561
+ cloneStatus.partialClone || void 0,
2562
+ preferredBase
2363
2563
  );
2364
2564
  return { nodes, operatingLevel, featureFlags, warnings };
2365
2565
  }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export type { AstDiffStageResult, AstTraceResult, AuthStatus, BlameResult, BlameStageResult, CacheEntry, ChangeType, CommitInfo, ComparisonResult, Confidence, ContentHash, CosmeticReason, FeatureFlags, GraphOptions, GraphResult, HealthReport, IssueInfo, LineRange, NormalizedResponse, OperatingLevel, PlatformAdapter, PlatformType, PRInfo, RateLimitInfo, RemoteInfo, SymbolInfo, SymbolKind, TraceNode, TraceNodeType, TraceOptions, TraceResult, TrackingMethod, } from './types/index.js';
1
+ export type { AstDiffStageResult, AstTraceResult, AuthStatus, BlameResult, BlameStageResult, CacheEntry, ChangeType, CommitInfo, ComparisonResult, Confidence, ContentHash, CosmeticReason, FeatureFlags, GraphOptions, GraphResult, HealthReport, IssueInfo, LineRange, NormalizedResponse, OperatingLevel, PlatformAdapter, PlatformType, PRInfo, RateLimitInfo, RemoteInfo, SymbolInfo, SymbolKind, TraceNode, TraceNodeType, TraceMode, TraceOptions, TraceResult, TrackingMethod, } from './types/index.js';
2
2
  export { LineLoreError, LineLoreErrorCode } from './errors.js';
3
3
  export { clearCache, graph, health, trace } from './core/core.js';
4
4
  export type { TraceFullResult } from './core/core.js';