@lumy-pack/line-lore 0.0.5 → 0.0.7

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
@@ -370,14 +370,51 @@ var init_executor = __esm({
370
370
  import { filter as filter4, isTruthy as isTruthy4 } from "@winglet/common-utils";
371
371
  async function findMergeCommit(commitSha, options) {
372
372
  const ref = options?.ref ?? "HEAD";
373
+ const budget = options?.timeout ?? DEFAULT_ANCESTRY_TIMEOUT;
374
+ const startTime = Date.now();
373
375
  const firstParentResult = await findMergeCommitWithArgs(
374
376
  commitSha,
375
377
  ref,
376
378
  ["--first-parent"],
377
- options
379
+ { ...options, timeout: budget }
378
380
  );
379
381
  if (firstParentResult) return firstParentResult;
380
- return findMergeCommitWithArgs(commitSha, ref, [], options);
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
+ });
389
+ }
390
+ async function verifyMergeIntroducesCommit(targetSha, mergeResult, options) {
391
+ if (mergeResult.parentShas.length < 2) return true;
392
+ const firstParent = mergeResult.parentShas[0];
393
+ const branchParents = mergeResult.parentShas.slice(1);
394
+ const onMainline = await isAncestor(targetSha, firstParent, options);
395
+ if (onMainline === null) return false;
396
+ if (onMainline) return false;
397
+ for (const branchParent of branchParents) {
398
+ const onBranch = await isAncestor(targetSha, branchParent, options);
399
+ if (onBranch === null) return false;
400
+ if (onBranch) return true;
401
+ }
402
+ return false;
403
+ }
404
+ async function isAncestor(commitA, commitB, options) {
405
+ try {
406
+ const result = await gitExec(
407
+ ["merge-base", "--is-ancestor", commitA, commitB],
408
+ {
409
+ cwd: options?.cwd,
410
+ timeout: options?.timeout ?? 5e3,
411
+ allowExitCodes: [1]
412
+ }
413
+ );
414
+ return result.exitCode === 0;
415
+ } catch {
416
+ return null;
417
+ }
381
418
  }
