Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@
sourceless external import identities scoped to their exact wiring sites so
imports in different files cannot merge their provenance.

- Improve source navigation and canvas space in exported graph workbenches.
VS Code continues to open and select local source ranges, while standalone
HTML opens commit-pinned GitHub, GitLab, or Bitbucket line links when the Git
origin and graph source commit are known. The navigation and inspector rails
now collapse independently, repository identity is consolidated in the left
header, and the inspector begins directly with search and node details.

## 0.3.12 - 2026-08-13

- Remove the 2 GiB aggregate canonical-payload limit from current SQLite
Expand Down
4 changes: 4 additions & 0 deletions COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ and artifact-lens models. Each view carries explicit bounded coverage. Plain
views, or using `compass export workbench-json`, returns the workbench contract.
Consumers must reject an unknown workbench major version. The HTML DOM and CSS
remain presentation details rather than machine contracts.
Standalone HTML may additionally embed optional, presentation-only source
navigation metadata for a recognized Git forge and full source commit. This
metadata is outside `compass.viewer.workbench/1`; `workbench-json` and the
versioned graph/view contracts are unchanged.
Passing `--store sqlite` also publishes a validated `store.sqlite3`
sidecar and typed `store.ref` selector. Typed code queries use JSON by default;
`--engine store` explicitly selects and validates the sidecar. The SQLite file
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/compass-cli/src/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,7 @@ const PAGES: &[Page] = &[
"export html",
"Generate the interactive graph HTML report",
["compass export html [OPTIONS]"],
"Options:\n --graph <PATH> Graph JSON [default: compass-out/graph.json]\n --labels <PATH> Community-label JSON\n --node-limit <N> 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. Interactive terminals ask before opening the generated HTML; scripts and --no-viz never prompt or open a browser."
"Options:\n --graph <PATH> Graph JSON [default: compass-out/graph.json]\n --labels <PATH> Community-label JSON\n --node-limit <N> 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."
),
page!(
"export callflow-html",
Expand Down
102 changes: 96 additions & 6 deletions crates/compass-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,16 @@ use compass_model::query_contract::{
};
use compass_output::{
AffectedLensOptions, AgentOrientation, ArtifactLens, CallflowOptions, CallflowSection,
CanvasOptions, HtmlOptions, ObsidianOptions, SvgOptions, TreeOptions, WikiOptions,
WorkbenchCoverage, WorkbenchCoverageStatus, WorkbenchModel, WorkbenchView,
CanvasOptions, HtmlOptions, ObsidianOptions, SourceNavigation, SvgOptions, TreeOptions,
WikiOptions, WorkbenchCoverage, WorkbenchCoverageStatus, WorkbenchModel, WorkbenchView,
WorkbenchViewContent, affected_lens_view_model, artifact_lens_view_model, callflow_view_model,
export_obsidian, export_wiki, graph_artifact_identity, graph_community_view_model_document,
graph_view_model_bundle_document, graph_view_model_document, node_filenames,
render_orientation_json, validate_orientation_graph_identity, write_callflow_html,
write_canvas, write_cypher, write_graphml, write_svg, write_tree_html, write_workbench_html,
write_canvas, write_cypher, write_graphml, write_svg, write_tree_html,
write_workbench_html_with_source_navigation,
};
use compass_prs::{ProcessRunner, SystemRunner};
use compass_query::{
DEFAULT_AFFECTED_RELATIONS, DEFAULT_TEXT_TOKEN_BUDGET, DiscoveryTextPageOptions,
TextPageOptions, TraversalMode, discovery_request_digest, format_affected, format_benchmark,
Expand Down Expand Up @@ -3621,7 +3623,10 @@ fn command_export(frontend: Frontend, args: &[String]) -> Outcome {
program_path: program_path.as_deref(),
},
)
.and_then(|model| export_workbench_html(&model, path))
.and_then(|model| {
let source_navigation = export_source_navigation(&inputs, &graph_path);
export_workbench_html(&model, source_navigation.as_ref(), path)
})
}
}
"json" | "viewer-json" => {
Expand Down Expand Up @@ -4124,8 +4129,13 @@ fn architecture_view_model(
})
}

