@lumy-pack/line-lore 0.0.8 → 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/cli.mjs CHANGED
@@ -809,7 +809,7 @@ var VERSION;
809
809
  var init_version = __esm({
810
810
  "src/version.ts"() {
811
811
  "use strict";
812
- VERSION = "0.0.8";
812
+ VERSION = "0.0.9";
813
813
  }
814
814
  });
815
815
 
@@ -2181,8 +2181,108 @@ async function executeBlame(file, lineRange, options) {
2181
2181
  const result = await gitExec(args, options);
2182
2182
  return parsePorcelainOutput(result.stdout);
2183
2183
  }
2184
- async function analyzeBlameResults(results, filePath, options) {
2185
- const uniqueShas = [...new Set(map6(results, (r) => r.commitHash))];
2184
+ async function executeDualBlame(file, lineRange, options) {
2185
+ if (options?.mode === "change") {
2186
+ const results = await executeBlame(file, lineRange, options);
2187
+ return { blame: results, changeBlame: [] };
2188
+ }
2189
+ const [originResult, changeResult] = await Promise.allSettled([
2190
+ executeBlame(file, lineRange, options),
2191
+ executeBlame(file, lineRange, { ...options, mode: "change" })
2192
+ ]);
2193
+ const blame = originResult.status === "fulfilled" ? originResult.value : [];
2194
+ const changeBlame = changeResult.status === "fulfilled" ? changeResult.value : [];
2195
+ if (originResult.status === "rejected") {
2196
+ throw originResult.reason;
2197
+ }
2198
+ return { blame, changeBlame };
2199
+ }
2200
+ async function verifyRename(originalFile, currentFile, options) {
2201
+ try {
2202
+ const result = await gitExec(
2203
+ [
2204
+ "log",
2205
+ "--diff-filter=R",
2206
+ "--find-renames",
2207
+ "--format=%H",
2208
+ "--",
2209
+ originalFile,
2210
+ currentFile
2211
+ ],
2212
+ options
2213
+ );
2214
+ return result.stdout.trim().length > 0;
2215
+ } catch {
2216
+ return false;
2217
+ }
2218
+ }
2219
+ function crossValidateBlame(originResults, changeResults) {
2220
+ const changeMap = /* @__PURE__ */ new Map();
2221
+ for (const r of changeResults) {
2222
+ changeMap.set(r.finalLine, r);
2223
+ }
2224
+ const validated = [];
2225
+ const crossValidatedFlags = [];
2226
+ const changeFallbackFlags = [];
2227
+ const renameChecks = [];
2228
+ for (let i = 0; i < originResults.length; i++) {
2229
+ const origin = originResults[i];
2230
+ const change = changeMap.get(origin.finalLine);
2231
+ if (!change) {
2232
+ validated.push(origin);
2233
+ crossValidatedFlags.push(false);
2234
+ changeFallbackFlags.push(false);
2235
+ continue;
2236
+ }
2237
+ if (origin.commitHash === change.commitHash) {
2238
+ validated.push(origin);
2239
+ crossValidatedFlags.push(true);
2240
+ changeFallbackFlags.push(false);
2241
+ continue;
2242
+ }
2243
+ if (origin.originalFile) {
2244
+ renameChecks.push({
2245
+ originalFile: origin.originalFile,
2246
+ lineIndex: i
2247
+ });
2248
+ }
2249
+ validated.push(change);
2250
+ crossValidatedFlags.push(true);
2251
+ changeFallbackFlags.push(true);
2252
+ }
2253
+ return { validated, renameChecks, crossValidatedFlags, changeFallbackFlags };
2254
+ }
2255
+ async function analyzeBlameResults(results, filePath, options, changeResults) {
2256
+ let effectiveResults = results;
2257
+ let crossValidatedFlags;
2258
+ let changeFallbackFlagsResult;
2259
+ if (changeResults && changeResults.length > 0) {
2260
+ const { validated, renameChecks, crossValidatedFlags: cvFlags, changeFallbackFlags } = crossValidateBlame(results, changeResults);
2261
+ if (renameChecks.length > 0) {
2262
+ const pendingChecks = /* @__PURE__ */ new Map();
2263
+ for (const check of renameChecks) {
2264
+ const cacheKey = `${check.originalFile}:${filePath}`;
2265
+ if (!pendingChecks.has(cacheKey)) {
2266
+ pendingChecks.set(cacheKey, verifyRename(check.originalFile, filePath, options));
2267
+ }
2268
+ }
2269
+ const renameResults = /* @__PURE__ */ new Map();
2270
+ for (const [key, promise] of pendingChecks) {
2271
+ renameResults.set(key, await promise);
2272
+ }
2273
+ for (const check of renameChecks) {
2274
+ const cacheKey = `${check.originalFile}:${filePath}`;
2275
+ if (renameResults.get(cacheKey)) {
2276
+ validated[check.lineIndex] = results[check.lineIndex];
2277
+ changeFallbackFlags[check.lineIndex] = false;
2278
+ }
2279
+ }
2280
+ }
2281
+ effectiveResults = validated;
2282
+ crossValidatedFlags = cvFlags;
2283
+ changeFallbackFlagsResult = changeFallbackFlags;
2284
+ }
2285
+ const uniqueShas = [...new Set(map6(effectiveResults, (r) => r.commitHash))];
2186
2286
  const cosmeticMap = /* @__PURE__ */ new Map();
2187
2287
  const zeroSha = "0".repeat(40);
2188
2288
  const tasks = [];
@@ -2191,7 +2291,7 @@ async function analyzeBlameResults(results, filePath, options) {
2191
2291
  tasks.push(
2192
2292
  (async () => {
2193
2293
  try {
2194
- const blameResult = results.find((r) => r.commitHash === sha);
2294
+ const blameResult = effectiveResults.find((r) => r.commitHash === sha);
2195
2295
  if (!blameResult) return;
2196
2296
  const file = blameResult.originalFile ?? filePath;
2197
2297
  const diff = await getCosmeticDiff(sha, file, options);
@@ -2203,12 +2303,14 @@ async function analyzeBlameResults(results, filePath, options) {
2203
2303
  );
2204
2304
  });
2205
2305
  await Promise.all(tasks);
2206
- return map6(results, (blame) => {
2306
+ return map6(effectiveResults, (blame, i) => {
2207
2307
  const cosmetic = cosmeticMap.get(blame.commitHash);
2208
2308
  return {
2209
2309
  blame,
2210
2310
  isCosmetic: cosmetic?.isCosmetic ?? false,
2211
- cosmeticReason: cosmetic?.reason
2311
+ cosmeticReason: cosmetic?.reason,
2312
+ crossValidated: crossValidatedFlags?.[i],
2313
+ usedChangeFallback: changeFallbackFlagsResult?.[i]
2212
2314
  };
2213
2315
  });
2214
2316
  }
@@ -2385,10 +2487,12 @@ async function runBlameAndAuth(adapter, options, execOptions) {
2385
2487
  const lineRange = parseLineRange(
2386
2488
  options.endLine ? `${options.line},${options.endLine}` : `${options.line}`
2387
2489
  );
2388
- const blameChain = executeBlame(options.file, lineRange, {
2490
+ const blameChain = executeDualBlame(options.file, lineRange, {
2389
2491
  ...execOptions,
2390
2492
  mode: options.mode
2391
- }).then((results) => analyzeBlameResults(results, options.file, execOptions));
2493
+ }).then(
2494
+ ({ blame, changeBlame }) => analyzeBlameResults(blame, options.file, execOptions, changeBlame.length > 0 ? changeBlame : void 0)
2495
+ );
2392
2496
  const [authResult, blameResult] = await Promise.allSettled([
2393
2497
  adapter ? adapter.checkAuth() : Promise.resolve({ authenticated: false }),
2394
2498
  blameChain
@@ -2426,7 +2530,7 @@ async function processEntry(entry, featureFlags, adapter, options, execOptions,
2426
2530
  const commitNode = {
2427
2531
  type: entry.isCosmetic ? "cosmetic_commit" : "original_commit",
2428
2532
  sha: entry.blame.commitHash,
2429
- trackingMethod: traceMode === "change" ? "blame" : "blame-CMw",
2533
+ trackingMethod: traceMode === "change" || entry.usedChangeFallback ? "blame" : "blame-CMw",
2430
2534
  confidence: "exact",
2431
2535
  note: entry.cosmeticReason ? `Cosmetic change: ${entry.cosmeticReason}` : void 0
2432
2536
  };
@@ -1,3 +1,11 @@
1
- import type { BlameExecOptions, BlameResult, BlameStageResult, GitExecOptions, LineRange } from '../../types/index.js';
1
+ import type { BlameExecOptions, BlameResult, BlameStageResult, DualBlameResult, GitExecOptions, LineRange } from '../../types/index.js';
2
2
  export declare function executeBlame(file: string, lineRange: LineRange, options?: BlameExecOptions): Promise<BlameResult[]>;
3
- export declare function analyzeBlameResults(results: BlameResult[], filePath: string, options?: GitExecOptions): Promise<BlameStageResult[]>;
3
+ /**
4
+ * Run dual blame for origin mode: origin blame (-C -C -M) and change blame (-w)
5
+ * in parallel. For change mode, only runs the single blame.
6
+ *
7
+ * Assumes both blames on the same -L range produce results in the same
8
+ * finalLine order (guaranteed by git blame line-ordered output).
9
+ */
10
+ export declare function executeDualBlame(file: string, lineRange: LineRange, options?: BlameExecOptions): Promise<DualBlameResult>;
11
+ export declare function analyzeBlameResults(results: BlameResult[], filePath: string, options?: GitExecOptions, changeResults?: BlameResult[]): Promise<BlameStageResult[]>;
@@ -1,4 +1,4 @@
1
- export { analyzeBlameResults, executeBlame } from './blame.js';
1
+ export { analyzeBlameResults, executeBlame, executeDualBlame } from './blame.js';
2
2
  export { getCosmeticDiff, isCosmeticDiff } from './detection/index.js';
3
3
  export type { CosmeticCheckResult } from './detection/index.js';
4
4
  export { parsePorcelainOutput } from './parsing/index.js';
package/dist/index.cjs CHANGED
@@ -2170,8 +2170,108 @@ async function executeBlame(file, lineRange, options) {
2170
2170
  const result = await gitExec(args, options);
2171
2171
  return parsePorcelainOutput(result.stdout);
2172
2172
  }
2173
- async function analyzeBlameResults(results, filePath, options) {
2174
- const uniqueShas = [...new Set((0, import_common_utils7.map)(results, (r) => r.commitHash))];
2173
+ async function executeDualBlame(file, lineRange, options) {
2174
+ if (options?.mode === "change") {
2175
+ const results = await executeBlame(file, lineRange, options);
2176
+ return { blame: results, changeBlame: [] };
2177
+ }
2178
+ const [originResult, changeResult] = await Promise.allSettled([
2179
+ executeBlame(file, lineRange, options),
2180
+ executeBlame(file, lineRange, { ...options, mode: "change" })
2181
+ ]);
2182
+ const blame = originResult.status === "fulfilled" ? originResult.value : [];
2183
+ const changeBlame = changeResult.status === "fulfilled" ? changeResult.value : [];
2184
+ if (originResult.status === "rejected") {
2185
+ throw originResult.reason;
2186
+ }
2187
+ return { blame, changeBlame };
2188
+ }
2189
+ async function verifyRename(originalFile, currentFile, options) {
2190
+ try {
2191
+ const result = await gitExec(
2192
+ [
2193
+ "log",
2194
+ "--diff-filter=R",
2195
+ "--find-renames",
2196
+ "--format=%H",
2197
+ "--",
2198
+ originalFile,
2199
+ currentFile
2200
+ ],
2201
+ options
2202
+ );
2203
+ return result.stdout.trim().length > 0;
2204
+ } catch {
2205
+ return false;
2206
+ }
2207
+ }
2208
+ function crossValidateBlame(originResults, changeResults) {
2209
+ const changeMap = /* @__PURE__ */ new Map();
2210
+ for (const r of changeResults) {
2211
+ changeMap.set(r.finalLine, r);
2212
+ }
2213
+ const validated = [];
2214
+ const crossValidatedFlags = [];
2215
+ const changeFallbackFlags = [];
2216
+ const renameChecks = [];
2217
+ for (let i = 0; i < originResults.length; i++) {
2218
+ const origin = originResults[i];
2219
+ const change = changeMap.get(origin.finalLine);
2220
+ if (!change) {
2221
+ validated.push(origin);
2222
+ crossValidatedFlags.push(false);
2223
+ changeFallbackFlags.push(false);
2224
+ continue;
2225
+ }
2226
+ if (origin.commitHash === change.commitHash) {
2227
+ validated.push(origin);
2228
+ crossValidatedFlags.push(true);
2229
+ changeFallbackFlags.push(false);
2230
+ continue;
2231
+ }
2232
+ if (origin.originalFile) {
2233
+ renameChecks.push({
2234
+ originalFile: origin.originalFile,
2235
+ lineIndex: i
2236
+ });
2237
+ }
2238
+ validated.push(change);
2239
+ crossValidatedFlags.push(true);
2240
+ changeFallbackFlags.push(true);
2241
+ }
2242
+ return { validated, renameChecks, crossValidatedFlags, changeFallbackFlags };
2243
+ }
2244
+ async function analyzeBlameResults(results, filePath, options, changeResults) {
2245
+ let effectiveResults = results;
2246
+ let crossValidatedFlags;
2247
+ let changeFallbackFlagsResult;
2248
+ if (changeResults && changeResults.length > 0) {
2249
+ const { validated, renameChecks, crossValidatedFlags: cvFlags, changeFallbackFlags } = crossValidateBlame(results, changeResults);
2250
+ if (renameChecks.length > 0) {
2251
+ const pendingChecks = /* @__PURE__ */ new Map();
2252
+ for (const check of renameChecks) {
2253
+ const cacheKey = `${check.originalFile}:${filePath}`;
2254
+ if (!pendingChecks.has(cacheKey)) {
2255
+ pendingChecks.set(cacheKey, verifyRename(check.originalFile, filePath, options));
2256
+ }
2257
+ }
2258
+ const renameResults = /* @__PURE__ */ new Map();
2259
+ for (const [key, promise] of pendingChecks) {
2260
+ renameResults.set(key, await promise);
2261
+ }
2262
+ for (const check of renameChecks) {
2263
+ const cacheKey = `${check.originalFile}:${filePath}`;
2264
+ if (renameResults.get(cacheKey)) {
2265
+ validated[check.lineIndex] = results[check.lineIndex];
2266
+ changeFallbackFlags[check.lineIndex] = false;
2267
+ }
2268
+ }
2269
+ }
2270
+ effectiveResults = validated;
2271
+ crossValidatedFlags = cvFlags;
2272
+ changeFallbackFlagsResult = changeFallbackFlags;
2273
+ }
2274
+ const uniqueShas = [...new Set((0, import_common_utils7.map)(effectiveResults, (r) => r.commitHash))];
2175
2275
  const cosmeticMap = /* @__PURE__ */ new Map();
2176
2276
  const zeroSha = "0".repeat(40);
2177
2277
  const tasks = [];
@@ -2180,7 +2280,7 @@ async function analyzeBlameResults(results, filePath, options) {
2180
2280
  tasks.push(
2181
2281
  (async () => {
2182
2282
  try {
2183
- const blameResult = results.find((r) => r.commitHash === sha);
2283
+ const blameResult = effectiveResults.find((r) => r.commitHash === sha);
2184
2284
  if (!blameResult) return;
2185
2285
  const file = blameResult.originalFile ?? filePath;
2186
2286
  const diff = await getCosmeticDiff(sha, file, options);
@@ -2192,12 +2292,14 @@ async function analyzeBlameResults(results, filePath, options) {
2192
2292
  );
2193
2293
  });
2194
2294
  await Promise.all(tasks);
2195
- return (0, import_common_utils7.map)(results, (blame) => {
2295
+ return (0, import_common_utils7.map)(effectiveResults, (blame, i) => {
2196
2296
  const cosmetic = cosmeticMap.get(blame.commitHash);
2197
2297
  return {
2198
2298
  blame,
2199
2299
  isCosmetic: cosmetic?.isCosmetic ?? false,
2200
- cosmeticReason: cosmetic?.reason
2300
+ cosmeticReason: cosmetic?.reason,
2301
+ crossValidated: crossValidatedFlags?.[i],
2302
+ usedChangeFallback: changeFallbackFlagsResult?.[i]
2201
2303
  };
2202
2304
  });
2203
2305
  }
@@ -2378,10 +2480,12 @@ async function runBlameAndAuth(adapter, options, execOptions) {
2378
2480
  const lineRange = parseLineRange(
2379
2481
  options.endLine ? `${options.line},${options.endLine}` : `${options.line}`
2380
2482
  );
2381
- const blameChain = executeBlame(options.file, lineRange, {
2483
+ const blameChain = executeDualBlame(options.file, lineRange, {
2382
2484
  ...execOptions,
2383
2485
  mode: options.mode
2384
- }).then((results) => analyzeBlameResults(results, options.file, execOptions));
2486
+ }).then(
2487
+ ({ blame, changeBlame }) => analyzeBlameResults(blame, options.file, execOptions, changeBlame.length > 0 ? changeBlame : void 0)
2488
+ );
2385
2489
  const [authResult, blameResult] = await Promise.allSettled([
2386
2490
  adapter ? adapter.checkAuth() : Promise.resolve({ authenticated: false }),
2387
2491
  blameChain
@@ -2419,7 +2523,7 @@ async function processEntry(entry, featureFlags, adapter, options, execOptions,
2419
2523
  const commitNode = {
2420
2524
  type: entry.isCosmetic ? "cosmetic_commit" : "original_commit",
2421
2525
  sha: entry.blame.commitHash,
2422
- trackingMethod: traceMode === "change" ? "blame" : "blame-CMw",
2526
+ trackingMethod: traceMode === "change" || entry.usedChangeFallback ? "blame" : "blame-CMw",
2423
2527
  confidence: "exact",
2424
2528
  note: entry.cosmeticReason ? `Cosmetic change: ${entry.cosmeticReason}` : void 0
2425
2529
  };
package/dist/index.mjs CHANGED
@@ -2145,8 +2145,108 @@ async function executeBlame(file, lineRange, options) {
2145
2145
  const result = await gitExec(args, options);
2146
2146
  return parsePorcelainOutput(result.stdout);
2147
2147
  }
2148
- async function analyzeBlameResults(results, filePath, options) {
2149
- 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))];
2150
2250
  const cosmeticMap = /* @__PURE__ */ new Map();
2151
2251
  const zeroSha = "0".repeat(40);
2152
2252
  const tasks = [];
@@ -2155,7 +2255,7 @@ async function analyzeBlameResults(results, filePath, options) {
2155
2255
  tasks.push(
2156
2256
  (async () => {
2157
2257
  try {
2158
- const blameResult = results.find((r) => r.commitHash === sha);
2258
+ const blameResult = effectiveResults.find((r) => r.commitHash === sha);
2159
2259
  if (!blameResult) return;
2160
2260
  const file = blameResult.originalFile ?? filePath;
2161
2261
  const diff = await getCosmeticDiff(sha, file, options);
@@ -2167,12 +2267,14 @@ async function analyzeBlameResults(results, filePath, options) {
2167
2267
  );
2168
2268
  });
2169
2269
  await Promise.all(tasks);
2170
- return map6(results, (blame) => {
2270
+ return map6(effectiveResults, (blame, i) => {
2171
2271
  const cosmetic = cosmeticMap.get(blame.commitHash);
2172
2272
  return {
2173
2273
  blame,
2174
2274
  isCosmetic: cosmetic?.isCosmetic ?? false,
2175
- cosmeticReason: cosmetic?.reason
2275
+ cosmeticReason: cosmetic?.reason,
2276
+ crossValidated: crossValidatedFlags?.[i],
2277
+ usedChangeFallback: changeFallbackFlagsResult?.[i]
2176
2278
  };
2177
2279
  });
2178
2280
  }
@@ -2353,10 +2455,12 @@ async function runBlameAndAuth(adapter, options, execOptions) {
2353
2455
  const lineRange = parseLineRange(
2354
2456
  options.endLine ? `${options.line},${options.endLine}` : `${options.line}`
2355
2457
  );
2356
- const blameChain = executeBlame(options.file, lineRange, {
2458
+ const blameChain = executeDualBlame(options.file, lineRange, {
2357
2459
  ...execOptions,
2358
2460
  mode: options.mode
2359
- }).then((results) => analyzeBlameResults(results, options.file, execOptions));
2461
+ }).then(
2462
+ ({ blame, changeBlame }) => analyzeBlameResults(blame, options.file, execOptions, changeBlame.length > 0 ? changeBlame : void 0)
2463
+ );
2360
2464
  const [authResult, blameResult] = await Promise.allSettled([
2361
2465
  adapter ? adapter.checkAuth() : Promise.resolve({ authenticated: false }),
2362
2466
  blameChain
@@ -2394,7 +2498,7 @@ async function processEntry(entry, featureFlags, adapter, options, execOptions,
2394
2498
  const commitNode = {
2395
2499
  type: entry.isCosmetic ? "cosmetic_commit" : "original_commit",
2396
2500
  sha: entry.blame.commitHash,
2397
- trackingMethod: traceMode === "change" ? "blame" : "blame-CMw",
2501
+ trackingMethod: traceMode === "change" || entry.usedChangeFallback ? "blame" : "blame-CMw",
2398
2502
  confidence: "exact",
2399
2503
  note: entry.cosmeticReason ? `Cosmetic change: ${entry.cosmeticReason}` : void 0
2400
2504
  };
@@ -19,6 +19,16 @@ export interface BlameResult {
19
19
  /** Original line number before any moves/renames */
20
20
  originalLine?: number;
21
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
+ }
22
32
  /**
23
33
  * Basic commit information from git log.
24
34
  */
@@ -1,5 +1,5 @@
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
4
  export type { GitExecResult, GitExecOptions, BlameExecOptions, RemoteInfo, HealthReport, CloneStatus, } from './git.js';
5
5
  export type { GraphOptions, GraphResult } from './graph.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;
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.8";
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.8",
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",