use std::path::Path;
use clap::{Args, Subcommand};
use tga::core::inspect::{attest, render, schema};
#[derive(Args, Debug)]
pub struct InspectArgs {
#[command(subcommand)]
pub what: InspectSubcommand,
}
#[derive(Subcommand, Debug)]
#[non_exhaustive]
pub enum InspectSubcommand {
Schema(InspectFormatArgs),
Attest(InspectFormatArgs),
}
#[derive(Args, Debug, Default)]
pub struct InspectFormatArgs {
#[arg(long)]
pub json: bool,
}
pub fn run(db_path: &Path, args: InspectArgs) -> anyhow::Result<()> {
let conn = tga::core::inspect::open_read_only(db_path)?;
let snapshot = schema::snapshot(&conn)?;
match args.what {
InspectSubcommand::Schema(fmt) => {
if fmt.json {
println!("{}", serde_json::to_string_pretty(&snapshot)?);
} else {
print!("{}", render::schema_report(&snapshot));
}
}
InspectSubcommand::Attest(fmt) => {
let attestation = attest::attest(&conn, &snapshot)?;
if fmt.json {
println!("{}", serde_json::to_string_pretty(&attestation)?);
} else {
print!("{}", render::attestation_report(&attestation));
}
if attestation.verdict == attest::Verdict::Findings {
let flagged: i64 = attestation
.scanned_columns
.iter()
.map(|s| s.diff_shaped_rows)
.sum();
anyhow::bail!(
"attestation findings: {} content-bearing column(s), {flagged} row(s) \
carrying diff-shaped text — the claim above does not hold unreviewed",
attestation.content_columns.len()
);
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn inspect_errors_on_a_missing_database() {
let mut path = std::env::temp_dir();
path.push(format!("tga-inspect-cmd-{}-absent.db", std::process::id()));
let _ = std::fs::remove_file(&path);
for what in [
InspectSubcommand::Schema(InspectFormatArgs::default()),
InspectSubcommand::Attest(InspectFormatArgs::default()),
] {
let err = run(&path, InspectArgs { what }).expect_err("must not succeed");
let text = err.to_string();
assert!(
text.contains("no database at"),
"the error must name the cause: {text}"
);
assert!(
text.contains("absent.db"),
"the error must name the path: {text}"
);
}
assert!(!path.exists(), "inspection must not create a database");
}
#[test]
fn attest_exits_non_zero_on_findings() {
let mut dir = std::env::temp_dir();
dir.push(format!("tga-inspect-verdict-{}", std::process::id()));
std::fs::create_dir_all(&dir).expect("mkdir");
let path = dir.join("tga.db");
{
let db = tga::core::db::Database::open(&path).expect("migrate");
db.connection()
.execute(
"INSERT INTO commits (sha, author_name, author_email, timestamp, message, repository) \
VALUES ('c1', 'Ada', 'ada@example.com', '2026-01-01T00:00:00Z', 'ok', 'r')",
[],
)
.expect("clean commit");
}
run(
&path,
InspectArgs {
what: InspectSubcommand::Attest(InspectFormatArgs::default()),
},
)
.expect("a clean database must attest cleanly");
{
let db = tga::core::db::Database::open(&path).expect("reopen");
db.connection()
.execute(
"UPDATE commits SET message = ?1 WHERE sha = 'c1'",
["fix\n\ndiff --git a/x b/x\n@@ -1 +1 @@\n-a\n+b\n"],
)
.expect("paste a diff");
}
let err = run(
&path,
InspectArgs {
what: InspectSubcommand::Attest(InspectFormatArgs::default()),
},
)
.expect_err("a diff in a free-text column must fail the gate");
assert!(
err.to_string().contains("diff-shaped text"),
"the error must name what was found: {err}"
);
let _ = std::fs::remove_dir_all(&dir);
}
}