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

## Unreleased

- Add source-scoped architecture and community views, compact hotspot rankings,
and optional labelled inferred/document connection layers.
- Warn about old graph revisions and working-tree changes under the recorded
source root, including cached answers. Keep machine stdout intact and expose
versioned freshness metadata in MCP envelopes.
- Return recovery commands for missing graph/Program artifacts and function
selectors, and reject unknown Program commands before loading artifacts.

- Default typed query and MCP text to compact, source-located answers. Keep
uncertainty visible and expose full paged audit detail with `--verbose` or
`--evidence`; raw machine contracts are unchanged.
Expand Down
19 changes: 16 additions & 3 deletions COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ results for the known nested-anchor limitation in that stable release.

Graph query, impact and traversal caches now bind to the SHA-256 of the bounded
JSON bytes read, including when file size and modification time are unchanged.
Their disposable header versions advance to `TRAILG02`, `TRAILA03` and
`TRAILT07`. Typed content caches use `.content-v2.cache` and `CGRPHV02`; their
Their disposable header versions advance to `TRAILG02`, `TRAILA04` and
`TRAILT08`. Typed content caches use `.content-v2.cache` and `CGRPHV02`; their
cache key, schema admission and decoded document come from one byte snapshot.
Older cache formats are ignored and rebuilt from the graph.

Expand Down Expand Up @@ -645,7 +645,7 @@ typed responses report truncation and an ambiguity diagnostic. Graph schemas,
stored identities, and extraction are unchanged. Existing graphs can receive
this lookup correction without re-extraction.

Traversal cache format `TRAILT07` retains deferred relationship flags and the
Traversal cache format `TRAILT08` retains deferred relationship flags and the
weakest confidence across all evidence (introduced in `TRAILT06`), including
explicit compatibility confidence. Missing or unknown confidence values in an
evidence item cannot establish an exact fact. Older disposable traversal
Expand Down Expand Up @@ -1727,3 +1727,16 @@ are bound to graph/Program IR, executable version, planner/ranker profiles,
complete request and semantic mode. A changed response-cache meaning requires a
new cache filename/version; missing/corrupt caches do not change native results.
Neither cache rewrites published graphs or historical realizations.

## Scoped usability and freshness

Unscoped architecture output and existing MCP community membership defaults are
unchanged. Opt-in scoped architecture output uses the new
`compass.architecture.scoped-view/1` wrapper. New community/hotspot commands and
`get_hotspots` have explicit schemas described in `docs/reference/outputs.md`.
MCP consumers with closed outer envelopes must admit optional `freshness`.
CLI machine stdout remains unchanged; warnings use stderr and are budgeted.
Graph, Program IR and immutable historical schemas are unchanged. Disposable
impact/traversal cache headers `TRAILA04` and `TRAILT08` retain the pinned build
commit; older caches are ignored and rebuilt. Missing Program IR exits with code 3 and recovery
commands; unknown Program subcommands exit with code 2 before artifact access.
11 changes: 11 additions & 0 deletions MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ sidecars. Its output root now preserves the familiar flat artifact shape so
file-based workflows can transition while Compass's snapshot and store
layout remains visible and clearly owned.

## Scoped views and freshness

No rebuild is required for new scoped views or hotspot ranking. Explicitly
scoped architecture JSON has a new wrapper; read the original projection from
`result`. Unscoped JSON retains its existing shape. Strict MCP envelope decoders
must allow optional `freshness`; resource transport metadata can also contain it.
CLI JSON remains on stdout, with advisory warnings on stderr. Older traversal
and impact caches rebuild automatically. Program signatures require
`compass update --program`;
use `--program PATH` when selecting an already published artifact.

## Compact output and budgets

Typed query text is compact by default. Select `--verbose` or `--evidence` to
Expand Down
16 changes: 16 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,3 +156,19 @@ ceiling. Lookup requires the verified graph identity and Program IR/profile/
request/mode identity, validates payload length, checksum and typed schema, and
checks deadlines. Source excerpts and partial responses are excluded. Cache
errors use native execution. Neither cache adds network or credential access.

