@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.
@@ -1,4 +1,7 @@
1
- import type { HealthReport } from '../types/index.js';
1
+ import type { CloneStatus, HealthReport } from '../types/index.js';
2
+ export declare function checkCloneStatus(options?: {
3
+ cwd?: string;
4
+ }): Promise<CloneStatus>;
2
5
  export declare function checkGitHealth(options?: {
3
6
  cwd?: string;
4
7
  }): Promise<HealthReport>;
@@ -1,3 +1,3 @@
1
1
  export { gitExec, gitPipe, shellExec } from './executor.js';
2
2
  export { detectPlatform, getRemoteInfo, parseRemoteUrl } from './remote.js';
3
- export { checkGitHealth } from './health.js';
3
+ export { checkCloneStatus, checkGitHealth } from './health.js';
package/dist/index.cjs CHANGED
@@ -383,14 +383,51 @@ var init_executor = __esm({
383
383
  // src/core/ancestry/ancestry.ts
384
384
  async function findMergeCommit(commitSha, options) {
385
385
  const ref = options?.ref ?? "HEAD";
386
+ const budget = options?.timeout ?? DEFAULT_ANCESTRY_TIMEOUT;
387
+ const startTime = Date.now();
386
388
  const firstParentResult = await findMergeCommitWithArgs(
387
389
  commitSha,
388
390
  ref,
389
391
  ["--first-parent"],
390
- options
392
+ { ...options, timeout: budget }
391
393
  );
392
394
  if (firstParentResult) return firstParentResult;
393
- return findMergeCommitWithArgs(commitSha, ref, [], options);
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
+ });
402
+ }
403
+ async function verifyMergeIntroducesCommit(targetSha, mergeResult, options) {
404
+ if (mergeResult.parentShas.length < 2) return true;
405
+ const firstParent = mergeResult.parentShas[0];
406
+ const branchParents = mergeResult.parentShas.slice(1);
407
+ const onMainline = await isAncestor(targetSha, firstParent, options);
408
+ if (onMainline === null) return false;
409
+ if (onMainline) return false;
410
+ for (const branchParent of branchParents) {
411
+ const onBranch = await isAncestor(targetSha, branchParent, options);
412
+ if (onBranch === null) return false;
413
+ if (onBranch) return true;
414
+ }
415
+ return false;
416
+ }
417
+ async function isAncestor(commitA, commitB, options) {
418
+ try {
419
+ const result = await gitExec(
420
+ ["merge-base", "--is-ancestor", commitA, commitB],
421
+ {
422
+ cwd: options?.cwd,
423
+ timeout: options?.timeout ?? 5e3,
424
+ allowExitCodes: [1]
425
+ }
426
+ );
427
+ return result.exitCode === 0;
428
+ } catch {
429
+ return null;
430
+ }
394
431
  }
