fix: complete remaining features and fix audit issues Fixes from code audit: - FileSchemaSidebar: fix $derived syntax (use $derived.by, not $derived with type parameter), remove unused KindGroup interface - PR page: add null safety for sourceRef/targetRef with optional chaining - get_project_schema: improve top names extraction to properly handle nested vertex IDs and deduplicate results Structural search (feature I): - Add searchStructural() and listDependents() to search.ts API - Search page now has two modes: "Repositories" (existing) and "Structural" (panproto expression language queries) - Structural mode includes anchor selector (functions, types, fields, ref updates) and panproto expression hints - Results show matched schema fields with repo links Migration impact (feature H): - CompatibilityBadge now accepts dependents prop - When breaking changes exist and dependents are known, shows expandable "Downstream impact: N dependent repos may be affected" section with links to each dependent repo - PR page fetches dependents via existing dependency.list endpoint with direction=dependents, in parallel with branch comparison
Author: Aaron Steven White
Commit
a00507c1cfca8ddb46d4dc59df8a24bd18736b53Parent: 30adc38401
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-cospan8 files changed +327 -59
@@ -14,3 +14,53 @@ export function searchRepos(params: {
1414 }): Promise<SearchReposResponse> { 1515 return xrpcQuery<SearchReposResponse>('dev.cospan.repo.search', params); 1616 } 17+ 18+// ── Structural search ────────────────────────────────────────────── 19+ 20+export interface StructuralSearchResult { 21+ [key: string]: unknown; 22+ _repo_did?: string; 23+ _repo_name?: string; 24+} 25+ 26+export interface StructuralSearchResponse { 27+ anchor: string; 28+ expression: string; 29+ limit: number; 30+ results: StructuralSearchResult[]; 31+ total: number; 32+} 33+ 34+export function searchStructural(params: { 35+ q: string; 36+ anchor?: string; 37+ limit?: number; 38+}): Promise<StructuralSearchResponse> { 39+ return xrpcQuery<StructuralSearchResponse>('dev.cospan.search.structural', params); 40+} 41+ 42+// ── Dependency impact ────────────────────────────────────────────── 43+ 44+export interface DependencyEntry { 45+ did: string; 46+ repo: string; 47+ [key: string]: unknown; 48+} 49+ 50+export interface DependencyListResponse { 51+ dependencies: DependencyEntry[]; 52+ cursor: string | null; 53+} 54+ 55+export function listDependents(params: { 56+ did: string; 57+ repo: string; 58+ limit?: number; 59+}): Promise<DependencyListResponse> { 60+ return xrpcQuery<DependencyListResponse>('dev.cospan.repo.dependency.list', { 61+ did: params.did, 62+ repo: params.repo, 63+ direction: 'dependents', 64+ limit: params.limit, 65+ }); 66+}
@@ -1,15 +1,19 @@
11 <script lang="ts"> 22 import type { BranchComparisonResponse } from '$lib/api/schema.js'; 3+ import type { DependencyEntry } from '$lib/api/search.js'; 34 45 let { 56 comparison, 7+ dependents = [], 68 loading = false, 79 }: { 810 comparison: BranchComparisonResponse | null; 11+ dependents?: DependencyEntry[]; 912 loading?: boolean; 1013 } = $props(); 1114 1215 let expanded = $state(false); 16+ let impactExpanded = $state(false); 1317 </script> 1418 1519 {#if loading}
@@ -77,5 +81,40 @@
7781 {/if} 7882 </div> 7983 {/if} 84+ 85+ <!-- Downstream impact (when breaking changes exist and dependents are known) --> 86+ {#if !comparison.compatible && dependents.length > 0} 87+ <div class="mt-3 border-t border-red-500/20 pt-3"> 88+ <button 89+ type="button" 90+ class="flex w-full items-center gap-2 text-xs font-medium text-amber-400 hover:text-amber-300 transition-colors" 91+ onclick={() => (impactExpanded = !impactExpanded)} 92+ > 93+ <svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> 94+ <path stroke-linecap="round" stroke-linejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" /> 95+ </svg> 96+ Downstream impact: {dependents.length} dependent {dependents.length === 1 ? 'repo' : 'repos'} may be affected 97+ <svg 98+ class="ml-auto h-3 w-3 transition-transform {impactExpanded ? 'rotate-90' : ''}" 99+ fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" 100+ > 101+ <path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" /> 102+ </svg> 103+ </button> 104+ {#if impactExpanded} 105+ <div class="mt-2 space-y-1"> 106+ {#each dependents as dep (dep.did + '/' + dep.repo)} 107+ <a 108+ href="/{dep.did}/{dep.repo}" 109+ class="flex items-center gap-2 rounded-md px-2 py-1 text-sm text-text-secondary hover:bg-surface-2 transition-colors" 110+ > 111+ <span class="font-mono text-xs text-accent">{dep.repo}</span> 112+ <span class="text-[10px] text-text-muted">{dep.did}</span> 113+ </a> 114+ {/each} 115+ </div> 116+ {/if} 117+ </div> 118+ {/if} 80119 </div> 81120 {/if}
@@ -7,13 +7,6 @@
77 fileSchema: FileSchemaResponse | null; 88 } = $props(); 99 10- // Group vertices by kind 11- interface KindGroup { 12- kind: string; 13- icon: string; 14- items: { name: string; humanLabel: string }[]; 15- } 16- 1710 function kindIcon(kind: string): string { 1811 const lower = kind.toLowerCase(); 1912 if (lower.includes('function') || lower.includes('method')) return 'f';
@@ -25,7 +18,7 @@
2518 return '.'; 2619 } 2720 28- let groups = $derived<KindGroup[]>(() => { 21+ let groups = $derived.by(() => { 2922 if (!fileSchema || fileSchema.vertices.length === 0) return []; 3023 const map = new Map<string, { name: string; humanLabel: string }[]>(); 3124 for (const v of fileSchema.vertices) {
@@ -80,7 +73,7 @@
8073 8174 <!-- Schema outline grouped by kind --> 8275 <div class="max-h-[60vh] space-y-2 overflow-y-auto"> 83- {#each groups() as group (group.kind)} 76+ {#each groups as group (group.kind)} 8477 <div> 8578 <button 8679 type="button"
@@ -5,6 +5,7 @@ import {
55 compareBranchSchemas, 66 type BranchComparisonResponse, 77 } from '$lib/api/schema.js'; 8+import { listDependents, type DependencyEntry } from '$lib/api/search.js'; 89 910 export const load: PageServerLoad = async ({ params }) => { 1011 try {
@@ -15,16 +16,29 @@ export const load: PageServerLoad = async ({ params }) => {
1516 1617 // Structural comparison between source and target branches 1718 let branchComparison: BranchComparisonResponse | null = null; 19+ let dependents: DependencyEntry[] = []; 20+ 1821 if (pull.sourceRef && pull.targetRef) { 19- try { 20- branchComparison = await compareBranchSchemas({ 22+ // Fetch comparison and dependents in parallel 23+ const results = await Promise.allSettled([ 24+ compareBranchSchemas({ 2125 did: params.did, 2226 repo: params.repo, 2327 base: pull.targetRef.replace('refs/heads/', ''), 2428 head: pull.sourceRef.replace('refs/heads/', ''), 25- }); 26- } catch { 27- // Node unreachable; badge won't appear 29+ }), 30+ listDependents({ 31+ did: params.did, 32+ repo: params.repo, 33+ limit: 20, 34+ }), 35+ ]); 36+ 37+ if (results[0].status === 'fulfilled') { 38+ branchComparison = results[0].value; 39+ } 40+ if (results[1].status === 'fulfilled') { 41+ dependents = results[1].value.dependencies; 2842 } 2943 } 3044
@@ -34,6 +48,7 @@ export const load: PageServerLoad = async ({ params }) => {
3448 pull, 3549 comments, 3650 branchComparison, 51+ dependents, 3752 }; 3853 } catch (err) { 3954 console.error(`Failed to load pull ${params.did}/${params.repo}/pulls/${params.rkey}:`, err);
@@ -8,8 +8,8 @@
88 let { data } = $props(); 99 1010 let basePath = $derived(`/${data.did}/${data.repo}`); 11- let sourceLabel = $derived(data.pull.sourceRef.replace('refs/heads/', '')); 12- let targetLabel = $derived(data.pull.targetRef.replace('refs/heads/', '')); 11+ let sourceLabel = $derived(data.pull.sourceRef?.replace('refs/heads/', '') ?? ''); 12+ let targetLabel = $derived(data.pull.targetRef?.replace('refs/heads/', '') ?? ''); 1313 1414 const repoLayout = getContext<any>('repoLayout'); 1515 $effect(() => {
@@ -51,7 +51,7 @@
5151 5252 <!-- Structural compatibility verdict --> 5353 <div class="mb-4"> 54- <CompatibilityBadge comparison={data.branchComparison} /> 54+ <CompatibilityBadge comparison={data.branchComparison} dependents={data.dependents ?? []} /> 5555 </div> 5656 5757 {#if data.pull.body}