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
2 changes: 1 addition & 1 deletion docs/src/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Each command does one thing. They compose.

| format | commands with a real renderer |
|---|---|
| `markdown` | `essence`, `anomalies`, `stats`, `invariants` |
| `markdown` | `essence`, `anomalies`, `stats`, `invariants`, `fingerprint`, `separation` |
| `compact-ai` | `essence` |

For every other command those formats produce the text output verbatim. Commands are being migrated onto a shared renderer, and a command only joins the table above once it genuinely renders that format — so the notice below can never claim more than is true.
Expand Down
226 changes: 137 additions & 89 deletions vajra-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,14 @@ enum Command {
/// accepting a format and ignoring it is the same failure as reporting
/// `errors: []` over a partial batch: the caller cannot distinguish "rendered
/// as Markdown" from "fell back to text".
const RENDERS_MARKDOWN: &[&str] = &["essence", "anomalies", "stats", "invariants"];
const RENDERS_MARKDOWN: &[&str] = &[
"essence",
"anomalies",
"stats",
"invariants",
"fingerprint",
"separation",
];

/// Commands with a bespoke compact-AI view.
const RENDERS_COMPACT_AI: &[&str] = &["essence"];
Expand Down Expand Up @@ -2088,15 +2095,33 @@ fn cmd_fingerprint(input: &str, min_nodes: u64, cli: &Cli) -> Result<()> {
println!("{json}");
}
Format::Text | Format::Markdown | Format::CompactAi => {
println!("=== Structural Fingerprints (streaming) ===");
println!(" Nodes: {node_count}");
let mut report = render::Report::new();
report.heading("Structural Fingerprints (streaming)");
let mut fields = vec![("Nodes".to_owned(), node_count.to_string())];
if suppressed {
println!(" Suppressed: node count below --min-nodes {min_nodes}");
fields.push((
"Suppressed".to_owned(),
format!("node count below --min-nodes {min_nodes}"),
));
} else {
println!(" Path set: {}", hex_slice(&result.path_set));
println!(" Typed path: {}", hex_slice(&result.typed_path));
fields.push(("Path set".to_owned(), hex_slice(&result.path_set)));
fields.push(("Typed path".to_owned(), hex_slice(&result.typed_path)));
}
println!(" Shape: (not available in streaming mode)");
fields.push((
"Shape".to_owned(),
"(not available in streaming mode)".to_owned(),
));
report.fields(fields);
report.note(
"Streaming mode cannot produce the Merkle shape hash, which needs the\nwhole tree in memory.",
);
print!(
"{}",
match cli.format {
Format::Markdown => report.to_markdown(),
_ => report.to_text(),
}
);
}
}
} else {
Expand All @@ -2113,29 +2138,49 @@ fn cmd_fingerprint(input: &str, min_nodes: u64, cli: &Cli) -> Result<()> {
println!("{json}");
}
Format::Text | Format::Markdown | Format::CompactAi => {
println!("=== Structural Fingerprints ===");
println!(" Nodes: {}", output.node_count);
let mut report = render::Report::new();
report.heading("Structural Fingerprints");

let mut fields = vec![("Nodes".to_owned(), output.node_count.to_string())];
if output.suppressed {
println!(" Suppressed: node count below --min-nodes {}", min_nodes);
println!(
" structural hashes are not discriminating at this size"
fields.push((
"Suppressed".to_owned(),
format!("node count below --min-nodes {min_nodes}"),
));
} else {
fields.push((
"Path set".to_owned(),
show(output.path_set.as_deref()).to_owned(),
));
fields.push((
"Typed path".to_owned(),
show(output.typed_path.as_deref()).to_owned(),
));
fields.push(("Shape".to_owned(), show(output.shape.as_deref()).to_owned()));
}
report.fields(fields);

if output.suppressed {
report.note(
"Structural hashes are not discriminating at this size: the space of\ndistinct small shapes is tiny, so trivial documents collide.",
);
} else {
println!(" Path set: {}", show(output.path_set.as_deref()));
println!(" Typed path: {}", show(output.typed_path.as_deref()));
println!(" Shape: {}", show(output.shape.as_deref()));
println!();

println!("=== Repeated Motifs ===");
if output.repeated_motifs.is_empty() {
println!(" (no repeated subtree shapes found)");
} else {
println!(" {:<66} {:>5}", "HASH", "COUNT");
for m in &output.repeated_motifs {
println!(" {:<66} {:>5}", m.hash, m.count);
}
report.heading("Repeated Motifs");
let mut t =
render::Table::new(&["HASH", "COUNT"], "no repeated subtree shapes found");
for m in &output.repeated_motifs {
t.push(vec![m.hash.clone(), m.count.to_string()]);
}
report.table(t);
}

print!(
"{}",
match cli.format {
Format::Markdown => report.to_markdown(),
_ => report.to_text(),
}
);
Comment on lines +2169 to +2183

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Avoid buffering unbounded rendered reports.

Both paths retain all rows in render::Table and then allocate the complete rendered output string. Fingerprint has no motif bound, and separation --top-k 0 emits every feature.

  • vajra-cli/src/main.rs#L2169-L2183: render repeated motifs through a bounded/streaming sink.
  • vajra-cli/src/main.rs#L2849-L2934: stream class, feature, and rule output; use a two-pass/spooled strategy if aligned text requires widths.

As per coding guidelines, “Every algorithm must support arbitrary input scale through streaming and bounded-memory designs; non-streaming algorithms do not ship.”

📍 Affects 1 file
  • vajra-cli/src/main.rs#L2169-L2183 (this comment)
  • vajra-cli/src/main.rs#L2849-L2934
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@vajra-cli/src/main.rs` around lines 2169 - 2183, The report-generation paths
retain unbounded rows and rendered strings; replace the repeated-motif flow
around report.table and the output flow around class, feature, and rule
reporting with bounded or streaming sinks. For aligned text, use a two-pass or
spooled approach so output is emitted incrementally without retaining the
complete report; update both vajra-cli/src/main.rs:2169-2183 and
vajra-cli/src/main.rs:2849-2934, preserving Markdown and text formatting.

Source: Coding guidelines

}
}
}
Expand Down Expand Up @@ -2798,98 +2843,101 @@ fn cmd_separation(
println!("{json}");
}
Format::Text | Format::Markdown | Format::CompactAi => {
println!("=== Separation: {} ===", report.label_field);
println!(" Labelled records: {}", report.labelled_records);
let mut out = render::Report::new();

out.heading(format!("Separation: {}", report.label_field));
let mut fields = vec![(
"Labelled records".to_owned(),
report.labelled_records.to_string(),
)];
for (class, count) in &report.classes {
println!(" {class}: {count}");
}
println!(" Baseline entropy: {:.4} bits", report.baseline_entropy);
if let Some(pos) = &report.positive_class {
println!(" Positive class: {pos}");
} else {
println!(
" Positive class: (none — {} classes, so AUC is undefined)",
report.classes.len()
);
fields.push((format!(" class {class}"), count.to_string()));
}
fields.push((
"Baseline entropy".to_owned(),
format!("{:.4} bits", report.baseline_entropy),
));
fields.push((
"Positive class".to_owned(),
report.positive_class.clone().unwrap_or_else(|| {
format!(
"(none — {} classes, so AUC is undefined)",
report.classes.len()
)
}),
));
if let Some(rate) = report.base_rate {
println!(" Assumed prevalence: {rate}");
fields.push(("Assumed prevalence".to_owned(), rate.to_string()));
}
println!();
out.fields(fields);

println!(
" {:<44} {:>4} {:>8} {:>8} {:>7}",
"FEATURE", "KIND", "MI(bits)", "STRENGTH", "SEP"
out.heading("Feature Separation");
let mut t = render::Table::new(
&["FEATURE", "KIND", "MI(bits)", "STRENGTH", "SEP"],
"no features to evaluate",
);
for f in &shown {
let sep = f
.separation
.map_or_else(|| " -- ".to_owned(), |s| format!("{s:>7.4}"));
println!(
" {:<44} {:>4} {:>8.4} {:>8.4} {}",
f.path,
t.push(vec![
f.path.clone(),
if f.kind == vajra_stats::FieldKind::Numeric {
"num"
} else {
"cat"
},
f.mutual_information,
f.relationship_strength,
sep,
);
}
.to_owned(),
format!("{:.4}", f.mutual_information),
format!("{:.4}", f.relationship_strength),
f.separation
.map_or_else(|| "--".to_owned(), |s| format!("{s:.4}")),
]);
}
println!();
println!(" Ranked by MI (symmetric, bits) — the only column comparable across");
println!(" field types. SEP is |2*AUC-1| and is reported for ordered fields only.");
out.table(t);
out.note(
"Ranked by MI (symmetric, bits) — the only column comparable across field\ntypes. SEP is |2*AUC-1| and is reported for ordered fields only.",
);

if report.base_rate.is_some() && !report.binary {
println!();
println!(
" --base-rate is only meaningful for a two-class label; this one has {}",
out.note(format!(
"--base-rate is only meaningful for a two-class label; this one has {} classes, so no single decision rule is defined.",
report.classes.len()
);
println!(" classes, so no single decision rule is defined.");
));
} else if report.base_rate.is_some() {
println!();
println!("=== Best single rule, priced at the assumed prevalence ===");
println!(
" {:<30} {:>7} {:>7} {:>9} RULE",
"FEATURE", "TPR", "FPR", "PRECISION"
out.heading("Best single rule, priced at the assumed prevalence");
let mut r = render::Table::new(
&["FEATURE", "TPR", "FPR", "PRECISION", "RULE"],
"no operating point available",
);
for f in &shown {
if let Some(op) = &f.operating_point {
let precision = op
.precision_at_base_rate
.map_or_else(|| " -- ".to_owned(), |p| format!("{p:>9.5}"));
println!(
" {:<30} {:>7.4} {:>7.4} {} {}",
truncate_path(&f.path, 30),
op.tpr,
op.fpr,
precision,
op.rule
);
r.push(vec![
f.path.clone(),
format!("{:.4}", op.tpr),
format!("{:.4}", op.fpr),
op.precision_at_base_rate
.map_or_else(|| "--".to_owned(), |p| format!("{p:.5}")),
op.rule.clone(),
]);
}
}
println!();
println!(" Precision here is what the rule would deliver in a population with");
println!(" the assumed prevalence — usually far below its corpus precision.");
out.table(r);
out.note(
"Precision here is what the rule would deliver in a population with the\nassumed prevalence — usually far below its corpus precision.",
);
}

print!(
"{}",
match cli.format {
Format::Markdown => out.to_markdown(),
_ => out.to_text(),
}
);
}
}

Ok(())
}

/// Trim a long JSONPath for fixed-width output.
fn truncate_path(path: &str, width: usize) -> String {
if path.len() <= width {
return path.to_owned();
}
let tail = path.len() - (width - 3);
format!("...{}", &path[tail..])
}

fn cmd_invariants(input: &str, top_k: usize, bin: &str, cli: &Cli) -> Result<()> {
let bins = parse_bin_flag(bin)?;
let doc = load_document(input, cli)?;
Expand Down
10 changes: 7 additions & 3 deletions vajra-cli/src/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,13 @@ impl Report {
let _ = writeln!(out, "=== {h} ===");
}
Block::Fields(pairs) => {
let w = pairs.iter().map(|(k, _)| k.len()).max().unwrap_or(0);
// A trailing colon on the label, matching the convention the
// hand-written output used, so anything grepping text output
// keeps working.
let w = pairs.iter().map(|(k, _)| k.len() + 1).max().unwrap_or(0);
for (k, v) in pairs {
let _ = writeln!(out, " {k:<w$} {v}", w = w);
let label = format!("{k}:");
let _ = writeln!(out, " {label:<w$} {v}", w = w);
}
}
Block::Table(t) => {
Expand Down Expand Up @@ -275,7 +279,7 @@ mod tests {
fn text_render_is_aligned_and_complete() {
let out = sample().to_text();
assert!(out.contains("=== Summary ==="));
assert!(out.contains("Records"));
assert!(out.contains("Records:"), "labels keep their colon:\n{out}");
assert!(out.contains("$.a"));
assert!(out.contains("Ranked by entropy."));
}
Expand Down
Loading
Loading