@lumy-pack/line-lore 0.0.6 → 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/cli.mjs CHANGED
@@ -386,6 +386,35 @@ async function findMergeCommit(commitSha, options) {
386
386
  timeout: remaining
387
387
  });
388
388
  }
389
+ async function verifyMergeIntroducesCommit(targetSha, mergeResult, options) {
390
+ if (mergeResult.parentShas.length < 2) return true;
391
+ const firstParent = mergeResult.parentShas[0];
392
+ const branchParents = mergeResult.parentShas.slice(1);
393
+ const onMainline = await isAncestor(targetSha, firstParent, options);
394
+ if (onMainline === null) return false;
395
+ if (onMainline) return false;
396
+ for (const branchParent of branchParents) {
397
+ const onBranch = await isAncestor(targetSha, branchParent, options);
398
+ if (onBranch === null) return false;
399
+ if (onBranch) return true;
400
+ }
401
+ return false;
402
+ }
403
+ async function isAncestor(commitA, commitB, options) {
404
+ try {
405
+ const result = await gitExec(
406
+ ["merge-base", "--is-ancestor", commitA, commitB],
407
+ {
408
+ cwd: options?.cwd,
409
+ timeout: options?.timeout ?? 5e3,
410
+ allowExitCodes: [1]
411
+ }
412
+ );
413
+ return result.exitCode === 0;
414
+ } catch {
415
+ return null;
416
+ }
417
+ }
389
418
  async function findMergeCommitWithArgs(commitSha, ref, extraArgs, options) {
390
419
  try {
391
420
  const result = await gitExec(
@@ -403,7 +432,25 @@ async function findMergeCommitWithArgs(commitSha, ref, extraArgs, options) {
403
432
  );
404
433
  const lines = filter4(result.stdout.trim().split("\n"), isTruthy4);
405
434
  if (lines.length === 0) return null;
406
- return parseMergeLogLine(lines[0]);
435
+ const candidateCount = Math.min(lines.length, MAX_CANDIDATES);
436
+ let verifiedCount = 0;
437
+ for (let i = 0; i < candidateCount; i++) {
438
+ const candidate = parseMergeLogLine(lines[i]);
439
+ if (!candidate) continue;
440
+ verifiedCount++;
441
+ const verified = await verifyMergeIntroducesCommit(
442
+ commitSha,
443
+ candidate,
444
+ options
445
+ );
446
+ if (verified) return candidate;
447
+ }
448
+ if (verifiedCount > 0 && options?.warnings) {
449
+ options.warnings.push(
450
+ `ancestry: all ${verifiedCount} merge candidate(s) failed verification for ${commitSha.slice(0, 8)}`
451
+ );
452
+ }
453
+ return null;
407
454
  } catch {
408
455
  return null;
409
456
  }
@@ -425,23 +472,38 @@ function parseMergeLogLine(line) {
425
472
  const subject = parts.slice(subjectStart).join(" ");
426
473
  return { mergeCommitSha, parentShas, subject };
427
474
  }
428
- function extractPRFromMergeMessage(subject) {
475
+ async function getCommitSubject(sha, options) {
476
+ try {
477
+ const result = await gitExec(["log", "-1", "--format=%s", sha], {
478
+ cwd: options?.cwd,
479
+ timeout: options?.timeout ?? 5e3
480
+ });
481
+ const subject = result.stdout.trim();
482
+ return subject || null;
483
+ } catch {
484
+ return null;
485
+ }
486
+ }
487
+ function extractPRFromMergeMessage(subject, platform) {
429
488
  const ghMatch = /Merge pull request #(\d+)/.exec(subject);
430
489
  if (ghMatch) return parseInt(ghMatch[1], 10);
431
490
  const squashMatch = /\(#(\d+)\)\s*$/.exec(subject);
432
491
  if (squashMatch) return parseInt(squashMatch[1], 10);
433
- const glMatch = /!(\d+)\s*$/.exec(subject);
434
- if (glMatch) return parseInt(glMatch[1], 10);
492
+ if (!platform || platform === "gitlab" || platform === "gitlab-self-hosted") {
493
+ const glMatch = /See merge request\s+\S*!(\d+)\s*$/.exec(subject);
494
+ if (glMatch) return parseInt(glMatch[1], 10);
495
+ }
435
496
  const adoMatch = /Merged PR (\d+):/.exec(subject);
436
497
  if (adoMatch) return parseInt(adoMatch[1], 10);
437
498
  return null;
438
499
  }
439
- var DEFAULT_ANCESTRY_TIMEOUT;
500
+ var DEFAULT_ANCESTRY_TIMEOUT, MAX_CANDIDATES;
440
501
  var init_ancestry = __esm({
441
502
  "src/core/ancestry/ancestry.ts"() {
442
503
  "use strict";
443
504
  init_executor();
444
505
  DEFAULT_ANCESTRY_TIMEOUT = 3e4;
506
+ MAX_CANDIDATES = 10;
445
507
  }
446
508
  });
447
509
 
@@ -569,7 +631,8 @@ function toCachedPR(pr) {
569
631
  url: pr.url,
570
632
  mergeCommit: pr.mergeCommit,
571
633
  baseBranch: pr.baseBranch,
572
- mergedAt: pr.mergedAt ? new Date(pr.mergedAt).getTime() : void 0
634
+ mergedAt: pr.mergedAt ? new Date(pr.mergedAt).getTime() : void 0,
635
+ resolvedVia: pr.resolvedVia
573
636
  };
574
637
  }
575
638
  function fromCachedPR(cached) {
@@ -584,7 +647,9 @@ function fromCachedPR(cached) {
584
647
  url: cached.url,
585
648
  mergeCommit: cached.mergeCommit,
586
649
  baseBranch: cached.baseBranch,
587
- mergedAt
650
+ mergedAt,
651
+ // Preserve original resolvedVia; fallback to url heuristic for legacy cache entries
652
+ resolvedVia: cached.resolvedVia ?? (cached.url ? "api" : "message")
588
653
  };
589
654
  }
590
655
  async function lookupPR(commitSha, adapter, options, _recursionDepth = 0) {
@@ -595,15 +660,30 @@ async function lookupPR(commitSha, adapter, options, _recursionDepth = 0) {
595
660
  const cached = await cache.get(commitSha);
596
661
  if (cached) return fromCachedPR(cached);
597
662
  if (options?.cacheOnly) return null;
663
+ const prSelectOptions = options?.preferredBase ? { preferredBase: options.preferredBase } : void 0;
664
+ if (adapter) {
665
+ const directPR = await adapter.getPRForCommit(commitSha, prSelectOptions);
666
+ if (directPR?.mergedAt) {
667
+ const result = { ...directPR, resolvedVia: "api" };
668
+ await cache.set(commitSha, toCachedPR(result));
669
+ return result;
670
+ }
671
+ }
598
672
  let mergeBasedPR = null;
599
673
  const mergeResult = await findMergeCommit(commitSha, options);
600
674
  if (mergeResult) {
601
- const prNumber = extractPRFromMergeMessage(mergeResult.subject);
675
+ const prNumber = extractPRFromMergeMessage(
676
+ mergeResult.subject,
677
+ options?.platform
678
+ );
602
679
  if (prNumber) {
603
680
  if (adapter) {
604
- const prInfo = await adapter.getPRForCommit(mergeResult.mergeCommitSha);
681
+ const prInfo = await adapter.getPRForCommit(
682
+ mergeResult.mergeCommitSha,
683
+ prSelectOptions
684
+ );
605
685
  if (prInfo?.mergedAt) {
606
- mergeBasedPR = prInfo;
686
+ mergeBasedPR = { ...prInfo, resolvedVia: "ancestry" };
607
687
  }
608
688
  }
609
689
  if (!mergeBasedPR) {
@@ -613,7 +693,8 @@ async function lookupPR(commitSha, adapter, options, _recursionDepth = 0) {
613
693
  author: "",
614
694
  url: "",
615
695
  mergeCommit: mergeResult.mergeCommitSha,
616
- baseBranch: ""
696
+ baseBranch: "",
697
+ resolvedVia: "ancestry"
617
698
  };
618
699
  }
619
700
  if (!options?.deep || mergeBasedPR.mergedAt) {
@@ -626,11 +707,24 @@ async function lookupPR(commitSha, adapter, options, _recursionDepth = 0) {
626
707
  await cache.set(commitSha, toCachedPR(mergeBasedPR));
627
708
  return mergeBasedPR;
628
709
  }
629
- if (adapter) {
630
- const prInfo = await adapter.getPRForCommit(commitSha);
631
- if (prInfo?.mergedAt) {
632
- await cache.set(commitSha, toCachedPR(prInfo));
633
- return prInfo;
710
+ const commitSubject = await getCommitSubject(commitSha, options);
711
+ if (commitSubject) {
712
+ const directPrNumber = extractPRFromMergeMessage(
713
+ commitSubject,
714
+ options?.platform
715
+ );
716
+ if (directPrNumber) {
717
+ const subjectPR = {
718
+ number: directPrNumber,
719
+ title: commitSubject,
720
+ author: "",
721
+ url: "",
722
+ mergeCommit: commitSha,
723
+ baseBranch: "",
724
+ resolvedVia: "message"
725
+ };
726
+ await cache.set(commitSha, toCachedPR(subjectPR));
727
+ return subjectPR;
634
728
  }
635
729
  }
636
730
  if (!options?.skipPatchIdScan && _recursionDepth < MAX_RECURSION_DEPTH) {
@@ -687,7 +781,7 @@ var VERSION;
687
781
  var init_version = __esm({
688
782
  "src/version.ts"() {
689
783
  "use strict";
690
- VERSION = "0.0.6";
784
+ VERSION = "0.0.7";
691
785
  }
692
786
  });
693
787
 
@@ -784,6 +878,7 @@ import { Command } from "commander";
784
878
 
785
879
  // src/core/core.ts
786
880
  import { createHash as createHash2 } from "crypto";
881
+ import { dirname, isAbsolute, relative } from "path";
787
882
  import { map as map8 } from "@winglet/common-utils";
788
883
 
789
884
  // src/ast/parser.ts
@@ -1062,7 +1157,7 @@ async function checkGitHealth(options) {
1062
1157
  const cloneStatus = await checkCloneStatus({ cwd: options?.cwd });
1063
1158
  if (cloneStatus.partialClone) {
1064
1159
  hints.push(
1065
- "Partial clone detected. Patch-ID scan (Strategy 4) will be skipped to avoid blob downloads."
1160
+ "Partial clone detected. Patch-ID scan (Strategy 5) will be skipped to avoid blob downloads."
1066
1161
  );
1067
1162
  }
1068
1163
  if (cloneStatus.shallow) {
@@ -1163,7 +1258,7 @@ var GitHubAdapter = class {
1163
1258
  return { authenticated: false, hostname: this.hostname };
1164
1259
  }
1165
1260
  }
1166
- async getPRForCommit(sha) {
1261
+ async getPRForCommit(sha, options) {
1167
1262
  if (this.scheduler.isRateLimited()) return null;
1168
1263
  try {
1169
1264
  const result = await shellExec(
@@ -1180,18 +1275,20 @@ var GitHubAdapter = class {
1180
1275
  );
1181
1276
  const prs = JSON.parse(result.stdout);
1182
1277
  if (!isArray(prs) || prs.length === 0) return null;
1183
- const defaultBranch = await this.detectDefaultBranch();
1184
- const defaultBranchPR = prs.find(
1185
- (pr) => pr.base === defaultBranch
1186
- );
1187
- const data = defaultBranchPR ?? prs[0];
1278
+ let data = prs[0];
1279
+ if (options?.preferredBase) {
1280
+ const preferred = prs.find(
1281
+ (pr) => pr.base === options.preferredBase
1282
+ );
1283
+ if (preferred) data = preferred;
1284
+ }
1188
1285
  return {
1189
1286
  number: data.number,
1190
1287
  title: data.title ?? "",
1191
1288
  author: data.user ?? "",
1192
1289
  url: data.html_url ?? "",
1193
1290
  mergeCommit: data.merge_commit_sha ?? sha,
1194
- baseBranch: data.base ?? defaultBranch,
1291
+ baseBranch: data.base ?? "",
1195
1292
  mergedAt: data.merged_at
1196
1293
  };
1197
1294
  } catch {
@@ -1368,7 +1465,7 @@ var GitLabAdapter = class {
1368
1465
  return { authenticated: false, hostname: this.hostname };
1369
1466
  }
1370
1467
  }
1371
- async getPRForCommit(sha) {
1468
+ async getPRForCommit(sha, options) {
1372
1469
  if (this.scheduler.isRateLimited()) return null;
1373
1470
  try {
1374
1471
  const result = await shellExec(
@@ -1392,18 +1489,20 @@ var GitLabAdapter = class {
1392
1489
  return aTime - bTime;
1393
1490
  });
1394
1491
  if (mergedMRs.length === 0) return null;
1395
- const defaultBranch = await this.detectDefaultBranch();
1396
- const defaultBranchMR = mergedMRs.find(
1397
- (mr2) => mr2.target_branch === defaultBranch
1398
- );
1399
- const mr = defaultBranchMR ?? mergedMRs[0];
1492
+ let mr = mergedMRs[0];
1493
+ if (options?.preferredBase) {
1494
+ const preferred = mergedMRs.find(
1495
+ (m) => m.target_branch === options.preferredBase
1496
+ );
1497
+ if (preferred) mr = preferred;
1498
+ }
1400
1499
  return {
1401
1500
  number: mr.iid,
1402
1501
  title: mr.title ?? "",
1403
1502
  author: mr.author?.username ?? "",
1404
1503
  url: mr.web_url ?? "",
1405
1504
  mergeCommit: mr.merge_commit_sha ?? sha,
1406
- baseBranch: mr.target_branch ?? defaultBranch,
1505
+ baseBranch: mr.target_branch ?? "",
1407
1506
  mergedAt: mr.merged_at
1408
1507
  };
1409
1508
  } catch {
@@ -2180,6 +2279,28 @@ async function traverse(adapter, type, number, depth, maxDepth, nodes, edges, vi
2180
2279
 
2181
2280
  // src/core/core.ts
2182
2281
  init_pr_lookup2();
2282
+ function resolvedViaToTrackingMethod(resolvedVia) {
2283
+ switch (resolvedVia) {
2284
+ case "api":
2285
+ return "api";
2286
+ case "ancestry":
2287
+ return "ancestry-path";
2288
+ case "message":
2289
+ return "message-parse";
2290
+ case "patch-id":
2291
+ return "patch-id";
2292
+ }
2293
+ }
2294
+ function resolvedViaToConfidence(resolvedVia) {
2295
+ switch (resolvedVia) {
2296
+ case "api":
2297
+ case "ancestry":
2298
+ return "exact";
2299
+ case "message":
2300
+ case "patch-id":
2301
+ return "heuristic";
2302
+ }
2303
+ }
2183
2304
  function computeFeatureFlags(operatingLevel, options) {
2184
2305
  return {
2185
2306
  astDiff: isAstAvailable() && !options.noAst,
@@ -2197,6 +2318,22 @@ async function resolveRepoIdentity(cwd) {
2197
2318
  return { host: "_local", owner: "_", repo: "_unknown" };
2198
2319
  }
2199
2320
  }
2321
+ async function resolveFileContext(file, cwd) {
2322
+ if (cwd || !isAbsolute(file)) return { file, cwd };
2323
+ const fileDir = dirname(file);
2324
+ try {
2325
+ const result = await gitExec(["rev-parse", "--show-toplevel"], {
2326
+ cwd: fileDir
2327
+ });
2328
+ const repoRoot = result.stdout.trim();
2329
+ return {
2330
+ file: relative(repoRoot, file),
2331
+ cwd: repoRoot
2332
+ };
2333
+ } catch {
2334
+ return { file, cwd };
2335
+ }
2336
+ }
2200
2337
  async function detectPlatform2(options) {
2201
2338
  const warnings = [];
2202
2339
  let adapter = null;
@@ -2243,7 +2380,7 @@ async function runBlameAndAuth(adapter, options, execOptions) {
2243
2380
  }
2244
2381
  return { analyzed: blameResult.value, operatingLevel, warnings };
2245
2382
  }
2246
- async function processEntry(entry, featureFlags, adapter, options, execOptions, repoId, skipPatchIdScan) {
2383
+ async function processEntry(entry, featureFlags, adapter, options, execOptions, repoId, skipPatchIdScan, preferredBase) {
2247
2384
  const nodes = [];
2248
2385
  const commitNode = {
2249
2386
  type: entry.isCosmetic ? "cosmetic_commit" : "original_commit",
@@ -2277,14 +2414,16 @@ async function processEntry(entry, featureFlags, adapter, options, execOptions,
2277
2414
  cacheOnly: options.cacheOnly,
2278
2415
  deep: featureFlags.deepTrace,
2279
2416
  repoId,
2280
- skipPatchIdScan
2417
+ skipPatchIdScan,
2418
+ preferredBase,
2419
+ platform: adapter?.platform
2281
2420
  });
2282
2421
  if (prInfo) {
2283
2422
  nodes.push({
2284
2423
  type: "pull_request",
2285
2424
  sha: prInfo.mergeCommit,
2286
- trackingMethod: prInfo.url ? "api" : "message-parse",
2287
- confidence: prInfo.url ? "exact" : "heuristic",
2425
+ trackingMethod: resolvedViaToTrackingMethod(prInfo.resolvedVia),
2426
+ confidence: resolvedViaToConfidence(prInfo.resolvedVia),
2288
2427
  prNumber: prInfo.number,
2289
2428
  prUrl: prInfo.url || void 0,
2290
2429
  prTitle: prInfo.title || void 0,
@@ -2294,7 +2433,7 @@ async function processEntry(entry, featureFlags, adapter, options, execOptions,
2294
2433
  }
2295
2434
  return nodes;
2296
2435
  }
2297
- async function buildTraceNodes(analyzed, featureFlags, adapter, options, execOptions, repoId, skipPatchIdScan) {
2436
+ async function buildTraceNodes(analyzed, featureFlags, adapter, options, execOptions, repoId, skipPatchIdScan, preferredBase) {
2298
2437
  const results = await Promise.allSettled(
2299
2438
  map8(
2300
2439
  analyzed,
@@ -2305,7 +2444,8 @@ async function buildTraceNodes(analyzed, featureFlags, adapter, options, execOpt
2305
2444
  options,
2306
2445
  execOptions,
2307
2446
  repoId,
2308
- skipPatchIdScan
2447
+ skipPatchIdScan,
2448
+ preferredBase
2309
2449
  )
2310
2450
  )
2311
2451
  );
@@ -2313,13 +2453,15 @@ async function buildTraceNodes(analyzed, featureFlags, adapter, options, execOpt
2313
2453
  }
2314
2454
  var legacyCacheCleaned = false;
2315
2455
  async function trace(options) {
2316
- const execOptions = { cwd: options.cwd };
2456
+ const { file, cwd } = await resolveFileContext(options.file, options.cwd);
2457
+ const warnings = [];
2458
+ const execOptions = { cwd, warnings };
2317
2459
  if (!legacyCacheCleaned) {
2318
2460
  legacyCacheCleaned = true;
2319
2461
  cleanupLegacyCache().catch(() => {
2320
2462
  });
2321
2463
  }
2322
- const platform = await detectPlatform2(options);
2464
+ const platform = await detectPlatform2({ ...options, cwd });
2323
2465
  let repoId;
2324
2466
  if (platform.remote) {
2325
2467
  repoId = {
@@ -2328,15 +2470,15 @@ async function trace(options) {
2328
2470
  repo: platform.remote.repo
2329
2471
  };
2330
2472
  } else {
2331
- repoId = await resolveRepoIdentity(options.cwd);
2473
+ repoId = await resolveRepoIdentity(cwd);
2332
2474
  }
2333
2475
  const blameAuth = await runBlameAndAuth(
2334
2476
  platform.adapter,
2335
- options,
2477
+ { ...options, file, cwd },
2336
2478
  execOptions
2337
2479
  );
2338
2480
  const operatingLevel = blameAuth.operatingLevel || platform.operatingLevel;
2339
- const warnings = [...platform.warnings, ...blameAuth.warnings];
2481
+ warnings.push(...platform.warnings, ...blameAuth.warnings);
2340
2482
  if (options.cacheOnly && options.noCache) {
2341
2483
  warnings.push(
2342
2484
  "Both cacheOnly and noCache are set. cacheOnly takes precedence \u2014 cache reads are enabled."
@@ -2345,13 +2487,13 @@ async function trace(options) {
2345
2487
  const featureFlags = computeFeatureFlags(operatingLevel, options);
2346
2488
  let cloneStatus = { partialClone: false, shallow: false };
2347
2489
  try {
2348
- const result = await checkCloneStatus({ cwd: options.cwd });
2490
+ const result = await checkCloneStatus({ cwd });
2349
2491
  if (result) cloneStatus = result;
2350
2492
  } catch {
2351
2493
  }
2352
2494
  if (cloneStatus.partialClone) {
2353
2495
  warnings.push(
2354
- "Partial clone detected. Patch-ID scan (Strategy 4) will be skipped to avoid blob downloads."
2496
+ "Partial clone detected. Patch-ID scan (Strategy 5) will be skipped to avoid blob downloads."
2355
2497
  );
2356
2498
  }
2357
2499
  if (cloneStatus.shallow) {
@@ -2359,14 +2501,27 @@ async function trace(options) {
2359
2501
  "Shallow repository detected. Ancestry-path results may be incomplete."
2360
2502
  );
2361
2503
  }
2504
+ let preferredBase;
2505
+ try {
2506
+ const branchResult = await gitExec(
2507
+ ["rev-parse", "--abbrev-ref", "HEAD"],
2508
+ execOptions
2509
+ );
2510
+ const branch = branchResult.stdout.trim();
2511
+ if (branch && branch !== "HEAD") {
2512
+ preferredBase = branch;
2513
+ }
2514
+ } catch {
2515
+ }
2362
2516
  const nodes = await buildTraceNodes(
2363
2517
  blameAuth.analyzed,
2364
2518
  featureFlags,
2365
2519
  platform.adapter,
2366
- options,
2520
+ { ...options, file, cwd },
2367
2521
  execOptions,
2368
2522
  repoId,
2369
- cloneStatus.partialClone || void 0
2523
+ cloneStatus.partialClone || void 0,
2524
+ preferredBase
2370
2525
  );
2371
2526
  return { nodes, operatingLevel, featureFlags, warnings };
2372
2527
  }
@@ -8,4 +8,17 @@ export declare const DEFAULT_ANCESTRY_TIMEOUT = 30000;
8
8
  export declare function findMergeCommit(commitSha: string, options?: GitExecOptions & {
9
9
  ref?: string;
10
10
  }): Promise<AncestryResult | null>;
11
- export declare function extractPRFromMergeMessage(subject: string): number | null;
11
+ /**
12
+ * Verify that a merge commit actually introduced the target commit
13
+ * through its branch side (non-first parent), not from the mainline.
14
+ *
15
+ * Dual condition:
16
+ * 1. Target IS an ancestor of at least one non-first parent (branch side)
17
+ * 2. Target is NOT an ancestor of the first parent (mainline side)
18
+ *
19
+ * Returns false on git command failure (fail-skip policy).
20
+ */
21
+ export declare function verifyMergeIntroducesCommit(targetSha: string, mergeResult: AncestryResult, options?: GitExecOptions): Promise<boolean>;
22
+ /** Retrieve the subject line of a single commit. Returns null on git failure. */
23
+ export declare function getCommitSubject(sha: string, options?: GitExecOptions): Promise<string | null>;
24
+ export declare function extractPRFromMergeMessage(subject: string, platform?: string): number | null;
@@ -1,2 +1,2 @@
1
- export { extractPRFromMergeMessage, findMergeCommit } from './ancestry.js';
1
+ export { extractPRFromMergeMessage, findMergeCommit, getCommitSubject, verifyMergeIntroducesCommit, } from './ancestry.js';
2
2
  export type { AncestryResult } from './ancestry.js';
@@ -1 +1,2 @@
1
1
  export { lookupPR, resetPRCache } from './pr-lookup.js';
2
+ export type { PRLookupResult, ResolvedVia } from './pr-lookup.js';
@@ -1,15 +1,31 @@
1
1
  import type { RepoIdentity } from '../../cache/index.js';
2
2
  import type { GitExecOptions, PRInfo, PlatformAdapter } from '../../types/index.js';
3
+ export type ResolvedVia = 'api' | 'ancestry' | 'message' | 'patch-id';
4
+ export interface PRLookupResult extends PRInfo {
5
+ resolvedVia: ResolvedVia;
6
+ }
3
7
  export interface PRLookupOptions extends GitExecOptions {
4
8
  noCache?: boolean;
5
9
  /** Return cached results only — skip all fallback strategies */
6
10
  cacheOnly?: boolean;
7
11
  deep?: boolean;
8
12
  repoId?: RepoIdentity;
9
- /** Skip Strategy 4 (patch-id scan) — set automatically for partial clone environments */
13
+ /** Skip Strategy 5 (patch-id scan) — set automatically for partial clone environments */
10
14
  skipPatchIdScan?: boolean;
15
+ /** Preferred base branch for PR selection — when multiple PRs match, prefer the one targeting this branch */
16
+ preferredBase?: string;
17
+ /** Platform type for platform-aware merge message parsing */
18
+ platform?: string;
11
19
  }
20
+ /**
21
+ * Multi-strategy PR lookup pipeline:
22
+ * Strategy 1: Cache
23
+ * Strategy 2: API direct (ground truth — Level 2)
24
+ * Strategy 3: Ancestry-path + merge commit verification (structural proof)
25
+ * Strategy 4: Blame commit message parsing (heuristic — squash merge detection)
26
+ * Strategy 5: Patch-ID matching + recursion (last resort)
27
+ */
12
28
  export declare function lookupPR(commitSha: string, adapter: PlatformAdapter | null, options?: PRLookupOptions,
13
29
  /** @internal recursion depth tracker — do not set from external callers */
14
- _recursionDepth?: number): Promise<PRInfo | null>;
30
+ _recursionDepth?: number): Promise<PRLookupResult | null>;
15
31
  export declare function resetPRCache(): void;