382
419
  async function findMergeCommitWithArgs(commitSha, ref, extraArgs, options) {
383
420
  try {
@@ -396,7 +433,25 @@ async function findMergeCommitWithArgs(commitSha, ref, extraArgs, options) {
396
433
  );
397
434
  const lines = filter4(result.stdout.trim().split("\n"), isTruthy4);
398
435
  if (lines.length === 0) return null;
399
- return parseMergeLogLine(lines[0]);
436
+ const candidateCount = Math.min(lines.length, MAX_CANDIDATES);
437
+ let verifiedCount = 0;
438
+ for (let i = 0; i < candidateCount; i++) {
439
+ const candidate = parseMergeLogLine(lines[i]);
440
+ if (!candidate) continue;
441
+ verifiedCount++;
442
+ const verified = await verifyMergeIntroducesCommit(
443
+ commitSha,
444
+ candidate,
445
+ options
446
+ );
447
+ if (verified) return candidate;
448
+ }
449
+ if (verifiedCount > 0 && options?.warnings) {
450
+ options.warnings.push(
451
+ `ancestry: all ${verifiedCount} merge candidate(s) failed verification for ${commitSha.slice(0, 8)}`
452
+ );
453
+ }
454
+ return null;
400
455
  } catch {
401
456
  return null;
402
457
  }
@@ -418,20 +473,39 @@ function parseMergeLogLine(line) {
418
473
  const subject = parts.slice(subjectStart).join(" ");
419
474
  return { mergeCommitSha, parentShas, subject };
420
475
  }
421
- function extractPRFromMergeMessage(subject) {
476
+ async function getCommitSubject(sha, options) {
477
+ try {
478
+ const result = await gitExec(["log", "-1", "--format=%s", sha], {
479
+ cwd: options?.cwd,
480
+ timeout: options?.timeout ?? 5e3
481
+ });
482
+ const subject = result.stdout.trim();
483
+ return subject || null;
484
+ } catch {
485
+ return null;
486
+ }
487
+ }
488
+ function extractPRFromMergeMessage(subject, platform) {
422
489
  const ghMatch = /Merge pull request #(\d+)/.exec(subject);
423
490
  if (ghMatch) return parseInt(ghMatch[1], 10);
424
491
  const squashMatch = /\(#(\d+)\)\s*$/.exec(subject);
425
492
  if (squashMatch) return parseInt(squashMatch[1], 10);
426
- const glMatch = /!(\d+)\s*$/.exec(subject);
427
- if (glMatch) return parseInt(glMatch[1], 10);
493
+ if (!platform || platform === "gitlab" || platform === "gitlab-self-hosted") {
494
+ const glMatch = /See merge request\s+\S*!(\d+)\s*$/.exec(subject);
495
+ if (glMatch) return parseInt(glMatch[1], 10);
496
+ }
497
+ const adoMatch = /Merged PR (\d+):/.exec(subject);
498
+ if (adoMatch) return parseInt(adoMatch[1], 10);
428
499
  return null;
429
500
  }
501
+ var DEFAULT_ANCESTRY_TIMEOUT, MAX_CANDIDATES;
430
502
  var init_ancestry = __esm({
431
503
  "src/core/ancestry/ancestry.ts"() {
432
504
  "use strict";
433
505
  init_esm_shims();
434
506
  init_executor();
507
+ DEFAULT_ANCESTRY_TIMEOUT = 3e4;
508
+ MAX_CANDIDATES = 10;
435
509
  }
436
510
  });
437
511
 
@@ -554,19 +628,67 @@ function getCache2(repoId, noCache) {
554
628
  }
555
629
  return cache;
556
630
  }
557
- async function lookupPR(commitSha, adapter, options) {
558
- const cache = getCache2(options?.repoId, options?.noCache);
631
+ function toCachedPR(pr) {
632
+ return {
633
+ number: pr.number,
634
+ title: pr.title,
635
+ author: pr.author,
636
+ url: pr.url,
637
+ mergeCommit: pr.mergeCommit,
638
+ baseBranch: pr.baseBranch,
639
+ mergedAt: pr.mergedAt ? new Date(pr.mergedAt).getTime() : void 0,
640
+ resolvedVia: pr.resolvedVia
641
+ };
642
+ }
643
+ function fromCachedPR(cached) {
644
+ let mergedAt;
645
+ if (cached.mergedAt != null) {
646
+ mergedAt = typeof cached.mergedAt === "number" ? new Date(cached.mergedAt).toISOString() : String(cached.mergedAt);
647
+ }
648
+ return {
649
+ number: cached.number,
650
+ title: cached.title,
651
+ author: cached.author,
652
+ url: cached.url,
653
+ mergeCommit: cached.mergeCommit,
654
+ baseBranch: cached.baseBranch,
655
+ mergedAt,
656
+ // Preserve original resolvedVia; fallback to url heuristic for legacy cache entries
657
+ resolvedVia: cached.resolvedVia ?? (cached.url ? "api" : "message")
658
+ };
659
+ }
660
+ async function lookupPR(commitSha, adapter, options, _recursionDepth = 0) {
661
+ const cache = getCache2(
662
+ options?.repoId,
663
+ options?.cacheOnly ? false : options?.noCache
664
+ );
559
665
  const cached = await cache.get(commitSha);
560
- if (cached) return cached;
666
+ if (cached) return fromCachedPR(cached);
667
+ if (options?.cacheOnly) return null;
668
+ const prSelectOptions = options?.preferredBase ? { preferredBase: options.preferredBase } : void 0;
669
+ if (adapter) {
670
+ const directPR = await adapter.getPRForCommit(commitSha, prSelectOptions);
671
+ if (directPR?.mergedAt) {
672
+ const result = { ...directPR, resolvedVia: "api" };
673
+ await cache.set(commitSha, toCachedPR(result));
674
+ return result;
675
+ }
676
+ }
561
677
  let mergeBasedPR = null;
562
678
  const mergeResult = await findMergeCommit(commitSha, options);
563
679
  if (mergeResult) {
564
- const prNumber = extractPRFromMergeMessage(mergeResult.subject);
680
+ const prNumber = extractPRFromMergeMessage(
681
+ mergeResult.subject,
682
+ options?.platform
683
+ );
565
684
  if (prNumber) {
566
685
  if (adapter) {
567
- const prInfo = await adapter.getPRForCommit(mergeResult.mergeCommitSha);
686
+ const prInfo = await adapter.getPRForCommit(
687
+ mergeResult.mergeCommitSha,
688
+ prSelectOptions
689
+ );
568
690
  if (prInfo?.mergedAt) {
569
- mergeBasedPR = prInfo;
691
+ mergeBasedPR = { ...prInfo, resolvedVia: "ancestry" };
570
692
  }
571
693
  }
572
694
  if (!mergeBasedPR) {
@@ -576,35 +698,56 @@ async function lookupPR(commitSha, adapter, options) {
576
698
  author: "",
577
699
  url: "",
578
700
  mergeCommit: mergeResult.mergeCommitSha,
579
- baseBranch: ""
701
+ baseBranch: "",
702
+ resolvedVia: "ancestry"
580
703
  };
581
704
  }
582
705
  if (!options?.deep || mergeBasedPR.mergedAt) {
583
- await cache.set(commitSha, mergeBasedPR);
706
+ await cache.set(commitSha, toCachedPR(mergeBasedPR));
584
707
  return mergeBasedPR;
585
708
  }
586
709
  }
587
710
  }
588
711
  if (mergeBasedPR) {
589
- await cache.set(commitSha, mergeBasedPR);
712
+ await cache.set(commitSha, toCachedPR(mergeBasedPR));
590
713
  return mergeBasedPR;
591
714
  }
592
- if (adapter) {
593
- const prInfo = await adapter.getPRForCommit(commitSha);
594
- if (prInfo?.mergedAt) {
595
- await cache.set(commitSha, prInfo);
596
- return prInfo;
715
+ const commitSubject = await getCommitSubject(commitSha, options);
716
+ if (commitSubject) {
717
+ const directPrNumber = extractPRFromMergeMessage(
718
+ commitSubject,
719
+ options?.platform
720
+ );
721
+ if (directPrNumber) {
722
+ const subjectPR = {
723
+ number: directPrNumber,
724
+ title: commitSubject,
725
+ author: "",
726
+ url: "",
727
+ mergeCommit: commitSha,
728
+ baseBranch: "",
729
+ resolvedVia: "message"
730
+ };
731
+ await cache.set(commitSha, toCachedPR(subjectPR));
732
+ return subjectPR;
597
733
  }
598
734
  }
599
- const patchIdMatch = await findPatchIdMatch(commitSha, {
600
- ...options,
601
- scanDepth: options?.deep ? DEEP_SCAN_DEPTH : void 0
602
- });
603
- if (patchIdMatch) {
604
- const result = await lookupPR(patchIdMatch.matchedSha, adapter, options);
605
- if (result) {
606
- await cache.set(commitSha, result);
607
- return result;
735
+ if (!options?.skipPatchIdScan && _recursionDepth < MAX_RECURSION_DEPTH) {
736
+ const patchIdMatch = await findPatchIdMatch(commitSha, {
737
+ ...options,
738
+ scanDepth: options?.deep ? DEEP_SCAN_DEPTH : void 0
739
+ });
740
+ if (patchIdMatch) {
741
+ const result = await lookupPR(
742
+ patchIdMatch.matchedSha,
743
+ adapter,
744
+ options,
745
+ _recursionDepth + 1
746
+ );
747
+ if (result) {
748
+ await cache.set(commitSha, toCachedPR(result));
749
+ return result;
750
+ }
608
751
  }
609
752
  }
610
753
  return null;
@@ -612,7 +755,7 @@ async function lookupPR(commitSha, adapter, options) {
612
755
  function resetPRCache() {
613
756
  cacheRegistry2.clear();
614
757
  }
615
- var cacheRegistry2, DEEP_SCAN_DEPTH;
758
+ var cacheRegistry2, DEEP_SCAN_DEPTH, MAX_RECURSION_DEPTH;
616
759
  var init_pr_lookup = __esm({
617
760
  "src/core/pr-lookup/pr-lookup.ts"() {
618
761
  "use strict";
@@ -622,6 +765,7 @@ var init_pr_lookup = __esm({
622
765
  init_patch_id2();
623
766
  cacheRegistry2 = /* @__PURE__ */ new Map();
624
767
  DEEP_SCAN_DEPTH = 2e3;
768
+ MAX_RECURSION_DEPTH = 2;
625
769
  }
626
770
  });
627
771
 
@@ -646,6 +790,7 @@ init_errors();
646
790
  // src/core/core.ts
647
791
  init_esm_shims();
648
792
  import { createHash as createHash2 } from "crypto";
793
+ import { dirname, isAbsolute, relative } from "path";
649
794
  import { map as map8 } from "@winglet/common-utils";
650
795
 
651
796
  // src/ast/index.ts
@@ -879,6 +1024,27 @@ function isVersionAtLeast(version, minVersion) {
879
1024
  }
880
1025
  return true;
881
1026
  }
1027
+ async function checkCloneStatus(options) {
1028
+ let partialClone = false;
1029
+ let shallow = false;
1030
+ try {
1031
+ const shallowResult = await gitExec(
1032
+ ["rev-parse", "--is-shallow-repository"],
1033
+ { cwd: options?.cwd }
1034
+ );
1035
+ shallow = shallowResult.stdout.trim() === "true";
1036
+ } catch {
1037
+ }
1038
+ try {
1039
+ const partialResult = await gitExec(
1040
+ ["config", "--get", "extensions.partialclone"],
1041
+ { cwd: options?.cwd }
1042
+ );
1043
+ partialClone = partialResult.stdout.trim().length > 0;
1044
+ } catch {
1045
+ }
1046
+ return { partialClone, shallow };
1047
+ }
882
1048
  async function checkGitHealth(options) {
883
1049
  const hints = [];
884
1050
  let gitVersion = "0.0.0";
@@ -905,7 +1071,18 @@ async function checkGitHealth(options) {
905
1071
  `Upgrade git to ${BLOOM_FILTER_MIN_VERSION.join(".")}+ for bloom filter support (current: ${gitVersion}).`
906
1072
  );
907
1073
  }
908
- return { commitGraph, bloomFilter, gitVersion, hints };
1074
+ const cloneStatus = await checkCloneStatus({ cwd: options?.cwd });
1075
+ if (cloneStatus.partialClone) {
1076
+ hints.push(
1077
+ "Partial clone detected. Patch-ID scan (Strategy 5) will be skipped to avoid blob downloads."
1078
+ );
1079
+ }
1080
+ if (cloneStatus.shallow) {
1081
+ hints.push(
1082
+ "Shallow repository detected. Ancestry-path results may be incomplete."
1083
+ );
1084
+ }
1085
+ return { commitGraph, bloomFilter, gitVersion, hints, ...cloneStatus };
909
1086
  }
910
1087
 
911
1088
  // src/platform/index.ts
@@ -1013,7 +1190,7 @@ var GitHubAdapter = class {
1013
1190
  return { authenticated: false, hostname: this.hostname };
1014
1191
  }
1015
1192
  }
1016
- async getPRForCommit(sha) {
1193
+ async getPRForCommit(sha, options) {
1017
1194
  if (this.scheduler.isRateLimited()) return null;
1018
1195
  try {
1019
1196
  const result = await shellExec(
@@ -1030,18 +1207,20 @@ var GitHubAdapter = class {
1030
1207
  );
1031
1208
  const prs = JSON.parse(result.stdout);
1032
1209
  if (!isArray(prs) || prs.length === 0) return null;
1033
- const defaultBranch = await this.detectDefaultBranch();
1034
- const defaultBranchPR = prs.find(
1035
- (pr) => pr.base === defaultBranch
1036
- );
1037
- const data = defaultBranchPR ?? prs[0];
1210
+ let data = prs[0];
1211
+ if (options?.preferredBase) {
1212
+ const preferred = prs.find(
1213
+ (pr) => pr.base === options.preferredBase
1214
+ );
1215
+ if (preferred) data = preferred;
1216
+ }
1038
1217
  return {
1039
1218
  number: data.number,
1040
1219
  title: data.title ?? "",
1041
1220
  author: data.user ?? "",
1042
1221
  url: data.html_url ?? "",
1043
1222
  mergeCommit: data.merge_commit_sha ?? sha,
1044
- baseBranch: data.base ?? defaultBranch,
1223
+ baseBranch: data.base ?? "",
1045
1224
  mergedAt: data.merged_at
1046
1225
  };
1047
1226
  } catch {
@@ -1223,7 +1402,7 @@ var GitLabAdapter = class {
1223
1402
  return { authenticated: false, hostname: this.hostname };
1224
1403
  }
1225
1404
  }
1226
- async getPRForCommit(sha) {
1405
+ async getPRForCommit(sha, options) {
1227
1406
  if (this.scheduler.isRateLimited()) return null;
1228
1407
  try {
1229
1408
  const result = await shellExec(
@@ -1247,18 +1426,20 @@ var GitLabAdapter = class {
1247
1426
  return aTime - bTime;
1248
1427
  });
1249
1428
  if (mergedMRs.length === 0) return null;
1250
- const defaultBranch = await this.detectDefaultBranch();
1251
- const defaultBranchMR = mergedMRs.find(
1252
- (mr2) => mr2.target_branch === defaultBranch
1253
- );
1254
- const mr = defaultBranchMR ?? mergedMRs[0];
1429
+ let mr = mergedMRs[0];
1430
+ if (options?.preferredBase) {
1431
+ const preferred = mergedMRs.find(
1432
+ (m) => m.target_branch === options.preferredBase
1433
+ );
1434
+ if (preferred) mr = preferred;
1435
+ }
1255
1436
  return {
1256
1437
  number: mr.iid,
1257
1438
  title: mr.title ?? "",
1258
1439
  author: mr.author?.username ?? "",
1259
1440
  url: mr.web_url ?? "",
1260
1441
  mergeCommit: mr.merge_commit_sha ?? sha,
1261
- baseBranch: mr.target_branch ?? defaultBranch,
1442
+ baseBranch: mr.target_branch ?? "",
1262
1443
  mergedAt: mr.merged_at
1263
1444
  };
1264
1445
  } catch {
@@ -2066,6 +2247,28 @@ async function traverse(adapter, type, number, depth, maxDepth, nodes, edges, vi
2066
2247
 
2067
2248
  // src/core/core.ts
2068
2249
  init_pr_lookup2();
2250
+ function resolvedViaToTrackingMethod(resolvedVia) {
2251
+ switch (resolvedVia) {
2252
+ case "api":
2253
+ return "api";
2254
+ case "ancestry":
2255
+ return "ancestry-path";
2256
+ case "message":
2257
+ return "message-parse";
2258
+ case "patch-id":
2259
+ return "patch-id";
2260
+ }
2261
+ }
2262
+ function resolvedViaToConfidence(resolvedVia) {
2263
+ switch (resolvedVia) {
2264
+ case "api":
2265
+ case "ancestry":
2266
+ return "exact";
2267
+ case "message":
2268
+ case "patch-id":
2269
+ return "heuristic";
2270
+ }
2271
+ }
2069
2272
  function computeFeatureFlags(operatingLevel, options) {
2070
2273
  return {
2071
2274
  astDiff: isAstAvailable() && !options.noAst,
@@ -2083,6 +2286,22 @@ async function resolveRepoIdentity(cwd) {
2083
2286
  return { host: "_local", owner: "_", repo: "_unknown" };
2084
2287
  }
2085
2288
  }
2289
+ async function resolveFileContext(file, cwd) {
2290
+ if (cwd || !isAbsolute(file)) return { file, cwd };
2291
+ const fileDir = dirname(file);
2292
+ try {
2293
+ const result = await gitExec(["rev-parse", "--show-toplevel"], {
2294
+ cwd: fileDir
2295
+ });
2296
+ const repoRoot = result.stdout.trim();
2297
+ return {
2298
+ file: relative(repoRoot, file),
2299
+ cwd: repoRoot
2300
+ };
2301
+ } catch {
2302
+ return { file, cwd };
2303
+ }
2304
+ }
2086
2305
  async function detectPlatform2(options) {
2087
2306
  const warnings = [];
2088
2307
  let adapter = null;
@@ -2129,7 +2348,7 @@ async function runBlameAndAuth(adapter, options, execOptions) {
2129
2348
  }
2130
2349
  return { analyzed: blameResult.value, operatingLevel, warnings };
2131
2350
  }
2132
- async function processEntry(entry, featureFlags, adapter, options, execOptions, repoId) {
2351
+ async function processEntry(entry, featureFlags, adapter, options, execOptions, repoId, skipPatchIdScan, preferredBase) {
2133
2352
  const nodes = [];
2134
2353
  const commitNode = {
2135
2354
  type: entry.isCosmetic ? "cosmetic_commit" : "original_commit",
@@ -2160,15 +2379,19 @@ async function processEntry(entry, featureFlags, adapter, options, execOptions,
2160
2379
  const prInfo = await lookupPR(targetSha, adapter, {
2161
2380
  ...execOptions,
2162
2381
  noCache: options.noCache,
2382
+ cacheOnly: options.cacheOnly,
2163
2383
  deep: featureFlags.deepTrace,
2164
- repoId
2384
+ repoId,
2385
+ skipPatchIdScan,
2386
+ preferredBase,
2387
+ platform: adapter?.platform
2165
2388
  });
2166
2389
  if (prInfo) {
2167
2390
  nodes.push({
2168
2391
  type: "pull_request",
2169
2392
  sha: prInfo.mergeCommit,
2170
- trackingMethod: prInfo.url ? "api" : "message-parse",
2171
- confidence: prInfo.url ? "exact" : "heuristic",
2393
+ trackingMethod: resolvedViaToTrackingMethod(prInfo.resolvedVia),
2394
+ confidence: resolvedViaToConfidence(prInfo.resolvedVia),
2172
2395
  prNumber: prInfo.number,
2173
2396
  prUrl: prInfo.url || void 0,
2174
2397
  prTitle: prInfo.title || void 0,
@@ -2178,24 +2401,35 @@ async function processEntry(entry, featureFlags, adapter, options, execOptions,
2178
2401
  }
2179
2402
  return nodes;
2180
2403
  }
2181
- async function buildTraceNodes(analyzed, featureFlags, adapter, options, execOptions, repoId) {
2404
+ async function buildTraceNodes(analyzed, featureFlags, adapter, options, execOptions, repoId, skipPatchIdScan, preferredBase) {
2182
2405
  const results = await Promise.allSettled(
2183
2406
  map8(
2184
2407
  analyzed,
2185
- (entry) => processEntry(entry, featureFlags, adapter, options, execOptions, repoId)
2408
+ (entry) => processEntry(
2409
+ entry,
2410
+ featureFlags,
2411
+ adapter,
2412
+ options,
2413
+ execOptions,
2414
+ repoId,
2415
+ skipPatchIdScan,
2416
+ preferredBase
2417
+ )
2186
2418
  )
2187
2419
  );
2188
2420
  return results.flatMap((r) => r.status === "fulfilled" ? r.value : []);
2189
2421
  }
2190
2422
  var legacyCacheCleaned = false;
2191
2423
  async function trace(options) {
2192
- const execOptions = { cwd: options.cwd };
2424
+ const { file, cwd } = await resolveFileContext(options.file, options.cwd);
2425
+ const warnings = [];
2426
+ const execOptions = { cwd, warnings };
2193
2427
  if (!legacyCacheCleaned) {
2194
2428
  legacyCacheCleaned = true;
2195
2429
  cleanupLegacyCache().catch(() => {
2196
2430
  });
2197
2431
  }
2198
- const platform = await detectPlatform2(options);
2432
+ const platform = await detectPlatform2({ ...options, cwd });
2199
2433
  let repoId;
2200
2434
  if (platform.remote) {
2201
2435
  repoId = {
@@ -2204,23 +2438,58 @@ async function trace(options) {
2204
2438
  repo: platform.remote.repo
2205
2439
  };
2206
2440
  } else {
2207
- repoId = await resolveRepoIdentity(options.cwd);
2441
+ repoId = await resolveRepoIdentity(cwd);
2208
2442
  }
2209
2443
  const blameAuth = await runBlameAndAuth(
2210
2444
  platform.adapter,
2211
- options,
2445
+ { ...options, file, cwd },
2212
2446
  execOptions
2213
2447
  );
2214
2448
  const operatingLevel = blameAuth.operatingLevel || platform.operatingLevel;
2215
- const warnings = [...platform.warnings, ...blameAuth.warnings];
2449
+ warnings.push(...platform.warnings, ...blameAuth.warnings);
2450
+ if (options.cacheOnly && options.noCache) {
2451
+ warnings.push(
2452
+ "Both cacheOnly and noCache are set. cacheOnly takes precedence \u2014 cache reads are enabled."
2453
+ );
2454
+ }
2216
2455
  const featureFlags = computeFeatureFlags(operatingLevel, options);
2456
+ let cloneStatus = { partialClone: false, shallow: false };
2457
+ try {
2458
+ const result = await checkCloneStatus({ cwd });
2459
+ if (result) cloneStatus = result;
2460
+ } catch {
2461
+ }
2462
+ if (cloneStatus.partialClone) {
2463
+ warnings.push(
2464
+ "Partial clone detected. Patch-ID scan (Strategy 5) will be skipped to avoid blob downloads."
2465
+ );
2466
+ }
2467
+ if (cloneStatus.shallow) {
2468
+ warnings.push(
2469
+ "Shallow repository detected. Ancestry-path results may be incomplete."
2470
+ );
2471
+ }
2472
+ let preferredBase;
2473
+ try {
2474
+ const branchResult = await gitExec(
2475
+ ["rev-parse", "--abbrev-ref", "HEAD"],
2476
+ execOptions
2477
+ );
2478
+ const branch = branchResult.stdout.trim();
2479
+ if (branch && branch !== "HEAD") {
2480
+ preferredBase = branch;
2481
+ }
2482
+ } catch {
2483
+ }
2217
2484
  const nodes = await buildTraceNodes(
2218
2485
  blameAuth.analyzed,
2219
2486
  featureFlags,
2220
2487
  platform.adapter,
2221
- options,
2488
+ { ...options, file, cwd },
2222
2489
  execOptions,
2223
- repoId
2490
+ repoId,
2491
+ cloneStatus.partialClone || void 0,
2492
+ preferredBase
2224
2493
  );
2225
2494
  return { nodes, operatingLevel, featureFlags, warnings };
2226
2495
  }
@@ -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[]>;
@@ -3,3 +3,16 @@ export interface CacheEntry<T> {
3
3
  value: T;
4
4
  createdAt: number;
5
5
  }
6
+ /** Disk-serialized PRInfo — date fields stored as numeric timestamps (ms) */
7
+ export interface CachedPRInfo {
8
+ number: number;
9
+ title: string;
10
+ author: string;
11
+ url: string;
12
+ mergeCommit: string;
13
+ baseBranch: string;
14
+ /** Unix timestamp in milliseconds, NOT ISO 8601 string */
15
+ mergedAt?: number;
16
+ /** Which strategy resolved this PR — absent in legacy cache entries */
17
+ resolvedVia?: string;
18
+ }
@@ -8,6 +8,8 @@ export interface GitExecOptions {
8
8
  cwd?: string;
9
9
  timeout?: number;
10
10
  allowExitCodes?: number[];
11
+ /** Mutable array for collecting diagnostic warnings throughout the pipeline */
12
+ warnings?: string[];
11
13
  }
12
14
  export interface RemoteInfo {
13
15
  owner: string;
@@ -15,9 +17,15 @@ export interface RemoteInfo {
15
17
  host: string;
16
18
  platform: PlatformType | 'unknown';
17
19
  }
20
+ export interface CloneStatus {
21
+ partialClone: boolean;
22
+ shallow: boolean;
23
+ }
18
24
  export interface HealthReport {
19
25
  commitGraph: boolean;
20
26
  bloomFilter: boolean;
21
27
  gitVersion: string;
22
28
  hints: string[];
29
+ partialClone: boolean;
30
+ shallow: boolean;
23
31
  }
@@ -1,7 +1,7 @@
1
1
  export type { SymbolKind, SymbolInfo, ContentHash, ChangeType, ComparisonResult, AstTraceResult, } from './ast.js';
2
2
  export type { BlameResult, CommitInfo } from './blame.js';
3
- export type { CacheEntry } from './cache.js';
4
- export type { GitExecResult, GitExecOptions, RemoteInfo, HealthReport, } from './git.js';
3
+ export type { CacheEntry, CachedPRInfo } from './cache.js';
4
+ export type { GitExecResult, GitExecOptions, RemoteInfo, HealthReport, CloneStatus, } from './git.js';
5
5
  export type { GraphOptions, GraphResult } from './graph.js';
6
6
  export type { NormalizedResponse } from './output.js';
7
7
  export type { TraceNodeType, TrackingMethod, Confidence, TraceNode, OperatingLevel, FeatureFlags, } from './pipeline.js';
@@ -36,7 +36,9 @@ export interface RateLimitInfo {
36
36
  export interface PlatformAdapter {
37
37
  readonly platform: PlatformType;
38
38
  checkAuth(): Promise<AuthStatus>;
39
- getPRForCommit(sha: string): Promise<PRInfo | null>;
39
+ getPRForCommit(sha: string, options?: {
40
+ preferredBase?: string;
41
+ }): Promise<PRInfo | null>;
40
42
  getPRCommits(prNumber: number): Promise<string[]>;
41
43
  getLinkedIssues(prNumber: number): Promise<IssueInfo[]>;
42
44
  getLinkedPRs(issueNumber: number): Promise<PRInfo[]>;