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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## Unreleased

- Turn the VS Code codebase query view into a multi-command workbench with
separate Ask, Explain, and CompassQL composers and durable result tabs.
Typed Ask diagnostics, symbol relationships, source links, and CompassQL rows
now render as readable UI instead of raw JSON. Typed `compass ask` also
accepts `--at REV` for immutable revision graphs.

- Keep Codebase Evolution strict when a stored revision uses an unsupported
artifact layout, and offer to rebuild the affected revision with the current
Compass version instead of mapping legacy history records.
Expand Down
4 changes: 4 additions & 0 deletions COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@ preserve the established text traversal and reject discovery controls.
CompassQL and explicit typed query commands remain unchanged. Discovery text
pagination uses the versioned `compass.query.discovery-text-page/1` cursor;
JSON rejects those presentation-only controls.
`compass ask --at REV` uses the same immutable trusted `compass.graph/1`
realization selection as revision discovery. The response remains the unchanged
`compass.query/1` contract; an older realization without that trusted graph is
rejected and must be rebuilt.

Default discovery JSON remains the strict `compass.query.discovery/1` shape.
The focused default neighborhood is 64 nodes and 128 edges. The existing hard
Expand Down
55 changes: 46 additions & 9 deletions crates/compass-cli/src/code_query_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ use compass_model::query_contract::{
CallRequest, CodeQueryLimits, CodeQueryResponse, ExploreRequest, ImpactRequest,
NodeTrailRequest, SearchRequest,
};
use compass_query::{EngineSelection, NaturalQueryRequest, open_with_engine};
use compass_query::{
EngineSelection, NaturalQueryRequest, open_with_engine, open_with_verified_document,
};

use crate::Outcome;

