feat: click any breaking/compatible change to see the exact code it refers to

Author: Aaron Steven White
Commit 26701d8b332b8d282088a0f5a50cb492eb502183
Parent: 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-cospan
1 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 
cospan · schematic version control on atproto built on AT Protocol