feat: click any breaking/compatible change to see the exact code it refers to
Author: Aaron Steven White
Commit
26701d8b332b8d282088a0f5a50cb492eb502183Parent: 5331690326
Structural diff unavailable
These commits were pushed via plain git push, so no pre-parsed
schemas are available. Install git-remote-cospan and re-push via panproto:// to
see scope-level changes, breaking change detection, and semantic diffs.
brew install panproto/tap/git-remote-cospan1 file changed +95 -14
@@ -168,6 +168,59 @@
168168 .sort((a, b) => b.changes.length - a.changes.length); 169169 } 170170 171+ // Extract search terms from a change entry to find matching code lines. 172+ // A change like { vertexId: "dev.cospan.repo:body.protocol", kind: "RemovedVertex" } 173+ // produces search terms ["protocol"] so we can find the line in the diff. 174+ function extractSearchTerms(change: { label: string; kind: string; vertexId?: string; src?: string; tgt?: string; name?: string }): string[] { 175+ const terms: string[] = []; 176+ // Extract the leaf name from vertexId 177+ if (change.vertexId) { 178+ const leaf = extractLeafName(change.vertexId); 179+ if (leaf && !leaf.startsWith('$')) terms.push(leaf); 180+ } 181+ // Edge name 182+ if (change.name && !change.name.startsWith('$')) terms.push(change.name); 183+ // Extract names from src/tgt 184+ if (change.src) { 185+ const s = extractLeafName(change.src); 186+ if (s && !s.startsWith('$')) terms.push(s); 187+ } 188+ if (change.tgt) { 189+ const t = extractLeafName(change.tgt); 190+ if (t && !t.startsWith('$')) terms.push(t); 191+ } 192+ // Deduplicate 193+ return [...new Set(terms)]; 194+ } 195+ 196+ // Find diff lines that match any of the search terms. 197+ // Returns matching lines plus 1 line of context on each side. 198+ function findMatchingLines( 199+ hunks: DiffFile['hunks'], 200+ terms: string[] 201+ ): DiffFile['hunks'][0]['lines'] { 202+ if (terms.length === 0) return []; 203+ const result: DiffFile['hunks'][0]['lines'] = []; 204+ for (const hunk of hunks) { 205+ for (let i = 0; i < hunk.lines.length; i++) { 206+ const line = hunk.lines[i]; 207+ if (terms.some(t => line.content.includes(t))) { 208+ // Add context: 1 line before and after 209+ if (i > 0 && !result.includes(hunk.lines[i - 1])) { 210+ result.push(hunk.lines[i - 1]); 211+ } 212+ if (!result.includes(line)) { 213+ result.push(line); 214+ } 215+ if (i + 1 < hunk.lines.length && !result.includes(hunk.lines[i + 1])) { 216+ result.push(hunk.lines[i + 1]); 217+ } 218+ } 219+ } 220+ } 221+ return result.slice(0, 20); // Cap at 20 lines 222+ } 223+ 171224 function shortVertex(v: string): string { 172225 const parts = v.split('::'); 173226 for (let i = parts.length - 1; i >= 0; i--) {
@@ -307,37 +360,65 @@
307360 </div> 308361 </div> 309362 310- <!-- Breaking changes - open by default since they're critical --> 363+ <!-- Breaking changes - open by default, each clickable to show code --> 311364 {#if sd.breakingChanges.length > 0} 312365 <details open class="mb-3 rounded-md border border-red-500/20"> 313366 <summary class="cursor-pointer px-3 py-2 text-xs font-semibold uppercase tracking-wider text-red-400 hover:bg-red-500/5"> 314367 ⚠ Breaking changes ({sd.breakingCount}) 315368 </summary> 316- <ul class="space-y-1 px-3 pb-2"> 369+ <div class="space-y-0.5 px-2 pb-2"> 317370 {#each sd.breakingChanges as bc (bc.label)} 318- <li class="flex items-start gap-2 rounded-md bg-red-500/5 px-3 py-1.5 text-sm"> 319- <span class="text-text-primary">{bc.label}</span> 320- <span class="ml-auto shrink-0 rounded bg-surface-2 px-1.5 py-0.5 text-[10px] text-text-muted">{bc.kind}</span> 321- </li> 371+ {@const searchTerms = extractSearchTerms(bc)} 372+ {@const matchingLines = findMatchingLines(file.hunks, searchTerms)} 373+ <details class="rounded-md"> 374+ <summary class="flex cursor-pointer items-start gap-2 rounded-md bg-red-500/5 px-3 py-1.5 text-sm hover:bg-red-500/10 transition-colors"> 375+ <span class="shrink-0 text-red-400 mt-0.5">⚠</span> 376+ <span class="text-text-primary flex-1">{bc.label}</span> 377+ {#if matchingLines.length > 0} 378+ <span class="shrink-0 text-[10px] text-text-muted mt-0.5">{matchingLines.length} lines</span> 379+ {/if} 380+ <span class="shrink-0 rounded bg-surface-2 px-1.5 py-0.5 text-[10px] text-text-muted">{bc.kind}</span> 381+ </summary> 382+ {#if matchingLines.length > 0} 383+ <pre class="mt-1 overflow-x-auto rounded bg-surface-0 font-mono text-[11px] leading-[18px] mx-3 mb-1">{#each matchingLines as line}<span class={lineClass(line.origin)}><span class="inline-block w-7 select-none text-right pr-1 text-[10px] text-text-muted/50">{line.oldLineno ?? ''}</span><span class="inline-block w-7 select-none text-right pr-1 text-[10px] text-text-muted/50">{line.newLineno ?? ''}</span><span class="inline-block w-3 select-none text-[10px]">{linePrefix(line.origin)}</span>{stripNewline(line.content)} 384+</span>{/each}</pre> 385+ {:else} 386+ <p class="mx-3 mb-1 text-[11px] text-text-muted italic">No matching code found in this diff</p> 387+ {/if} 388+ </details> 322389 {/each} 323- </ul> 390+ </div> 324391 </details> 325392 {/if} 326393 327- <!-- Compatible changes - collapsed by default --> 394+ <!-- Compatible changes - collapsed, each clickable to show code --> 328395 {#if sd.nonBreakingChanges.length > 0} 329396 <details class="mb-3 rounded-md border border-emerald-500/20"> 330397 <summary class="cursor-pointer px-3 py-2 text-xs font-semibold uppercase tracking-wider text-emerald-400 hover:bg-emerald-500/5"> 331398 ✓ Compatible changes ({sd.nonBreakingCount}) 332399 </summary> 333- <ul class="space-y-1 px-3 pb-2"> 400+ <div class="space-y-0.5 px-2 pb-2"> 334401 {#each sd.nonBreakingChanges as nb (nb.label)} 335- <li class="flex items-start gap-2 rounded-md bg-emerald-500/5 px-3 py-1.5 text-sm"> 336- <span class="text-text-primary">{nb.label}</span> 337- <span class="ml-auto shrink-0 rounded bg-surface-2 px-1.5 py-0.5 text-[10px] text-text-muted">{nb.kind}</span> 338- </li> 402+ {@const searchTerms = extractSearchTerms(nb)} 403+ {@const matchingLines = findMatchingLines(file.hunks, searchTerms)} 404+ <details class="rounded-md"> 405+ <summary class="flex cursor-pointer items-start gap-2 rounded-md bg-emerald-500/5 px-3 py-1.5 text-sm hover:bg-emerald-500/10 transition-colors"> 406+ <span class="shrink-0 text-emerald-400 mt-0.5">✓</span> 407+ <span class="text-text-primary flex-1">{nb.label}</span> 408+ {#if matchingLines.length > 0} 409+ <span class="shrink-0 text-[10px] text-text-muted mt-0.5">{matchingLines.length} lines</span> 410+ {/if} 411+ <span class="shrink-0 rounded bg-surface-2 px-1.5 py-0.5 text-[10px] text-text-muted">{nb.kind}</span> 412+ </summary> 413+ {#if matchingLines.length > 0} 414+ <pre class="mt-1 overflow-x-auto rounded bg-surface-0 font-mono text-[11px] leading-[18px] mx-3 mb-1">{#each matchingLines as line}<span class={lineClass(line.origin)}><span class="inline-block w-7 select-none text-right pr-1 text-[10px] text-text-muted/50">{line.oldLineno ?? ''}</span><span class="inline-block w-7 select-none text-right pr-1 text-[10px] text-text-muted/50">{line.newLineno ?? ''}</span><span class="inline-block w-3 select-none text-[10px]">{linePrefix(line.origin)}</span>{stripNewline(line.content)} 415+</span>{/each}</pre> 416+ {:else} 417+ <p class="mx-3 mb-1 text-[11px] text-text-muted italic">No matching code found in this diff</p> 418+ {/if} 419+ </details> 339420 {/each} 340- </ul> 421+ </div> 341422 </details> 342423 {/if} 343424