fix/audit-findings #5

Merged
trevon merged 14 commits from fix/audit-findings into main 2026-09-25 02:39:30 +00:00
3 changed files with 82 additions and 1 deletions
Showing only changes of commit 3cf4d7660a - Show all commits

fix(build,check): report empty scans instead of scoring them 100

A directory with no supported source (Go/Java only) built
"1 nodes / 0 edges; score 100.0" with no diagnostic, and
`check --min-score 90 --max-cycles 0` passed it. The one-line summary
also hid diagnostics entirely: the yams run's 7,428 warnings (505 files
with grammar errors) never appeared.

- scan records a warning diagnostic when no source file was scanned;
- `build` prints the diagnostic and warning counts in its summary and
  warns on stderr when the model is empty;
- `check` fails a model that contains no scanned source files.

Test: diff_cli::a_scan_with_no_supported_source_is_reported_and_fails_check
fails before this change (no diagnostic; check passes).
trvon 2026-09-24 20:04:00 -06:00
Signed by: trevon
GPG key ID: 38686EA797D7A8A7

View file

@ -182,16 +182,28 @@ fn run(cli: Cli) -> Result<()> {
.count();
write_json(&building, &json)?;
render::render(&building, &html, &cytoscape_url)?;
let warnings = building
.diagnostics
.iter()
.filter(|diagnostic| diagnostic.level == "warning")
.count();
println!(
"built {} nodes / {} edges; score {:.1}; imported {} Fold findings / {} p4c diagnostics\nJSON: {}\nHTML: {}",
"built {} nodes / {} edges; score {:.1}; imported {} Fold findings / {} p4c diagnostics; {} diagnostics ({} warnings)\nJSON: {}\nHTML: {}",
building.metrics.node_count,
building.metrics.edge_count,
building.metrics.structural_health.score,
imported_fold,
imported_p4c,
building.diagnostics.len(),
warnings,
json.display(),
html.display()
);
if !has_source_files(&building) {
eprintln!(
"brick: warning: no supported source files were found; the model is empty and its score describes nothing"
);
}
}
Command::Mcp { input } => {
let building = read_json(&input)?;
@ -240,6 +252,11 @@ fn run(cli: Cli) -> Result<()> {
);
}
let metrics = &recomputed.metrics;
if !has_source_files(&building) {
failures.push(
"the model contains no scanned source files; nothing was analysed".into(),
);
}
if metrics.structural_health.score < min_score {
failures.push(format!(
"score {:.1} is below {:.1}",
@ -413,6 +430,14 @@ fn write_json(building: &Building, path: &PathBuf) -> Result<()> {
.with_context(|| format!("cannot write graph {}", path.display()))
}
/// At least one source file was scanned into the model.
fn has_source_files(building: &Building) -> bool {
building
.nodes
.iter()
.any(|node| node.kind == brick::model::NodeKind::File)
}
fn read_json(path: &PathBuf) -> Result<Building> {
let raw = fs::read_to_string(path)
.with_context(|| format!("cannot read graph {}", path.display()))?;

View file

@ -100,6 +100,14 @@ pub fn scan_with_options(root: &Path, options: &ScanOptions) -> Result<Building>
state.walk(&root)?;
state.resolve_relationships();
state.annotate_indirect_uses();
// An analysis of nothing must not read as a healthy repository.
if !state.nodes.iter().any(|node| node.kind == NodeKind::File) {
state.diagnostics.push(Diagnostic {
level: "warning".into(),
message: "no supported source files were found; the model is empty".into(),
path: None,
});
}
let mut building = Building {
schema_version: SCHEMA_VERSION,

View file

@ -305,3 +305,51 @@ fn check_recomputes_metrics_instead_of_trusting_the_file() {
);
assert!(text.contains("stored metrics"), "{text}");
}
#[test]
fn a_scan_with_no_supported_source_is_reported_and_fails_check() {
// A Go/Java-only directory built "1 nodes / 0 edges; score 100.0" with no
// diagnostic, and `check --min-score 90 --max-cycles 0` passed: an analysis of
// nothing looked like a perfectly healthy repository.
let directory = tempfile::tempdir().unwrap();
let project = directory.path().join("project");
fs::create_dir(&project).unwrap();
fs::write(project.join("main.go"), "package main\nfunc main() {}\n").unwrap();
fs::write(project.join("App.java"), "class App {}\n").unwrap();
let json = directory.path().join("model.json");
let html = directory.path().join("model.html");
let build = Command::new(env!("CARGO_BIN_EXE_brick"))
.args([
"build",
project.to_str().unwrap(),
"--json",
json.to_str().unwrap(),
"--html",
html.to_str().unwrap(),
])
.output()
.unwrap();
assert!(build.status.success());
let stderr = String::from_utf8_lossy(&build.stderr);
assert!(stderr.contains("no supported source files"), "{stderr}");
let model: Value = serde_json::from_slice(&fs::read(&json).unwrap()).unwrap();
assert!(
model["diagnostics"]
.as_array()
.is_some_and(|d| d.iter().any(|d| d["message"]
.as_str()
.is_some_and(|m| m.contains("no supported source files"))))
);
let check = Command::new(env!("CARGO_BIN_EXE_brick"))
.args([
"check",
json.to_str().unwrap(),
"--min-score",
"90",
"--max-cycles",
"0",
])
.output()
.unwrap();
assert!(!check.status.success(), "an empty model passed check");
}