Graph freshness checks read the bounded `source-root.txt` marker beside a selected
artifact, canonicalize its absolute root, and invoke Git with separate arguments.
They do not infer the root from the caller's current directory. Each invocation
has a 500 ms deadline and bounded captured output (admitted up to 1 MiB).
Lazy object fetching, filesystem monitors, external diff programs, text
conversions and rename heuristics are disabled. Configured clean/process
conversion filters and indexed submodules cause unknown freshness before
working-file checks, avoiding filter execution and child repository processes.
Git versions without the
no-lazy-fetch option report unknown freshness. These are read-only local observations; no checkout,
hook, network fetch or graph rewrite is performed. Failed checks remain unknown.
The marked root can be outside the current project; its path appears in the
advisory recovery command and MCP freshness metadata. Shell quoting is display
only. Source-root markers are local build metadata, not a trust assertion that
arbitrary imported graph bytes describe that checkout.
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,11 @@ commands print `compass output ID --offset N --budget N` to read the saved
output without repeating the command. Saved output is worktree-local and
bounded to sixteen records. Streaming `watch`, `serve`, and JSONL sessions reject
this presentation control; noninteractive `init --yes` supports it.

### hotspots

`compass hotspots --scope app/services --include-inferred --include-documents` ranks published symbol connectivity and incoming dependents. Optional evidence layers stay labelled.

### community

