From 0285449eb09c2c1f9306f7715a8f6b2ca3a5ac54 Mon Sep 17 00:00:00 2001 From: forhappy Date: Sun, 16 Aug 2026 19:57:49 -0700 Subject: [PATCH] Harden CLI help and export contracts --- crates/compass-cli/src/help.rs | 114 ++++++++++++++++++ crates/compass-cli/src/lib.rs | 25 +++- crates/compass-cli/tests/coverage_paths.rs | 22 ++++ crates/compass-cli/tests/help_cli.rs | 21 ++++ crates/compass-cli/tests/viewer_export_cli.rs | 22 ++++ 5 files changed, 198 insertions(+), 6 deletions(-) diff --git a/crates/compass-cli/src/help.rs b/crates/compass-cli/src/help.rs index d21d394b..e905b4b7 100644 --- a/crates/compass-cli/src/help.rs +++ b/crates/compass-cli/src/help.rs @@ -191,6 +191,30 @@ const PAGES: &[Page] = &[ ], "Options:\n [PATH] Project or compass-out directory [default: compass-out]\n --output DIR New backup directory\n --from DIR Backup directory to restore\n --into DIR New output directory for a restore\n --format Output format [default: text]\n\nExamples:\n compass store status --format json\n compass store validate compass-out\n compass store backup compass-out --output /safe/backups/project\n compass store restore --from /safe/backups/project --into restored-out\n\nNotes:\n Backup and restore are SQLite local operations. Stop writers before backup; restore only into a new or empty output directory. A stale or corrupt sidecar remains rebuildable with `compass update --force --store sqlite`. The optional redb adapter is library-only and is not selected by this command." ), + page!( + "store status", + "Inspect graph and SQLite sidecar status", + ["compass store status [PATH] [OPTIONS]"], + "Arguments:\n [PATH] Project or compass-out directory [default: compass-out]\n\nOptions:\n --format Output format [default: text]\n\nExamples:\n compass store status\n compass store status compass-out --format json" + ), + page!( + "store validate", + "Validate graph and SQLite sidecar integrity", + ["compass store validate [PATH] [OPTIONS]"], + "Arguments:\n [PATH] Project or compass-out directory [default: compass-out]\n\nOptions:\n --format Output format [default: text]\n\nExamples:\n compass store validate compass-out\n compass store validate --format json" + ), + page!( + "store backup", + "Create a validated local graph-store backup", + ["compass store backup [PATH] --output DIR [OPTIONS]"], + "Arguments:\n [PATH] Project or compass-out directory [default: compass-out]\n --output DIR New backup directory\n\nOptions:\n --format Output format [default: text]\n\nExamples:\n compass store backup compass-out --output /safe/backups/project\n compass store backup --output ./backup --format json\n\nNotes:\n Stop graph writers before taking a backup." + ), + page!( + "store restore", + "Restore a validated graph-store backup into a new directory", + ["compass store restore --from DIR --into DIR [OPTIONS]"], + "Options:\n --from DIR Backup directory to restore\n --into DIR New or empty output directory\n --format Output format [default: text]\n\nExamples:\n compass store restore --from /safe/backups/project --into restored-out\n compass store restore --from ./backup --into ./restored --format json\n\nNotes:\n Restore never overwrites an existing output directory." + ), page!( "extract", "Build a graph with optional semantic sources and model enrichment", @@ -299,6 +323,54 @@ const PAGES: &[Page] = &[ ["compass program [OPTIONS]"], "Commands:\n summary Show artifact and evidence counts\n coverage Aggregate capability coverage and reasons\n functions List functions and filter by file, language, or name\n show Show a function, summary, coverage, and callers\n callers List resolved callers\n explain-call Explain calls containing a source byte\n call-graph Build a bounded caller/callee graph from --symbol or --at\n query Query the Program IR graph projection\n\nCommon options:\n --program Program artifact [default: compass-out/program.json]\n --format Inspection output format [default: text]\n\nExamples:\n compass program coverage\n compass program functions --language rust --format json\n compass program show 0123abcd\n compass program explain-call src/lib.rs:240\n compass program call-graph --at src/lib.rs:240 --direction both --depth 2 --format json\n compass program query \"MATCH (f) WHERE f.kind = 'program_function' RETURN f LIMIT 10\"\n\nNotes:\n Program inspection is offline and read-only. Conclusions must be gated by capability coverage." ), + page!( + "program summary", + "Show Program IR artifact and evidence counts", + ["compass program summary [OPTIONS]"], + "Options:\n --program Program artifact [default: compass-out/program.json]\n --format Output format [default: text]\n\nExamples:\n compass program summary\n compass program summary --format json" + ), + page!( + "program coverage", + "Show Program IR capability coverage and reasons", + ["compass program coverage [OPTIONS]"], + "Options:\n --program Program artifact [default: compass-out/program.json]\n --format Output format [default: text]\n\nExamples:\n compass program coverage\n compass program coverage --format json" + ), + page!( + "program functions", + "List functions from the canonical Program IR", + ["compass program functions [OPTIONS]"], + "Options:\n --program Program artifact [default: compass-out/program.json]\n --file Filter by source file\n --language Filter by language\n --name Filter by function name\n --limit Maximum functions\n --format Output format [default: text]\n\nExamples:\n compass program functions\n compass program functions --language rust --name build --format json" + ), + page!( + "program show", + "Show one Program IR function and its evidence", + ["compass program show [OPTIONS]"], + "Arguments:\n Function symbol or stable identifier\n\nOptions:\n --program Program artifact [default: compass-out/program.json]\n --format Output format [default: text]\n\nExamples:\n compass program show 0123abcd\n compass program show 0123abcd --format json" + ), + page!( + "program callers", + "List resolved Program IR callers", + ["compass program callers [OPTIONS]"], + "Arguments:\n Function symbol or stable identifier\n\nOptions:\n --program Program artifact [default: compass-out/program.json]\n --format Output format [default: text]\n\nExamples:\n compass program callers 0123abcd\n compass program callers 0123abcd --format json" + ), + page!( + "program explain-call", + "Explain Program IR calls at an exact source byte", + ["compass program explain-call [OPTIONS]"], + "Arguments:\n Repository-relative file and byte offset\n\nOptions:\n --program Program artifact [default: compass-out/program.json]\n --format Output format [default: text]\n\nExamples:\n compass program explain-call src/lib.rs:240\n compass program explain-call src/lib.rs:240 --format json" + ), + page!( + "program call-graph", + "Build a bounded Program IR caller/callee graph", + ["compass program call-graph [OPTIONS]"], + "Options:\n --program Program artifact [default: compass-out/program.json]\n --graph Optional structural graph for enrichment\n --symbol Start from a function symbol\n --at Start from an exact source byte\n --direction Traversal direction [default: both]\n --depth Traversal depth [default: 2]\n --max-nodes Maximum returned nodes [default: 250]\n --max-edges Maximum returned edges [default: 500]\n --format json Required machine-readable output\n\nExamples:\n compass program call-graph --symbol 0123abcd --format json\n compass program call-graph --at src/lib.rs:240 --direction both --depth 2 --format json" + ), + page!( + "program query", + "Query the read-only Program IR graph projection", + ["compass program query [OPTIONS]"], + "Arguments:\n CompassQL query\n\nOptions:\n --program Program artifact [default: compass-out/program.json]\n --file Read CompassQL from a file\n --stdin Read CompassQL from standard input\n --param Bind a parameter; repeatable\n --params-file Read parameters from JSON\n --format Result format [default: table]\n --output Write results to a file\n --timeout-ms Query timeout\n --max-rows Row limit [default: 10000]\n --max-path-depth Path-depth limit [default: 32]\n --max-expanded-relationships Relationship expansion limit\n --max-memory-bytes Query memory limit [default: 268435456]\n\nExamples:\n compass program query \"MATCH (f) RETURN f LIMIT 10\"\n compass program query \"MATCH (f) RETURN f LIMIT 10\" --format json\n\nNotes:\n Program queries are offline and read-only. --graph, --at, and --repl are not supported." + ), page!( "path", "Find the shortest relationship path between two graph nodes", @@ -413,6 +485,24 @@ const PAGES: &[Page] = &[ ["compass history gc [OPTIONS]"], "Options:\n --prune-non-preferred Include alternate realizations in the plan\n --yes Apply non-preferred pruning; requires --prune-non-preferred\n --format Output format [default: text]\n\nExamples:\n compass history gc\n compass history gc --prune-non-preferred\n compass history gc --prune-non-preferred --yes\n\nNotes:\n Non-preferred pruning is a dry run until repeated with --yes." ), + page!( + "history cache", + "Inspect or reclaim derived history cache artifacts", + ["compass history cache [OPTIONS]"], + "Commands:\n status Show cache file and byte counts\n gc Inspect or apply bounded cache cleanup\n\nExamples:\n compass history cache status\n compass history cache status --format json\n compass history cache gc --max-bytes 100000000\n\nNotes:\n Cache cleanup never changes immutable history realizations." + ), + page!( + "history cache status", + "Show derived history cache file and byte counts", + ["compass history cache status [OPTIONS]"], + "Options:\n --format Output format [default: text]\n\nExamples:\n compass history cache status\n compass history cache status --format json" + ), + page!( + "history cache gc", + "Inspect or reclaim derived history cache artifacts", + ["compass history cache gc [OPTIONS]"], + "Options:\n --max-bytes Retain at most this many bytes\n --max-age-days Remove entries older than this age\n --yes Apply the cleanup plan\n --format Output format [default: text]\n\nExamples:\n compass history cache gc\n compass history cache gc --max-bytes 100000000 --yes\n\nNotes:\n Without --yes, cleanup is a dry run." + ), page!( "diff", "Review semantic changes between two Git revisions", @@ -437,6 +527,24 @@ const PAGES: &[Page] = &[ ["compass export json [OPTIONS]"], "Options:\n --graph Graph JSON [default: compass-out/graph.json]\n --labels Community-label JSON\n --node-limit Maximum overview or detail nodes [default: 5000]\n --community Export one complete community detail\n\nExamples:\n compass export json\n compass export json --community 7\n\nNotes:\n The payload schema is compass.viewer.graph/1. viewer-json remains a deprecated compatibility alias." ), + page!( + "export viewer-json", + "Export the graph presentation model using the compatibility alias", + ["compass export viewer-json [OPTIONS]"], + "Options:\n --graph Graph JSON [default: compass-out/graph.json]\n --labels Community-label JSON\n --node-limit Maximum overview or detail nodes [default: 5000]\n --community Export one complete community detail\n\nExamples:\n compass export viewer-json\n compass export viewer-json --community 7\n\nNotes:\n `viewer-json` is a deprecated compatibility alias for `export json`." + ), + page!( + "export workbench-json", + "Export the multi-view graph workbench contract", + ["compass export workbench-json [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 --view Repeatable workbench view specification\n --direction Call-view direction\n --depth View traversal depth\n --max-nodes View node bound\n --max-edges View edge bound\n --relation Repeatable affected-view relation\n --include-heuristic Include heuristic impact evidence\n --program Program IR enrichment for call views\n\nExamples:\n compass export workbench-json\n compass export workbench-json --view code --view architecture\n\nNotes:\n Views are emitted in request order as compass.viewer.workbench/1." + ), + page!( + "export orientation-json", + "Export the atomically published Agent Orientation", + ["compass export orientation-json [OPTIONS]"], + "Options:\n --graph Graph JSON [default: compass-out/graph.json]\n\nExamples:\n compass export orientation-json\n compass export orientation-json --graph compass-out/graph.json\n\nNotes:\n The orientation is accepted only when its generation, graph digest, and publication metadata match the selected graph." + ), page!( "export html", "Generate the interactive graph HTML report", @@ -449,6 +557,12 @@ const PAGES: &[Page] = &[ ["compass export callflow-html [GRAPH_OR_DIR] [OPTIONS]"], "Arguments:\n [GRAPH_OR_DIR] Graph JSON or project/output directory\n\nOptions:\n --graph Graph JSON\n --labels Community-label JSON\n --report GRAPH_REPORT.md path\n --sections JSON section definitions\n --output Output page\n --lang Report language [default: auto]\n --max-sections Maximum derived sections [default: 15]\n --diagram-scale Mermaid scale [default: 1.0]\n --max-diagram-nodes Nodes per diagram [default: 18]\n --max-diagram-edges Edges per diagram [default: 24]\n\nExamples:\n compass export callflow-html\n compass export callflow-html ./compass-out --lang en --max-sections 10\n\nNotes:\n Interactive terminals ask before opening the generated HTML; scripts never prompt or open a browser." ), + page!( + "export callflow-json", + "Export the structured call-flow report", + ["compass export callflow-json [GRAPH_OR_DIR] [OPTIONS]"], + "Arguments:\n [GRAPH_OR_DIR] Graph JSON or project/output directory\n\nOptions:\n --graph Graph JSON\n --labels Community-label JSON\n --report GRAPH_REPORT.md path\n --sections JSON section definitions\n --output Atomically write the JSON document\n --lang Report language [default: auto]\n --max-sections Maximum derived sections [default: 15]\n --diagram-scale Mermaid scale [default: 1.0]\n --max-diagram-nodes Nodes per diagram [default: 18]\n --max-diagram-edges Edges per diagram [default: 24]\n\nExamples:\n compass export callflow-json\n compass export callflow-json --graph compass-out/graph.json --output callflow.json\n\nNotes:\n The JSON output is the machine-readable counterpart to callflow-html." + ), page!( "export obsidian", "Export graph notes for an Obsidian vault", diff --git a/crates/compass-cli/src/lib.rs b/crates/compass-cli/src/lib.rs index b9122816..70418194 100644 --- a/crates/compass-cli/src/lib.rs +++ b/crates/compass-cli/src/lib.rs @@ -53,6 +53,7 @@ use compass_core::{ }; use compass_files::{ BuildScope, DetectOptions, Detection, Manifest, ManifestKind, ProjectConfig, detect, + write_text_atomic, }; use compass_global::{GlobalPaths, global_add}; use compass_graph::god_nodes; @@ -1581,10 +1582,11 @@ fn command_benchmark(args: &[String]) -> Outcome { .and_then(|bytes| serde_json::from_slice::(&bytes).ok()) .and_then(|value| value.get("total_words").and_then(serde_json::Value::as_u64)) .and_then(|value| usize::try_from(value).ok()); - Outcome::success(format_benchmark( - &run_benchmark(&document, corpus_words, None), - true, - )) + let result = run_benchmark(&document, corpus_words, None); + if result.error.is_some() { + return Outcome::failure(format_benchmark(&result, true)); + } + Outcome::success(format_benchmark(&result, true)) } fn command_build(frontend: Frontend, args: &[String], operation: BuildOperation) -> Outcome { @@ -2996,6 +2998,7 @@ fn validate_export_options( "--labels", "--report", "--sections", + "--output", "--lang", "--max-sections", "--diagram-scale", @@ -3721,7 +3724,17 @@ fn command_export(frontend: Frontend, args: &[String]) -> Outcome { max_diagram_nodes, max_diagram_edges, ) - .map(ExportOutput::text), + .and_then(|json| { + if let Some(path) = output_path.clone() { + write_text_atomic(&path, &json) + .map_err(|error| error.to_string()) + .map(|()| { + ExportOutput::text(format!("Call-flow JSON written: {}", path.display())) + }) + } else { + Ok(ExportOutput::text(json)) + } + }), "svg" => write_svg( &inputs.document, &inputs.communities, @@ -4566,7 +4579,7 @@ fn safe_output_name(value: &str) -> String { } fn export_help() -> String { - "Usage: compass export \n orientation-json [--graph PATH]\n html [--graph PATH] [--output HTML] [VIEW ...]\n json [--graph PATH] [--node-limit N] [--community ID] [VIEW ...]\n workbench-json [--graph PATH] [VIEW ...]\n callflow-html [GRAPH|DIR] [--graph PATH] [--labels PATH] [--report PATH] [--sections PATH] [--output HTML]\n callflow-json [GRAPH|DIR] [--graph PATH] [--labels PATH] [--report PATH] [--sections PATH]\n obsidian [--graph PATH] [--labels PATH] [--dir PATH]\n wiki [--graph PATH] [--labels PATH]\n svg [--graph PATH] [--labels PATH]\n graphml [--graph PATH]\n neo4j [--graph PATH] [--push URI] [--user U] [--password P]\n falkordb [--graph PATH] [--push URI] [--user U] [--password P]\n\nVIEW may be repeated: --code-graph, --architecture-graph, --call-graph SYMBOL, --impact-graph SYMBOL, --affected-graph NODE, --history-graph OLD..NEW, --artifact-lens LENS, or --view SPEC.".to_owned() + "Usage: compass export \n orientation-json [--graph PATH]\n html [--graph PATH] [--output HTML] [VIEW ...]\n json [--graph PATH] [--node-limit N] [--community ID] [VIEW ...]\n workbench-json [--graph PATH] [VIEW ...]\n callflow-html [GRAPH|DIR] [--graph PATH] [--labels PATH] [--report PATH] [--sections PATH] [--output HTML]\n callflow-json [GRAPH|DIR] [--graph PATH] [--labels PATH] [--report PATH] [--sections PATH] [--output JSON]\n obsidian [--graph PATH] [--labels PATH] [--dir PATH]\n wiki [--graph PATH] [--labels PATH]\n svg [--graph PATH] [--labels PATH]\n graphml [--graph PATH]\n neo4j [--graph PATH] [--push URI] [--user U] [--password P]\n falkordb [--graph PATH] [--push URI] [--user U] [--password P]\n\nVIEW may be repeated: --code-graph, --architecture-graph, --call-graph SYMBOL, --impact-graph SYMBOL, --affected-graph NODE, --history-graph OLD..NEW, --artifact-lens LENS, or --view SPEC.".to_owned() } fn export_workbench_help(format: &str) -> String { diff --git a/crates/compass-cli/tests/coverage_paths.rs b/crates/compass-cli/tests/coverage_paths.rs index 96c23405..eb8ef15f 100644 --- a/crates/compass-cli/tests/coverage_paths.rs +++ b/crates/compass-cli/tests/coverage_paths.rs @@ -1001,3 +1001,25 @@ fn semantic_provider_failures_are_formatted_after_ast_detection() -> Result<(), ); Ok(()) } + +#[test] +fn benchmark_without_matching_sample_questions_is_a_failure() -> Result<(), Box> { + let directory = tempfile::tempdir()?; + let graph = directory.path().join("graph.json"); + write_diagnostic_graph(&graph, 1)?; + + let outcome = invoke_owned( + Frontend::Compass, + &["benchmark".to_owned(), graph.to_string_lossy().into_owned()], + ); + assert_eq!(outcome.code, 1); + assert!( + outcome + .stderr + .contains("Benchmark error: No matching nodes found"), + "{}", + outcome.stderr + ); + assert!(outcome.stdout.is_empty()); + Ok(()) +} diff --git a/crates/compass-cli/tests/help_cli.rs b/crates/compass-cli/tests/help_cli.rs index 5dedce59..159395bd 100644 --- a/crates/compass-cli/tests/help_cli.rs +++ b/crates/compass-cli/tests/help_cli.rs @@ -125,14 +125,21 @@ fn every_public_nested_command_has_a_dedicated_page() { "show", "prefer", "export", + "cache", "gc", ][..], ), + ("history cache", &["status", "gc"][..]), ( "export", &[ "html", + "json", + "viewer-json", + "workbench-json", + "orientation-json", "callflow-html", + "callflow-json", "obsidian", "wiki", "svg", @@ -141,6 +148,20 @@ fn every_public_nested_command_has_a_dedicated_page() { "falkordb", ], ), + ("store", &["status", "validate", "backup", "restore"][..]), + ( + "program", + &[ + "summary", + "coverage", + "functions", + "show", + "callers", + "explain-call", + "call-graph", + "query", + ][..], + ), ("provider", &["add", "list", "show", "remove"]), ("global", &["add", "remove", "list", "path"]), ("hook", &["install", "uninstall", "status"]), diff --git a/crates/compass-cli/tests/viewer_export_cli.rs b/crates/compass-cli/tests/viewer_export_cli.rs index 7c5fb270..c623b4e1 100644 --- a/crates/compass-cli/tests/viewer_export_cli.rs +++ b/crates/compass-cli/tests/viewer_export_cli.rs @@ -546,5 +546,27 @@ fn callflow_json_exposes_the_shared_architecture_model() -> Result<(), Box= 2) ); + + let output_path = directory.path().join("callflow.json"); + let written = support::compass_command() + .args([ + "export", + "callflow-json", + "--graph", + graph.to_string_lossy().as_ref(), + "--output", + output_path.to_string_lossy().as_ref(), + ]) + .current_dir(directory.path()) + .output()?; + assert_eq!( + written.status.code(), + Some(0), + "{}", + String::from_utf8_lossy(&written.stderr) + ); + assert!(String::from_utf8_lossy(&written.stdout).contains("Call-flow JSON written:")); + let written_value: Value = serde_json::from_slice(&std::fs::read(&output_path)?)?; + assert_eq!(written_value["schema"], "compass.viewer.callflow/1"); Ok(()) }