Skip to content

Commit 4d31c2f

Browse files
astefanoclaude
andcommitted
Per-language GitHub links from Schema 2.0 envelope inputs
buildGitHubLink now reads per-language source configs (repo URL, commit ref, path prefix) extracted from the Schema 2.0 inputs array, so Rust nodes link to dalek-cryptography/curve25519-dalek and Lean nodes link to Beneficial-AI-Foundation/curve25519-dalek-lean-verify without any manual URL parameters. Made-with: Cursor Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b4f56b5 commit 4d31c2f

3 files changed

Lines changed: 82 additions & 34 deletions

File tree

web/src/graph-loader.ts

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
2-
D3Graph, D3Node, D3Link, SimplifiedNode, ProbeAtom,
2+
D3Graph, D3Node, D3Link, SimplifiedNode, ProbeAtom, SourceConfig,
3+
Schema2Envelope, Schema2Source,
34
isSimplifiedFormat, isD3GraphFormat, isAtomDictFormat, isSchema2Envelope,
45
VerificationStatus,
56
} from './types';
@@ -183,9 +184,35 @@ export function convertAtomDictToD3Graph(atoms: Record<string, ProbeAtom>): D3Gr
183184
* Supports D3Graph format, simplified format, probe atom dict format,
184185
* and Schema 2.0 envelopes.
185186
*/
187+
/**
188+
* Extract per-language GitHub source configs from a Schema 2.0 envelope.
189+
* Uses the commit hash as the git ref (always valid on GitHub).
190+
* For Rust workspace crates the package name becomes the path prefix.
191+
*/
192+
function extractSourceConfigs(envelope: Schema2Envelope): SourceConfig[] {
193+
const entries: Schema2Source[] = [];
194+
if (envelope.inputs) {
195+
for (const input of envelope.inputs) entries.push(input.source);
196+
} else if (envelope.source) {
197+
entries.push(envelope.source);
198+
}
199+
200+
return entries.map(src => ({
201+
github_url: src.repo.replace(/\.git$/, ''),
202+
ref: src.commit,
203+
path_prefix: src.language === 'rust' ? src.package : '',
204+
language: src.language,
205+
}));
206+
}
207+
186208
export function parseAndNormalizeGraph(data: unknown): D3Graph {
187209
if (isSchema2Envelope(data)) {
188-
return parseAndNormalizeGraph(data.data);
210+
const sourceConfigs = extractSourceConfigs(data);
211+
const graph = parseAndNormalizeGraph(data.data);
212+
if (sourceConfigs.length > 0) {
213+
graph.metadata.source_configs = sourceConfigs;
214+
}
215+
return graph;
189216
}
190217
if (isD3GraphFormat(data)) {
191218
return data;

web/src/main.ts

Lines changed: 44 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -162,42 +162,54 @@ function showLargeGraphPrompt(fileSize: number): void {
162162

163163

164164
/**
165-
* Build a GitHub link to the source code
165+
* Build a full-path string from a relative_path and optional prefix,
166+
* avoiding duplication when the path already starts with the prefix.
166167
*/
167-
function buildGitHubLink(node: D3Node): string | null {
168-
if (!githubBaseUrl) return null;
169-
170-
// Clean up the base URL (remove trailing slash)
171-
const baseUrl = githubBaseUrl.replace(/\/$/, '');
172-
173-
// Clean up path prefix (remove leading/trailing slashes)
174-
const prefix = githubPathPrefix.replace(/^\/|\/$/g, '');
175-
176-
// Build the full path, but avoid duplicating if relative_path already starts with prefix
177-
let fullPath: string;
178-
if (prefix) {
179-
// Check if relative_path already starts with the prefix (avoid duplication)
180-
if (node.relative_path.startsWith(prefix + '/') || node.relative_path === prefix) {
181-
fullPath = node.relative_path;
182-
} else {
183-
fullPath = `${prefix}/${node.relative_path}`;
184-
}
185-
} else {
186-
fullPath = node.relative_path;
168+
function buildFullPath(relativePath: string, prefix: string): string {
169+
const cleanPrefix = prefix.replace(/^\/|\/$/g, '');
170+
if (!cleanPrefix) return relativePath;
171+
if (relativePath.startsWith(cleanPrefix + '/') || relativePath === cleanPrefix) {
172+
return relativePath;
187173
}
188-
189-
// Build the link with line numbers if available
190-
let link = `${baseUrl}/blob/${githubBranch}/${fullPath}`;
191-
192-
if (node.start_line) {
193-
link += `#L${node.start_line}`;
194-
// Only add end line if it's greater than start line (sanity check)
195-
if (node.end_line && node.end_line > node.start_line) {
196-
link += `-L${node.end_line}`;
174+
return `${cleanPrefix}/${relativePath}`;
175+
}
176+
177+
/**
178+
* Append #L<start>-L<end> line-number fragment to a link.
179+
*/
180+
function appendLineFragment(link: string, start?: number, end?: number): string {
181+
if (!start) return link;
182+
link += `#L${start}`;
183+
if (end && end > start) link += `-L${end}`;
184+
return link;
185+
}
186+
187+
/**
188+
* Build a GitHub link to the source code.
189+
* Prefers per-language source configs from the Schema 2.0 envelope,
190+
* falling back to the global githubBaseUrl / githubBranch / githubPathPrefix.
191+
*/
192+
function buildGitHubLink(node: D3Node): string | null {
193+
if (!node.relative_path) return null;
194+
195+
// Try per-language source config (from Schema 2.0 envelope metadata)
196+
if (node.language && state.fullGraph?.metadata.source_configs) {
197+
const config = state.fullGraph.metadata.source_configs.find(
198+
c => c.language === node.language,
199+
);
200+
if (config) {
201+
const fullPath = buildFullPath(node.relative_path, config.path_prefix);
202+
const link = `${config.github_url}/blob/${config.ref}/${fullPath}`;
203+
return appendLineFragment(link, node.start_line, node.end_line);
197204
}
198205
}
199-
200-
return link;
206+
207+
// Fallback: global settings
208+
if (!githubBaseUrl) return null;
209+
const baseUrl = githubBaseUrl.replace(/\/$/, '');
210+
const fullPath = buildFullPath(node.relative_path, githubPathPrefix);
211+
const link = `${baseUrl}/blob/${githubBranch}/${fullPath}`;
212+
return appendLineFragment(link, node.start_line, node.end_line);
201213
}
202214

203215
/**

web/src/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,12 +179,21 @@ export interface D3Link {
179179
type: LinkType | string; // 'inner' | 'precondition' | 'postcondition' (or legacy 'calls')
180180
}
181181

182+
/** Per-language GitHub source config derived from Schema 2.0 envelope inputs. */
183+
export interface SourceConfig {
184+
github_url: string;
185+
ref: string;
186+
path_prefix: string;
187+
language: string;
188+
}
189+
182190
export interface D3GraphMetadata {
183191
total_nodes: number;
184192
total_edges: number;
185193
project_root: string;
186194
generated_at: string;
187195
github_url?: string;
196+
source_configs?: SourceConfig[];
188197
}
189198

190199
export interface D3Graph {

0 commit comments

Comments
 (0)