@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/index.cjs CHANGED
@@ -400,6 +400,35 @@ async function findMergeCommit(commitSha, options) {
400
400
  timeout: remaining
401
401
  });
402
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
+ }
431
+ }
403
432
  async function findMergeCommitWithArgs(commitSha, ref, extraArgs, options) {
404
433
  try {
405
434
  const result = await gitExec(
@@ -417,7 +446,25 @@ async function findMergeCommitWithArgs(commitSha, ref, extraArgs, options) {
417
446
  );
418
447
  const lines = (0, import_common_utils9.filter)(result.stdout.trim().split("\n"), import_common_utils9.isTruthy);
419
448
  if (lines.length === 0) return null;
420
- 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;
421
468
  } catch {
422
469
  return null;
423
470
  }
@@ -439,18 +486,32 @@ function parseMergeLogLine(line) {
439
486
  const subject = parts.slice(subjectStart).join(" ");
440
487
  return { mergeCommitSha, parentShas, subject };
441
488
  }
442
- 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) {
443
502
  const ghMatch = /Merge pull request #(\d+)/.exec(subject);
444
503
  if (ghMatch) return parseInt(ghMatch[1], 10);
445
504
  const squashMatch = /\(#(\d+)\)\s*$/.exec(subject);
446
505
  if (squashMatch) return parseInt(squashMatch[1], 10);
447
- const glMatch = /!(\d+)\s*$/.exec(subject);
448
- 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
+ }
449
510
  const adoMatch = /Merged PR (\d+):/.exec(subject);
450
511
  if (adoMatch) return parseInt(adoMatch[1], 10);
451
512
  return null;
452
513
  }
453
- var import_common_utils9, DEFAULT_ANCESTRY_TIMEOUT;
514
+ var import_common_utils9, DEFAULT_ANCESTRY_TIMEOUT, MAX_CANDIDATES;
454
515
  var init_ancestry = __esm({
455
516
  "src/core/ancestry/ancestry.ts"() {
456
517
  "use strict";
@@ -458,6 +519,7 @@ var init_ancestry = __esm({
458
519
  import_common_utils9 = require("@winglet/common-utils");
459
520
  init_executor();
460
521
  DEFAULT_ANCESTRY_TIMEOUT = 3e4;
522
+ MAX_CANDIDATES = 10;
461
523
  }
462
524
  });
463
525
 
@@ -588,7 +650,8 @@ function toCachedPR(pr) {
588
650
  url: pr.url,
589
651
  mergeCommit: pr.mergeCommit,
590
652
  baseBranch: pr.baseBranch,
591
- mergedAt: pr.mergedAt ? new Date(pr.mergedAt).getTime() : void 0
653
+ mergedAt: pr.mergedAt ? new Date(pr.mergedAt).getTime() : void 0,
654
+ resolvedVia: pr.resolvedVia
592
655
  };
593
656
  }
594
657
  function fromCachedPR(cached) {
@@ -603,7 +666,9 @@ function fromCachedPR(cached) {
603
666
  url: cached.url,
604
667
  mergeCommit: cached.mergeCommit,
605
668
  baseBranch: cached.baseBranch,
606
- mergedAt
669
+ mergedAt,
670
+ // Preserve original resolvedVia; fallback to url heuristic for legacy cache entries
671
+ resolvedVia: cached.resolvedVia ?? (cached.url ? "api" : "message")
607
672
  };
608
673
  }
609
674
  async function lookupPR(commitSha, adapter, options, _recursionDepth = 0) {
@@ -614,15 +679,30 @@ async function lookupPR(commitSha, adapter, options, _recursionDepth = 0) {
614
679
  const cached = await cache.get(commitSha);
615
680
  if (cached) return fromCachedPR(cached);
616
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
+ }
617
691
  let mergeBasedPR = null;
618
692
  const mergeResult = await findMergeCommit(commitSha, options);
619
693
  if (mergeResult) {
620
- const prNumber = extractPRFromMergeMessage(mergeResult.subject);
694
+ const prNumber = extractPRFromMergeMessage(
695
+ mergeResult.subject,
696
+ options?.platform
697
+ );
621
698
  if (prNumber) {
622
699
  if (adapter) {
623
- const prInfo = await adapter.getPRForCommit(mergeResult.mergeCommitSha);
700
+ const prInfo = await adapter.getPRForCommit(
701
+ mergeResult.mergeCommitSha,
702
+ prSelectOptions
703
+ );
624
704
  if (prInfo?.mergedAt) {
625
- mergeBasedPR = prInfo;
705
+ mergeBasedPR = { ...prInfo, resolvedVia: "ancestry" };
626
706
  }
627
707
  }
628
708
  if (!mergeBasedPR) {
@@ -632,7 +712,8 @@ async function lookupPR(commitSha, adapter, options, _recursionDepth = 0) {
632
712
  author: "",
633
713
  url: "",
634
714
  mergeCommit: mergeResult.mergeCommitSha,
635
- baseBranch: ""
715
+ baseBranch: "",
716
+ resolvedVia: "ancestry"
636
717
  };
637
718
  }
638
719
  if (!options?.deep || mergeBasedPR.mergedAt) {
@@ -645,11 +726,24 @@ async function lookupPR(commitSha, adapter, options, _recursionDepth = 0) {
645
726
  await cache.set(commitSha, toCachedPR(mergeBasedPR));
646
727
  return mergeBasedPR;
647
728
  }
648
- if (adapter) {
649
- const prInfo = await adapter.getPRForCommit(commitSha);
650
- if (prInfo?.mergedAt) {
651
- await cache.set(commitSha, toCachedPR(prInfo));
652
- 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;
653
747
  }
654
748
  }
655
749
  if (!options?.skipPatchIdScan && _recursionDepth < MAX_RECURSION_DEPTH) {
@@ -721,6 +815,7 @@ init_errors();
721
815
  // src/core/core.ts
722
816
  init_cjs_shims();
723
817
  var import_node_crypto2 = require("crypto");
818
+ var import_node_path2 = require("path");
724
819
  var import_common_utils11 = require("@winglet/common-utils");
725
820
 
726
821
  // src/ast/index.ts
@@ -1004,7 +1099,7 @@ async function checkGitHealth(options) {
1004
1099
  const cloneStatus = await checkCloneStatus({ cwd: options?.cwd });
1005
1100
  if (cloneStatus.partialClone) {
1006
1101
  hints.push(
1007
- "Partial clone detected. Patch-ID scan (Strategy 4) will be skipped to avoid blob downloads."
1102
+ "Partial clone detected. Patch-ID scan (Strategy 5) will be skipped to avoid blob downloads."
1008
1103
  );
1009
1104
  }
1010
1105
  if (cloneStatus.shallow) {
@@ -1120,7 +1215,7 @@ var GitHubAdapter = class {
1120
1215
  return { authenticated: false, hostname: this.hostname };
1121
1216
  }
1122
1217
  }
1123
- async getPRForCommit(sha) {
1218
+ async getPRForCommit(sha, options) {
1124
1219
  if (this.scheduler.isRateLimited()) return null;
1125
1220
  try {
1126
1221
  const result = await shellExec(
@@ -1137,18 +1232,20 @@ var GitHubAdapter = class {
1137
1232
  );
1138
1233
  const prs = JSON.parse(result.stdout);
1139
1234
  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];
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
+ }
1145
1242
  return {
1146
1243
  number: data.number,
1147
1244
  title: data.title ?? "",
1148
1245
  author: data.user ?? "",
1149
1246
  url: data.html_url ?? "",
1150
1247
  mergeCommit: data.merge_commit_sha ?? sha,
1151
- baseBranch: data.base ?? defaultBranch,
1248
+ baseBranch: data.base ?? "",
1152
1249
  mergedAt: data.merged_at
1153
1250
  };
1154
1251
  } catch {
@@ -1330,7 +1427,7 @@ var GitLabAdapter = class {
1330
1427
  return { authenticated: false, hostname: this.hostname };
1331
1428
  }
1332
1429
  }
1333
- async getPRForCommit(sha) {
1430
+ async getPRForCommit(sha, options) {
1334
1431
  if (this.scheduler.isRateLimited()) return null;
1335
1432
  try {
1336
1433
  const result = await shellExec(
@@ -1354,18 +1451,20 @@ var GitLabAdapter = class {
1354
1451
  return aTime - bTime;
1355
1452
  });
1356
1453
  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];
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
+ }
1362
1461
  return {
1363
1462
  number: mr.iid,
1364
1463
  title: mr.title ?? "",
1365
1464
  author: mr.author?.username ?? "",
1366
1465
  url: mr.web_url ?? "",
1367
1466
  mergeCommit: mr.merge_commit_sha ?? sha,
1368
- baseBranch: mr.target_branch ?? defaultBranch,
1467
+ baseBranch: mr.target_branch ?? "",
1369
1468
  mergedAt: mr.merged_at
1370
1469
  };
1371
1470
  } catch {
@@ -2173,6 +2272,28 @@ async function traverse(adapter, type, number, depth, maxDepth, nodes, edges, vi
2173
2272
 
2174
2273
  // src/core/core.ts
2175
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
+ }
2176
2297
  function computeFeatureFlags(operatingLevel, options) {
2177
2298
  return {
2178
2299
  astDiff: isAstAvailable() && !options.noAst,
@@ -2190,6 +2311,22 @@ async function resolveRepoIdentity(cwd) {
2190
2311
  return { host: "_local", owner: "_", repo: "_unknown" };
2191
2312
  }
2192
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
+ }
2193
2330
  async function detectPlatform2(options) {
2194
2331
  const warnings = [];
2195
2332
  let adapter = null;
@@ -2236,7 +2373,7 @@ async function runBlameAndAuth(adapter, options, execOptions) {
2236
2373
  }
2237
2374
  return { analyzed: blameResult.value, operatingLevel, warnings };
2238
2375
  }
2239
- async function processEntry(entry, featureFlags, adapter, options, execOptions, repoId, skipPatchIdScan) {
2376
+ async function processEntry(entry, featureFlags, adapter, options, execOptions, repoId, skipPatchIdScan, preferredBase) {
2240
2377
  const nodes = [];
2241
2378
  const commitNode = {
2242
2379
  type: entry.isCosmetic ? "cosmetic_commit" : "original_commit",
@@ -2270,14 +2407,16 @@ async function processEntry(entry, featureFlags, adapter, options, execOptions,
2270
2407
  cacheOnly: options.cacheOnly,
2271
2408
  deep: featureFlags.deepTrace,
2272
2409
  repoId,
2273
- skipPatchIdScan
2410
+ skipPatchIdScan,
2411
+ preferredBase,
2412
+ platform: adapter?.platform
2274
2413
  });
2275
2414
  if (prInfo) {
2276
2415
  nodes.push({
2277
2416
  type: "pull_request",
2278
2417
  sha: prInfo.mergeCommit,
2279
- trackingMethod: prInfo.url ? "api" : "message-parse",
2280
- confidence: prInfo.url ? "exact" : "heuristic",
2418
+ trackingMethod: resolvedViaToTrackingMethod(prInfo.resolvedVia),
2419
+ confidence: resolvedViaToConfidence(prInfo.resolvedVia),
2281
2420
  prNumber: prInfo.number,
2282
2421
  prUrl: prInfo.url || void 0,
2283
2422
  prTitle: prInfo.title || void 0,
@@ -2287,7 +2426,7 @@ async function processEntry(entry, featureFlags, adapter, options, execOptions,
2287
2426
  }
2288
2427
  return nodes;
2289
2428
  }
2290
- async function buildTraceNodes(analyzed, featureFlags, adapter, options, execOptions, repoId, skipPatchIdScan) {
2429
+ async function buildTraceNodes(analyzed, featureFlags, adapter, options, execOptions, repoId, skipPatchIdScan, preferredBase) {
2291
2430
  const results = await Promise.allSettled(
2292
2431
  (0, import_common_utils11.map)(
2293
2432
  analyzed,
@@ -2298,7 +2437,8 @@ async function buildTraceNodes(analyzed, featureFlags, adapter, options, execOpt
2298
2437
  options,
2299
2438
  execOptions,
2300
2439
  repoId,
2301
- skipPatchIdScan
2440
+ skipPatchIdScan,
2441
+ preferredBase
2302
2442
  )
2303
2443
  )
2304
2444
  );
@@ -2306,13 +2446,15 @@ async function buildTraceNodes(analyzed, featureFlags, adapter, options, execOpt
2306
2446
  }
2307
2447
  var legacyCacheCleaned = false;
2308
2448
  async function trace(options) {
2309
- const execOptions = { cwd: options.cwd };
2449
+ const { file, cwd } = await resolveFileContext(options.file, options.cwd);
2450
+ const warnings = [];
2451
+ const execOptions = { cwd, warnings };
2310
2452
  if (!legacyCacheCleaned) {
2311
2453
  legacyCacheCleaned = true;
2312
2454
  cleanupLegacyCache().catch(() => {
2313
2455
  });
2314
2456
  }
2315
- const platform = await detectPlatform2(options);
2457
+ const platform = await detectPlatform2({ ...options, cwd });
2316
2458
  let repoId;
2317
2459
  if (platform.remote) {
2318
2460
  repoId = {
@@ -2321,15 +2463,15 @@ async function trace(options) {
2321
2463
  repo: platform.remote.repo
2322
2464
  };
2323
2465
  } else {
2324
- repoId = await resolveRepoIdentity(options.cwd);
2466
+ repoId = await resolveRepoIdentity(cwd);
2325
2467
  }
2326
2468
  const blameAuth = await runBlameAndAuth(
2327
2469
  platform.adapter,
2328
- options,
2470
+ { ...options, file, cwd },
2329
2471
  execOptions
2330
2472
  );
2331
2473
  const operatingLevel = blameAuth.operatingLevel || platform.operatingLevel;
2332
- const warnings = [...platform.warnings, ...blameAuth.warnings];
2474
+ warnings.push(...platform.warnings, ...blameAuth.warnings);
2333
2475
  if (options.cacheOnly && options.noCache) {
2334
2476
  warnings.push(
2335
2477
  "Both cacheOnly and noCache are set. cacheOnly takes precedence \u2014 cache reads are enabled."
@@ -2338,13 +2480,13 @@ async function trace(options) {
2338
2480
  const featureFlags = computeFeatureFlags(operatingLevel, options);
2339
2481
  let cloneStatus = { partialClone: false, shallow: false };
2340
2482
  try {
2341
- const result = await checkCloneStatus({ cwd: options.cwd });
2483
+ const result = await checkCloneStatus({ cwd });
2342
2484
  if (result) cloneStatus = result;
2343
2485
  } catch {
2344
2486
  }
2345
2487
  if (cloneStatus.partialClone) {
2346
2488
  warnings.push(
2347
- "Partial clone detected. Patch-ID scan (Strategy 4) will be skipped to avoid blob downloads."
2489
+ "Partial clone detected. Patch-ID scan (Strategy 5) will be skipped to avoid blob downloads."
2348
2490
  );
2349
2491
  }
2350
2492
  if (cloneStatus.shallow) {
@@ -2352,14 +2494,27 @@ async function trace(options) {
2352
2494
  "Shallow repository detected. Ancestry-path results may be incomplete."
2353
2495
  );
2354
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
+ }
2355
2509
  const nodes = await buildTraceNodes(
2356
2510
  blameAuth.analyzed,
2357
2511
  featureFlags,
2358
2512
  platform.adapter,
2359
- options,
2513
+ { ...options, file, cwd },
2360
2514
  execOptions,
2361
2515
  repoId,
2362
- cloneStatus.partialClone || void 0
2516
+ cloneStatus.partialClone || void 0,
2517
+ preferredBase
2363
2518
  );
2364
2519
  return { nodes, operatingLevel, featureFlags, warnings };
2365
2520
  }