fn export_workbench_html(model: &WorkbenchModel, path: PathBuf) -> Result<ExportOutput, String> {
write_workbench_html(model, &path).map_err(|error| error.to_string())?;
fn export_workbench_html(
model: &WorkbenchModel,
source_navigation: Option<&SourceNavigation>,
path: PathBuf,
) -> Result<ExportOutput, String> {
write_workbench_html_with_source_navigation(model, source_navigation, &path)
.map_err(|error| error.to_string())?;
Ok(ExportOutput::html(
format!(
"{} written - open in any browser, no server needed",
Expand All @@ -4135,6 +4145,86 @@ fn export_workbench_html(model: &WorkbenchModel, path: PathBuf) -> Result<Export
))
}

fn export_source_navigation(inputs: &ExportInputs, graph_path: &Path) -> Option<SourceNavigation> {
const GIT_SOURCE_LINK_TIMEOUT: Duration = Duration::from_secs(2);
let revision = inputs
.document
.graph
.get("build")
.and_then(serde_json::Value::as_object)
.and_then(|build| build.get("sourceCommit"))
.and_then(serde_json::Value::as_str)
.or_else(|| {
inputs
.document
.extras
.get("built_at_commit")
.and_then(serde_json::Value::as_str)
})?;
let directory = graph_path.parent()?.to_str()?;
if !matches!(revision.len(), 40 | 64) || !revision.bytes().all(|byte| byte.is_ascii_hexdigit())
{
return None;
}
let root = SystemRunner
.run(
"git",
&[
"-C".to_owned(),
directory.to_owned(),
"rev-parse".to_owned(),
"--show-toplevel".to_owned(),
],
GIT_SOURCE_LINK_TIMEOUT,
)
.ok()?;
if root.code != 0 {
return None;
}
let root = root.stdout.trim();
if root.is_empty()
|| root
.chars()
.any(|character| matches!(character, '\0' | '\n' | '\r'))
{
return None;
}
let commit_object = format!("{revision}^{{commit}}");
let commit = SystemRunner
.run(
"git",
&[
"-C".to_owned(),
root.to_owned(),
"cat-file".to_owned(),
"-e".to_owned(),
commit_object,
],
GIT_SOURCE_LINK_TIMEOUT,
)
.ok()?;
if commit.code != 0 {
return None;
}
let remote = SystemRunner
.run(
"git",
&[
"-C".to_owned(),
root.to_owned(),
"remote".to_owned(),
"get-url".to_owned(),
"origin".to_owned(),
],
GIT_SOURCE_LINK_TIMEOUT,
)
.ok()?;
if remote.code != 0 {
return None;
}
SourceNavigation::from_git_remote(remote.stdout.trim(), revision)
}

#[allow(clippy::too_many_arguments)]
fn export_callflow_json(
inputs: &ExportInputs,
Expand Down
47 changes: 46 additions & 1 deletion crates/compass-cli/tests/viewer_export_cli.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
mod support;

use std::error::Error;
use std::process::Command;

use serde_json::{Value, json};

Expand Down Expand Up @@ -393,14 +394,54 @@ fn impact_graph_export_uses_the_typed_query_contract() -> Result<(), Box<dyn Err
#[test]
fn html_export_embeds_one_workbench_for_multiple_views() -> Result<(), Box<dyn Error>> {
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", "git@gitlab.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",
"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"},
"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},
{"id":"target","label":"target","kind":"function","community":0,"source_file":"src/lib.rs","line_start":2}
Expand Down Expand Up @@ -434,6 +475,10 @@ fn html_export_embeds_one_workbench_for_multiple_views() -> Result<(), Box<dyn E
assert_eq!(document.matches("id=\"compass-viewer-model\"").count(), 1);
assert!(document.contains("compass.viewer.workbench/1"));
assert!(document.contains("\"kind\":\"call\""));
assert!(document.contains("id=\"compass-source-navigation\""));
assert!(document.contains("\"provider\":\"gitlab\""));
assert!(document.contains("\"repositoryUrl\":\"https://gitlab.com/acme/compass\""));
assert!(document.contains(&format!("\"revision\":\"{source_commit}\"")));
Ok(())
}

Expand Down
1 change: 1 addition & 0 deletions crates/compass-output/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ compass-model = { path = "../compass-model", version = "0.3.12" }
compass-pr-intelligence = { path = "../compass-pr-intelligence", version = "0.3.12" }
compass-query = { path = "../compass-query", version = "0.3.12" }
unicode-normalization.workspace = true
url.workspace = true

[dev-dependencies]
tempfile.workspace = true
Expand Down
110 changes: 55 additions & 55 deletions crates/compass-output/assets/viewer/graph.js

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions crates/compass-output/assets/viewer/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
"viewerSchema": "compass.viewer.graph/1",
"files": {
"graph.js": {
"bytes": 1065510,
"sha256": "260c5c317a7c59a250f4fbf9c04a3b12df442bbf3b52cb3d310a6e9757e8de80"
"bytes": 1068010,
"sha256": "ff9756063d6fede2a9dc26983badf28f6d8b57fda0e3c91b22048ab96c360a02"
},
"viewer.css": {
"bytes": 233074,
"sha256": "ab2710f4547006d440941e1ca4280b8392163d936b777c67cdf2d8a6fffe5a8c"
"bytes": 234489,
"sha256": "9c39d5cea6a512de1b9f46d10e051480fff84510ae92a873423186c3b30b32a8"
}
}
}
2 changes: 1 addition & 1 deletion crates/compass-output/assets/viewer/viewer.css

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions crates/compass-output/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,10 @@ pub use viewer_model::{
};
pub use wiki::{WikiExport, WikiOptions, export_wiki};
pub use workbench::{
WORKBENCH_SCHEMA, WorkbenchCoverage, WorkbenchCoverageStatus, WorkbenchModel, WorkbenchView,
WorkbenchViewContent, workbench_html_document, write_workbench_html,
SourceNavigation, SourceProvider, WORKBENCH_SCHEMA, WorkbenchCoverage, WorkbenchCoverageStatus,
WorkbenchModel, WorkbenchView, WorkbenchViewContent, workbench_html_document,
workbench_html_document_with_source_navigation, write_workbench_html,
write_workbench_html_with_source_navigation,
};

#[derive(Debug, thiserror::Error)]
Expand Down
Loading
Loading