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

## Unreleased

- Prevent oversized universal-evidence collections from collapsing code graphs
to file scaffolding. Default-low builds compact unused leaf declaration
details, project retained declarations before resolution, and resolve safe
exact relationships in deterministic bounded partitions. Any remaining
omissions are published through `universal_resolution_partial`, counted as
omitted edges, and make the build exit nonzero while retaining the useful
partial graph. Pipeline workers now use an explicit portable stack bound so
deeply nested generated sources fail neither the resolver nor the process.

- Improve Markdown as first-class agent context. Repeated automatic heading
slugs now follow deterministic source-order suffixes, every structural block
has a section-qualified identity, and project resolution connects exact,
Expand Down
10 changes: 10 additions & 0 deletions COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,16 @@ The additive `compass.graph/1` endpoint matrix accepts exact `calls` edges to
and object properties without changing node or edge identity; consumers that
validate endpoint kinds should accept this existing-major widening.

Large universal-evidence collections now degrade explicitly instead of
silently publishing file scaffolding. Compass retains source declarations and
safe exact relationships in deterministic bounded partitions, records omitted
relationship candidates in publication statistics, and adds the
`universal_resolution_partial` error diagnostic to `compass.graph/1`. A build
that emits this diagnostic publishes the useful partial artifact but exits
nonzero. This is an additive diagnostic and completeness behavior change; the
graph schema major and identities of successfully resolved records are
unchanged.

