refactor(dependency_tracker): Phase B language-advantage pass — dichotomy wave

CLI mirror hardening, behavior-identical (frozen battery + parity re-green):
subcommand dispatch is now an enum parsed once with an exhaustive match
(adding a subcommand is a compiler error until every arm is extended;
no-cmd/unknown stay explicit), rendering goes through fmt::Write (writeln!
into the output String — no intermediate format! allocations), and the
three propagate sections share one section() renderer parameterized by
glyph/header/field instead of three copy-pasted loops.

cargo battery 1/1 (frozen truth verbatim), parity 72/72, clippy -D
warnings + fmt clean. Evidence: verification/dt-phaseb-*.exit.
This commit is contained in:
Nick committed 2026-10-14 21:10:31 +01:00
1 parent 795e429159
commit e4ff44c011
4 files changed
+88 -61

No files matched your search

+85 -61
View File
@@ -32,6 +32,7 @@
//! The caller frees returned strings with `dt_free_string`.
use std::ffi::{CStr, CString};
use std::fmt::Write as _;
use std::os::raw::{c_char, c_void};
use serde_json::{json, Value};
@@ -239,6 +240,89 @@ pub fn get_full_map() -> Value {
/// stdout non-tty). prog comes from argv[0], pinned in the corpus.
const HELP_TEXT: &str = "usage: dependency_tracker.py [-h] {check,propagate,map} ...\n\nDependency tracker for umbrella systems\n\npositional arguments:\n {check,propagate,map}\n check Check what depends on a file\n propagate Propagate changes to dependents\n map Show full dependency map\n\noptions:\n -h, --help show this help message and exit\n";
/// The CLI's subcommands — parsed once, dispatched by exhaustive match
/// (adding a subcommand is a compiler error until every arm is extended;
/// the no-command and unknown arms stay explicit).
enum Subcommand {
Check { file: String },
Propagate { file: String, dry_run: bool },
Map,
NoCommand,
Unknown,
}
fn parse_argv(argv: &[String]) -> Subcommand {
match argv.get(1).map(String::as_str) {
None => Subcommand::NoCommand,
Some("check") => Subcommand::Check {
file: argv.get(2).cloned().unwrap_or_default(),
},
Some("propagate") => Subcommand::Propagate {
file: argv.get(2).cloned().unwrap_or_default(),
dry_run: argv.iter().skip(1).any(|a| a == "--dry-run"),
},
Some("map") => Subcommand::Map,
Some(_) => Subcommand::Unknown,
}
}
fn section(out: &mut String, glyph: &str, header: &str, rows: &[Value], field: &str) {
let _ = writeln!(out, "{header} ({}):", rows.len());
for row in rows {
let _ = writeln!(
out,
" {glyph} {}: {}",
row["file"].as_str().unwrap_or(""),
row[field].as_str().unwrap_or("")
);
}
}
fn render_cli(cmd: &Subcommand, scripts_dir: &str) -> (String, i32) {
match cmd {
Subcommand::NoCommand => (HELP_TEXT.to_string(), 1),
Subcommand::Check { file } => {
let deps = get_dependents(file);
let mut out = String::new();
let _ = writeln!(out, "File: {}", python_basename(file));
let _ = writeln!(out, "Dependents ({}):", deps.len());
for d in &deps {
let _ = writeln!(out, " \u{2192} {d}");
}
(out, 0)
}
Subcommand::Propagate { file, dry_run } => {
let r = propagate(file, *dry_run, scripts_dir);
let arr = |key: &str| r[key].as_array().cloned().unwrap_or_default();
let mut out = String::new();
let _ = writeln!(
out,
"Propagation for: {}",
r["changed_file"].as_str().unwrap_or("")
);
let prop = arr("propagated");
section(&mut out, "\u{2713}", "Propagated", &prop, "action");
let skip = arr("skipped");
section(&mut out, "\u{2298}", "Skipped", &skip, "reason");
let errs = arr("errors");
if !errs.is_empty() {
section(&mut out, "\u{2717}", "Errors", &errs, "error");
}
(out, 0)
}
Subcommand::Map => {
let mut out = String::from("Dependency Map:\n");
for (file, deps) in DEPENDENCY_GRAPH {
let _ = writeln!(out, " {file} \u{2192} {}", deps.join(", "));
}
(out, 0)
}
// argparse machinery outside the frozen corpus: stderr + rc 2 in
// Python; empty stdout + rc 2 here
Subcommand::Unknown => (String::new(), 2),
}
}
/// CLI mirror of the oracle's main(): a pure function over argv
/// returning {"stdout", "rc"} — the battery/parity surface. The facade's
/// main() itself stays Python argparse (process-boundary tail; it never
@@ -246,67 +330,7 @@ const HELP_TEXT: &str = "usage: dependency_tracker.py [-h] {check,propagate,map}
/// missing positionals are argparse machinery outside the frozen corpus
/// (stderr + rc 2 there; empty stdout + rc 2 here).
pub fn main(argv: &[String], scripts_dir: &str) -> Value {
let (stdout, rc) = match argv.get(1).map(String::as_str) {
None => (HELP_TEXT.to_string(), 1),
Some("check") => {
let file = argv.get(2).cloned().unwrap_or_default();
let deps = get_dependents(&file);
let mut s = format!("File: {}\n", python_basename(&file));
s.push_str(&format!("Dependents ({}):\n", deps.len()));
for d in &deps {
s.push_str(&format!(" \u{2192} {d}\n"));
}
(s, 0)
}
Some("propagate") => {
let file = argv.get(2).cloned().unwrap_or_default();
let dry_run = argv.iter().skip(1).any(|a| a == "--dry-run");
let r = propagate(&file, dry_run, scripts_dir);
let arr = |key: &str| r[key].as_array().cloned().unwrap_or_default();
let mut s = format!(
"Propagation for: {}\n",
r["changed_file"].as_str().unwrap_or("")
);
let prop = arr("propagated");
s.push_str(&format!("Propagated ({}):\n", prop.len()));
for p in &prop {
s.push_str(&format!(
" \u{2713} {}: {}\n",
p["file"].as_str().unwrap_or(""),
p["action"].as_str().unwrap_or("")
));
}
let skip = arr("skipped");
s.push_str(&format!("Skipped ({}):\n", skip.len()));
for sk in &skip {
s.push_str(&format!(
" \u{2298} {}: {}\n",
sk["file"].as_str().unwrap_or(""),
sk["reason"].as_str().unwrap_or("")
));
}
let errs = arr("errors");
if !errs.is_empty() {
s.push_str(&format!("Errors ({}):\n", errs.len()));
for e in &errs {
s.push_str(&format!(
" \u{2717} {}: {}\n",
e["file"].as_str().unwrap_or(""),
e["error"].as_str().unwrap_or("")
));
}
}
(s, 0)
}
Some("map") => {
let mut s = String::from("Dependency Map:\n");
for (file, deps) in DEPENDENCY_GRAPH {
s.push_str(&format!(" {file} \u{2192} {}\n", deps.join(", ")));
}
(s, 0)
}
Some(_) => (String::new(), 2),
};
let (stdout, rc) = render_cli(&parse_argv(argv), scripts_dir);
json!({"stdout": stdout, "rc": rc})
}
+1
View File
@@ -0,0 +1 @@
0
+1
View File
@@ -0,0 +1 @@
0
+1
View File
@@ -0,0 +1 @@
0