395
432
  async function findMergeCommitWithArgs(commitSha, ref, extraArgs, options) {
396
433
  try {
@@ -409,7 +446,25 @@ async function findMergeCommitWithArgs(commitSha, ref, extraArgs, options) {
409
446
  );
410
447
  const lines = (0, import_common_utils9.filter)(result.stdout.trim().split("\n"), import_common_utils9.isTruthy);
411
448
  if (lines.length === 0) return null;
412
- return parseMergeLogLine(lines[0]);
449
+ const candidateCount = Math.min(lines.length, MAX_CANDIDATES);
450
+ let verifiedCount = 0;
451
+ for (let i = 0; i < candidateCount; i++) {
452
+ const candidate = parseMergeLogLine(lines[i]);
453
+ if (!candidate) continue;
454
+ verifiedCount++;
455
+ const verified = await verifyMergeIntroducesCommit(
456
+ commitSha,
457
+ candidate,
458
+ options
459
+ );
460
+ if (verified) return candidate;
461
+ }
462
+ if (verifiedCount > 0 && options?.warnings) {
463
+ options.warnings.push(
464
+ `ancestry: all ${verifiedCount} merge candidate(s) failed verification for ${commitSha.slice(0, 8)}`
465
+ );
466
+ }
467
+ return null;
413
468
  } catch {
414
469
  return null;
415
470
  }
@@ -431,22 +486,40 @@ function parseMergeLogLine(line) {
431
486
  const subject = parts.slice(subjectStart).join(" ");
432
487
  return { mergeCommitSha, parentShas, subject };
433
488
  }
434
- function extractPRFromMergeMessage(subject) {
489
+ async function getCommitSubject(sha, options) {
490
+ try {
491
+ const result = await gitExec(["log", "-1", "--format=%s", sha], {
492
+ cwd: options?.cwd,
493
+ timeout: options?.timeout ?? 5e3
494
+ });
495
+ const subject = result.stdout.trim();
496
+ return subject || null;
497
+ } catch {
498
+ return null;
499
+ }
500
+ }
501
+ function extractPRFromMergeMessage(subject, platform) {
435
502
  const ghMatch = /Merge pull request #(\d+)/.exec(subject);
436
503
  if (ghMatch) return parseInt(ghMatch[1], 10);
437
504
  const squashMatch = /\(#(\d+)\)\s*$/.exec(subject);
438
505
  if (squashMatch) return parseInt(squashMatch[1], 10);
439
- const glMatch = /!(\d+)\s*$/.exec(subject);
440
- if (glMatch) return parseInt(glMatch[1], 10);
506
+ if (!platform || platform === "gitlab" || platform === "gitlab-self-hosted") {
507
+ const glMatch = /See merge request\s+\S*!(\d+)\s*$/.exec(subject);
508
+ if (glMatch) return parseInt(glMatch[1], 10);
509
+ }
510
+ const adoMatch = /Merged PR (\d+):/.exec(subject);
511
+ if (adoMatch) return parseInt(adoMatch[1], 10);
441
512
  return null;
442
513
  }
443
- var import_common_utils9;
514
+ var import_common_utils9, DEFAULT_ANCESTRY_TIMEOUT, MAX_CANDIDATES;
444
515
  var init_ancestry = __esm({
445
516
  "src/core/ancestry/ancestry.ts"() {
446
517
  "use strict";
447
518
  init_cjs_shims();
448
519
  import_common_utils9 = require("@winglet/common-utils");
449
520
  init_executor();
521
+ DEFAULT_ANCESTRY_TIMEOUT = 3e4;
522
+ MAX_CANDIDATES = 10;
450
523
  }
451
524
  });
452
525
 
@@ -569,19 +642,67 @@ function getCache2(repoId, noCache) {
569
642
  }
570
643
  return cache;
571
644
  }
572
- async function lookupPR(commitSha, adapter, options) {
573
- const cache = getCache2(options?.repoId, options?.noCache);
645
+ function toCachedPR(pr) {
646
+ return {
647
+ number: pr.number,
648
+ title: pr.title,
649
+ author: pr.author,
650
+ url: pr.url,
651
+ mergeCommit: pr.mergeCommit,
652
+ baseBranch: pr.baseBranch,
653
+ mergedAt: pr.mergedAt ? new Date(pr.mergedAt).getTime() : void 0,
654
+ resolvedVia: pr.resolvedVia
655
+ };
656
+ }
657
+ function fromCachedPR(cached) {
658
+ let mergedAt;
659
+ if (cached.mergedAt != null) {
660
+ mergedAt = typeof cached.mergedAt === "number" ? new Date(cached.mergedAt).toISOString() : String(cached.mergedAt);
661
+ }
662
+ return {
663
+ number: cached.number,
664
+ title: cached.title,
665
+ author: cached.author,
666
+ url: cached.url,
667
+ mergeCommit: cached.mergeCommit,
668
+ baseBranch: cached.baseBranch,
669
+ mergedAt,
670
+ // Preserve original resolvedVia; fallback to url heuristic for legacy cache entries
671
+ resolvedVia: cached.resolvedVia ?? (cached.url ? "api" : "message")
672
+ };
673
+ }
674
+ async function lookupPR(commitSha, adapter, options, _recursionDepth = 0) {
675
+ const cache = getCache2(
676
+ options?.repoId,
677
+ options?.cacheOnly ? false : options?.noCache
678
+ );
574
679
  const cached = await cache.get(commitSha);
575
- if (cached) return cached;
680
+ if (cached) return fromCachedPR(cached);
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
+ }
576
691
  let mergeBasedPR = null;
577
692
  const mergeResult = await findMergeCommit(commitSha, options);
578
693
  if (mergeResult) {
579
- const prNumber = extractPRFromMergeMessage(mergeResult.subject);
694
+ const prNumber = extractPRFromMergeMessage(
695
+ mergeResult.subject,
696
+ options?.platform
697
+ );
580
698
  if (prNumber) {
581
699
  if (adapter) {
582
- const prInfo = await adapter.getPRForCommit(mergeResult.mergeCommitSha);
700
+ const prInfo = await adapter.getPRForCommit(
701
+ mergeResult.mergeCommitSha,
702
+ prSelectOptions
703
+ );
583
704
  if (prInfo?.mergedAt) {
584
- mergeBasedPR = prInfo;
705
+ mergeBasedPR = { ...prInfo, resolvedVia: "ancestry" };
585
706
  }
586
707
  }
587
708
  if (!mergeBasedPR) {
@@ -591,35 +712,56 @@ async function lookupPR(commitSha, adapter, options) {
591
712
  author: "",
592
713
  url: "",
593
714
  mergeCommit: mergeResult.mergeCommitSha,
594
- baseBranch: ""
715
+ baseBranch: "",
716
+ resolvedVia: "ancestry"
595
717
  };
596
718
  }
597
719
  if (!options?.deep || mergeBasedPR.mergedAt) {
598
- await cache.set(commitSha, mergeBasedPR);
720
+ await cache.set(commitSha, toCachedPR(mergeBasedPR));
599
721
  return mergeBasedPR;
600
722
  }
601
723
  }
602
724
  }
603
725
  if (mergeBasedPR) {
604
- await cache.set(commitSha, mergeBasedPR);
726
+ await cache.set(commitSha, toCachedPR(mergeBasedPR));
605
727
  return mergeBasedPR;
606
728
  }
607
- if (adapter) {
608
- const prInfo = await adapter.getPRForCommit(commitSha);
609
- if (prInfo?.mergedAt) {
610
- await cache.set(commitSha, prInfo);
611
- return prInfo;
729
+ const commitSubject = await getCommitSubject(commitSha, options);
730
+ if (commitSubject) {
731
+ const directPrNumber = extractPRFromMergeMessage(
732
+ commitSubject,
733
+ options?.platform
734
+ );
735
+ if (directPrNumber) {
736
+ const subjectPR = {
737
+ number: directPrNumber,
738
+ title: commitSubject,
739
+ author: "",
740
+ url: "",
741
+ mergeCommit: commitSha,
742
+ baseBranch: "",
743
+ resolvedVia: "message"
744
+ };
745
+ await cache.set(commitSha, toCachedPR(subjectPR));
746
+ return subjectPR;
612
747
  }
613
748
  }
614
- const patchIdMatch = await findPatchIdMatch(commitSha, {
615
- ...options,
616
- scanDepth: options?.deep ? DEEP_SCAN_DEPTH : void 0
617
- });
618
- if (patchIdMatch) {
619
- const result = await lookupPR(patchIdMatch.matchedSha, adapter, options);
620
- if (result) {
621
- await cache.set(commitSha, result);
622
- return result;
749
+ if (!options?.skipPatchIdScan && _recursionDepth < MAX_RECURSION_DEPTH) {
750
+ const patchIdMatch = await findPatchIdMatch(commitSha, {
751
+ ...options,
752
+ scanDepth: options?.deep ? DEEP_SCAN_DEPTH : void 0
753
+ });
754
+ if (patchIdMatch) {
755
+ const result = await lookupPR(
756
+ patchIdMatch.matchedSha,
757
+ adapter,
758
+ options,
759
+ _recursionDepth + 1
760
+ );
761
+ if (result) {
762
+ await cache.set(commitSha, toCachedPR(result));
763
+ return result;
764
+ }
623
765
  }
624
766
  }
625
767
  return null;
@@ -627,7 +769,7 @@ async function lookupPR(commitSha, adapter, options) {
627
769
  function resetPRCache() {
628
770
  cacheRegistry2.clear();
629
771
  }
630
- var cacheRegistry2, DEEP_SCAN_DEPTH;
772
+ var cacheRegistry2, DEEP_SCAN_DEPTH, MAX_RECURSION_DEPTH;
631
773
  var init_pr_lookup = __esm({
632
774
  "src/core/pr-lookup/pr-lookup.ts"() {
633
775
  "use strict";
@@ -637,6 +779,7 @@ var init_pr_lookup = __esm({
637
779
  init_patch_id2();
638
780
  cacheRegistry2 = /* @__PURE__ */ new Map();
639
781
  DEEP_SCAN_DEPTH = 2e3;
782
+ MAX_RECURSION_DEPTH = 2;
640
783
  }
641
784
  });
642
785
 
@@ -672,6 +815,7 @@ init_errors();
672
815
  // src/core/core.ts
673
816
  init_cjs_shims();
674
817
  var import_node_crypto2 = require("crypto");
818
+ var import_node_path2 = require("path");
675
819
  var import_common_utils11 = require("@winglet/common-utils");
676
820
 
677
821
  // src/ast/index.ts
@@ -905,6 +1049,27 @@ function isVersionAtLeast(version, minVersion) {
905
1049
  }
906
1050
  return true;
907
1051
  }
1052
+ async function checkCloneStatus(options) {
1053
+ let partialClone = false;
1054
+ let shallow = false;
1055
+ try {
1056
+ const shallowResult = await gitExec(
1057
+ ["rev-parse", "--is-shallow-repository"],
1058
+ { cwd: options?.cwd }
1059
+ );
1060
+ shallow = shallowResult.stdout.trim() === "true";
1061
+ } catch {
1062
+ }
1063
+ try {
1064
+ const partialResult = await gitExec(
1065
+ ["config", "--get", "extensions.partialclone"],
1066
+ { cwd: options?.cwd }
1067
+ );
1068
+ partialClone = partialResult.stdout.trim().length > 0;
1069
+ } catch {
1070
+ }
1071
+ return { partialClone, shallow };
1072
+ }
908
1073
  async function checkGitHealth(options) {
909
1074
  const hints = [];
910
1075
  let gitVersion = "0.0.0";
@@ -931,7 +1096,18 @@ async function checkGitHealth(options) {
931
1096
  `Upgrade git to ${BLOOM_FILTER_MIN_VERSION.join(".")}+ for bloom filter support (current: ${gitVersion}).`
932
1097
  );
933
1098
  }
934
- return { commitGraph, bloomFilter, gitVersion, hints };
1099
+ const cloneStatus = await checkCloneStatus({ cwd: options?.cwd });
1100
+ if (cloneStatus.partialClone) {
1101
+ hints.push(
1102
+ "Partial clone detected. Patch-ID scan (Strategy 5) will be skipped to avoid blob downloads."
1103
+ );
1104
+ }
1105
+ if (cloneStatus.shallow) {
1106
+ hints.push(
1107
+ "Shallow repository detected. Ancestry-path results may be incomplete."
1108
+ );
1109
+ }
1110
+ return { commitGraph, bloomFilter, gitVersion, hints, ...cloneStatus };
935
1111
  }
936
1112
 
937
1113
  // src/platform/index.ts
@@ -1039,7 +1215,7 @@ var GitHubAdapter = class {
1039
1215
  return { authenticated: false, hostname: this.hostname };
1040
1216
  }
1041
1217
  }
1042
- async getPRForCommit(sha) {
1218
+ async getPRForCommit(sha, options) {
1043
1219
  if (this.scheduler.isRateLimited()) return null;
1044
1220
  try {
1045
1221
  const result = await shellExec(
@@ -1056,18 +1232,20 @@ var GitHubAdapter = class {
1056
1232
  );
1057
1233
  const prs = JSON.parse(result.stdout);
1058
1234
  if (!(0, import_common_utils3.isArray)(prs) || prs.length === 0) return null;
1059
- const defaultBranch = await this.detectDefaultBranch();
1060
- const defaultBranchPR = prs.find(
1061
- (pr) => pr.base === defaultBranch
1062
- );
1063
- const data = defaultBranchPR ?? prs[0];
1235
+ let data = prs[0];
1236
+ if (options?.preferredBase) {
1237
+ const preferred = prs.find(
1238
+ (pr) => pr.base === options.preferredBase
1239
+ );
1240
+ if (preferred) data = preferred;
1241
+ }
1064
1242
  return {
1065
1243
  number: data.number,
1066
1244
  title: data.title ?? "",
1067
1245
  author: data.user ?? "",
1068
1246
  url: data.html_url ?? "",
1069
1247
  mergeCommit: data.merge_commit_sha ?? sha,
1070
- baseBranch: data.base ?? defaultBranch,
1248
+ baseBranch: data.base ?? "",
1071
1249
  mergedAt: data.merged_at
1072
1250
  };
1073
1251
  } catch {
@@ -1249,7 +1427,7 @@ var GitLabAdapter = class {
1249
1427
  return { authenticated: false, hostname: this.hostname };
1250
1428
  }
1251
1429
  }
1252
- async getPRForCommit(sha) {
1430
+ async getPRForCommit(sha, options) {
1253
1431
  if (this.scheduler.isRateLimited()) return null;
1254
1432
  try {
1255
1433
  const result = await shellExec(
@@ -1273,18 +1451,20 @@ var GitLabAdapter = class {
1273
1451
  return aTime - bTime;
1274
1452
  });
1275
1453
  if (mergedMRs.length === 0) return null;
1276
- const defaultBranch = await this.detectDefaultBranch();
1277
- const defaultBranchMR = mergedMRs.find(
1278
- (mr2) => mr2.target_branch === defaultBranch
1279
- );
1280
- const mr = defaultBranchMR ?? mergedMRs[0];
1454
+ let mr = mergedMRs[0];
1455
+ if (options?.preferredBase) {
1456
+ const preferred = mergedMRs.find(
1457
+ (m) => m.target_branch === options.preferredBase
1458
+ );
1459
+ if (preferred) mr = preferred;
1460
+ }
1281
1461
  return {
1282
1462
  number: mr.iid,
1283
1463
  title: mr.title ?? "",
1284
1464
  author: mr.author?.username ?? "",
1285
1465
  url: mr.web_url ?? "",
1286
1466
  mergeCommit: mr.merge_commit_sha ?? sha,
1287
- baseBranch: mr.target_branch ?? defaultBranch,
1467
+ baseBranch: mr.target_branch ?? "",
1288
1468
  mergedAt: mr.merged_at
1289
1469
  };
1290
1470
  } catch {
@@ -2092,6 +2272,28 @@ async function traverse(adapter, type, number, depth, maxDepth, nodes, edges, vi
2092
2272
 
2093
2273
  // src/core/core.ts
2094
2274
  init_pr_lookup2();
2275
+ function resolvedViaToTrackingMethod(resolvedVia) {
2276
+ switch (resolvedVia) {
2277
+ case "api":
2278
+ return "api";
2279
+ case "ancestry":
2280
+ return "ancestry-path";
2281
+ case "message":
2282
+ return "message-parse";
2283
+ case "patch-id":
2284
+ return "patch-id";
2285
+ }
2286
+ }
2287
+ function resolvedViaToConfidence(resolvedVia) {
2288
+ switch (resolvedVia) {
2289
+ case "api":
2290
+ case "ancestry":
2291
+ return "exact";
2292
+ case "message":
2293
+ case "patch-id":
2294
+ return "heuristic";
2295
+ }
2296
+ }
2095
2297
  function computeFeatureFlags(operatingLevel, options) {
2096
2298
  return {
2097
2299
  astDiff: isAstAvailable() && !options.noAst,
@@ -2109,6 +2311,22 @@ async function resolveRepoIdentity(cwd) {
2109
2311
  return { host: "_local", owner: "_", repo: "_unknown" };
2110
2312
  }
2111
2313
  }
2314
+ async function resolveFileContext(file, cwd) {
2315
+ if (cwd || !(0, import_node_path2.isAbsolute)(file)) return { file, cwd };
2316
+ const fileDir = (0, import_node_path2.dirname)(file);
2317
+ try {
2318
+ const result = await gitExec(["rev-parse", "--show-toplevel"], {
2319
+ cwd: fileDir
2320
+ });
2321
+ const repoRoot = result.stdout.trim();
2322
+ return {
2323
+ file: (0, import_node_path2.relative)(repoRoot, file),
2324
+ cwd: repoRoot
2325
+ };
2326
+ } catch {
2327
+ return { file, cwd };
2328
+ }
2329
+ }
2112
2330
  async function detectPlatform2(options) {
2113
2331
  const warnings = [];
2114
2332
  let adapter = null;
@@ -2155,7 +2373,7 @@ async function runBlameAndAuth(adapter, options, execOptions) {
2155
2373
  }
2156
2374
  return { analyzed: blameResult.value, operatingLevel, warnings };
2157
2375
  }
2158
- async function processEntry(entry, featureFlags, adapter, options, execOptions, repoId) {
2376
+ async function processEntry(entry, featureFlags, adapter, options, execOptions, repoId, skipPatchIdScan, preferredBase) {
2159
2377
  const nodes = [];
2160
2378
  const commitNode = {
2161
2379
  type: entry.isCosmetic ? "cosmetic_commit" : "original_commit",
@@ -2186,15 +2404,19 @@ async function processEntry(entry, featureFlags, adapter, options, execOptions,
2186
2404
  const prInfo = await lookupPR(targetSha, adapter, {
2187
2405
  ...execOptions,
2188
2406
  noCache: options.noCache,
2407
+ cacheOnly: options.cacheOnly,
2189
2408
  deep: featureFlags.deepTrace,
2190
- repoId
2409
+ repoId,
2410
+ skipPatchIdScan,
2411
+ preferredBase,
2412
+ platform: adapter?.platform
2191
2413
  });
2192
2414
  if (prInfo) {
2193
2415
  nodes.push({
2194
2416
  type: "pull_request",
2195
2417
  sha: prInfo.mergeCommit,
2196
- trackingMethod: prInfo.url ? "api" : "message-parse",
2197
- confidence: prInfo.url ? "exact" : "heuristic",
2418
+ trackingMethod: resolvedViaToTrackingMethod(prInfo.resolvedVia),
2419
+ confidence: resolvedViaToConfidence(prInfo.resolvedVia),
2198
2420
  prNumber: prInfo.number,
2199
2421
  prUrl: prInfo.url || void 0,
2200
2422
  prTitle: prInfo.title || void 0,
@@ -2204,24 +2426,35 @@ async function processEntry(entry, featureFlags, adapter, options, execOptions,
2204
2426
  }
2205
2427
  return nodes;
2206
2428
  }
2207
- async function buildTraceNodes(analyzed, featureFlags, adapter, options, execOptions, repoId) {
2429
+ async function buildTraceNodes(analyzed, featureFlags, adapter, options, execOptions, repoId, skipPatchIdScan, preferredBase) {
2208
2430
  const results = await Promise.allSettled(
2209
2431
  (0, import_common_utils11.map)(
2210
2432
  analyzed,
2211
- (entry) => processEntry(entry, featureFlags, adapter, options, execOptions, repoId)
2433
+ (entry) => processEntry(
2434
+ entry,
2435
+ featureFlags,
2436
+ adapter,
2437
+ options,
2438
+ execOptions,
2439
+ repoId,
2440
+ skipPatchIdScan,
2441
+ preferredBase
2442
+ )
2212
2443
  )
2213
2444
  );
2214
2445
  return results.flatMap((r) => r.status === "fulfilled" ? r.value : []);
2215
2446
  }
2216
2447
  var legacyCacheCleaned = false;
2217
2448
  async function trace(options) {
2218
- const execOptions = { cwd: options.cwd };
2449
+ const { file, cwd } = await resolveFileContext(options.file, options.cwd);
2450
+ const warnings = [];
2451
+ const execOptions = { cwd, warnings };
2219
2452
  if (!legacyCacheCleaned) {
2220
2453
  legacyCacheCleaned = true;
2221
2454
  cleanupLegacyCache().catch(() => {
2222
2455
  });
2223
2456
  }
2224
- const platform = await detectPlatform2(options);
2457
+ const platform = await detectPlatform2({ ...options, cwd });
2225
2458
  let repoId;
2226
2459
  if (platform.remote) {
2227
2460
  repoId = {
@@ -2230,23 +2463,58 @@ async function trace(options) {
2230
2463
  repo: platform.remote.repo
2231
2464
  };
2232
2465
  } else {
2233
- repoId = await resolveRepoIdentity(options.cwd);
2466
+ repoId = await resolveRepoIdentity(cwd);
2234
2467
  }
2235
2468
  const blameAuth = await runBlameAndAuth(
2236
2469
  platform.adapter,
2237
- options,
2470
+ { ...options, file, cwd },
2238
2471
  execOptions
2239
2472
  );
2240
2473
  const operatingLevel = blameAuth.operatingLevel || platform.operatingLevel;
2241
- const warnings = [...platform.warnings, ...blameAuth.warnings];
2474
+ warnings.push(...platform.warnings, ...blameAuth.warnings);
2475
+ if (options.cacheOnly && options.noCache) {
2476
+ warnings.push(
2477
+ "Both cacheOnly and noCache are set. cacheOnly takes precedence \u2014 cache reads are enabled."
2478
+ );
2479
+ }
2242
2480
  const featureFlags = computeFeatureFlags(operatingLevel, options);
2481
+ let cloneStatus = { partialClone: false, shallow: false };
2482
+ try {
2483
+ const result = await checkCloneStatus({ cwd });
2484
+ if (result) cloneStatus = result;
2485
+ } catch {
2486
+ }
2487
+ if (cloneStatus.partialClone) {
2488
+ warnings.push(
2489
+ "Partial clone detected. Patch-ID scan (Strategy 5) will be skipped to avoid blob downloads."
2490
+ );
2491
+ }
2492
+ if (cloneStatus.shallow) {
2493
+ warnings.push(
2494
+ "Shallow repository detected. Ancestry-path results may be incomplete."
2495
+ );
2496
+ }
2497
+ let preferredBase;
2498
+ try {
2499
+ const branchResult = await gitExec(
2500
+ ["rev-parse", "--abbrev-ref", "HEAD"],
2501
+ execOptions
2502
+ );
2503
+ const branch = branchResult.stdout.trim();
2504
+ if (branch && branch !== "HEAD") {
2505
+ preferredBase = branch;
2506
+ }
2507
+ } catch {
2508
+ }
2243
2509
  const nodes = await buildTraceNodes(
2244
2510
  blameAuth.analyzed,
2245
2511
  featureFlags,
2246
2512
  platform.adapter,
2247
- options,
2513
+ { ...options, file, cwd },
2248
2514
  execOptions,
2249
- repoId
2515
+ repoId,
2516
+ cloneStatus.partialClone || void 0,
2517
+ preferredBase
2250
2518
  );
2251
2519
  return { nodes, operatingLevel, featureFlags, warnings };
2252
2520
  }