@lumy-pack/line-lore 0.0.7 → 0.0.9

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
@@ -368,25 +368,6 @@ var init_executor = __esm({
368
368
 
369
369
  // src/core/ancestry/ancestry.ts
370
370
  import { filter as filter4, isTruthy as isTruthy4 } from "@winglet/common-utils";
371
- async function findMergeCommit(commitSha, options) {
372
- const ref = options?.ref ?? "HEAD";
373
- const budget = options?.timeout ?? DEFAULT_ANCESTRY_TIMEOUT;
374
- const startTime = Date.now();
375
- const firstParentResult = await findMergeCommitWithArgs(
376
- commitSha,
377
- ref,
378
- ["--first-parent"],
379
- { ...options, timeout: budget }
380
- );
381
- if (firstParentResult) return firstParentResult;
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
371
  async function verifyMergeIntroducesCommit(targetSha, mergeResult, options) {
391
372
  if (mergeResult.parentShas.length < 2) return true;
392
373
  const firstParent = mergeResult.parentShas[0];
@@ -416,7 +397,7 @@ async function isAncestor(commitA, commitB, options) {
416
397
  return null;
417
398
  }
418
399
  }
419
- async function findMergeCommitWithArgs(commitSha, ref, extraArgs, options) {
400
+ async function findMergeCommitsWithArgs(commitSha, ref, extraArgs, options) {
420
401
  try {
421
402
  const result = await gitExec(
422
403
  [
@@ -432,28 +413,29 @@ async function findMergeCommitWithArgs(commitSha, ref, extraArgs, options) {
432
413
  { cwd: options?.cwd, timeout: options?.timeout }
433
414
  );
434
415
  const lines = filter4(result.stdout.trim().split("\n"), isTruthy4);
435
- if (lines.length === 0) return null;
416
+ if (lines.length === 0) return [];
417
+ const verifiedCandidates = [];
436
418
  const candidateCount = Math.min(lines.length, MAX_CANDIDATES);
437
- let verifiedCount = 0;
419
+ let attemptedCount = 0;
438
420
  for (let i = 0; i < candidateCount; i++) {
439
421
  const candidate = parseMergeLogLine(lines[i]);
440
422
  if (!candidate) continue;
441
- verifiedCount++;
423
+ attemptedCount++;
442
424
  const verified = await verifyMergeIntroducesCommit(
443
425
  commitSha,
444
426
  candidate,
445
427
  options
446
428
  );
447
- if (verified) return candidate;
429
+ if (verified) verifiedCandidates.push(candidate);
448
430
  }
449
- if (verifiedCount > 0 && options?.warnings) {
431
+ if (attemptedCount > 0 && verifiedCandidates.length === 0 && options?.warnings) {
450
432
  options.warnings.push(
451
- `ancestry: all ${verifiedCount} merge candidate(s) failed verification for ${commitSha.slice(0, 8)}`
433
+ `ancestry: all ${attemptedCount} merge candidate(s) failed verification for ${commitSha.slice(0, 8)}`
452
434
  );
453
435
  }
454
- return null;
436
+ return verifiedCandidates;
455
437
  } catch {
456
- return null;
438
+ return [];
457
439
  }
458
440
  }
459
441
  function parseMergeLogLine(line) {
@@ -473,6 +455,38 @@ function parseMergeLogLine(line) {
473
455
  const subject = parts.slice(subjectStart).join(" ");
474
456
  return { mergeCommitSha, parentShas, subject };
475
457
  }
458
+ async function findMergeCommits(commitSha, options) {
459
+ const ref = options?.ref ?? "HEAD";
460
+ const budget = options?.timeout ?? DEFAULT_ANCESTRY_TIMEOUT;
461
+ const startTime = Date.now();
462
+ const results = [];
463
+ const seen = /* @__PURE__ */ new Set();
464
+ const pushUnique = (candidates) => {
465
+ for (const candidate of candidates) {
466
+ if (seen.has(candidate.mergeCommitSha)) continue;
467
+ seen.add(candidate.mergeCommitSha);
468
+ results.push(candidate);
469
+ if (results.length >= MAX_CANDIDATES) break;
470
+ }
471
+ };
472
+ const firstParent = await findMergeCommitsWithArgs(
473
+ commitSha,
474
+ ref,
475
+ ["--first-parent"],
476
+ { ...options, timeout: budget }
477
+ );
478
+ pushUnique(firstParent);
479
+ const elapsed = Date.now() - startTime;
480
+ const remaining = budget - elapsed;
481
+ if (remaining > 0 && results.length < MAX_CANDIDATES) {
482
+ const full = await findMergeCommitsWithArgs(commitSha, ref, [], {
483
+ ...options,
484
+ timeout: remaining
485
+ });
486
+ pushUnique(full);
487
+ }
488
+ return results;
489
+ }
476
490
  async function getCommitSubject(sha, options) {
477
491
  try {
478
492
  const result = await gitExec(["log", "-1", "--format=%s", sha], {
@@ -675,16 +689,17 @@ async function lookupPR(commitSha, adapter, options, _recursionDepth = 0) {
675
689
  }
676
690
  }
677
691
  let mergeBasedPR = null;
678
- const mergeResult = await findMergeCommit(commitSha, options);
679
- if (mergeResult) {
692
+ const mergeCandidates = await findMergeCommits(commitSha, options);
693
+ const hasAncestryMerges = mergeCandidates.length > 0;
694
+ for (const candidate of mergeCandidates) {
680
695
  const prNumber = extractPRFromMergeMessage(
681
- mergeResult.subject,
696
+ candidate.subject,
682
697
  options?.platform
683
698
  );
684
699
  if (prNumber) {
685
700
  if (adapter) {
686
701
  const prInfo = await adapter.getPRForCommit(
687
- mergeResult.mergeCommitSha,
702
+ candidate.mergeCommitSha,
688
703
  prSelectOptions
689
704
  );
690
705
  if (prInfo?.mergedAt) {
@@ -694,23 +709,32 @@ async function lookupPR(commitSha, adapter, options, _recursionDepth = 0) {
694
709
  if (!mergeBasedPR) {
695
710
  mergeBasedPR = {
696
711
  number: prNumber,
697
- title: mergeResult.subject,
712
+ title: candidate.subject,
698
713
  author: "",
699
714
  url: "",
700
- mergeCommit: mergeResult.mergeCommitSha,
715
+ mergeCommit: candidate.mergeCommitSha,
701
716
  baseBranch: "",
702
717
  resolvedVia: "ancestry"
703
718
  };
704
719
  }
705
- if (!options?.deep || mergeBasedPR.mergedAt) {
706
- await cache.set(commitSha, toCachedPR(mergeBasedPR));
707
- return mergeBasedPR;
720
+ break;
721
+ }
722
+ if (adapter) {
723
+ const mergeCommitPR = await adapter.getPRForCommit(
724
+ candidate.mergeCommitSha,
725
+ prSelectOptions
726
+ );
727
+ if (mergeCommitPR?.mergedAt) {
728
+ mergeBasedPR = { ...mergeCommitPR, resolvedVia: "ancestry" };
729
+ break;
708
730
  }
709
731
  }
710
732
  }
711
733
  if (mergeBasedPR) {
712
- await cache.set(commitSha, toCachedPR(mergeBasedPR));
713
- return mergeBasedPR;
734
+ if (!options?.deep || mergeBasedPR.mergedAt) {
735
+ await cache.set(commitSha, toCachedPR(mergeBasedPR));
736
+ return mergeBasedPR;
737
+ }
714
738
  }
715
739
  const commitSubject = await getCommitSubject(commitSha, options);
716
740
  if (commitSubject) {
@@ -732,7 +756,7 @@ async function lookupPR(commitSha, adapter, options, _recursionDepth = 0) {
732
756
  return subjectPR;
733
757
  }
734
758
  }
735
- if (!options?.skipPatchIdScan && _recursionDepth < MAX_RECURSION_DEPTH) {
759
+ if (!options?.skipPatchIdScan && _recursionDepth < MAX_RECURSION_DEPTH && (!hasAncestryMerges || options?.deep)) {
736
760
  const patchIdMatch = await findPatchIdMatch(commitSha, {
737
761
  ...options,
738
762
  scanDepth: options?.deep ? DEEP_SCAN_DEPTH : void 0
@@ -750,6 +774,10 @@ async function lookupPR(commitSha, adapter, options, _recursionDepth = 0) {
750
774
  }
751
775
  }
752
776
  }
777
+ if (mergeBasedPR) {
778
+ await cache.set(commitSha, toCachedPR(mergeBasedPR));
779
+ return mergeBasedPR;
780
+ }
753
781
  return null;
754
782
  }
755
783
  function resetPRCache() {
@@ -2058,6 +2086,7 @@ function parsePorcelainOutput(output) {
2058
2086
  }
2059
2087
  let commitHash = headerMatch[1];
2060
2088
  const originalLine = parseInt(headerMatch[2], 10);
2089
+ const finalLine = parseInt(headerMatch[3], 10) || 0;
2061
2090
  const isBoundary = commitHash.startsWith("^");
2062
2091
  if (isBoundary) {
2063
2092
  commitHash = commitHash.slice(1).padStart(40, "0");
@@ -2101,6 +2130,7 @@ function parsePorcelainOutput(output) {
2101
2130
  authorEmail: cleanEmail,
2102
2131
  date,
2103
2132
  lineContent,
2133
+ finalLine,
2104
2134
  originalFile,
2105
2135
  originalLine: originalFile ? originalLine : void 0
2106
2136
  });
@@ -2111,14 +2141,112 @@ function parsePorcelainOutput(output) {
2111
2141
  // src/core/blame/blame.ts
2112
2142
  async function executeBlame(file, lineRange, options) {
2113
2143
  const lineSpec = `${lineRange.start},${lineRange.end}`;
2114
- const result = await gitExec(
2115
- ["blame", "-w", "-C", "-C", "-M", "--porcelain", "-L", lineSpec, file],
2116
- options
2117
- );
2144
+ const args = options?.mode === "change" ? ["blame", "-w", "--porcelain", "-L", lineSpec, file] : ["blame", "-w", "-C", "-C", "-M", "--porcelain", "-L", lineSpec, file];
2145
+ const result = await gitExec(args, options);
2118
2146
  return parsePorcelainOutput(result.stdout);
2119
2147
  }
2120
- async function analyzeBlameResults(results, filePath, options) {
2121
- const uniqueShas = [...new Set(map6(results, (r) => r.commitHash))];
2148
+ async function executeDualBlame(file, lineRange, options) {
2149
+ if (options?.mode === "change") {
2150
+ const results = await executeBlame(file, lineRange, options);
2151
+ return { blame: results, changeBlame: [] };
2152
+ }
2153
+ const [originResult, changeResult] = await Promise.allSettled([
2154
+ executeBlame(file, lineRange, options),
2155
+ executeBlame(file, lineRange, { ...options, mode: "change" })
2156
+ ]);
2157
+ const blame = originResult.status === "fulfilled" ? originResult.value : [];
2158
+ const changeBlame = changeResult.status === "fulfilled" ? changeResult.value : [];
2159
+ if (originResult.status === "rejected") {
2160
+ throw originResult.reason;
2161
+ }
2162
+ return { blame, changeBlame };
2163
+ }
2164
+ async function verifyRename(originalFile, currentFile, options) {
2165
+ try {
2166
+ const result = await gitExec(
2167
+ [
2168
+ "log",
2169
+ "--diff-filter=R",
2170
+ "--find-renames",
2171
+ "--format=%H",
2172
+ "--",
2173
+ originalFile,
2174
+ currentFile
2175
+ ],
2176
+ options
2177
+ );
2178
+ return result.stdout.trim().length > 0;
2179
+ } catch {
2180
+ return false;
2181
+ }
2182
+ }
2183
+ function crossValidateBlame(originResults, changeResults) {
2184
+ const changeMap = /* @__PURE__ */ new Map();
2185
+ for (const r of changeResults) {
2186
+ changeMap.set(r.finalLine, r);
2187
+ }
2188
+ const validated = [];
2189
+ const crossValidatedFlags = [];
2190
+ const changeFallbackFlags = [];
2191
+ const renameChecks = [];
2192
+ for (let i = 0; i < originResults.length; i++) {
2193
+ const origin = originResults[i];
2194
+ const change = changeMap.get(origin.finalLine);
2195
+ if (!change) {
2196
+ validated.push(origin);
2197
+ crossValidatedFlags.push(false);
2198
+ changeFallbackFlags.push(false);
2199
+ continue;
2200
+ }
2201
+ if (origin.commitHash === change.commitHash) {
2202
+ validated.push(origin);
2203
+ crossValidatedFlags.push(true);
2204
+ changeFallbackFlags.push(false);
2205
+ continue;
2206
+ }
2207
+ if (origin.originalFile) {
2208
+ renameChecks.push({
2209
+ originalFile: origin.originalFile,
2210
+ lineIndex: i
2211
+ });
2212
+ }
2213
+ validated.push(change);
2214
+ crossValidatedFlags.push(true);
2215
+ changeFallbackFlags.push(true);
2216
+ }
2217
+ return { validated, renameChecks, crossValidatedFlags, changeFallbackFlags };
2218
+ }
2219
+ async function analyzeBlameResults(results, filePath, options, changeResults) {
2220
+ let effectiveResults = results;
2221
+ let crossValidatedFlags;
2222
+ let changeFallbackFlagsResult;
2223
+ if (changeResults && changeResults.length > 0) {
2224
+ const { validated, renameChecks, crossValidatedFlags: cvFlags, changeFallbackFlags } = crossValidateBlame(results, changeResults);
2225
+ if (renameChecks.length > 0) {
2226
+ const pendingChecks = /* @__PURE__ */ new Map();
2227
+ for (const check of renameChecks) {
2228
+ const cacheKey = `${check.originalFile}:${filePath}`;
2229
+ if (!pendingChecks.has(cacheKey)) {
2230
+ pendingChecks.set(cacheKey, verifyRename(check.originalFile, filePath, options));
2231
+ }
2232
+ }
2233
+ const renameResults = /* @__PURE__ */ new Map();
2234
+ for (const [key, promise] of pendingChecks) {
2235
+ renameResults.set(key, await promise);
2236
+ }
2237
+ for (const check of renameChecks) {
2238
+ const cacheKey = `${check.originalFile}:${filePath}`;
2239
+ if (renameResults.get(cacheKey)) {
2240
+ validated[check.lineIndex] = results[check.lineIndex];
2241
+ changeFallbackFlags[check.lineIndex] = false;
2242
+ }
2243
+ }
2244
+ }
2245
+ effectiveResults = validated;
2246
+ crossValidatedFlags = cvFlags;
2247
+ changeFallbackFlagsResult = changeFallbackFlags;
2248
+ }
2249
+ const uniqueShas = [...new Set(map6(effectiveResults, (r) => r.commitHash))];
2122
2250
  const cosmeticMap = /* @__PURE__ */ new Map();
2123
2251
  const zeroSha = "0".repeat(40);
2124
2252
  const tasks = [];
@@ -2127,7 +2255,7 @@ async function analyzeBlameResults(results, filePath, options) {
2127
2255
  tasks.push(
2128
2256
  (async () => {
2129
2257
  try {
2130
- const blameResult = results.find((r) => r.commitHash === sha);
2258
+ const blameResult = effectiveResults.find((r) => r.commitHash === sha);
2131
2259
  if (!blameResult) return;
2132
2260
  const file = blameResult.originalFile ?? filePath;
2133
2261
  const diff = await getCosmeticDiff(sha, file, options);
@@ -2139,12 +2267,14 @@ async function analyzeBlameResults(results, filePath, options) {
2139
2267
  );
2140
2268
  });
2141
2269
  await Promise.all(tasks);
2142
- return map6(results, (blame) => {
2270
+ return map6(effectiveResults, (blame, i) => {
2143
2271
  const cosmetic = cosmeticMap.get(blame.commitHash);
2144
2272
  return {
2145
2273
  blame,
2146
2274
  isCosmetic: cosmetic?.isCosmetic ?? false,
2147
- cosmeticReason: cosmetic?.reason
2275
+ cosmeticReason: cosmetic?.reason,
2276
+ crossValidated: crossValidatedFlags?.[i],
2277
+ usedChangeFallback: changeFallbackFlagsResult?.[i]
2148
2278
  };
2149
2279
  });
2150
2280
  }
@@ -2325,8 +2455,11 @@ async function runBlameAndAuth(adapter, options, execOptions) {
2325
2455
  const lineRange = parseLineRange(
2326
2456
  options.endLine ? `${options.line},${options.endLine}` : `${options.line}`
2327
2457
  );
2328
- const blameChain = executeBlame(options.file, lineRange, execOptions).then(
2329
- (results) => analyzeBlameResults(results, options.file, execOptions)
2458
+ const blameChain = executeDualBlame(options.file, lineRange, {
2459
+ ...execOptions,
2460
+ mode: options.mode
2461
+ }).then(
2462
+ ({ blame, changeBlame }) => analyzeBlameResults(blame, options.file, execOptions, changeBlame.length > 0 ? changeBlame : void 0)
2330
2463
  );
2331
2464
  const [authResult, blameResult] = await Promise.allSettled([
2332
2465
  adapter ? adapter.checkAuth() : Promise.resolve({ authenticated: false }),
@@ -2348,12 +2481,24 @@ async function runBlameAndAuth(adapter, options, execOptions) {
2348
2481
  }
2349
2482
  return { analyzed: blameResult.value, operatingLevel, warnings };
2350
2483
  }
2351
- async function processEntry(entry, featureFlags, adapter, options, execOptions, repoId, skipPatchIdScan, preferredBase) {
2484
+ function resolveTraceMode(mode) {
2485
+ return mode ?? "origin";
2486
+ }
2487
+ function deduplicatedLookupPR(sha, adapter, options, inflight) {
2488
+ const existing = inflight.get(sha);
2489
+ if (existing) return existing;
2490
+ const promise = lookupPR(sha, adapter, options);
2491
+ inflight.set(sha, promise);
2492
+ promise.finally(() => inflight.delete(sha));
2493
+ return promise;
2494
+ }
2495
+ async function processEntry(entry, featureFlags, adapter, options, execOptions, repoId, inflightPR, skipPatchIdScan, preferredBase) {
2352
2496
  const nodes = [];
2497
+ const traceMode = resolveTraceMode(options.mode);
2353
2498
  const commitNode = {
2354
2499
  type: entry.isCosmetic ? "cosmetic_commit" : "original_commit",
2355
2500
  sha: entry.blame.commitHash,
2356
- trackingMethod: "blame-CMw",
2501
+ trackingMethod: traceMode === "change" || entry.usedChangeFallback ? "blame" : "blame-CMw",
2357
2502
  confidence: "exact",
2358
2503
  note: entry.cosmeticReason ? `Cosmetic change: ${entry.cosmeticReason}` : void 0
2359
2504
  };
@@ -2375,17 +2520,18 @@ async function processEntry(entry, featureFlags, adapter, options, execOptions,
2375
2520
  }
2376
2521
  }
2377
2522
  const targetSha = nodes[nodes.length - 1].sha;
2523
+ const prLookupOptions = {
2524
+ ...execOptions,
2525
+ noCache: options.noCache,
2526
+ cacheOnly: options.cacheOnly,
2527
+ deep: featureFlags.deepTrace,
2528
+ repoId,
2529
+ skipPatchIdScan,
2530
+ preferredBase,
2531
+ platform: adapter?.platform
2532
+ };
2378
2533
  if (targetSha) {
2379
- const prInfo = await lookupPR(targetSha, adapter, {
2380
- ...execOptions,
2381
- noCache: options.noCache,
2382
- cacheOnly: options.cacheOnly,
2383
- deep: featureFlags.deepTrace,
2384
- repoId,
2385
- skipPatchIdScan,
2386
- preferredBase,
2387
- platform: adapter?.platform
2388
- });
2534
+ const prInfo = await deduplicatedLookupPR(targetSha, adapter, prLookupOptions, inflightPR);
2389
2535
  if (prInfo) {
2390
2536
  nodes.push({
2391
2537
  type: "pull_request",
@@ -2402,6 +2548,7 @@ async function processEntry(entry, featureFlags, adapter, options, execOptions,
2402
2548
  return nodes;
2403
2549
  }
2404
2550
  async function buildTraceNodes(analyzed, featureFlags, adapter, options, execOptions, repoId, skipPatchIdScan, preferredBase) {
2551
+ const inflightPR = /* @__PURE__ */ new Map();
2405
2552
  const results = await Promise.allSettled(
2406
2553
  map8(
2407
2554
  analyzed,
@@ -2412,6 +2559,7 @@ async function buildTraceNodes(analyzed, featureFlags, adapter, options, execOpt
2412
2559
  options,
2413
2560
  execOptions,
2414
2561
  repoId,
2562
+ inflightPR,
2415
2563
  skipPatchIdScan,
2416
2564
  preferredBase
2417
2565
  )
@@ -2421,6 +2569,7 @@ async function buildTraceNodes(analyzed, featureFlags, adapter, options, execOpt
2421
2569
  }
2422
2570
  var legacyCacheCleaned = false;
2423
2571
  async function trace(options) {
2572
+ const mode = resolveTraceMode(options.mode);
2424
2573
  const { file, cwd } = await resolveFileContext(options.file, options.cwd);
2425
2574
  const warnings = [];
2426
2575
  const execOptions = { cwd, warnings };
@@ -2442,7 +2591,7 @@ async function trace(options) {
2442
2591
  }
2443
2592
  const blameAuth = await runBlameAndAuth(
2444
2593
  platform.adapter,
2445
- { ...options, file, cwd },
2594
+ { ...options, mode, file, cwd },
2446
2595
  execOptions
2447
2596
  );
2448
2597
  const operatingLevel = blameAuth.operatingLevel || platform.operatingLevel;
@@ -2485,7 +2634,7 @@ async function trace(options) {
2485
2634
  blameAuth.analyzed,
2486
2635
  featureFlags,
2487
2636
  platform.adapter,
2488
- { ...options, file, cwd },
2637
+ { ...options, mode, file, cwd },
2489
2638
  execOptions,
2490
2639
  repoId,
2491
2640
  cloneStatus.partialClone || void 0,
@@ -12,11 +12,23 @@ export interface BlameResult {
12
12
  date: string;
13
13
  /** The actual content of the blamed line */
14
14
  lineContent: string;
15
+ /** Final line number in the current file */
16
+ finalLine: number;
15
17
  /** Original filename if the line was moved/renamed */
16
18
  originalFile?: string;
17
19
  /** Original line number before any moves/renames */
18
20
  originalLine?: number;
19
21
  }
22
+ /**
23
+ * Result of running dual blame (origin + change) in parallel.
24
+ * Used by origin mode to cross-validate blame results.
25
+ */
26
+ export interface DualBlameResult {
27
+ /** Primary blame results for the requested mode */
28
+ blame: BlameResult[];
29
+ /** Change-mode blame for cross-validation (populated only in origin mode) */
30
+ changeBlame: BlameResult[];
31
+ }
20
32
  /**
21
33
  * Basic commit information from git log.
22
34
  */
@@ -1,4 +1,5 @@
1
1
  import type { PlatformType } from './platform.js';
2
+ import type { TraceMode } from './trace.js';
2
3
  export interface GitExecResult {
3
4
  stdout: string;
4
5
  stderr: string;
@@ -11,6 +12,10 @@ export interface GitExecOptions {
11
12
  /** Mutable array for collecting diagnostic warnings throughout the pipeline */
12
13
  warnings?: string[];
13
14
  }
15
+ export interface BlameExecOptions extends GitExecOptions {
16
+ /** Blame semantics used by trace mode selection */
17
+ mode?: TraceMode;
18
+ }
14
19
  export interface RemoteInfo {
15
20
  owner: string;
16
21
  repo: string;
@@ -1,11 +1,11 @@
1
1
  export type { SymbolKind, SymbolInfo, ContentHash, ChangeType, ComparisonResult, AstTraceResult, } from './ast.js';
2
- export type { BlameResult, CommitInfo } from './blame.js';
2
+ export type { BlameResult, CommitInfo, DualBlameResult } from './blame.js';
3
3
  export type { CacheEntry, CachedPRInfo } from './cache.js';
4
- export type { GitExecResult, GitExecOptions, RemoteInfo, HealthReport, CloneStatus, } from './git.js';
4
+ export type { GitExecResult, GitExecOptions, BlameExecOptions, 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';
8
8
  export type { PlatformType, AuthStatus, PRInfo, IssueInfo, RateLimitInfo, PlatformAdapter, } from './platform.js';
9
9
  export type { CosmeticReason, BlameStageResult, AstDiffStageResult, } from './stage.js';
10
- export type { TraceResult, TraceOptions } from './trace.js';
10
+ export type { TraceMode, TraceResult, TraceOptions } from './trace.js';
11
11
  export type { LineRange } from './util.js';
@@ -6,6 +6,10 @@ export interface BlameStageResult {
6
6
  blame: BlameResult;
7
7
  isCosmetic: boolean;
8
8
  cosmeticReason?: CosmeticReason;
9
+ /** Whether this result was cross-validated against change blame (origin mode only) */
10
+ crossValidated?: boolean;
11
+ /** Whether the change blame result was used instead of origin (false positive filtered) */
12
+ usedChangeFallback?: boolean;
9
13
  }
10
14
  export interface AstDiffStageResult {
11
15
  originalSha: string;
@@ -9,6 +9,7 @@ export interface TraceResult {
9
9
  /** PR information if found, null if commit is not from a PR */
10
10
  pr: PRInfo | null;
11
11
  }
12
+ export type TraceMode = 'origin' | 'change';
12
13
  /**
13
14
  * Options for the trace operation (library API).
14
15
  */
@@ -31,4 +32,6 @@ export interface TraceOptions {
31
32
  noCache?: boolean;
32
33
  /** Return cached results only — skip API calls, ancestry traversal, and patch-id scan */
33
34
  cacheOnly?: boolean;
35
+ /** Trace mode. `origin` follows copy/move history, `change` finds the last meaningful local change. */
36
+ mode?: TraceMode;
34
37
  }
package/dist/version.d.ts CHANGED
@@ -2,4 +2,4 @@
2
2
  * Current package version from package.json
3
3
  * Automatically synchronized during build process
4
4
  */
5
- export declare const VERSION = "0.0.7";
5
+ export declare const VERSION = "0.0.9";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumy-pack/line-lore",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "CLI tool for tracing code lines to their originating Pull Requests via git blame",
5
5
  "keywords": [
6
6
  "cli",