diff --git a/CHANGELOG.md b/CHANGELOG.md index a520f40a..2b111e61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +- Prevent standalone HTML exports from publishing dead forge source links for + local-only graph commits. Immutable links now require the recorded commit to + be reachable from a local `origin` remote-tracking ref. + - Make community detail graphs easier to scan in both exported HTML and VS Code by grouping node kinds into accessible color-and-shape families, coloring edges by relationship purpose while retaining confidence strokes, diff --git a/crates/compass-cli/src/help.rs b/crates/compass-cli/src/help.rs index e905b4b7..b8843122 100644 --- a/crates/compass-cli/src/help.rs +++ b/crates/compass-cli/src/help.rs @@ -549,7 +549,7 @@ const PAGES: &[Page] = &[ "export html", "Generate the interactive graph HTML report", ["compass export html [OPTIONS]"], - "Options:\n --graph Graph JSON [default: compass-out/graph.json]\n --labels Community-label JSON\n --node-limit Maximum nodes rendered [default: 5000]\n --no-viz Skip visualization output\n\nExamples:\n compass export html\n compass export html --node-limit 2000\n\nNotes:\n Large exports embed a bounded set of complete community details; use VS Code or export json --community ID for an omitted detail. Source actions open immutable commit links for recognized GitHub, GitLab, and Bitbucket origins when the graph records a full source commit. Interactive terminals ask before opening the generated HTML; scripts and --no-viz never prompt or open a browser." + "Options:\n --graph Graph JSON [default: compass-out/graph.json]\n --labels Community-label JSON\n --node-limit Maximum nodes rendered [default: 5000]\n --no-viz Skip visualization output\n\nExamples:\n compass export html\n compass export html --node-limit 2000\n\nNotes:\n Large exports embed a bounded set of complete community details; use VS Code or export json --community ID for an omitted detail. Source actions open immutable commit links for recognized GitHub, GitLab, and Bitbucket origins when the graph records a full source commit reachable from a local origin-tracking ref. Interactive terminals ask before opening the generated HTML; scripts and --no-viz never prompt or open a browser." ), page!( "export callflow-html", diff --git a/crates/compass-cli/src/lib.rs b/crates/compass-cli/src/lib.rs index 70418194..ac7d50e4 100644 --- a/crates/compass-cli/src/lib.rs +++ b/crates/compass-cli/src/lib.rs @@ -4235,7 +4235,26 @@ fn export_source_navigation(inputs: &ExportInputs, graph_path: &Path) -> Option< if remote.code != 0 { return None; } - SourceNavigation::from_git_remote(remote.stdout.trim(), revision) + let navigation = SourceNavigation::from_git_remote(remote.stdout.trim(), revision)?; + let remote_reachability = SystemRunner + .run( + "git", + &[ + "-C".to_owned(), + root.to_owned(), + "for-each-ref".to_owned(), + "--count=1".to_owned(), + "--format=%(refname)".to_owned(), + format!("--contains={revision}"), + "refs/remotes/origin".to_owned(), + ], + GIT_SOURCE_LINK_TIMEOUT, + ) + .ok()?; + if remote_reachability.code != 0 || remote_reachability.stdout.trim().is_empty() { + return None; + } + Some(navigation) } #[allow(clippy::too_many_arguments)] diff --git a/crates/compass-cli/tests/viewer_export_cli.rs b/crates/compass-cli/tests/viewer_export_cli.rs index c623b4e1..613716ee 100644 --- a/crates/compass-cli/tests/viewer_export_cli.rs +++ b/crates/compass-cli/tests/viewer_export_cli.rs @@ -431,6 +431,11 @@ fn html_export_embeds_one_workbench_for_multiple_views() -> Result<(), Box Result<(), Box Result<(), Box> { + let directory = tempfile::tempdir()?; + let initialized = Command::new("git") + .args(["init", "--quiet"]) + .current_dir(directory.path()) + .status()?; + assert!(initialized.success()); + let remote = Command::new("git") + .args([ + "remote", + "add", + "origin", + "https://github.com/acme/compass.git", + ]) + .current_dir(directory.path()) + .status()?; + assert!(remote.success()); + std::fs::create_dir_all(directory.path().join("src"))?; + std::fs::write(directory.path().join("src/lib.rs"), "fn caller() {}\n")?; + let added = Command::new("git") + .args(["add", "src/lib.rs"]) + .current_dir(directory.path()) + .status()?; + assert!(added.success()); + let committed = Command::new("git") + .args([ + "-c", + "user.name=Compass Test", + "-c", + "user.email=compass@example.com", + "commit", + "--quiet", + "-m", + "local-only fixture", + ]) + .current_dir(directory.path()) + .status()?; + assert!(committed.success()); + let source_commit = Command::new("git") + .args(["rev-parse", "HEAD"]) + .current_dir(directory.path()) + .output()?; + assert!(source_commit.status.success()); + let source_commit = String::from_utf8(source_commit.stdout)?.trim().to_owned(); + let graph = directory.path().join("graph.json"); + let html = directory.path().join("review.html"); + std::fs::write( + &graph, + serde_json::to_vec(&json!({ + "directed": true, + "multigraph": false, + "graph": { + "schema":"compass.graph/1", + "build":{"sourceCommit":source_commit} + }, + "nodes": [ + {"id":"caller","label":"caller","kind":"function","community":0,"source_file":"src/lib.rs","line_start":1} + ], + "links": [] + }))?, + )?; + let output = support::compass_command() + .args([ + "export", + "html", + "--graph", + graph.to_string_lossy().as_ref(), + "--output", + html.to_string_lossy().as_ref(), + "--code-graph", + ]) + .current_dir(directory.path()) + .output()?; + assert_eq!( + output.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + let document = std::fs::read_to_string(html)?; + assert!(!document.contains("id=\"compass-source-navigation\"")); + assert!(!document.contains(&format!( + "https://github.com/acme/compass/blob/{source_commit}/" + ))); + Ok(()) +} + #[test] fn export_rejects_unknown_and_view_incompatible_options_before_io() -> Result<(), Box> { let unknown = support::compass_command() diff --git a/docs/reference/outputs.md b/docs/reference/outputs.md index 3b144901..2b5070de 100644 --- a/docs/reference/outputs.md +++ b/docs/reference/outputs.md @@ -333,9 +333,11 @@ file and highlights its recorded lines in the VS Code extension. A standalone HTML export instead opens an immutable forge permalink when all required evidence is available: the graph records a full source commit, the graph is inside a Git worktree with a recognized `origin`, and that origin is GitHub, -GitLab, or Bitbucket. The link uses the graph's commit rather than a mutable -branch; historical comparisons use the commit for the selected side. If any -part of that evidence is absent or unsafe, the viewer does not invent a link. +GitLab, or Bitbucket. The recorded commit must also be reachable from a local +`origin` remote-tracking ref, preventing a local-only object from becoming a +dead forge URL. The link uses the graph's commit rather than a mutable branch; +historical comparisons use the commit for the selected side. If any part of +that evidence is absent or unsafe, the viewer does not invent a link. No repository URL is added to `compass.viewer.workbench/1` or `workbench-json`; standalone HTML carries the optional presentation metadata separately.