@lumy-pack/line-lore 0.0.6 → 0.0.8

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/README.md CHANGED
@@ -25,9 +25,12 @@ yarn add @lumy-pack/line-lore
25
25
  ### CLI Usage
26
26
 
27
27
  ```bash
28
- # Trace a single line to its PR
28
+ # Trace a single line to its originating PR
29
29
  npx @lumy-pack/line-lore trace src/auth.ts -L 42
30
30
 
31
+ # Trace the last meaningful change to a line
32
+ npx @lumy-pack/line-lore trace src/auth.ts -L 42 --mode change
33
+
31
34
  # Trace a line range
32
35
  npx @lumy-pack/line-lore trace src/config.ts -L 10,50
33
36
 
@@ -61,14 +64,21 @@ npx @lumy-pack/line-lore trace src/auth.ts -L 42 --quiet
61
64
  ```typescript
62
65
  import { trace, graph, health, clearCache } from '@lumy-pack/line-lore';
63
66
 
64
- // Trace a line to its PR
65
- const result = await trace({
67
+ // Trace a line to its originating PR (default mode)
68
+ const originResult = await trace({
69
+ file: 'src/auth.ts',
70
+ line: 42,
71
+ });
72
+
73
+ // Trace the last meaningful change to a line
74
+ const changeResult = await trace({
66
75
  file: 'src/auth.ts',
67
76
  line: 42,
77
+ mode: 'change',
68
78
  });
69
79
 
70
80
  // Find the PR node
71
- const prNode = result.nodes.find(n => n.type === 'pull_request');
81
+ const prNode = originResult.nodes.find(n => n.type === 'pull_request');
72
82
  if (prNode) {
73
83
  console.log(`PR #${prNode.prNumber}: ${prNode.prTitle}`);
74
84
  }
@@ -89,9 +99,11 @@ console.log(`Git version: ${report.gitVersion}`);
89
99
 
90
100
  ## How It Works
91
101
 
92
- @lumy-pack/line-lore executes a 4-stage deterministic pipeline:
102
+ @lumy-pack/line-lore executes a deterministic pipeline with two trace modes:
93
103
 
94
- 1. **Line → Commit (Blame)**: Git blame with `-C -C -M` flags to detect renames and copies
104
+ 1. **Line → Commit (Blame)**:
105
+ - `origin`: `git blame -w -C -C -M` — follows copy/move history across renames; whitespace-only changes ignored
106
+ - `change`: `git blame -w` — finds the last meaningful local change; ignores whitespace but does **not** track renames/copies, so a rename commit itself is attributed as the change
95
107
  2. **Cosmetic Detection**: AST structural comparison to skip formatting-only changes
96
108
  3. **Commit → Merge Commit**: Ancestry-path traversal + patch-id matching to resolve merge commits
97
109
  4. **Merge Commit → PR**: Commit message parsing + platform API lookup (filters unmerged PRs)
@@ -111,19 +123,21 @@ Strategy 1 — Cache ─────────────────── c
111
123
  │ hit? → return cached PRInfo
112
124
  │ miss + --cache-only? → return null (skip all fallbacks)
113
125
 
114
- Strategy 2 — Ancestry-path + Message cost: 1 git-log
115
- 1st: git log --merges --ancestry-path --first-parent sha..HEAD
116
- 2nd: (fallback) full ancestry-path without --first-parent
126
+ Strategy 2 — Platform API ──────────── cost: 1 HTTP request
127
+ gh api repos/{owner}/{repo}/commits/{sha}/pulls
128
+ Filter: merged PRs only (mergedAt != null)
129
+ │ found? → return PRInfo
130
+
131
+ Strategy 3 — Ancestry-path + Message ─ cost: 1-2 git-log traversals
132
+ │ Search verified merge candidates:
133
+ │ • first-parent ancestry path first
134
+ │ • full ancestry path second
117
135
  │ Parse merge subject with 3 regex patterns:
118
136
  │ • /Merge pull request #(\d+)/ — GitHub merge commit
119
137
  │ • /\(#(\d+)\)\s*$/ — Squash merge convention
120
- │ • /!(\d+)\s*$/ — GitLab merge commit
121
- │ If PR# found + adapter available enrich via API
122
- found? return PRInfo
123
-
124
- Strategy 3 — Platform API ──────────── cost: 1 HTTP request
125
- │ gh api repos/{owner}/{repo}/commits/{sha}/pulls
126
- │ Filter: merged PRs only (mergedAt != null)
138
+ │ • /See merge request ...!(\d+)$/ — GitLab merge commit
139
+ │ If message has no PR number and API is available:
140
+ query the merge commit SHA directly
127
141
  │ found? → return PRInfo
128
142
 
129
143
  Strategy 4 — Patch-ID matching ─────── cost: streaming 500+ commits
@@ -136,7 +150,7 @@ Strategy 4 — Patch-ID matching ─────── cost: streaming 500+ comm
136
150
  All failed → null
137
151
  ```
138
152
 
139
- **Why this order?** The chain is sorted by cost. Most repositories use merge or squash workflows, so Strategy 2 resolves >90% of lookups with zero API calls. Strategy 3 (single HTTP) is cheaper than Strategy 4 (streaming hundreds of commit diffs), so API is tried before patch-id scanning.
153
+ **Why this order?** Direct API lookup is the strongest Level 2 signal, so it runs before local ancestry heuristics. Verified ancestry is still cheaper than patch-id scanning and resolves most merge-based workflows without diff streaming.
140
154
 
141
155
  **Patch-ID explained**: `git patch-id --stable` generates a content-based hash from a commit's diff, ignoring all metadata (author, date, message). When a commit is rebased, its SHA changes but the patch-id stays the same — enabling deterministic matching of rebased commits.
142
156
 
@@ -170,7 +184,7 @@ interface TraceNode {
170
184
 
171
185
  | Type | Symbol | Meaning | When it appears |
172
186
  |------|--------|---------|-----------------|
173
- | `original_commit` | `●` | The commit that introduced or last modified this line | Always (at least one) |
187
+ | `original_commit` | `●` | The commit selected by the active trace mode | Always (at least one) |
174
188
  | `cosmetic_commit` | `○` | A formatting-only change (whitespace, imports) | When AST detects no logic change |
175
189
  | `merge_commit` | `◆` | The merge commit on the base branch | Merge-based workflows |
176
190
  | `rebased_commit` | `◇` | A rebased version of the original commit | Rebase workflows with patch-id match |
@@ -206,6 +220,13 @@ interface TraceNode {
206
220
  └─ https://github.com/org/repo/pull/42
207
221
  ```
208
222
 
223
+ **Last meaningful change mode (`--mode change`):**
224
+ ```
225
+ ● Commit e4f5a6b [exact] via blame
226
+ ▸ PR #55 refactor: update validation logic
227
+ └─ https://github.com/org/repo/pull/55
228
+ ```
229
+
209
230
  **Squash merge (Level 2):**
210
231
  ```
211
232
  ● Commit e4f5a6b [exact] via blame-CMw
@@ -334,7 +355,7 @@ import { trace, graph, health, clearCache, LineLoreError } from '@lumy-pack/line
334
355
 
335
356
  ### `trace(options): Promise<TraceFullResult>`
336
357
 
337
- Trace a code line to its originating PR.
358
+ Trace a code line to its originating or last-change PR, depending on the selected mode.
338
359
 
339
360
  **Options (`TraceOptions`):**
340
361
 
@@ -344,6 +365,7 @@ Trace a code line to its originating PR.
344
365
  | `line` | `number` | yes | — | Starting line number (1-indexed) |
345
366
  | `endLine` | `number` | no | — | Ending line for range queries |
346
367
  | `remote` | `string` | no | `'origin'` | Git remote name |
368
+ | `mode` | `'origin' \| 'change'` | no | `'origin'` | `origin` uses `git blame -w -C -C -M` (follows copy/move history across renames), `change` uses `git blame -w` (finds the last meaningful local change, ignoring whitespace but not copy/move) |
347
369
  | `deep` | `boolean` | no | `false` | Expand patch-id scan range (500→2000), continue search after merge commit match |
348
370
  | `noAst` | `boolean` | no | `false` | Disable AST analysis |
349
371
  | `noCache` | `boolean` | no | `false` | Disable cache reads and writes |
@@ -389,6 +411,7 @@ const result = await trace({
389
411
  file: 'src/config.ts',
390
412
  line: 10,
391
413
  endLine: 50,
414
+ mode: 'origin',
392
415
  deep: true, // search harder for squash merges
393
416
  noCache: true, // skip cache for fresh results
394
417
  });
@@ -631,8 +654,9 @@ import type {
631
654
 
632
655
  | Command | Purpose |
633
656
  |---------|---------|
634
- | `npx @lumy-pack/line-lore trace <file>` | Trace a line to its PR |
657
+ | `npx @lumy-pack/line-lore trace <file>` | Trace a line to its origin or last-change PR |
635
658
  | `-L, --line <num>` | Starting line (required) |
659
+ | `--mode <origin\|change>` | Choose between content origin and last meaningful change |
636
660
  | `--end-line <num>` | Ending line for range |
637
661
  | `--deep` | Deep trace (squash merges) |
638
662
  | `--output <format>` | Output as json, llm, or human |