The self-contained HTML viewer embeds `compass.viewer.workbench/1`, an additive
ordered container for code, call, impact, affected, architecture, historical,
and artifact-lens models. Each view carries explicit bounded coverage. Plain
Expand Down
46 changes: 39 additions & 7 deletions crates/compass-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2217,9 +2217,7 @@ fn command_build_with_validation_inner(
output.push_str(&notes.join("\n"));
}
let mut outcome = Outcome::success(output);
if let Some(warning) = format_partial_graph_warning(&result) {
outcome.stderr = warning;
}
apply_build_quality_outcome(&result, &mut outcome);
if let Some(warning) = global_warning {
if !outcome.stderr.is_empty() {
outcome.stderr.push('\n');
Expand Down Expand Up @@ -5450,13 +5448,29 @@ fn format_program_analysis(result: &BuildResult) -> String {

fn format_partial_graph_warning(result: &BuildResult) -> Option<String> {
result.partial_graph.then(|| {
format!(
"warning: Compass published a partial graph after omitting {} nodes and {} edges; {} identity collisions quarantined.",
result.omitted_nodes, result.omitted_edges, result.identity_collisions
)
if result.resolution_degraded {
format!(
"error: Compass published a bounded partial graph because universal collection resolution omitted {} relationship candidates; declarations remain available. See graph diagnostic 'universal_resolution_partial'.",
result.omitted_edges
)
} else {
format!(
"warning: Compass published a partial graph after omitting {} nodes and {} edges; {} identity collisions quarantined.",
result.omitted_nodes, result.omitted_edges, result.identity_collisions
)
}
})
}

fn apply_build_quality_outcome(result: &BuildResult, outcome: &mut Outcome) {
if let Some(warning) = format_partial_graph_warning(result) {
outcome.stderr = warning;
}
if result.resolution_degraded {
outcome.code = 1;
}
}

#[cfg(test)]
mod mcp_option_tests {
use super::*;
Expand Down Expand Up @@ -5491,6 +5505,7 @@ mod mcp_option_tests {
omitted_edges: 0,
identity_collisions: 0,
partial_graph: false,
resolution_degraded: false,
html_written,
outputs_changed,
program_modules: 0,
Expand Down Expand Up @@ -5522,6 +5537,23 @@ mod mcp_option_tests {
);
}

#[test]
fn degraded_resolution_is_a_non_success_with_a_machine_diagnostic_pointer() {
let mut result = sample_build_result(true, false);
result.partial_graph = true;
result.resolution_degraded = true;
result.omitted_edges = 7;
let mut outcome = Outcome::success("graph published".to_owned());

apply_build_quality_outcome(&result, &mut outcome);

assert_eq!(outcome.code, 1);
assert_eq!(
outcome.stderr,
"error: Compass published a bounded partial graph because universal collection resolution omitted 7 relationship candidates; declarations remain available. See graph diagnostic 'universal_resolution_partial'."
);
}

#[test]
fn html_open_confirmation_requires_explicit_yes() -> Result<(), Box<dyn std::error::Error>> {
let directory = tempfile::tempdir()?;
Expand Down
61 changes: 52 additions & 9 deletions crates/compass-core/src/pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ use compass_output::{
use compass_resolve::{
ResolutionAdmission, apply_program_projection, collect_program_projection_sites,
merge_decl_def_classes_if_needed, merge_decl_def_classes_if_needed_changed,
resolve_prevalidated_owned_with_root_at_inference,
resolve_prevalidated_owned_with_root_at_inference, universal_resolution_report,
};
use compass_store::{
GRAPH_SCHEMA_V1, STORE_FILE_NAME, STORE_REF_FILE_NAME, SqliteStore, StoreRef,
Expand Down Expand Up @@ -82,6 +82,10 @@ use crate::raw_guard::enforce_incomplete_raw_guard;
pub const DEFAULT_MAX_SOURCE_BYTES: u64 = 16 * 1024 * 1024;
const SEMANTIC_MARKER_FILE: &str = "semantic-marker.json";
const PIPELINE_RAYON_WORKER_CAP: usize = 12;
// Debug builds and deeply nested parser inputs can exhaust Rayon's platform
// default (commonly 2 MiB) while one worker owns the full collection pipeline.
// Keep the bound explicit and portable; stack pages remain demand-paged.
const PIPELINE_RAYON_STACK_SIZE_BYTES: usize = 8 * 1024 * 1024;
const PARALLEL_AST_FACT_DIGEST_MIN_FILES: usize = 32;
const STORE_SNAPSHOT_EXCLUSIONS: [&str; 3] =
[STORE_FILE_NAME, "store.sqlite3-wal", "store.sqlite3-shm"];
Expand Down Expand Up @@ -202,6 +206,8 @@ struct OutputStats {
omitted_edges: usize,
#[serde(default)]
identity_collisions: usize,
#[serde(default)]
resolution_degraded: bool,
}

#[derive(Clone, Debug, Deserialize, Serialize)]
Expand Down Expand Up @@ -1855,6 +1861,7 @@ fn publish_fact_neutral_incremental(
let published_nodes = current.nodes.len();
let published_edges = current.links.len();
let omissions = saved_publication_omissions(&output_dir);
let resolution_degraded = saved_resolution_degraded(&output_dir);
let graph_path = output_dir.join("graph.json");
let (store_metrics, graph_seal) = if options.graph_storage.publishes_store() {
let previous = previous.ok_or_else(|| {
Expand Down Expand Up @@ -1929,6 +1936,7 @@ fn publish_fact_neutral_incremental(
communities,
clustered,
omissions,
resolution_degraded,
)?;
write_ast_fact_digest_state(&output_dir, fact_state)?;
write_semantic_marker(&output_dir, None)?;
Expand Down Expand Up @@ -1978,7 +1986,8 @@ fn publish_fact_neutral_incremental(
omitted_nodes: omissions.nodes,
omitted_edges: omissions.edges,
identity_collisions: omissions.identity_collisions,
partial_graph: omissions.is_partial(),
partial_graph: omissions.is_partial() || resolution_degraded,
resolution_degraded,
html_written: false,
outputs_changed: true,
program_modules: 0,
Expand Down Expand Up @@ -2010,6 +2019,8 @@ pub struct BuildResult {
pub omitted_edges: usize,
pub identity_collisions: usize,
pub partial_graph: bool,
/// Universal collection resolution omitted candidates under its bounded strategy.
pub resolution_degraded: bool,
pub html_written: bool,
pub outputs_changed: bool,
pub program_modules: usize,
Expand Down Expand Up @@ -2245,6 +2256,7 @@ fn build_graph_inner(
let worker_count = pipeline_rayon_workers(options);
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(worker_count)
.stack_size(PIPELINE_RAYON_STACK_SIZE_BYTES)
.thread_name(|index| format!("compass-pipeline-{index}"))
.build()
.map_err(|error| CoreError::WorkerPool(error.to_string()))?;
Expand Down Expand Up @@ -2463,7 +2475,9 @@ fn build_graph_inner_unscoped(
identity_collisions: state.stats.identity_collisions,
partial_graph: state.stats.omitted_nodes > 0
|| state.stats.omitted_edges > 0
|| state.stats.identity_collisions > 0,
|| state.stats.identity_collisions > 0
|| saved_resolution_degraded(&output_dir),
resolution_degraded: saved_resolution_degraded(&output_dir),
html_written: output_dir.join("graph.html").is_file(),
outputs_changed: false,
program_modules: state.stats.program_modules,
Expand Down Expand Up @@ -2546,7 +2560,8 @@ fn build_graph_inner_unscoped(
omitted_nodes: stats.omitted_nodes,
omitted_edges: stats.omitted_edges,
identity_collisions: stats.identity_collisions,
partial_graph: stats.omissions().is_partial(),
partial_graph: stats.omissions().is_partial() || stats.resolution_degraded,
resolution_degraded: stats.resolution_degraded,
html_written: output_dir.join("graph.html").is_file(),
outputs_changed: false,
program_modules: program_modules(unchanged_program.as_ref()),
Expand Down Expand Up @@ -3232,6 +3247,9 @@ fn build_graph_inner_unscoped(
&root,
resolution_admission,
);
let resolution_report = universal_resolution_report(&resolved).unwrap_or_default();
let resolution_degraded = resolution_report.degraded;
let resolution_omitted_candidates = resolution_report.omitted_candidates;
profile_internal("cross-file resolution total", &mut internal_started);
drop(source_text);
internal_started = Instant::now();
Expand Down Expand Up @@ -3310,6 +3328,7 @@ fn build_graph_inner_unscoped(
&& !source_removed
&& supplemental.is_empty()
&& semantic.is_some_and(semantic_layer_is_empty)
&& !resolution_degraded
&& let Ok(document) = GraphDocument::load(&output_dir.join("graph.json"))
{
let omissions = saved_publication_omissions(&output_dir);
Expand Down Expand Up @@ -3360,6 +3379,7 @@ fn build_graph_inner_unscoped(
omitted_edges: omissions.edges,
identity_collisions: omissions.identity_collisions,
partial_graph: omissions.is_partial(),
resolution_degraded: false,
html_written: false,
outputs_changed: false,
program_modules: program_modules(program.as_ref()),
Expand Down Expand Up @@ -3436,7 +3456,10 @@ fn build_graph_inner_unscoped(
if published.document.nodes.is_empty() {
return Err(CoreError::EmptyGraph);
}
let omissions = published.omissions;
let mut omissions = published.omissions;
omissions.edges = omissions
.edges
.saturating_add(resolution_omitted_candidates);
let published_nodes = published.document.nodes.len();
let published_edges = published.document.links.len();
let no_cluster_graph_write_started = Instant::now();
Expand Down Expand Up @@ -3481,6 +3504,7 @@ fn build_graph_inner_unscoped(
0,
false,
omissions,
resolution_degraded,
)?;
write_ast_fact_digest_state(&output_dir, &current_fact_state)?;
write_semantic_marker(&output_dir, semantic)?;
Expand Down Expand Up @@ -3556,7 +3580,8 @@ fn build_graph_inner_unscoped(
omitted_nodes: omissions.nodes,
omitted_edges: omissions.edges,
identity_collisions: omissions.identity_collisions,
partial_graph: omissions.is_partial(),
partial_graph: omissions.is_partial() || resolution_degraded,
resolution_degraded,
html_written: false,
outputs_changed: true,
program_modules: program_modules(program.as_ref()),
Expand Down Expand Up @@ -3633,7 +3658,8 @@ fn build_graph_inner_unscoped(
apply_inference_level(&mut preflight.document, options.inference_level);
let preflight_document = preflight.document.to_legacy_document()?;
profile_internal_duration("graph.json v1 preflight", preflight_started.elapsed());
if !preflight.omissions.is_partial()
if !resolution_degraded
&& !preflight.omissions.is_partial()
&& GraphDocument::load(&output_dir.join("graph.json"))
.is_ok_and(|existing| topology_is_unchanged(&existing, &preflight_document))
{
Expand Down Expand Up @@ -3693,6 +3719,7 @@ fn build_graph_inner_unscoped(
omitted_edges: 0,
identity_collisions: 0,
partial_graph: false,
resolution_degraded: false,
html_written: output_dir.join("graph.html").is_file(),
outputs_changed: false,
program_modules: program_modules(program.as_ref()),
Expand Down Expand Up @@ -3762,7 +3789,10 @@ fn build_graph_inner_unscoped(
if published.document.nodes.is_empty() {
return Err(CoreError::EmptyGraph);
}
let omissions = published.omissions;
let mut omissions = published.omissions;
omissions.edges = omissions
.edges
.saturating_add(resolution_omitted_candidates);
let report_health = current_orientation_health(options, omissions);
// Legacy clustering and report code needs a compatibility projection, but
// retaining it beside the complete typed authority doubles the dominant
Expand Down Expand Up @@ -4063,6 +4093,7 @@ fn build_graph_inner_unscoped(
communities.len(),
true,
omissions,
resolution_degraded,
)
},
);
Expand Down Expand Up @@ -4114,7 +4145,8 @@ fn build_graph_inner_unscoped(
omitted_nodes: omissions.nodes,
omitted_edges: omissions.edges,
identity_collisions: omissions.identity_collisions,
partial_graph: omissions.is_partial(),
partial_graph: omissions.is_partial() || resolution_degraded,
resolution_degraded,
html_written,
outputs_changed: true,
program_modules: program_modules(program.as_ref()),
Expand Down Expand Up @@ -6851,6 +6883,7 @@ fn unchanged_output_stats(options: &BuildOptions, output_dir: &Path) -> Option<O
omitted_nodes: 0,
omitted_edges: 0,
identity_collisions: 0,
resolution_degraded: false,
};
let _ = write_json_atomic(output_dir.join(OUTPUT_STATS_FILE), &stats, true);
return Some(stats);
Expand Down Expand Up @@ -6879,6 +6912,7 @@ fn unchanged_output_stats(options: &BuildOptions, output_dir: &Path) -> Option<O
omitted_nodes: 0,
omitted_edges: 0,
identity_collisions: 0,
resolution_degraded: false,
};
let _ = write_json_atomic(output_dir.join(OUTPUT_STATS_FILE), &stats, true);
Some(stats)
Expand All @@ -6891,13 +6925,21 @@ fn saved_publication_omissions(output_dir: &Path) -> PublicationOmissions {
.map_or_else(PublicationOmissions::default, |stats| stats.omissions())
}

fn saved_resolution_degraded(output_dir: &Path) -> bool {
fs::read(output_dir.join(OUTPUT_STATS_FILE))
.ok()
.and_then(|bytes| serde_json::from_slice::<OutputStats>(&bytes).ok())
.is_some_and(|stats| stats.resolution_degraded)
}

fn save_output_stats(
output_dir: &Path,
nodes: usize,
edges: usize,
communities: usize,
clustered: bool,
omissions: PublicationOmissions,
resolution_degraded: bool,
) -> Result<(), CoreError> {
let graph_bytes = fs::metadata(output_dir.join("graph.json"))
.map_err(|source| compass_files::FileError::Io {
Expand All @@ -6916,6 +6958,7 @@ fn save_output_stats(
omitted_nodes: omissions.nodes,
omitted_edges: omissions.edges,
identity_collisions: omissions.identity_collisions,
resolution_degraded,
},
true,
)?;
Expand Down
Loading
Loading