Expand All @@ -30,6 +32,17 @@ pub(crate) fn command(operation: &str, args: &[String]) -> Outcome {
fn execute(operation: &str, args: &[String]) -> Result<CodeQueryResponse, String> {
let positional = positional(args);
let graph_option = option(args, "--graph");
let revision = option(args, "--at");
if graph_option.is_some() && revision.is_some() {
return Err("--graph and --at are mutually exclusive".to_owned());
}
if revision.is_some() {
for current_only in ["--engine", "--program", "--cache"] {
if option(args, current_only).is_some() {
return Err(format!("{current_only} cannot be combined with --at"));
}
}
}
let output =
PathBuf::from(std::env::var("COMPASS_OUT").unwrap_or_else(|_| "compass-out".to_owned()));
let requested_graph = graph_option.map_or_else(|| output.join("graph.json"), PathBuf::from);
Expand All @@ -52,18 +65,41 @@ fn execute(operation: &str, args: &[String]) -> Result<CodeQueryResponse, String
.unwrap_or_else(|| std::path::Path::new("."))
.join("cache")
});
let graph = if graph_option.is_some() {
resolve_snapshot_artifact(requested_graph)?
} else {
compass_files::BuildGuard::resolve_artifact(&output, "graph.json")
.map_err(|error| error.to_string())?
};
let program = option(args, "--program")
.map(PathBuf::from)
.map(resolve_snapshot_artifact)
.transpose()?;
let engine = open_with_engine(&graph, program.as_deref(), &cache, engine)
.map_err(|error| error.to_string())?;
let engine = if let Some(revision) = revision {
let (realization, document) = super::history_commands::load_typed_graph_at(revision)?;
let current = std::env::current_dir().map_err(|error| error.to_string())?;
let history_cache = current
.join(".compass")
.join("cache")
.join("history-query")
.join(realization.to_string());
let history_graph = current
.join(".compass")
.join("history-query")
.join(realization.to_string())
.join("graph.json");
open_with_verified_document(
document,
realization.as_hex(),
&history_graph,
program.as_deref(),
&history_cache,
)
.map_err(|error| error.to_string())?
} else {
let graph = if graph_option.is_some() {
resolve_snapshot_artifact(requested_graph)?
} else {
compass_files::BuildGuard::resolve_artifact(&output, "graph.json")
.map_err(|error| error.to_string())?
};
open_with_engine(&graph, program.as_deref(), &cache, engine)
.map_err(|error| error.to_string())?
};
let limits = limits(args)?;
match operation {
"ask" => engine.query_natural(NaturalQueryRequest {
Expand Down Expand Up @@ -151,6 +187,7 @@ fn option<'a>(args: &'a [String], name: &str) -> Option<&'a str> {
fn positional(args: &[String]) -> Vec<String> {
let value_options = [
"--graph",
"--at",
"--program",
"--cache",
"--engine",
Expand Down
2 changes: 1 addition & 1 deletion crates/compass-cli/src/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ const PAGES: &[Page] = &[
"ask",
"Route a natural-language question to a typed code-graph query",
["compass ask <QUESTION> [OPTIONS]"],
"Arguments:\n <QUESTION> Natural-language code-graph question\n\nOptions:\n --graph <PATH> Typed graph [default: compass-out/graph.json]\n --program <PATH> Optional Program IR enrichment\n --cache <DIR> Query-index cache directory\n --engine <default|json|store> Graph storage engine [default: default]\n --max-depth <N> Traversal radius\n --max-nodes <N> Node bound\n --max-edges <N> Edge bound\n --max-paths <N> Path bound\n --max-candidates <N> Candidate bound\n --include-heuristic Include heuristic evidence\n --format <text|json> Output format [default: text]\n\nExamples:\n compass ask \"who calls PaymentService.charge?\"\n compass ask \"what does CheckoutController.create call?\" --format json\n compass ask \"path from CheckoutController.create to PaymentGateway.charge\"\n\nNotes:\n High-confidence callers, callees, impact, and path questions route to the matching typed operation. Contradictory or low-confidence input falls back to bounded symbol search. The response uses compass.query/1."
"Arguments:\n <QUESTION> Natural-language code-graph question\n\nOptions:\n --graph <PATH> Typed graph [default: compass-out/graph.json]\n --at <REV> Use an immutable trusted revision graph\n --program <PATH> Optional Program IR enrichment\n --cache <DIR> Query-index cache directory\n --engine <default|json|store> Graph storage engine [default: default]\n --max-depth <N> Traversal radius\n --max-nodes <N> Node bound\n --max-edges <N> Edge bound\n --max-paths <N> Path bound\n --max-candidates <N> Candidate bound\n --include-heuristic Include heuristic evidence\n --format <text|json> Output format [default: text]\n\nExamples:\n compass ask \"who calls PaymentService.charge?\"\n compass ask \"what does CheckoutController.create call?\" --format json\n compass ask \"path from CheckoutController.create to PaymentGateway.charge\"\n compass ask \"who calls PaymentService.charge?\" --at HEAD~2 --format json\n\nNotes:\n High-confidence callers, callees, impact, and path questions route to the matching typed operation. Contradictory or low-confidence input falls back to bounded symbol search. --at is mutually exclusive with --graph, --program, --cache, and --engine. The response uses compass.query/1."
),
page!(
"call-graph",
Expand Down
47 changes: 47 additions & 0 deletions crates/compass-cli/tests/code_query_cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,53 @@ fn typed_query_commands_share_the_versioned_json_contract() -> Result<(), Box<dy
Ok(())
}

#[test]
fn typed_ask_rejects_conflicting_current_and_revision_graph_sources() -> Result<(), Box<dyn Error>>
{
let directory = tempfile::tempdir()?;
let graph = support::write_typed_graph(directory.path())?;
let outcome = run(
Frontend::Compass,
[
OsString::from("ask"),
OsString::from("who calls Target?"),
OsString::from("--graph"),
graph.into_os_string(),
OsString::from("--at"),
OsString::from("HEAD"),
OsString::from("--format"),
OsString::from("json"),
],
);

assert_eq!(outcome.code, 1);
assert_eq!(
outcome.stderr,
"error: --graph and --at are mutually exclusive"
);
for option in ["--engine", "--program", "--cache"] {
let outcome = run(
Frontend::Compass,
[
OsString::from("ask"),
OsString::from("who calls Target?"),
OsString::from("--at"),
OsString::from("HEAD"),
OsString::from(option),
OsString::from("ignored"),
OsString::from("--format"),
OsString::from("json"),
],
);
assert_eq!(outcome.code, 1);
assert_eq!(
outcome.stderr,
format!("error: {option} cannot be combined with --at")
);
}
Ok(())
}

#[test]
fn natural_query_defaults_to_discovery_and_preserves_explicit_legacy_traversal()
-> Result<(), Box<dyn Error>> {
Expand Down
24 changes: 24 additions & 0 deletions crates/compass-cli/tests/history_cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1931,6 +1931,30 @@ fn current_snapshot_promotion_matches_an_exact_rebuild() -> Result<(), Box<dyn s
String::from_utf8_lossy(&promoted.stderr)
);
let promoted: serde_json::Value = serde_json::from_slice(&promoted.stdout)?;
let asked = run(
compass,
directory.path(),
&[
"ask",
"where is Service run?",
"--at",
"HEAD",
"--format=json",
],
)?;
assert!(
asked.status.success(),
"{}",
String::from_utf8_lossy(&asked.stderr)
);
let asked: serde_json::Value = serde_json::from_slice(&asked.stdout)?;
assert_eq!(asked["schema"], "compass.query/1");
assert!(
asked["nodes"]
.as_array()
.is_some_and(|nodes| !nodes.is_empty()),
"typed revision ask returned no nodes: {asked}"
);
let rebuilt = run(
compass,
directory.path(),
Expand Down
4 changes: 2 additions & 2 deletions crates/compass-output/assets/viewer/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
"sha256": "69176c8da4b7a8517b99f537ad06da9d874fe60654fccab6c624de4e220027bf"
},
"viewer.css": {
"bytes": 239213,
"sha256": "b76180f1f87b430689d640e5f2f4d1a18a9e7636a09f63e150898522dd4966c8"
"bytes": 245168,
"sha256": "7164b4818090065fbda5b110bb530587a5eb20a0aebc3ba6ee8eaebffcd51b86"
}
}
}
2 changes: 1 addition & 1 deletion crates/compass-output/assets/viewer/viewer.css

Large diffs are not rendered by default.

15 changes: 12 additions & 3 deletions docs/guides/vscode.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,18 @@ subsystem cards, or the route-table alternative for another level of detail.

## Query

Use **Query Codebase** for natural-language discovery or deterministic
CompassQL. CompassQL parameters and limits are sent as literal process
arguments. Use Cmd/Ctrl+Enter to run a query.
Use **Query Codebase** as a three-command workbench:

- **Ask** routes a question through the typed `compass ask` contract and
presents graph matches, paths, diagnostics, and source links.
- **Explain** inspects one symbol and itemizes its incoming and outgoing
relationships. Ambiguous names remain explicit and offer full node IDs.
- **CompassQL** executes deterministic read-only rows with literal parameters
and limits.

Each run opens a separate bounded result tab, so answers can be compared
without replacing earlier output. Use Cmd/Ctrl+Enter to run the selected
command.

## Evolution

Expand Down
13 changes: 13 additions & 0 deletions docs/reference/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,19 @@ an explicit `wiring=FILE:LOCATION` site, and traversed relationships render
their occurrence as `at=FILE:LOCATION`; neither is presented as a declaration
location.

Typed intent routing:

```text
compass ask "<question>"
[--graph PATH | --at REV]
[--format text|json]
```

`ask` chooses a bounded typed search, callers, callees, impact, or node-trail
operation and returns `compass.query/1`. `--at REV` reads one immutable trusted
revision graph; it does not fall back to a legacy projection. Rebuild a revision
whose realization does not contain the current trusted graph contract.

CompassQL:

```text
Expand Down
4 changes: 3 additions & 1 deletion editors/vscode/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ is found first on `PATH`, Compass stops before running a workflow and offers
- Read the broader architecture flow document in a separate editor tab, with a
horizontally scrollable map, draggable subsystem cards, and a route-table
alternative for large systems.
- Run natural-language queries or deterministic CompassQL.
- Run typed Ask questions, inspect one symbol with Explain, or execute
deterministic CompassQL. Each command keeps its own input and opens a
separate result tab with readable diagnostics and source links.
- Browse every reachable Git commit with graph states: graph available, not
materialized, building, or failed.
- Explicitly build missing historical graphs, load exact revisions, and compare
Expand Down
38 changes: 37 additions & 1 deletion editors/vscode/src/commands/queryArguments.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,41 @@
import { describe, expect, it } from "vitest";
import { buildCqlArgs } from "./queryArguments";
import {
buildAskArgs,
buildCqlArgs,
buildExplainArgs
} from "./queryArguments";

describe("buildAskArgs", () => {
it("uses the typed ask contract for the working tree", () => {
expect(buildAskArgs({
query: "who calls checkout?",
graph: "/repo/compass-out/graph.json"
})).toEqual([
"ask", "who calls checkout?",
"--graph", "/repo/compass-out/graph.json",
"--format", "json"
]);
});

it("selects an immutable revision without mixing graph sources", () => {
expect(buildAskArgs({ query: "who calls checkout?", revision: "HEAD~2" }))
.toEqual([
"ask", "who calls checkout?", "--at", "HEAD~2", "--format", "json"
]);
});
});

describe("buildExplainArgs", () => {
it("requests a readable explanation for one exact symbol", () => {
expect(buildExplainArgs({
query: "crate::Checkout::run",
graph: "/repo/compass-out/graph.json"
})).toEqual([
"explain", "crate::Checkout::run",
"--graph", "/repo/compass-out/graph.json"
]);
});
});

describe("buildCqlArgs", () => {
it("keeps the query and parameters as literal arguments", () => {
Expand Down
39 changes: 30 additions & 9 deletions editors/vscode/src/commands/queryArguments.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,35 @@
export function buildNaturalQueryArgs(options: {
query: string;
type GraphSelection = {
graph?: string | undefined;
revision?: string | undefined;
}): string[] {
};

function graphSelection(options: GraphSelection): string[] {
return options.revision
? ["--at", options.revision]
: options.graph
? ["--graph", options.graph]
: [];
}

export function buildAskArgs(options: {
query: string;
} & GraphSelection): string[] {
return [
"query",
"ask",
options.query,
...graphSelection(options),
"--format",
"json"
];
}

export function buildExplainArgs(options: {
query: string;
} & GraphSelection): string[] {
return [
"explain",
options.query,
...(options.revision ? ["--at", options.revision] : options.graph ? ["--graph", options.graph] : [])
...graphSelection(options)
];
}

Expand All @@ -15,9 +38,7 @@ export function buildCqlArgs(options: {
params: Record<string, string>;
timeoutMs: number;
maxRows: number;
graph?: string | undefined;
revision?: string | undefined;
}): string[] {
} & GraphSelection): string[] {
return [
"query",
"--cql",
Expand All @@ -29,7 +50,7 @@ export function buildCqlArgs(options: {
String(options.timeoutMs),
"--max-rows",
String(options.maxRows),
...(options.revision ? ["--at", options.revision] : options.graph ? ["--graph", options.graph] : []),
...graphSelection(options),
"--format",
"json"
];
Expand Down
Loading
Loading