`compass community [ID] --scope app/services` lists a scoped community window. Use `--format json` for typed results.
14 changes: 11 additions & 3 deletions crates/compass-cli/src/call_graph_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ pub(crate) fn command(frontend: Frontend, args: &[String]) -> Outcome {
Err(error) => {
return Outcome::failure_with_code(
format!(
"error: could not resolve graph {}: {error}",
"error: could not resolve graph {}: {error}; run compass ensure or select an existing graph with --graph PATH",
options.graph.display()
),
3,
Expand All @@ -34,17 +34,25 @@ pub(crate) fn command(frontend: Frontend, args: &[String]) -> Outcome {
Err(error) => {
return Outcome::failure_with_code(
format!(
"error: could not load graph {}: {error}",
"error: could not load graph {}: {error}; run compass ensure or select an existing graph with --graph PATH",
options.graph.display()
),
3,
);
}
};
crate::freshness::record(&resolved_graph, crate::graph_source_commit(&graph));
let analysis = match options.program {
Some(path) => match crate::program_commands::load_program(&path) {
Ok(analysis) => Some(analysis),
Err(error) => return Outcome::failure_with_code(format!("error: {error}"), 3),
Err(error) => {
return Outcome::failure_with_code(
format!(
"error: {error}; run compass update --program, or select an existing artifact with --program PATH"
),
3,
);
}
},
None => None,
};
Expand Down
8 changes: 6 additions & 2 deletions crates/compass-cli/src/code_query_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -275,8 +275,7 @@ fn execute(
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())?
open_with_engine(&graph, program.as_deref(), &cache, engine).map_err(query_error)?
}
.with_deadline(deadline)
.with_semantic_search(args.iter().any(|arg| arg == "--semantic-search"));
Expand Down Expand Up @@ -413,6 +412,7 @@ fn execute(
}
_ => unreachable!(),
};
super::freshness::record(engine.graph_path(), engine.source_commit());
let mut context = AgentQueryContext::new(
response.operation.into(),
engine.graph_identity().to_owned(),
Expand Down Expand Up @@ -467,6 +467,10 @@ fn timeout(args: &[String]) -> Result<Duration, String> {
fn query_error(error: QueryError) -> String {
if error.kind() == QueryErrorKind::Timeout {
format!("{error}; raise --timeout-ms or lower --max-nodes/--max-edges")
} else if error.kind() == QueryErrorKind::UnsupportedSchema {
format!(
"{error}; run compass update to build artifacts with this version, or use --graph PATH for a supported graph"
)
} else {
error.to_string()
}
Expand Down
92 changes: 92 additions & 0 deletions crates/compass-cli/src/freshness.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
//! Request-local advisory messages collected while readers pin their graphs.
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};

use crate::Outcome;
use compass_core::{GraphFreshness, graph_freshness};

type Frame = BTreeMap<PathBuf, Option<GraphFreshness>>;
thread_local! { static FRAMES: RefCell<Vec<Frame>> = const { RefCell::new(Vec::new()) }; }

pub(super) struct Scope {
active: bool,
}
impl Scope {
pub(super) fn begin() -> Self {
let active = FRAMES.with(|frames| {
let mut frames = frames.borrow_mut();
if frames.len() >= 16 {
false
} else {
frames.push(BTreeMap::new());
true
}
});
Self { active }
}
pub(super) fn finish(mut self, mut output: Outcome) -> Outcome {
if self.active {
let frame = FRAMES.with(|frames| frames.borrow_mut().pop());
self.active = false;
for warning in frame
.into_iter()
.flat_map(BTreeMap::into_values)
.flatten()
.filter_map(|value| value.warning())
{
if !output.stderr.is_empty() {
output.stderr.push('\n');
}
output.stderr.push_str(&warning);
output.stderr_trailing_newline = true;
}
}
output
}
}
impl Drop for Scope {
fn drop(&mut self) {
if self.active {
FRAMES.with(|frames| {
frames.borrow_mut().pop();
});
}
}
}

pub(super) fn record(path: &Path, commit: Option<&str>) {
record_with_mode(path, commit, false);
}

pub(super) fn record_program(path: &Path) {
record_with_mode(path, None, true);
}

fn record_with_mode(path: &Path, commit: Option<&str>, program: bool) {
let capture = FRAMES.with(|frames| {
let mut frames = frames.borrow_mut();
let Some(frame) = frames.last_mut() else {
return false;
};
if frame.len() >= 8 || frame.contains_key(path) {
return false;
}
frame.insert(path.to_owned(), None);
true
});
if capture {
let mut report = graph_freshness(path, commit);
if program && let Some(report) = report.as_mut() {
report.update_command.push_str(" --program");
if report.reason.as_deref() == Some("graph has no valid build commit") {
report.reason = Some("Program IR has no recorded build commit".into());
}
}
FRAMES.with(|frames| {
if let Some(frame) = frames.borrow_mut().last_mut() {
frame.insert(path.to_owned(), report);
}
});
}
}
18 changes: 16 additions & 2 deletions crates/compass-cli/src/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ const GROUPS: &[Group] = &[
"affected",
"benchmark",
"output",
"hotspots",
"community",
],
},
Group {
Expand Down Expand Up @@ -149,6 +151,18 @@ const GROUPS: &[Group] = &[
];

const PAGES: &[Page] = &[
page!(
"hotspots",
"Rank connected symbols and incoming dependents",
["compass hotspots [OPTIONS]"],
"Options:\n --scope <PATH|module:NAME> Repeatable source scope\n --limit <N> Rows per ranking, 1-100 [default: 10]\n --include-inferred Include labelled inferred contacts\n --include-documents Include labelled document contacts\n --graph <PATH> Selected graph\n --format <text|json> Output format\n\nExamples:\n compass hotspots --scope app/services\n compass hotspots --include-inferred --include-documents --format json"
),
page!(
"community",
"List scoped communities or inspect one community",
["compass community [ID] [OPTIONS]"],
"Options:\n --scope <PATH|module:NAME> Repeatable source scope\n --limit <N> Community/member window, 1-100 [default: 20]\n --graph <PATH> Selected graph\n --format <text|json> Output format\n\nExamples:\n compass community --scope app/services\n compass community 7 --scope module:app.services --format json"
),
page!(
"output",
"Read the rest of an immutable saved command output",
Expand Down Expand Up @@ -444,7 +458,7 @@ const PAGES: &[Page] = &[
"architecture",
"Summarize the bounded architecture projection for agent inspection",
["compass architecture [OPTIONS]"],
"Options:\n --graph <PATH> Graph JSON [default: compass-out/graph.json]\n --labels <PATH> Community-label JSON\n --format <text|agent-json|json> Output format [default: text]\n\nExamples:\n compass architecture\n compass architecture --format agent-json\n compass architecture --graph compass-out/graph.json --format json\n\nNotes:\n Detailed JSON is capped at 5,000 nodes and 20,000 relationships. Above either cap, Compass returns exact totals, bounded kind counts, and samples from the 12 largest communities (3 nodes each), ordered deterministically by community size, community ID, and node ID. Oversized sample fields are bounded or counted as omitted in the versioned summary."
"Options:\n --scope <PATH|module:NAME> Repeatable path/module filter (OR)\n --graph <PATH> Graph JSON [default: compass-out/graph.json]\n --labels <PATH> Community-label JSON\n --format <text|agent-json|json> Output format [default: text]\n\nExamples:\n compass architecture\n compass architecture --format agent-json\n compass architecture --graph compass-out/graph.json --format json\n\nNotes:\n Detailed JSON is capped at 5,000 nodes and 20,000 relationships. Above either cap, Compass returns exact totals, bounded kind counts, and samples from the 12 largest communities (3 nodes each), ordered deterministically by community size, community ID, and node ID. Oversized sample fields are bounded or counted as omitted in the versioned summary."
),
page!(
"affected",
Expand Down Expand Up @@ -1341,7 +1355,7 @@ mod tests {
#[test]
fn catalog_has_unique_complete_public_roots() {
let roots = root_commands();
assert_eq!(roots.len(), 55);
assert_eq!(roots.len(), 57);
for root in roots {
let matches = PAGES.iter().filter(|page| page.path == root).count();
assert_eq!(matches, 1, "{root}");
Expand Down
Loading
Loading