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:
1 parent
795e429159
commit
e4ff44c011
4 files changed
+88
-61
No files matched your search
@@ -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})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
Reference in new issue
Block a user