use std::path::{Path, PathBuf};
use std::process::ExitCode;
use clap::{Parser, Subcommand};
use antigen::{audit, presents, scan};
#[derive(Debug, Parser)]
#[command(name = "cargo-antigen", bin_name = "cargo", version)]
struct CargoCli {
#[command(subcommand)]
command: CargoSubcommand,
}
#[derive(Debug, Subcommand)]
enum CargoSubcommand {
Antigen(AntigenCli),
}
#[derive(Debug, Parser)]
#[command(version)]
struct AntigenCli {
#[command(subcommand)]
command: AntigenSubcommand,
}
#[derive(Debug, Subcommand)]
enum AntigenSubcommand {
Scan(ScanArgs),
#[command(hide = true)]
New {
name: String,
},
#[command(hide = true)]
Vaccinate {
antigen: String,
pattern: String,
},
Audit(AuditArgs),
Attest(AttestCli),
Tolerate(TolerateCli),
Oracle(OracleCli),
Verify(VerifyCli),
Vcs(VcsCli),
MucosalMap(MucosalMapArgs),
Fingerprint(FingerprintArgs),
}
#[derive(Debug, Parser)]
struct ScanArgs {
#[arg(long, default_value = ".")]
root: PathBuf,
#[arg(long, default_value = "human")]
format: OutputFormat,
#[arg(long)]
strict: bool,
#[arg(long)]
include_deps: bool,
#[arg(long)]
workspace: bool,
#[arg(long)]
category: Option<String>,
#[arg(long, value_name = "FILE")]
output: Option<PathBuf>,
}
#[derive(Debug, Parser)]
struct AuditArgs {
#[arg(long, default_value = ".")]
root: PathBuf,
#[arg(long, default_value = "human")]
format: OutputFormat,
#[arg(long)]
strict: bool,
#[arg(long)]
category: Option<String>,
#[arg(long)]
workspace: bool,
#[arg(long, value_name = "FILE")]
output: Option<PathBuf>,
}
#[derive(Debug, Parser)]
struct FingerprintArgs {
#[arg(long, default_value = ".")]
root: PathBuf,
#[arg(long)]
antigen: Option<String>,
#[arg(long)]
item_path: Option<String>,
#[arg(long, default_value = "human")]
format: OutputFormat,
}
#[derive(Debug, Parser)]
struct AttestCli {
#[command(subcommand)]
command: AttestSubcommand,
}
#[presents(antigen::stdlib::dogfood::DeclaredCapabilityWithNoProductionPath)]
#[derive(Debug, Subcommand)]
enum AttestSubcommand {
Scaffold(AttestScaffoldArgs),
Sign(AttestSignArgs),
Check(AttestCheckArgs),
Delta(AttestDeltaArgs),
#[command(hide = true, name = "oracle")]
Oracle,
List(AttestListArgs),
Gc(AttestGcArgs),
}
#[derive(Debug, Parser)]
struct AttestDeltaArgs {
#[arg(long)]
sidecar: PathBuf,
#[arg(long)]
item_path: String,
#[arg(long)]
signer: Option<String>,
#[arg(long)]
role: Option<String>,
#[arg(long)]
fingerprint: String,
#[arg(long)]
prior_fingerprint: String,
#[arg(long)]
rationale: String,
#[arg(long, default_value = "git-trust")]
strength: SignatureStrengthArg,
}
#[derive(Debug, Parser)]
struct AttestListArgs {
#[arg(long, default_value = ".")]
root: PathBuf,
#[arg(long)]
tolerance_only: bool,
#[arg(long)]
orphan_scan: bool,
#[arg(long, default_value = "human")]
format: OutputFormat,
}
#[derive(Debug, Parser)]
struct AttestGcArgs {
#[arg(long, default_value = ".")]
root: PathBuf,
#[arg(long)]
force: bool,
}
#[derive(Debug, Parser)]
struct AttestScaffoldArgs {
#[arg(long)]
antigen: String,
#[arg(long)]
source_file: PathBuf,
#[arg(long, default_value = "")]
item_path: String,
#[arg(long, default_value = "")]
fingerprint: String,
#[arg(long, default_value = "immunity")]
kind: RatificationKindArg,
#[arg(long)]
force: bool,
}
#[derive(Debug, Parser)]
struct AttestSignArgs {
#[arg(long)]
sidecar: PathBuf,
#[arg(long)]
item_path: String,
#[arg(long)]
signer: String,
#[arg(long)]
fingerprint: String,
#[arg(long)]
role: Option<String>,
#[arg(long)]
reasoning: Option<String>,
#[arg(long, default_value = "git-trust")]
strength: SignatureStrengthArg,
}
#[derive(Debug, Parser)]
struct AttestCheckArgs {
#[arg(long)]
sidecar: PathBuf,
#[arg(long)]
predicate: String,
#[arg(long)]
item_path: Option<String>,
#[arg(long)]
fingerprint: Option<String>,
}
#[derive(Debug, Clone, clap::ValueEnum)]
enum RatificationKindArg {
Immunity,
Tolerance,
}
impl From<RatificationKindArg> for antigen_attestation::RatificationKind {
fn from(k: RatificationKindArg) -> Self {
match k {
RatificationKindArg::Immunity => Self::Immunity,
RatificationKindArg::Tolerance => Self::Tolerance,
}
}
}
#[derive(Debug, Clone, clap::ValueEnum)]
enum SignatureStrengthArg {
TextStamp,
GitTrust,
CryptoSigned,
}
impl From<SignatureStrengthArg> for antigen_attestation::SignatureStrength {
fn from(s: SignatureStrengthArg) -> Self {
match s {
SignatureStrengthArg::TextStamp => Self::TextStamp,
SignatureStrengthArg::GitTrust => Self::GitTrust,
SignatureStrengthArg::CryptoSigned => Self::CryptoSigned,
}
}
}
#[derive(Debug, Parser)]
struct TolerateCli {
#[command(subcommand)]
command: TolerateSubcommand,
}
#[derive(Debug, Subcommand)]
enum TolerateSubcommand {
Scaffold(AttestScaffoldArgs),
Sign(AttestSignArgs),
Check(AttestCheckArgs),
List(AttestListArgs),
}
#[derive(Debug, Parser)]
struct OracleCli {
#[command(subcommand)]
command: OracleSubcommand,
}
#[derive(Debug, Subcommand)]
enum OracleSubcommand {
List(OracleListArgs),
Status(OracleStatusArgs),
Declare(OracleDeclareArgs),
Complete(OracleCompleteArgs),
Deprecate(OracleDeprecateArgs),
Retire(OracleRetireArgs),
Revoke(OracleRevokeArgs),
}
#[derive(Debug, Parser)]
struct OracleListArgs {
#[arg(long, default_value = ".")]
root: PathBuf,
#[arg(long, default_value = "human")]
format: OutputFormat,
}
#[derive(Debug, Parser)]
struct OracleStatusArgs {
#[arg(long)]
id: String,
#[arg(long, default_value = ".")]
root: PathBuf,
}
#[derive(Debug, Parser)]
struct OracleDeclareArgs {
#[arg(long)]
id: String,
#[arg(long)]
kind: OracleRefKindArg,
#[arg(long)]
reference: String,
#[arg(long, action = clap::ArgAction::Append)]
steward: Vec<String>,
#[arg(long)]
rationale: String,
#[arg(long)]
version: Option<String>,
#[arg(long, default_value = ".")]
root: PathBuf,
}
#[derive(Debug, Clone, clap::ValueEnum)]
#[clap(rename_all = "kebab-case")]
enum OracleRefKindArg {
LocalFile,
Url,
Doi,
Arxiv,
GithubIssue,
Other,
}
#[derive(Debug, Parser)]
struct OracleCompleteArgs {
#[arg(long)]
id: String,
#[arg(long)]
steward: Option<String>,
#[arg(long)]
version: String,
#[arg(long)]
rationale: String,
#[arg(long, default_value = ".")]
root: PathBuf,
}
#[derive(Debug, Parser)]
struct OracleDeprecateArgs {
#[arg(long)]
id: String,
#[arg(long)]
steward: Option<String>,
#[arg(long)]
superseded_by: Option<String>,
#[arg(long)]
rationale: String,
#[arg(long, default_value = ".")]
root: PathBuf,
}
#[derive(Debug, Parser)]
struct OracleRetireArgs {
#[arg(long)]
id: String,
#[arg(long)]
steward: Option<String>,
#[arg(long)]
rationale: String,
#[arg(long, default_value = ".")]
root: PathBuf,
}
#[derive(Debug, Parser)]
struct OracleRevokeArgs {
#[arg(long)]
id: String,
#[arg(long)]
steward: Option<String>,
#[arg(long)]
rationale: String,
#[arg(long)]
invalidates_prior: bool,
#[arg(long, default_value = ".")]
root: PathBuf,
}
#[derive(Debug, Clone, clap::ValueEnum)]
enum OutputFormat {
Human,
Json,
}
#[derive(Debug, Parser)]
struct VerifyCli {
#[command(subcommand)]
command: VerifySubcommand,
}
#[derive(Debug, Subcommand)]
enum VerifySubcommand {
Deps(VerifyDepsArgs),
MaintainerChanges(VerifyMaintainerChangesArgs),
DepAttest(VerifyDepAttestArgs),
DepPin(VerifyDepPinArgs),
ContentHash(VerifyContentHashArgs),
#[command(name = "proc-macro-sandbox")]
ProcMacroSandbox,
Sandbox,
BehavioralDiff,
}
#[derive(Debug, Parser)]
struct VerifyDepsArgs {
#[arg(long, default_value = ".")]
root: PathBuf,
#[arg(long, default_value = "human")]
format: OutputFormat,
#[arg(long)]
crate_name: Option<String>,
#[arg(long)]
strict: bool,
}
#[derive(Debug, Parser)]
struct VerifyMaintainerChangesArgs {
#[arg(long, default_value = ".")]
root: PathBuf,
#[arg(long)]
crate_name: String,
#[arg(long)]
since_version: String,
}
#[presents(antigen::stdlib::dogfood::UnvalidatedSealedEnumAcceptance)]
#[derive(Debug, Parser)]
struct VerifyDepAttestArgs {
#[arg(long, default_value = ".")]
root: PathBuf,
crate_at_version: String,
#[arg(long, required = true)]
reviewable_artifact: PathBuf,
#[arg(long, default_value = "metadata-only")]
review_scope: String,
#[arg(long, default_value_t = true)]
exact_version: bool,
#[arg(long)]
signed_by: Option<String>,
#[arg(long)]
rationale: Option<String>,
}
#[derive(Debug, Parser)]
struct VerifyDepPinArgs {
#[arg(long, default_value = ".")]
root: PathBuf,
#[arg(long)]
write: bool,
}
#[derive(Debug, Parser)]
struct VerifyContentHashArgs {
#[command(subcommand)]
command: VerifyContentHashSubcommand,
}
#[derive(Debug, Subcommand)]
enum VerifyContentHashSubcommand {
Record(VerifyContentHashRecordArgs),
Check(VerifyContentHashCheckArgs),
}
#[derive(Debug, Parser)]
struct VerifyContentHashRecordArgs {
#[arg(long, default_value = ".")]
root: PathBuf,
crate_at_version: String,
#[arg(long)]
signed_by: Option<String>,
}
#[derive(Debug, Parser)]
struct VerifyContentHashCheckArgs {
#[arg(long, default_value = ".")]
root: PathBuf,
crate_at_version: String,
#[arg(long)]
live: bool,
#[arg(long)]
strict: bool,
}
fn run_verify(cli: VerifyCli) -> ExitCode {
match cli.command {
VerifySubcommand::Deps(args) => run_verify_deps(args),
VerifySubcommand::MaintainerChanges(args) => run_verify_maintainer_changes(args),
VerifySubcommand::DepAttest(args) => run_verify_dep_attest(args),
VerifySubcommand::DepPin(args) => run_verify_dep_pin(args),
VerifySubcommand::ContentHash(args) => match args.command {
VerifyContentHashSubcommand::Record(a) => run_verify_content_hash_record(a),
VerifyContentHashSubcommand::Check(a) => run_verify_content_hash_check(a),
},
VerifySubcommand::ProcMacroSandbox => run_verify_stub(
"proc-macro-sandbox",
"v0.4+",
"Active proc-macro sandboxing (ADR-025 tooling-phase 3).",
),
VerifySubcommand::Sandbox => run_verify_stub(
"sandbox",
"v0.4+",
"Active build.rs sandboxing (ADR-025 tooling-phase 3).",
),
VerifySubcommand::BehavioralDiff => run_verify_stub(
"behavioral-diff",
"v0.5+",
"Behavioral-fingerprint comparison across versions (ADR-025 tooling-phase 4).",
),
}
}
fn run_verify_deps(args: VerifyDepsArgs) -> ExitCode {
use antigen::supply_chain::evaluate::evaluate_dep_pinned;
use antigen::supply_chain::witness::DepPinnedState;
let state = evaluate_dep_pinned(&args.root, args.crate_name.as_deref());
let unpinned_names = match &state {
DepPinnedState::AllPinned => Vec::new(),
DepPinnedState::Unpinned { unpinned_deps } => unpinned_deps.clone(),
DepPinnedState::NotInManifest { crate_name } => vec![crate_name.clone()],
};
match args.format {
OutputFormat::Human => {
if unpinned_names.is_empty() {
println!("verify deps: all checked dependencies are exact-pinned.");
} else {
println!(
"verify deps: {n} dep(s) without exact-pin `=` specifiers:",
n = unpinned_names.len()
);
for n in &unpinned_names {
println!(" - {n}");
}
println!();
println!("To fix:");
println!(" edit Cargo.toml; change each entry to `<name> = \"=X.Y.Z\"`.");
println!("Per ADR-025 §UnpinnedDependency — exact-pin is the discipline.");
}
}
OutputFormat::Json => {
println!(
"{}",
serde_json::to_string(&serde_json::json!({
"subcommand": "verify-deps",
"unpinned_deps": unpinned_names,
"passed": unpinned_names.is_empty(),
}))
.unwrap_or_else(|_| "{}".to_string())
);
}
}
if args.strict && !unpinned_names.is_empty() {
ExitCode::from(1)
} else {
ExitCode::SUCCESS
}
}
fn run_verify_maintainer_changes(args: VerifyMaintainerChangesArgs) -> ExitCode {
use antigen::supply_chain::evaluate::{evaluate_maintainer_unchanged, is_valid_crate_name};
use antigen::supply_chain::witness::MaintainerState;
if !is_valid_crate_name(&args.crate_name) {
eprintln!(
"error: --crate-name must contain only ASCII alphanumeric characters, `_`, or `-` \
(got `{}`). Path traversal sequences are not permitted.",
args.crate_name
);
return ExitCode::from(2);
}
println!(
"verify maintainer-changes: checking {crate_name} @ {since_version}",
crate_name = args.crate_name,
since_version = args.since_version,
);
println!();
println!("WARNING: this subcommand MUST run BEFORE `cargo update`. After");
println!("`cargo update`, the new maintainer's code is already in Cargo.lock");
println!("and the gate has effectively already passed. Per ADR-025.");
println!();
let state = evaluate_maintainer_unchanged(&args.root, &args.crate_name, &args.since_version);
match state {
MaintainerState::Unchanged => {
println!("result: maintainer-unchanged (snapshot matches)");
ExitCode::SUCCESS
}
MaintainerState::Changed { added, removed } => {
println!("result: MAINTAINER-CHANGE-WITHOUT-REATTESTATION");
println!(" added owners: {added:?}");
println!(" removed owners: {removed:?}");
ExitCode::from(1)
}
MaintainerState::SnapshotMissing => {
println!("result: snapshot missing");
println!(
" expected: .attest/supply-chain/maintainer/{crate_name}.json",
crate_name = args.crate_name
);
println!(" (v0.2: snapshot is operator-managed; v0.3+ adds live crates.io query)");
ExitCode::from(1)
}
MaintainerState::CratesIoQueryUnavailable => {
println!("result: crates.io query unavailable (v0.2 limitation per ADR-025)");
ExitCode::from(2)
}
}
}
fn run_verify_dep_attest(args: VerifyDepAttestArgs) -> ExitCode {
use antigen::supply_chain::evaluate::dep_attest_path;
use antigen::supply_chain::schema::{DepAttestation, ReviewScope};
if args.reviewable_artifact.as_os_str().is_empty()
|| args.reviewable_artifact.to_string_lossy().trim().is_empty()
{
eprintln!(
"error: --reviewable-artifact must be a non-empty, non-whitespace path. \
Empty values create a rubber-stamp sidecar that the audit will flag with \
dep-attest-without-reviewable-artifact. Per ADR-025 + ATK-SC-1-A."
);
return ExitCode::from(2);
}
let Some((crate_name, version)) = parse_crate_at_version(&args.crate_at_version) else {
eprintln!("error: argument must be `<crate>@<version>` (e.g., `serde@1.0.197`)");
return ExitCode::from(2);
};
let scope = match args.review_scope.as_str() {
"full" => ReviewScope::Full,
"diff" => ReviewScope::Diff,
"build-script-only" | "build_script_only" => ReviewScope::BuildScriptOnly,
"proc-macro-only" | "proc_macro_only" => ReviewScope::ProcMacroOnly,
"metadata-only" | "metadata_only" => ReviewScope::MetadataOnly,
other => {
eprintln!(
"error: --review-scope must be one of: full, diff, build-script-only, \
proc-macro-only, metadata-only (got `{other}`)"
);
return ExitCode::from(2);
}
};
let signed_by = match resolve_steward_name(args.signed_by.as_deref()) {
Ok(n) => n,
Err(e) => {
eprintln!("{e}");
return ExitCode::from(2);
}
};
let date = chrono::Utc::now().format("%Y-%m-%d").to_string();
let attest = DepAttestation {
crate_name: crate_name.clone(),
version: version.clone(),
exact_version: args.exact_version,
reviewable_artifact: args.reviewable_artifact,
review_scope: scope,
signed_by,
date,
rationale: args.rationale,
};
let path = dep_attest_path(&args.root, &crate_name, &version);
if let Some(parent) = path.parent() {
if let Err(e) = std::fs::create_dir_all(parent) {
eprintln!("error: could not create {}: {e}", parent.display());
return ExitCode::from(2);
}
}
let json = match serde_json::to_string_pretty(&attest) {
Ok(j) => j,
Err(e) => {
eprintln!("error: serialize attestation: {e}");
return ExitCode::from(2);
}
};
if let Err(e) = std::fs::write(&path, &json) {
eprintln!("error: write {}: {e}", path.display());
return ExitCode::from(2);
}
println!("recorded dep-attestation at {}", path.display());
ExitCode::SUCCESS
}
const PIN_REWRITE_TABLES: [&str; 3] = ["dependencies", "dev-dependencies", "build-dependencies"];
fn pin_dep_in_table(table: &mut toml_edit::Table, name: &str, version: &str) -> bool {
let pinned = format!("={version}");
let Some(item) = table.get_mut(name) else {
return false;
};
match item {
toml_edit::Item::Value(toml_edit::Value::String(_)) => {
*item = toml_edit::value(pinned);
true
}
toml_edit::Item::Value(toml_edit::Value::InlineTable(t)) if t.contains_key("version") => {
t.insert("version", toml_edit::Value::from(pinned));
true
}
toml_edit::Item::Table(t) if t.contains_key("version") => {
t.insert("version", toml_edit::value(pinned));
true
}
_ => false,
}
}
fn run_verify_dep_pin(args: VerifyDepPinArgs) -> ExitCode {
use antigen::supply_chain::evaluate::{evaluate_dep_pinned, resolved_version_from_lockfile};
use antigen::supply_chain::witness::DepPinnedState;
let state = evaluate_dep_pinned(&args.root, None);
match state {
DepPinnedState::AllPinned => {
println!("verify dep-pin: all Cargo.toml deps already exact-pinned. No edits needed.");
ExitCode::SUCCESS
}
DepPinnedState::Unpinned { unpinned_deps } => {
let lockfile = args.root.join("Cargo.lock");
let resolved: Vec<(String, String)> = unpinned_deps
.iter()
.filter_map(|name| {
resolved_version_from_lockfile(&lockfile, name).map(|v| (name.clone(), v))
})
.collect();
let unresolved: Vec<&String> = unpinned_deps
.iter()
.filter(|n| !resolved.iter().any(|(rn, _)| rn == *n))
.collect();
if args.write {
return write_dep_pins(&args.root, &resolved, &unresolved);
}
println!(
"verify dep-pin: {n} unpinned dep(s) detected. Suggested edits:",
n = unpinned_deps.len()
);
for (name, version) in &resolved {
println!(" - in Cargo.toml [dependencies]: pin `{name}` to `={version}`");
}
for name in &unresolved {
println!(
" - in Cargo.toml [dependencies]: pin `{name}` to `=<RESOLVED_VERSION>` \
(no Cargo.lock entry — run `cargo generate-lockfile` first)"
);
}
println!();
if !unresolved.is_empty() {
println!(
"note: {} dep(s) had no resolved version in Cargo.lock.",
unresolved.len()
);
}
println!(
"Re-run with `--write` to apply these pins IN PLACE (format-preserving; \
deps with no resolved version are left untouched). Rewriting the adopter's \
manifest is opt-in by design — never the default."
);
ExitCode::SUCCESS
}
DepPinnedState::NotInManifest { crate_name } => {
eprintln!("error: crate `{crate_name}` not found in manifest");
ExitCode::from(2)
}
}
}
fn write_dep_pins(
root: &std::path::Path,
resolved: &[(String, String)],
unresolved: &[&String],
) -> ExitCode {
let manifest_path = root.join("Cargo.toml");
let content = match std::fs::read_to_string(&manifest_path) {
Ok(c) => c,
Err(e) => {
eprintln!("error: read {}: {e}", manifest_path.display());
return ExitCode::from(2);
}
};
let mut doc = match content.parse::<toml_edit::DocumentMut>() {
Ok(d) => d,
Err(e) => {
eprintln!("error: {} is not valid TOML: {e}", manifest_path.display());
return ExitCode::from(2);
}
};
let mut applied: Vec<&str> = Vec::new();
let mut not_found: Vec<&str> = Vec::new();
for (name, version) in resolved {
let mut hit = false;
for table_name in PIN_REWRITE_TABLES {
if let Some(table) = doc
.get_mut(table_name)
.and_then(toml_edit::Item::as_table_mut)
{
if pin_dep_in_table(table, name, version) {
hit = true;
}
}
}
if hit {
applied.push(name.as_str());
} else {
not_found.push(name.as_str());
}
}
if applied.is_empty() {
println!("verify dep-pin --write: no rewritable unpinned deps with a resolved version.");
} else if let Err(e) = std::fs::write(&manifest_path, doc.to_string()) {
eprintln!("error: write {}: {e}", manifest_path.display());
return ExitCode::from(2);
} else {
println!(
"verify dep-pin --write: pinned {} dep(s) in place in {}:",
applied.len(),
manifest_path.display()
);
for (name, version) in resolved {
if applied.contains(&name.as_str()) {
println!(" - `{name}` → `={version}`");
}
}
}
if !not_found.is_empty() {
println!();
println!(
"note: {} resolved dep(s) were not in a rewritable [dependencies]/[dev-dependencies]/\
[build-dependencies] table (e.g. target-conditional) — pin them manually: {}",
not_found.len(),
not_found.join(", ")
);
}
if !unresolved.is_empty() {
println!();
println!(
"note: {} unpinned dep(s) had no resolved version in Cargo.lock and were left \
untouched (run `cargo generate-lockfile`, then re-run): {}",
unresolved.len(),
unresolved
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>()
.join(", ")
);
}
ExitCode::SUCCESS
}
fn run_verify_content_hash_record(args: VerifyContentHashRecordArgs) -> ExitCode {
use antigen::supply_chain::evaluate::{current_hash_from_lockfile, save_content_hash_record};
use antigen::supply_chain::schema::ContentHashRecord;
let Some((crate_name, version)) = parse_crate_at_version(&args.crate_at_version) else {
eprintln!("error: argument must be `<crate>@<version>`");
return ExitCode::from(2);
};
let lockfile = args.root.join("Cargo.lock");
let Some(checksum) = current_hash_from_lockfile(&lockfile, &crate_name, &version) else {
eprintln!(
"error: no `[[package]] name = \"{crate_name}\" version = \"{version}\" checksum = ...` \
entry in {}. The crate must be in the lockfile with a checksum.",
lockfile.display()
);
return ExitCode::from(2);
};
let signed_by = match resolve_steward_name(args.signed_by.as_deref()) {
Ok(n) => n,
Err(e) => {
eprintln!("{e}");
return ExitCode::from(2);
}
};
let date = chrono::Utc::now().format("%Y-%m-%d").to_string();
let record = ContentHashRecord {
crate_name,
version,
content_hash: checksum,
hash_source: "cargo-lock-checksum".to_string(),
signed_by,
date,
};
match save_content_hash_record(&args.root, &record) {
Ok(p) => {
println!("recorded content-hash at {}", p.display());
println!(" hash-source: cargo-lock-checksum (v0.2; tarball SHA-256 is v0.3+)");
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("error: write record: {e}");
ExitCode::from(2)
}
}
}
fn cratesio_index_path(name: &str) -> Option<String> {
let lower = name.to_lowercase();
let n = lower.chars().count();
match n {
0 => None,
1 => Some(format!("1/{lower}")),
2 => Some(format!("2/{lower}")),
3 => Some(format!("3/{}/{lower}", &lower[..1])),
_ => Some(format!("{}/{}/{lower}", &lower[..2], &lower[2..4])),
}
}
fn fetch_cratesio_cksum(name: &str, version: &str) -> Option<String> {
let index_path = cratesio_index_path(name)?;
let url = format!("https://index.crates.io/{index_path}");
let agent = ureq::AgentBuilder::new()
.timeout(std::time::Duration::from_secs(5))
.build();
let body = agent.get(&url).call().ok()?.into_string().ok()?;
for line in body.lines() {
let Ok(entry) = serde_json::from_str::<serde_json::Value>(line) else {
continue;
};
if entry.get("vers").and_then(|v| v.as_str()) == Some(version) {
return entry
.get("cksum")
.and_then(|c| c.as_str())
.map(str::to_owned);
}
}
None }
fn run_verify_content_hash_check(args: VerifyContentHashCheckArgs) -> ExitCode {
use antigen::supply_chain::evaluate::evaluate_content_hash_matches;
use antigen::supply_chain::witness::ContentHashState;
let Some((crate_name, version)) = parse_crate_at_version(&args.crate_at_version) else {
eprintln!("error: argument must be `<crate>@<version>`");
return ExitCode::from(2);
};
let live_escalates = if args.live {
run_live_cksum_check(&args.root, &crate_name, &version, args.strict)
} else {
false
};
let state = evaluate_content_hash_matches(&args.root, &crate_name, &version);
let local = match state {
ContentHashState::Matches => {
println!("content-hash: MATCH for {crate_name}@{version}");
ExitCode::SUCCESS
}
ContentHashState::Mismatch { recorded, current } => {
println!("content-hash: MISMATCH for {crate_name}@{version}");
println!(" recorded: {recorded}");
println!(" current: {current}");
println!();
println!("This is the chalk/debug/eslint-config attack signal. Investigate before");
println!("re-recording — if the change is legitimate, re-attest with a fresh");
println!("signer + review artifact. Per ADR-025 §ContentHashMismatch.");
ExitCode::from(1)
}
ContentHashState::NoAttestation => {
println!("content-hash: no first-attestation for {crate_name}@{version}");
println!(" run: cargo antigen verify content-hash record {crate_name}@{version}");
ExitCode::from(1)
}
ContentHashState::CrateNotInLockfile { crate_name: cn } => {
eprintln!("error: crate `{cn}` not found in Cargo.lock (no checksum to compare)");
ExitCode::from(2)
}
ContentHashState::SidecarMalformed { error } => {
eprintln!(
"error: content-hash sidecar exists but did NOT deserialize cleanly. \
Per ATK-SC-2-A this is distinct from no-attestation — corrupting the \
sidecar to silently downgrade a Mismatch into a NoAttestation is exactly \
the attack the audit blocks. Inspect the file and re-record. \
Parse error: {error}"
);
ExitCode::from(1)
}
};
if live_escalates {
return ExitCode::from(1);
}
local
}
fn run_live_cksum_check(
root: &std::path::Path,
crate_name: &str,
version: &str,
strict: bool,
) -> bool {
use antigen::supply_chain::evaluate::{compare_live_cksum, current_hash_from_lockfile};
use antigen::supply_chain::witness::LiveCksumState;
let lockfile = root.join("Cargo.lock");
let Some(expected) = current_hash_from_lockfile(&lockfile, crate_name, version) else {
println!(
"content-hash --live: SKIPPED — no local Cargo.lock checksum for \
{crate_name}@{version} to compare against the registry."
);
return false;
};
let served = fetch_cratesio_cksum(crate_name, version);
match compare_live_cksum(served.as_deref(), &expected) {
LiveCksumState::Verified { hash } => {
println!(
"content-hash --live: VERIFIED for {crate_name}@{version} — the crates.io \
sparse-index cksum matches the local lockfile checksum ({hash})."
);
false
}
LiveCksumState::Mismatch { expected, served } => {
println!("content-hash --live: MISMATCH for {crate_name}@{version}");
println!(" local (lockfile): {expected}");
println!(" crates.io served: {served}");
println!();
println!(
"The registry serves a DIFFERENT hash than your lockfile records — a \
supply-chain substitution / yank-and-republish signal. Investigate before \
trusting this dependency."
);
if strict {
println!(" (--strict: escalating to a non-zero exit)");
}
strict
}
LiveCksumState::Unverifiable { reason } => {
println!("content-hash --live: UNVERIFIABLE for {crate_name}@{version} — skipped.");
println!(" reason: {reason}");
false
}
}
}
fn run_verify_stub(name: &str, version_target: &str, description: &str) -> ExitCode {
println!("cargo antigen verify {name}: {version_target} stub");
println!();
println!(" {description}");
println!();
println!("Per ADR-005 Amendment 2 honest-tier-naming: this subcommand is");
println!("a deliberate stub. It does NOT silently pass — the supply-chain");
println!("audit hints surface the un-evaluated witness as the appropriate");
println!("unsandboxed-* hint, NOT as a passing evaluation.");
ExitCode::from(2)
}
#[derive(Debug, Parser)]
struct VcsCli {
#[command(subcommand)]
command: VcsSubcommand,
}
#[derive(Debug, Subcommand)]
enum VcsSubcommand {
CheckCommit(VcsCheckCommitArgs),
Scan(VcsScanArgs),
BranchArchive(VcsBranchArchiveArgs),
RollbackPrepare(VcsRollbackPrepareArgs),
Attest,
Recurrence(VcsRecurrenceArgs),
}
#[derive(Debug, Parser)]
struct VcsCheckCommitArgs {
#[arg(long, default_value = "HEAD")]
commit: String,
#[arg(long, default_value = "human")]
format: OutputFormat,
#[arg(long)]
strict: bool,
}
#[derive(Debug, Parser)]
struct VcsScanArgs {
#[arg(long, default_value = "50")]
depth: usize,
#[arg(long, default_value = "human")]
format: OutputFormat,
}
#[derive(Debug, Parser)]
struct VcsBranchArchiveArgs {
branch: String,
#[arg(long)]
by: String,
#[arg(long)]
rationale: String,
}
#[derive(Debug, Parser)]
struct VcsRollbackPrepareArgs {
#[arg(long)]
decision: String,
#[arg(long)]
target: String,
}
#[derive(Debug, Parser)]
struct VcsRecurrenceArgs {
#[arg(long, default_value = "200")]
depth: usize,
#[arg(long, default_value = "human")]
format: OutputFormat,
}
fn run_vcs(cli: VcsCli) -> ExitCode {
match cli.command {
VcsSubcommand::CheckCommit(args) => run_vcs_check_commit(args),
VcsSubcommand::Scan(args) => run_vcs_scan(args),
VcsSubcommand::BranchArchive(args) => run_vcs_branch_archive(args),
VcsSubcommand::RollbackPrepare(args) => run_vcs_rollback_prepare(args),
VcsSubcommand::Attest => run_vcs_attest_stub(),
VcsSubcommand::Recurrence(args) => run_vcs_recurrence(args),
}
}
struct RecurrenceObservation {
antigen: &'static str,
substrate: &'static str,
count: usize,
}
fn count_commits_touching(depth: usize, pathspec: &str) -> Option<usize> {
let out = std::process::Command::new("git")
.args(["log", &format!("-{depth}"), "--format=%H", "--", pathspec])
.output()
.ok()?;
if !out.status.success() {
return None;
}
Some(String::from_utf8_lossy(&out.stdout).lines().count())
}
fn count_commits_changing_line(depth: usize, regex: &str, pathspec: &str) -> Option<usize> {
let out = std::process::Command::new("git")
.args([
"log",
&format!("-{depth}"),
"--format=%H",
&format!("-G{regex}"),
"--",
pathspec,
])
.output()
.ok()?;
if !out.status.success() {
return None;
}
Some(String::from_utf8_lossy(&out.stdout).lines().count())
}
fn run_vcs_recurrence(args: VcsRecurrenceArgs) -> ExitCode {
let msrv = count_commits_changing_line(args.depth, "rust-version", "*Cargo.toml");
let gitignore = count_commits_touching(args.depth, ".gitignore");
let lockfile = count_commits_touching(args.depth, "Cargo.lock");
let (Some(msrv), Some(gitignore), Some(lockfile)) = (msrv, gitignore, lockfile) else {
match args.format {
OutputFormat::Json => {
println!(
"{}",
serde_json::json!({ "observable": false, "reason": "git unavailable (not a repo, or git missing)" })
);
}
OutputFormat::Human => {
eprintln!(
"cargo antigen vcs recurrence: git unavailable (not a repo, or git \
missing) — recurrence is UNOBSERVABLE here, not zero."
);
}
}
return ExitCode::SUCCESS;
};
let observations = [
RecurrenceObservation {
antigen: "MsrvCreepAfterMajorVersionBump",
substrate: "commits changing a `rust-version` line in any Cargo.toml",
count: msrv,
},
RecurrenceObservation {
antigen: "GitignorePatternDriftOverReleases",
substrate: "commits touching .gitignore",
count: gitignore,
},
RecurrenceObservation {
antigen: "LockfileChurnFromUnpinnedTooling",
substrate: "commits touching Cargo.lock",
count: lockfile,
},
];
match args.format {
OutputFormat::Json => {
let arr: Vec<_> = observations
.iter()
.map(|o| {
serde_json::json!({
"antigen": o.antigen,
"substrate": o.substrate,
"recurrence_count": o.count,
"window_commits": args.depth,
})
})
.collect();
println!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"observable": true,
"observations": arr,
}))
.unwrap_or_else(|_| "{}".to_string())
);
}
OutputFormat::Human => {
println!(
"Recurrent-emergence mine over the last {} commits (recurrent-emergence \
family, ADR-024):",
args.depth
);
println!();
for o in &observations {
println!(" {:>4}× {}", o.count, o.antigen);
println!(" substrate: {}", o.substrate);
}
println!();
println!(
" These are OBSERVATIONS, not verdicts. A high count is evidence the \
failure-class recurs in this repo — anchor it with \
#[recurrence_anchor(<Antigen>)] + #[itch(<Antigen>)] so the next \
occurrence is recognized, not re-discovered. Whether a count is \
'high enough' to anchor is your call (the recognition seam)."
);
}
}
ExitCode::SUCCESS
}
fn read_commit_trailers(commit: &str) -> Vec<(String, String)> {
let body = std::process::Command::new("git")
.args(["show", "--no-patch", "--format=%B", commit])
.output();
let body_bytes = match body {
Ok(o) if o.status.success() => o.stdout,
_ => return Vec::new(),
};
let parsed = std::process::Command::new("git")
.args(["interpret-trailers", "--parse"])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.spawn()
.and_then(|mut child| {
use std::io::Write;
if let Some(stdin) = child.stdin.take() {
let mut stdin = stdin;
let _ = stdin.write_all(&body_bytes);
}
child.wait_with_output()
});
match parsed {
Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout)
.lines()
.filter_map(|l| {
let (k, v) = l.split_once(':')?;
Some((k.trim().to_owned(), v.trim().to_owned()))
})
.collect(),
_ => Vec::new(),
}
}
fn run_vcs_check_commit(args: VcsCheckCommitArgs) -> ExitCode {
use antigen::vcs_witness::RollbackTriageState;
let trailers = read_commit_trailers(&args.commit);
let state = RollbackTriageState::evaluate(&trailers);
match args.format {
OutputFormat::Json => {
println!(
"{}",
serde_json::to_string_pretty(&state).unwrap_or_else(|_| "{}".to_string())
);
}
OutputFormat::Human => match &state {
RollbackTriageState::ChainPresent { decision } => {
println!(
"commit {}: rollback-triage chain present (Triage-Decision: {})",
args.commit,
decision.as_str()
);
}
RollbackTriageState::ChainMalformed { value } => {
println!(
"commit {}: Triage-Decision trailer present but value {value:?} is not a \
valid triage decision (black|red|yellow|green|white)",
args.commit
);
}
RollbackTriageState::ChainAbsent => {
println!(
"commit {}: no Triage-Decision trailer — backs \
vcs-rollback-without-triage-commit if this is a rollback",
args.commit
);
}
},
}
if args.strict && !state.is_pass() {
return ExitCode::from(1);
}
ExitCode::SUCCESS
}
fn run_vcs_scan(args: VcsScanArgs) -> ExitCode {
use antigen::vcs_witness::RollbackTriageState;
let log = std::process::Command::new("git")
.args(["log", &format!("-{}", args.depth), "--format=%H%x1f%s"])
.output();
let log_text = match log {
Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout).into_owned(),
_ => {
eprintln!("cargo antigen vcs scan: git log unavailable (not a repo, or git missing)");
return ExitCode::from(2);
}
};
let mut flagged = 0usize;
let mut entries: Vec<(String, String, RollbackTriageState)> = Vec::new();
for line in log_text.lines() {
let Some((sha, subject)) = line.split_once('\u{1f}') else {
continue;
};
let looks_like_rollback = subject.to_lowercase().contains("revert")
|| subject.to_lowercase().contains("rollback");
if !looks_like_rollback {
continue;
}
let state = RollbackTriageState::evaluate(&read_commit_trailers(sha));
if !state.is_pass() {
flagged += 1;
}
entries.push((sha.to_owned(), subject.to_owned(), state));
}
match args.format {
OutputFormat::Json => {
let arr: Vec<_> = entries
.iter()
.map(|(sha, subject, state)| {
serde_json::json!({
"sha": sha,
"subject": subject,
"chain_pass": state.is_pass(),
})
})
.collect();
println!(
"{}",
serde_json::to_string_pretty(&arr).unwrap_or_else(|_| "[]".to_string())
);
}
OutputFormat::Human => {
if entries.is_empty() {
println!(
"cargo antigen vcs scan: no revert/rollback-shaped commits in the last {} \
commits",
args.depth
);
} else {
for (sha, subject, state) in &entries {
let short = &sha[..sha.len().min(8)];
let mark = if state.is_pass() { "ok " } else { "!! " };
println!("{mark}{short} {subject}");
}
println!();
println!(
"{flagged} rollback-shaped commit(s) without a valid Triage-Decision chain"
);
}
}
}
ExitCode::SUCCESS
}
fn run_vcs_branch_archive(args: VcsBranchArchiveArgs) -> ExitCode {
if args.rationale.trim().is_empty() {
eprintln!("cargo antigen vcs branch-archive: --rationale cannot be empty");
return ExitCode::from(1);
}
let sidecar = serde_json::json!({
"branch": args.branch,
"by_role": args.by,
"rationale": args.rationale,
"attested_at": chrono::Utc::now().to_rfc3339(),
});
println!(
"{}",
serde_json::to_string_pretty(&sidecar).unwrap_or_else(|_| "{}".to_string())
);
eprintln!();
eprintln!(
"Write this to .attest/vcs/branch-archive/{}.json and commit it BEFORE \
deleting the branch (v0.2 advisory; in-place write is v0.2.x).",
args.branch
);
ExitCode::SUCCESS
}
fn run_vcs_rollback_prepare(args: VcsRollbackPrepareArgs) -> ExitCode {
use antigen::vcs::TriageDecision;
if TriageDecision::parse_decision(&args.decision).is_none() {
eprintln!(
"cargo antigen vcs rollback-prepare: --decision {:?} is not a valid triage \
decision (black|red|yellow|green|white)",
args.decision
);
return ExitCode::from(1);
}
println!("Add this trailer to your rollback commit message (ADR-026 Amendment 4):");
println!();
println!(" Triage-Decision: {}", args.decision.to_lowercase());
println!();
println!(
"Rolling back to {}. The Triage-Decision trailer is the commit-intent signal \
the rollback-triage chain reads — NOT a source-code scan.",
args.target
);
ExitCode::SUCCESS
}
fn run_vcs_attest_stub() -> ExitCode {
println!("cargo antigen vcs attest: v0.2.x stub");
println!();
println!("Generic VCS attestation-sidecar recording lands in v0.2.x. For now,");
println!("use `branch-archive` (branch-deletion attestation) or `rollback-prepare`");
println!("(triage-commit trailer scaffolding). Per ADR-005 Amendment 2 honest-tier-");
println!("naming: this stub does NOT silently pass.");
ExitCode::from(2)
}
#[derive(Debug, Parser)]
struct MucosalMapArgs {
#[arg(long, default_value = ".")]
root: PathBuf,
#[arg(long, default_value = "human")]
format: OutputFormat,
#[arg(long)]
undefended: bool,
#[arg(long)]
tolerant: bool,
#[arg(long)]
kind: Option<String>,
}
fn run_mucosal_map(args: MucosalMapArgs) -> ExitCode {
use antigen::mucosal::MucosalKind;
use antigen::scan::MucosalKindTag;
if !args.root.is_dir() {
eprintln!("error: expected a directory: {}", args.root.display());
return ExitCode::from(2);
}
let report = match scan::scan_workspace(&args.root, None) {
Ok(r) => r,
Err(e) => {
eprintln!("error: scan failed: {e}");
return ExitCode::from(2);
}
};
let mucosal_audit = audit::audit_mucosal(&report);
let kind_filter: Option<MucosalKind> = match args.kind.as_deref() {
None => None,
Some(k) => {
let Some(mk) = MucosalKind::parse_kind(k) else {
eprintln!(
"error: unknown MucosalKind {k:?}; expected a kebab-case variant \
(e.g. user-input, database-query, shell-argument)"
);
return ExitCode::from(2);
};
Some(mk)
}
};
let entries: Vec<&antigen::audit::MucosalAudit> = mucosal_audit
.audits
.iter()
.filter(|a| {
if args.tolerant && a.declaration.tag != MucosalKindTag::MucosalTolerant {
return false;
}
if args.undefended && a.hints.is_empty() {
return false;
}
if let Some(kf) = kind_filter {
let decl_kind = a
.declaration
.boundary_kind
.as_deref()
.and_then(MucosalKind::parse_kind);
if decl_kind != Some(kf) {
return false;
}
}
true
})
.collect();
match args.format {
OutputFormat::Json => {
let arr: Vec<_> = entries
.iter()
.map(|a| {
serde_json::json!({
"tag": a.declaration.tag,
"boundary_kind": a.declaration.boundary_kind,
"file": a.declaration.file,
"line": a.declaration.line,
"hints": a.hints,
})
})
.collect();
println!(
"{}",
serde_json::to_string_pretty(&arr).unwrap_or_else(|_| "[]".to_string())
);
}
OutputFormat::Human => {
if entries.is_empty() {
println!("cargo antigen mucosal-map: no matching mucosal declarations");
} else {
for a in &entries {
let kind = a.declaration.boundary_kind.as_deref().unwrap_or("?");
let mark = if a.hints.is_empty() { "ok " } else { "!! " };
let file = a.declaration.file.display();
println!(
"{mark}{:<18} {kind:<20} {file}:{}",
format!("{:?}", a.declaration.tag),
a.declaration.line
);
for hint in &a.hints {
println!(" - {hint:?}");
}
}
println!();
println!(
"{} declaration(s); {} clean, {} with concerns",
entries.len(),
entries.iter().filter(|a| a.hints.is_empty()).count(),
entries.iter().filter(|a| !a.hints.is_empty()).count(),
);
}
}
}
ExitCode::SUCCESS
}
fn parse_crate_at_version(s: &str) -> Option<(String, String)> {
let (c, v) = s.split_once('@')?;
if c.is_empty() || v.is_empty() || v.contains('@') {
return None;
}
let crate_ok = c
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '-');
let version_ok = v
.chars()
.all(|ch| ch.is_ascii_alphanumeric() || matches!(ch, '.' | '-' | '+'));
if !crate_ok || !version_ok {
return None;
}
Some((c.to_string(), v.to_string()))
}
fn main() -> ExitCode {
let cli = CargoCli::parse();
let CargoSubcommand::Antigen(antigen_cli) = cli.command;
match antigen_cli.command {
AntigenSubcommand::Scan(args) => run_scan(args),
AntigenSubcommand::New { name } => run_new(name),
AntigenSubcommand::Vaccinate { antigen, pattern } => run_vaccinate(antigen, pattern),
AntigenSubcommand::Audit(args) => run_audit(args),
AntigenSubcommand::Attest(cli) => run_attest(cli),
AntigenSubcommand::Tolerate(cli) => run_tolerate(cli),
AntigenSubcommand::Oracle(cli) => run_oracle(cli),
AntigenSubcommand::Verify(cli) => run_verify(cli),
AntigenSubcommand::Vcs(cli) => run_vcs(cli),
AntigenSubcommand::MucosalMap(args) => run_mucosal_map(args),
AntigenSubcommand::Fingerprint(args) => run_fingerprint(args),
}
}
fn run_oracle(cli: OracleCli) -> ExitCode {
match cli.command {
OracleSubcommand::List(args) => run_oracle_list(args),
OracleSubcommand::Status(args) => run_oracle_status(args),
OracleSubcommand::Declare(args) => run_oracle_declare(args),
OracleSubcommand::Complete(args) => run_oracle_complete(args),
OracleSubcommand::Deprecate(args) => run_oracle_deprecate(args),
OracleSubcommand::Retire(args) => run_oracle_retire(args),
OracleSubcommand::Revoke(args) => run_oracle_revoke(args),
}
}
fn oracle_json_path(root: &std::path::Path, id: &str) -> std::path::PathBuf {
root.join(".antigen")
.join("oracles")
.join(format!("{id}.oracle.json"))
}
fn load_oracle(
root: &std::path::Path,
id: &str,
) -> Result<antigen_attestation::schema::Oracle, String> {
let path = oracle_json_path(root, id);
let content = std::fs::read_to_string(&path).map_err(|e| {
format!(
"error: oracle `{id}` not found at `{}`: {e}",
path.display()
)
})?;
serde_json::from_str(&content)
.map_err(|e| format!("error: oracle `{id}` is not valid Oracle JSON: {e}"))
}
fn save_oracle(
root: &std::path::Path,
oracle: &antigen_attestation::schema::Oracle,
) -> Result<(), String> {
oracle
.validate()
.map_err(|e| format!("error: oracle validation failed: {e}"))?;
let dir = root.join(".antigen").join("oracles");
std::fs::create_dir_all(&dir).map_err(|e| {
format!(
"error: could not create oracle directory `{}`: {e}",
dir.display()
)
})?;
let path = oracle_json_path(root, &oracle.id);
let json = serde_json::to_string_pretty(oracle)
.map_err(|e| format!("error: failed to serialize oracle: {e}"))?;
std::fs::write(&path, &json)
.map_err(|e| format!("error: failed to write oracle `{}`: {e}", path.display()))?;
Ok(())
}
fn resolve_steward_name(explicit: Option<&str>) -> Result<String, String> {
explicit.map_or_else(
|| {
let out = std::process::Command::new("git")
.args(["config", "user.name"])
.output();
match out {
Ok(o) if o.status.success() => {
Ok(String::from_utf8_lossy(&o.stdout).trim().to_owned())
}
_ => Err(
"error: --steward not provided and `git config user.name` failed".to_owned(),
),
}
},
|s| Ok(s.to_owned()),
)
}
fn run_oracle_list(args: OracleListArgs) -> ExitCode {
let oracle_dir = args.root.join(".antigen").join("oracles");
if !oracle_dir.exists() {
eprintln!("No oracle records found under `{}`.", args.root.display());
return ExitCode::SUCCESS;
}
let entries = match std::fs::read_dir(&oracle_dir) {
Ok(e) => e,
Err(e) => {
eprintln!("error: could not read oracle directory: {e}");
return ExitCode::from(2);
}
};
let mut found = 0usize;
for entry in entries.flatten() {
let path = entry.path();
if path.extension().is_some_and(|e| e == "json") {
let content = match std::fs::read_to_string(&path) {
Ok(c) => c,
Err(e) => {
eprintln!("warning: could not read `{}`: {e}", path.display());
continue;
}
};
let oracle = match serde_json::from_str::<antigen_attestation::schema::Oracle>(&content)
{
Ok(o) => o,
Err(e) => {
eprintln!(
"warning: `{}` is not valid Oracle JSON: {e}",
path.display()
);
continue;
}
};
match args.format {
OutputFormat::Human => {
println!(
"{} [{:?}] — {} steward(s)",
oracle.id,
oracle.state,
oracle.stewards.len()
);
}
OutputFormat::Json => {
let obj = serde_json::json!({ "id": oracle.id, "state": format!("{:?}", oracle.state) });
println!("{obj}");
}
}
found += 1;
}
}
if found == 0 {
eprintln!("No oracle records found under `{}`.", args.root.display());
} else {
eprintln!("{found} oracle(s) listed.");
}
ExitCode::SUCCESS
}
fn run_oracle_status(args: OracleStatusArgs) -> ExitCode {
match load_oracle(&args.root, &args.id) {
Ok(oracle) => {
println!("Oracle: {}", oracle.id);
println!("State: {:?}", oracle.state);
println!("Stewards ({}):", oracle.stewards.len());
for s in &oracle.stewards {
println!(" {} ({})", s.name, s.authorization_basis);
}
println!("Transitions ({}):", oracle.transitions.len());
for t in &oracle.transitions {
println!(
" {} → {} by {} on {} — {}",
t.from, t.to, t.authorized_by, t.at, t.rationale
);
}
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("{e}");
ExitCode::from(1)
}
}
}
fn compute_delta_chain_depth(signers: &[antigen_attestation::Signer], signer_name: &str) -> u32 {
let last_fresh_index = signers
.iter()
.enumerate()
.filter(|(_, s)| s.name == signer_name && s.basis.is_fresh())
.map(|(i, _)| i)
.next_back();
last_fresh_index.map_or(1, |fresh_idx| {
let count = signers[fresh_idx + 1..]
.iter()
.filter(|s| s.name == signer_name && s.basis.is_delta())
.count();
u32::try_from(count).unwrap_or(u32::MAX).saturating_add(1)
})
}
fn validate_delta_rationale(rationale: &str) -> Result<(), ExitCode> {
use antigen_attestation::schema::DEFAULT_DELTA_RATIONALE_MIN_CHARS;
let trimmed = rationale.trim();
if trimmed.is_empty() {
eprintln!("error: --rationale must be non-empty (anti-laundering safeguard T2R-C)");
return Err(ExitCode::from(1));
}
if trimmed.chars().count() < DEFAULT_DELTA_RATIONALE_MIN_CHARS {
eprintln!(
"error: --rationale is too short ({} chars); minimum is {} chars \
(anti-laundering safeguard T2R-B). Rubber-stamp rationales are rejected.",
trimmed.chars().count(),
DEFAULT_DELTA_RATIONALE_MIN_CHARS,
);
return Err(ExitCode::from(1));
}
Ok(())
}
fn build_oracle_ref(
kind: OracleRefKindArg,
reference: &str,
) -> Result<antigen_attestation::schema::OracleRef, ExitCode> {
use antigen_attestation::schema::OracleRef;
match kind {
OracleRefKindArg::LocalFile => Ok(OracleRef::LocalFile {
path: std::path::PathBuf::from(reference),
status_field: None,
expected_status: None,
}),
OracleRefKindArg::Url => Ok(OracleRef::Url {
url: reference.to_owned(),
label: None,
}),
OracleRefKindArg::Doi => Ok(OracleRef::Doi {
doi: reference.to_owned(),
section: None,
}),
OracleRefKindArg::Arxiv => Ok(OracleRef::Arxiv {
arxiv_id: reference.to_owned(),
section: None,
}),
OracleRefKindArg::GithubIssue => {
let parts: Vec<&str> = reference.splitn(2, '#').collect();
if parts.len() != 2 {
eprintln!("error: github-issue reference must be `owner/repo#N`");
return Err(ExitCode::from(1));
}
let Ok(issue) = parts[1].parse::<u32>() else {
eprintln!("error: issue number must be a positive integer");
return Err(ExitCode::from(1));
};
Ok(OracleRef::GitHubIssue {
repo: parts[0].to_owned(),
issue,
})
}
OracleRefKindArg::Other => Ok(OracleRef::Other {
subkind: "other".to_owned(),
reference: reference.to_owned(),
label: None,
}),
}
}
fn run_oracle_declare(args: OracleDeclareArgs) -> ExitCode {
use antigen_attestation::schema::{Oracle, OracleState, OracleVersion, Provenance, Steward};
use chrono::Local;
if args.rationale.trim().is_empty() {
eprintln!("error: --rationale must be non-empty (Amendment 2 discipline)");
return ExitCode::from(1);
}
let mut steward_names = args.steward.clone();
if steward_names.is_empty() {
match resolve_steward_name(None) {
Ok(name) => steward_names.push(name),
Err(e) => {
eprintln!("{e}");
return ExitCode::from(1);
}
}
}
if steward_names.len() < 2 {
eprintln!(
"warning: only {} steward(s) declared; minimum 2 required for oracle to be transitioned \
to Complete (ATK-021-13 succession mitigation). Add a second steward via --steward.",
steward_names.len()
);
}
let stewards: Vec<Steward> = steward_names
.iter()
.map(|name| Steward {
name: name.clone(),
role: None,
authorization_basis: args.rationale.clone(),
})
.collect();
let reference = match build_oracle_ref(args.kind, &args.reference) {
Ok(r) => r,
Err(code) => return code,
};
let today = Local::now().date_naive();
let oracle = Oracle {
id: args.id.clone(),
reference,
state: OracleState::Draft,
stewards,
created: Provenance {
recorded_by: steward_names[0].clone(),
at: today,
},
version: OracleVersion {
pinned: args.version.unwrap_or_else(|| format!("declared-{today}")),
pinned_at: today,
},
transitions: vec![],
extensions: std::collections::BTreeMap::default(),
};
match save_oracle(&args.root, &oracle) {
Ok(()) => {
eprintln!("Oracle `{}` created in DRAFT state.", args.id);
eprintln!("Path: {}", oracle_json_path(&args.root, &args.id).display());
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("{e}");
ExitCode::from(2)
}
}
}
const fn oracle_state_discriminant(
state: &antigen_attestation::schema::OracleState,
) -> &'static str {
use antigen_attestation::schema::OracleState;
match state {
OracleState::Draft => "draft",
OracleState::Complete => "complete",
OracleState::Deprecated { .. } => "deprecated",
OracleState::Retired { .. } => "retired",
OracleState::Revoked { .. } => "revoked",
}
}
fn run_oracle_transition(
root: &std::path::Path,
id: &str,
steward: Option<String>,
rationale: &str,
new_state: antigen_attestation::schema::OracleState,
) -> ExitCode {
use antigen_attestation::schema::StateTransition;
use chrono::Local;
if rationale.trim().is_empty() {
eprintln!("error: --rationale must be non-empty (Amendment 2 discipline)");
return ExitCode::from(1);
}
let steward_name = match resolve_steward_name(steward.as_deref()) {
Ok(n) => n,
Err(e) => {
eprintln!("{e}");
return ExitCode::from(1);
}
};
let mut oracle = match load_oracle(root, id) {
Ok(o) => o,
Err(e) => {
eprintln!("{e}");
return ExitCode::from(1);
}
};
if !oracle.stewards.iter().any(|s| s.name == steward_name) {
eprintln!(
"error: `{steward_name}` is not a declared steward of oracle `{id}`.\n\
Declared stewards: {}",
oracle
.stewards
.iter()
.map(|s| s.name.as_str())
.collect::<Vec<_>>()
.join(", ")
);
return ExitCode::from(1);
}
let from_label = oracle_state_discriminant(&oracle.state).to_owned();
let to_label = oracle_state_discriminant(&new_state).to_owned();
let today = Local::now().date_naive();
oracle.transitions.push(StateTransition {
from: from_label.clone(),
to: to_label.clone(),
authorized_by: steward_name.clone(),
at: today,
rationale: rationale.to_owned(),
});
oracle.state = new_state;
match save_oracle(root, &oracle) {
Ok(()) => {
eprintln!("Oracle `{id}` transitioned {from_label}→{to_label} by {steward_name}.");
ExitCode::SUCCESS
}
Err(e) => {
eprintln!("{e}");
ExitCode::from(2)
}
}
}
fn run_oracle_complete(args: OracleCompleteArgs) -> ExitCode {
use antigen_attestation::schema::{OracleState, OracleVersion};
use chrono::Local;
let mut oracle = match load_oracle(&args.root, &args.id) {
Ok(o) => o,
Err(e) => {
eprintln!("{e}");
return ExitCode::from(1);
}
};
oracle.version = OracleVersion {
pinned: args.version.clone(),
pinned_at: Local::now().date_naive(),
};
match save_oracle(&args.root, &oracle) {
Ok(()) => {}
Err(e) => {
eprintln!("{e}");
return ExitCode::from(2);
}
}
run_oracle_transition(
&args.root,
&args.id,
args.steward,
&args.rationale,
OracleState::Complete,
)
}
fn run_oracle_deprecate(args: OracleDeprecateArgs) -> ExitCode {
use antigen_attestation::schema::OracleState;
let new_state = OracleState::Deprecated {
superseded_by: args.superseded_by,
reason: args.rationale.clone(),
};
run_oracle_transition(
&args.root,
&args.id,
args.steward,
&args.rationale,
new_state,
)
}
fn run_oracle_retire(args: OracleRetireArgs) -> ExitCode {
use antigen_attestation::schema::OracleState;
let steward_name = match resolve_steward_name(args.steward.as_deref()) {
Ok(n) => n,
Err(e) => {
eprintln!("{e}");
return ExitCode::from(1);
}
};
let new_state = OracleState::Retired {
reason: args.rationale.clone(),
retired_by: steward_name.clone(),
};
run_oracle_transition(
&args.root,
&args.id,
Some(steward_name),
&args.rationale,
new_state,
)
}
fn run_oracle_revoke(args: OracleRevokeArgs) -> ExitCode {
use antigen_attestation::schema::OracleState;
let steward_name = match resolve_steward_name(args.steward.as_deref()) {
Ok(n) => n,
Err(e) => {
eprintln!("{e}");
return ExitCode::from(1);
}
};
let new_state = OracleState::Revoked {
reason: args.rationale.clone(),
revoked_by: steward_name.clone(),
invalidates_prior_attestations: args.invalidates_prior,
};
run_oracle_transition(
&args.root,
&args.id,
Some(steward_name),
&args.rationale,
new_state,
)
}
fn parse_category_filter(
raw: Option<&str>,
) -> Result<Option<antigen::category::AntigenCategory>, ()> {
let Some(s) = raw else {
return Ok(None);
};
antigen::category::AntigenCategory::parse_category(s)
.map(Some)
.ok_or_else(|| {
eprintln!(
"error: unrecognized --category `{s}`; \
expected `substrate-alignment` or `functional-correctness`"
);
})
}
fn filter_report_by_category(
report: &mut scan::ScanReport,
cat: antigen::category::AntigenCategory,
) {
use antigen::category::AntigenCategory;
let matches = |decl_category: &[AntigenCategory]| -> bool {
if decl_category.is_empty() {
cat == AntigenCategory::FunctionalCorrectness
} else {
decl_category.contains(&cat)
}
};
report.antigens.retain(|a| matches(&a.category));
let kept: std::collections::HashSet<String> = report
.antigens
.iter()
.map(|a| a.type_name.clone())
.collect();
report
.presentations
.retain(|p| kept.contains(&p.antigen_type));
report.immunities.retain(|i| kept.contains(&i.antigen_type));
report.tolerances.retain(|t| kept.contains(&t.antigen_type));
}
fn acquire_scan_report(args: &ScanArgs) -> Result<scan::ScanReport, ExitCode> {
if !args.root.exists() {
eprintln!("error: path does not exist: {}", args.root.display());
return Err(ExitCode::from(2));
}
if !args.root.is_dir() {
eprintln!(
"error: expected a directory, got a file: {}",
args.root.display()
);
return Err(ExitCode::from(2));
}
if args.workspace {
eprintln!("Scanning workspace (member-aware): {}", args.root.display());
scan::scan_workspace_multi_crate(&args.root)
} else {
eprintln!("Scanning workspace: {}", args.root.display());
scan::scan_workspace(&args.root, None)
}
.map_err(|e| {
eprintln!("error: scan failed: {e}");
ExitCode::from(2)
})
}
fn run_scan(args: ScanArgs) -> ExitCode {
let Ok(category_filter) = parse_category_filter(args.category.as_deref()) else {
return ExitCode::from(2);
};
let mut report = match acquire_scan_report(&args) {
Ok(r) => r,
Err(code) => return code,
};
if let Some(cat) = category_filter {
filter_report_by_category(&mut report, cat);
}
let unaddressed = report.unaddressed_presentations();
let dep_reports = if args.include_deps {
match scan::enumerate_dep_crate_roots(&args.root, false) {
Ok(roots) => {
eprintln!("Scanning {} dependency crate(s)...", roots.len());
let mut out: Vec<DepScanResult> = Vec::with_capacity(roots.len());
for dep in roots {
let mut r = match scan::scan_workspace(&dep.crate_root, None) {
Ok(r) => r,
Err(e) => {
eprintln!(
" warning: scan failed for `{}` v{}: {e}",
dep.package_name, dep.version
);
continue;
}
};
let crate_id = format!("{}@{}", dep.package_name, dep.version);
r.stamp_canonical_path(&crate_id);
out.push(DepScanResult {
package_name: dep.package_name,
version: dep.version,
origin: dep.origin,
report: r,
});
}
Some(out)
}
Err(e) => {
eprintln!("error: cargo metadata failed: {e}");
return ExitCode::from(2);
}
}
} else {
None
};
let enveloped = ReportEnvelope::new(
&args.root,
JsonReport {
report: &report,
unaddressed: &unaddressed,
orphaned_lineage_edges: report.orphaned_lineage_edges(),
dangling_child_lineage_edges: report.dangling_child_lineage_edges(),
dep_reports: dep_reports.as_deref(),
},
);
if let Some(path) = args.output.as_ref() {
if let Err(e) = write_report_render(path, &enveloped) {
eprintln!("error: {e}");
return ExitCode::from(2);
}
}
match args.format {
OutputFormat::Human => {
print_human_report(&report, &unaddressed);
if let Some(deps) = dep_reports.as_ref() {
print_human_dep_summary(deps);
}
}
OutputFormat::Json => match serde_json::to_string_pretty(&enveloped) {
Ok(s) => println!("{s}"),
Err(e) => {
eprintln!("error: failed to serialize report: {e}");
return ExitCode::from(2);
}
},
}
let unaddressed_explicit_count = unaddressed
.iter()
.filter(|u| u.presentation.match_kind == antigen::scan::MatchKind::ExplicitMarker)
.count();
let lineage_broken = !report.orphaned_lineage_edges().is_empty()
|| !report.dangling_child_lineage_edges().is_empty();
if args.strict
&& (unaddressed_explicit_count > 0
|| !report.orphaned_tolerances().is_empty()
|| lineage_broken)
{
ExitCode::from(1)
} else {
ExitCode::SUCCESS
}
}
fn print_human_dep_summary(deps: &[DepScanResult]) {
println!();
println!("Cross-crate dep scan ({} crates):", deps.len());
let mut deps_with_antigens: Vec<&DepScanResult> = deps
.iter()
.filter(|d| !d.report.antigens.is_empty())
.collect();
deps_with_antigens.sort_by_key(|d| d.package_name.clone());
if deps_with_antigens.is_empty() {
println!(" No antigen declarations found in any dependency.");
println!(" (P5 finding 2026-05-09: zero `#[antigen(...)]` instances in the wild.)");
} else {
for d in deps_with_antigens {
println!(
" {} v{}: {} antigen(s), {} presentation(s), {} immunity claim(s)",
d.package_name,
d.version,
d.report.antigens.len(),
d.report.presentations.len(),
d.report.immunities.len()
);
}
}
}
fn print_human_report(report: &scan::ScanReport, unaddressed: &[scan::UnaddressedPresentation]) {
use antigen::scan::MatchKind;
let explicit_count = report
.presentations
.iter()
.filter(|p| p.match_kind == MatchKind::ExplicitMarker)
.count();
let fingerprint_count = report
.presentations
.iter()
.filter(|p| p.match_kind == MatchKind::FingerprintMatch)
.count();
println!();
println!(
"Scanned {} files, found {} antigen-related declarations:",
report.files_scanned,
report.total_declarations()
);
println!(" - {} antigen declarations", report.antigens.len());
println!(" - {} explicit #[presents] markers", explicit_count);
if fingerprint_count > 0 {
println!(" - {fingerprint_count} fingerprint matches (candidate sites — see below)");
}
if !report.tolerances.is_empty() {
println!(
" - {} tolerated sites (#[antigen_tolerance])",
report.tolerances.len()
);
}
if !report.defenses.is_empty() {
println!(" - {} #[defended_by] declarations", report.defenses.len());
}
if !report.immunities.is_empty() {
println!(
" - {} #[immune] declarations (deprecated — migrate to #[defended_by]/#[presents])",
report.immunities.len()
);
}
if !report.parse_failures.is_empty() {
println!(
" - {} parse failures (see --format json for details)",
report.parse_failures.len()
);
}
println!();
print_fingerprint_matches(report);
print_orphaned_tolerances(report);
print_lineage_integrity(report);
print_unaddressed(unaddressed);
}
fn print_lineage_integrity(report: &scan::ScanReport) {
let orphaned = report.orphaned_lineage_edges();
let dangling = report.dangling_child_lineage_edges();
if orphaned.is_empty() && dangling.is_empty() {
return;
}
if !orphaned.is_empty() {
println!(
"{} orphaned lineage edge(s) — #[descended_from] names a parent antigen \
not declared in the workspace:",
orphaned.len()
);
println!();
for e in &orphaned {
println!(
" {}:{} {} ⟶ {} [parent antigen `{}` not found]",
e.file.display(),
e.line,
e.child,
e.parent,
e.parent
);
}
println!();
}
if !dangling.is_empty() {
println!(
"{} dangling lineage edge(s) — the child of a #[descended_from] is not \
itself an #[antigen] declaration:",
dangling.len()
);
println!();
for e in &dangling {
println!(
" {}:{} {} ⟶ {} [child `{}` has no #[antigen] declaration]",
e.file.display(),
e.line,
e.child,
e.parent,
e.child
);
}
println!();
}
println!(
" A lineage claim must resolve to real declarations on both ends. Either \
declare the missing antigen, or remove/correct the #[descended_from] edge."
);
println!();
}
fn print_deferred_defenses_loud(report: &audit::DeferredDefenseAuditReport) {
use antigen::audit::AuditHint;
use antigen::scan::DeferredDefenseKind;
if report.audits.is_empty() {
return;
}
println!(
"⚠ {} deferred-defense declaration(s) — intentional, accepted defense gaps \
(NOT blocking; must be EXPLICITLY removed to resolve):",
report.audits.len()
);
println!(
" {} active · {} past-deadline · {} stale",
report.active_count, report.expired_count, report.stale_count
);
println!();
for a in &report.audits {
let d = &a.declaration;
let kind = match d.kind {
DeferredDefenseKind::Anergy => "anergy",
DeferredDefenseKind::Immunosuppress => "immunosuppress",
DeferredDefenseKind::Poxparty => "poxparty",
DeferredDefenseKind::Orient => "orient",
};
let antigen = d.antigen_type.as_deref().unwrap_or("(unlinked)");
let state = match a.hint {
AuditHint::AnergyStale => "STALE — long past review",
AuditHint::AnergyCostimulationNotArrived
| AuditHint::ImmunosuppressExpired
| AuditHint::PoxpartyOutcomePending
| AuditHint::OrientPendingActionRequired => "PAST DEADLINE — action required",
_ => "active",
};
println!(
" {}:{} #[{kind}] on `{antigen}` — {state}",
d.file.display(),
d.line
);
if !d.text.trim().is_empty() {
println!(" reason: {}", d.text.trim());
}
if let Some(until) = d.until.as_deref() {
if !until.is_empty() {
println!(" until: {until}");
}
}
}
println!();
println!(
" These suppress the BLOCK, never the GAP — the failure-class is still \
present and undefended at these sites. Remove the declaration when the \
defense lands (or the gap is genuinely accepted forever)."
);
println!();
}
fn print_convergent_evidence_concerns(report: &audit::ConvergentEvidenceAuditReport) {
if report.concern_count == 0 {
return;
}
println!(
"⚠ {} convergent-evidence declaration(s) with concerns (ADR-024):",
report.concern_count
);
println!();
for a in &report.audits {
if a.hints.is_empty() {
continue;
}
let d = &a.declaration;
let hints = a
.hints
.iter()
.map(audit_hint_kebab)
.collect::<Vec<_>>()
.join(", ");
println!(
" {}:{} #[{}] — {hints}",
d.file.display(),
d.line,
convergent_kind_str(&d.kind)
);
}
println!();
}
fn print_recurrent_concerns(report: &audit::RecurrentAuditReport) {
if report.concern_count == 0 {
return;
}
println!(
"⚠ {} recurrent-emergence declaration(s) with concerns (ADR-024):",
report.concern_count
);
println!();
for a in &report.audits {
if a.hints.is_empty() {
continue;
}
let d = &a.declaration;
let hints = a
.hints
.iter()
.map(audit_hint_kebab)
.collect::<Vec<_>>()
.join(", ");
let label = d
.antigen_type
.as_deref()
.or(d.name.as_deref())
.unwrap_or("(unlinked)");
println!(
" {}:{} #[{}] `{label}` — {hints}",
d.file.display(),
d.line,
recurrent_kind_str(d.kind)
);
}
println!();
}
fn print_lineage_fidelity_advisory(report: &audit::LineageFidelityAuditReport) {
if report.divergences.is_empty() {
return;
}
println!(
"ℹ {} #[descended_from] lineage(s) whose child fingerprint does not refine \
the parent (ADVISORY — lineage-fidelity, ADR-029-adjacent):",
report.divergences.len()
);
println!();
for d in &report.divergences {
println!(
" {}:{} {} ⟶ {} — {}",
d.file.display(),
d.line,
d.child,
d.parent,
d.detail
);
}
println!();
println!(
" A `#[descended_from(Parent)]` claims the child is a more-specific case of \
the parent's failure-class; if the child's fingerprint matches items the \
parent's does not, the lineage is structurally unsound. Advisory only in \
v0.3 (hard-fail deferred to a future ADR — biology: negative selection is \
strict). Tighten the child's fingerprint, or correct the lineage edge."
);
println!();
}
fn print_coverage_frontier(report: &audit::CoverageAuditReport) {
if report.is_complete() {
return;
}
println!(
"⚠ {} site(s) the scanner did not reach (the ignorance frontier — the 4th \
peripheral-tolerance mechanism: tolerance-by-non-encounter):",
report.unreached_sites.len()
);
println!();
for site in &report.unreached_sites {
let cause = match site.cause {
audit::UnreachedCause::Barrier => "barrier (never enumerated)",
audit::UnreachedCause::SubThreshold => "sub-threshold (scanned, not recognized)",
audit::UnreachedCause::Cryptic => "cryptic (present, unparsed form)",
};
println!(" {} [{}]", site.region, cause);
println!(" → {}", site.remedy);
}
println!();
println!(
" An unreached site is not defended and not tolerated — it is UNSEEN. \
Ignorance is observed, never declared (there is no #[ignorance] marker: \
to write one you'd have reached the site). Address each by its remedy above."
);
println!();
}
const fn prescriptive_kind_str(kind: antigen::scan::PrescriptiveKind) -> &'static str {
use antigen::scan::PrescriptiveKind as K;
match kind {
K::Panel => "panel",
K::Rx => "rx",
K::Refer => "refer",
K::Biopsy => "biopsy",
K::Ddx => "ddx",
K::Triage => "triage",
K::Culture => "culture",
K::Quarantine => "quarantine",
}
}
const fn work_verdict_tag(verdict: audit::WorkVerdict) -> &'static str {
use audit::WorkVerdict as V;
match verdict {
V::Overdue => "OVERDUE",
V::OutOfFrame => "out-of-frame",
V::Pending => "pending",
V::Fulfilled => "fulfilled",
}
}
fn print_prescriptive_board(report: &audit::PrescriptiveAuditReport) {
if report.verdicts.is_empty() {
return;
}
let overdue = report.overdue_count();
let out_of_frame = report.count_by_verdict(audit::WorkVerdict::OutOfFrame);
let pending = report.count_by_verdict(audit::WorkVerdict::Pending);
let fulfilled = report.count_by_verdict(audit::WorkVerdict::Fulfilled);
println!();
println!(
"── Work board (ADR-033): {} work-need(s) — {overdue} overdue, \
{out_of_frame} out-of-frame, {pending} pending, {fulfilled} fulfilled",
report.verdicts.len()
);
if report.is_clean() {
println!(" (no overdue work — the board is quiet)");
}
println!(
" code IS the board — this is a live projection of the current code, \
recomputed every run (never stored, never drifts)."
);
println!();
for v in report.board_ordered() {
let decl = &v.declaration;
let macro_name = prescriptive_kind_str(decl.kind);
let tag = work_verdict_tag(v.verdict);
let need = decl
.need_text
.as_deref()
.or_else(|| decl.items.first().map(String::as_str))
.unwrap_or("(no need text)");
let loud = if v.verdict.is_loud() { "‼ " } else { " " };
println!(
"{loud}[{tag}] #[{macro_name}] {need} ({}:{})",
decl.file.display(),
decl.line
);
if let Some(frame) = decl.frame.as_deref() {
println!(" frame: {frame}");
}
if let Some(blocking) = v.blocking.as_deref() {
println!(" → {blocking}");
}
if let Some(cause) = v.out_of_frame_cause {
let cause_tag = match cause {
audit::OutOfFrameCause::UnknownWhoRef => "unknown-who-ref",
audit::OutOfFrameCause::MissingWorkStep => "missing-work-step",
audit::OutOfFrameCause::UnparseableFrame => "unparseable-frame",
audit::OutOfFrameCause::UnresolvableRef => "unresolvable-ref",
};
println!(" cause: {cause_tag} — remedy: {}", cause.remedy());
}
for step in &v.steps {
let mark = match step.state {
audit::StepState::Attested => "✓",
audit::StepState::Unattested => "·",
audit::StepState::Unevaluable => "?",
};
println!(" {mark} {}: {}", step.role, step.reference);
}
}
println!();
}
const fn convergent_kind_str(kind: &antigen::scan::ConvergentEvidenceKind) -> &'static str {
use antigen::scan::ConvergentEvidenceKind as K;
match kind {
K::Diagnostic => "diagnostic",
K::Clonal => "clonal",
K::Igg => "igg",
K::Crossreactive => "crossreactive",
K::Polyclonal => "polyclonal",
K::Monoclonal => "monoclonal",
K::Adcc => "adcc",
}
}
const fn recurrent_kind_str(kind: antigen::scan::RecurrentKind) -> &'static str {
use antigen::scan::RecurrentKind as K;
match kind {
K::Itch => "itch",
K::RecurrenceAnchor => "recurrence_anchor",
K::Crystallize => "crystallize",
K::Chronic => "chronic",
K::Saturate => "saturate",
K::Strand => "strand",
}
}
fn audit_hint_kebab(hint: &audit::AuditHint) -> String {
serde_json::to_value(hint)
.ok()
.and_then(|v| v.as_str().map(str::to_owned))
.unwrap_or_else(|| format!("{hint:?}"))
}
const MAX_FINGERPRINT_MATCHES_PER_ANTIGEN: usize = 10;
fn print_fingerprint_matches(report: &scan::ScanReport) {
use antigen::scan::MatchKind;
use std::collections::BTreeMap;
let fp_matches: Vec<_> = report
.presentations
.iter()
.filter(|p| p.match_kind == MatchKind::FingerprintMatch)
.collect();
if fp_matches.is_empty() {
return;
}
let mut by_antigen: BTreeMap<&str, Vec<&&scan::Presentation>> = BTreeMap::new();
for p in &fp_matches {
by_antigen
.entry(p.antigen_type.as_str())
.or_default()
.push(p);
}
println!(
"{} fingerprint match(es) across {} antigen type(s) — candidate sites \
(expected noise; the witness layer refines them, per the filter/proof split). \
Not a TODO list.",
fp_matches.len(),
by_antigen.len()
);
println!();
for (antigen_type, sites) in &by_antigen {
for p in sites.iter().take(MAX_FINGERPRINT_MATCHES_PER_ANTIGEN) {
println!(
" {}:{} {} on {} [fingerprint match]",
p.file.display(),
p.line,
p.antigen_type,
p.item_kind
);
}
if let Some(extra) = sites
.len()
.checked_sub(MAX_FINGERPRINT_MATCHES_PER_ANTIGEN)
.filter(|n| *n > 0)
{
println!(
" … +{extra} more `{antigen_type}` candidate(s) — `cargo antigen scan \
--format json` for the full list."
);
}
}
println!();
println!(
" These are CANDIDATES, not failures. If a site genuinely presents the \
failure-class, acknowledge it:"
);
println!(" #[presents(<antigen>)] to mark the site explicitly,");
println!(" then defend it: #[defended_by(<antigen>)] on a test (code-tier), or");
println!(" #[presents(<antigen>, requires = ...)] for substrate-witness evidence,");
println!(" #[antigen_tolerance(<antigen>, rationale = \"...\")] to document intent.");
println!();
}
fn print_orphaned_tolerances(report: &scan::ScanReport) {
let orphans = report.orphaned_tolerances();
if orphans.is_empty() {
return;
}
println!(
"{} orphaned tolerance(s) — antigen no longer declared in workspace:",
orphans.len()
);
println!();
for t in &orphans {
println!(
" {}:{} {} [tolerance for unknown antigen]",
t.file.display(),
t.line,
t.antigen_type
);
}
println!();
println!(" Remove or update these tolerances — the antigen they suppress is gone.");
println!();
}
fn print_unaddressed(unaddressed: &[scan::UnaddressedPresentation]) {
use antigen::scan::MatchKind;
let explicit_unaddressed: Vec<_> = unaddressed
.iter()
.filter(|u| u.presentation.match_kind == MatchKind::ExplicitMarker)
.collect();
if explicit_unaddressed.is_empty() {
println!("All explicit presentations are addressed.");
return;
}
println!(
"{} unaddressed explicit presentation(s):",
explicit_unaddressed.len()
);
println!();
for u in &explicit_unaddressed {
let p = &u.presentation;
println!(
" {}:{} {} on {}",
p.file.display(),
p.line,
p.antigen_type,
p.item_kind
);
if !u.antigen_known {
println!(
" note: antigen `{}` was not declared in the scanned workspace",
p.antigen_type
);
}
}
println!();
println!("To address each site, use the antigen type shown above:");
println!(" #[defended_by(<antigen>)] on a test that exercises the defense (code-tier),");
println!(
" OR #[presents(<antigen>, requires = ...)] on the site for substrate-witness evidence,"
);
println!(" OR #[antigen_tolerance(<antigen>, rationale = \"...\")] to document intent.");
}
#[derive(serde::Serialize)]
struct ReportProvenance {
antigen_version: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
git_sha: Option<String>,
generated_at: String,
report_schema_version: u32,
}
const REPORT_SCHEMA_VERSION: u32 = 1;
impl ReportProvenance {
fn gather(root: &Path) -> Self {
Self {
antigen_version: env!("CARGO_PKG_VERSION"),
git_sha: git_head_sha(root),
generated_at: chrono::Utc::now().to_rfc3339(),
report_schema_version: REPORT_SCHEMA_VERSION,
}
}
}
fn git_head_sha(dir: &Path) -> Option<String> {
let out = std::process::Command::new("git")
.args(["rev-parse", "HEAD"])
.current_dir(dir)
.output()
.ok()?;
if !out.status.success() {
return None;
}
let sha = String::from_utf8_lossy(&out.stdout).trim().to_owned();
if sha.is_empty() {
None
} else {
Some(sha)
}
}
#[derive(serde::Serialize)]
struct ReportEnvelope<T> {
#[serde(flatten)]
provenance: ReportProvenance,
#[serde(flatten)]
payload: T,
}
impl<T: serde::Serialize> ReportEnvelope<T> {
fn new(root: &Path, payload: T) -> Self {
Self {
provenance: ReportProvenance::gather(root),
payload,
}
}
}
fn write_report_render<T: serde::Serialize>(path: &Path, report: &T) -> Result<(), String> {
let json =
serde_json::to_string_pretty(report).map_err(|e| format!("failed to serialize: {e}"))?;
std::fs::write(path, json).map_err(|e| format!("failed to write {}: {e}", path.display()))?;
eprintln!("Wrote report render to {}", path.display());
Ok(())
}
#[derive(serde::Serialize)]
struct JsonReport<'a> {
report: &'a scan::ScanReport,
unaddressed: &'a [scan::UnaddressedPresentation],
orphaned_lineage_edges: Vec<&'a scan::LineageEdge>,
dangling_child_lineage_edges: Vec<&'a scan::LineageEdge>,
#[serde(skip_serializing_if = "Option::is_none")]
dep_reports: Option<&'a [DepScanResult]>,
}
#[derive(serde::Serialize)]
struct DepScanResult {
package_name: String,
version: String,
origin: scan::CrateOrigin,
report: scan::ScanReport,
}
fn run_new(name: String) -> ExitCode {
eprintln!(
"cargo-antigen new {} — design phase\n\
\n\
Antigen scaffolding (interactive scaffolding for a new declaration) is\n\
under design. The eventual command will:\n\
- Prompt for family (one of the 8 first-principles classes or custom)\n\
- Prompt for fingerprint (assist with structural pattern composition)\n\
- Prompt for witness type (test, proptest, lint, formal-verification)\n\
- Generate a starter declaration file in your project's antigen module\n\
\n\
For now, please write antigen declarations by hand. See the project's\n\
docs/expedition/conventions.md for naming guidance.\n",
name
);
ExitCode::FAILURE
}
fn run_vaccinate(antigen: String, pattern: String) -> ExitCode {
eprintln!(
"cargo-antigen vaccinate {} {} — design phase\n\
\n\
Vaccination (apply known immunity pattern across a structural family) is\n\
under design. The eventual command will:\n\
- Search the workspace for items matching `pattern`\n\
- For each match without existing immunity, scaffold a witness stub\n\
- Add #[presents] and #[immune] markers atomically with confirmation\n\
\n\
For now, apply immunity manually per site.\n",
antigen, pattern
);
ExitCode::FAILURE
}
#[derive(serde::Serialize)]
struct FingerprintMatch {
antigen_type: String,
item_path: String,
site_kind: &'static str,
structural_fingerprint: String,
file: String,
line: usize,
}
fn run_fingerprint(args: FingerprintArgs) -> ExitCode {
if !args.root.exists() {
eprintln!("error: path does not exist: {}", args.root.display());
return ExitCode::from(2);
}
let report = match scan::scan_workspace(&args.root, None) {
Ok(r) => r,
Err(e) => {
eprintln!("error: scan failed: {e}");
return ExitCode::from(2);
}
};
let antigen_filter = args.antigen.as_deref();
let item_filter = args.item_path.as_deref();
let keep = |antigen_type: &str, target: &scan::ItemTarget| {
antigen_filter.is_none_or(|a| antigen_type == a)
&& item_filter.is_none_or(|p| target.label() == p)
};
let mut matches: Vec<FingerprintMatch> = report
.immunities
.iter()
.filter(|i| keep(&i.antigen_type, &i.item_target))
.map(|i| FingerprintMatch {
antigen_type: i.antigen_type.clone(),
item_path: i.item_target.label(),
site_kind: "immune",
structural_fingerprint: i.structural_fingerprint.clone(),
file: i.file.display().to_string(),
line: i.line,
})
.collect();
matches.extend(
report
.presentations
.iter()
.filter(|p| keep(&p.antigen_type, &p.item_target))
.filter(|p| !p.structural_fingerprint.is_empty())
.map(|p| FingerprintMatch {
antigen_type: p.antigen_type.clone(),
item_path: p.item_target.label(),
site_kind: "presents",
structural_fingerprint: p.structural_fingerprint.clone(),
file: p.file.display().to_string(),
line: p.line,
}),
);
match args.format {
OutputFormat::Json => match serde_json::to_string_pretty(&matches) {
Ok(s) => println!("{s}"),
Err(e) => {
eprintln!("error: failed to serialize fingerprints: {e}");
return ExitCode::from(2);
}
},
OutputFormat::Human => {
if matches.is_empty() {
let scope = match (antigen_filter, item_filter) {
(Some(a), Some(p)) => format!(" for antigen `{a}` at item `{p}`"),
(Some(a), None) => format!(" for antigen `{a}`"),
(None, Some(p)) => format!(" at item `{p}`"),
(None, None) => String::new(),
};
eprintln!(
"no immune/presents site with an obtainable fingerprint found{scope} \
under {}.",
args.root.display()
);
} else {
for m in &matches {
println!(
"{} {} [{}] on {} {}:{}",
m.structural_fingerprint,
m.antigen_type,
m.site_kind,
m.item_path,
m.file,
m.line
);
}
}
}
}
if matches.is_empty() && (antigen_filter.is_some() || item_filter.is_some()) {
ExitCode::from(1)
} else {
ExitCode::SUCCESS
}
}
fn run_audit(args: AuditArgs) -> ExitCode {
if !args.root.exists() {
eprintln!("error: path does not exist: {}", args.root.display());
return ExitCode::from(2);
}
if !args.root.is_dir() {
eprintln!(
"error: expected a directory, got a file: {}",
args.root.display()
);
return ExitCode::from(2);
}
let Ok(category_filter) = parse_category_filter(args.category.as_deref()) else {
return ExitCode::from(2);
};
let scan_result = if args.workspace {
eprintln!("Auditing workspace (member-aware): {}", args.root.display());
scan::scan_workspace_multi_crate(&args.root)
} else {
eprintln!("Auditing workspace: {}", args.root.display());
scan::scan_workspace(&args.root, None)
};
let mut scan_report = match scan_result {
Ok(r) => r,
Err(e) => {
eprintln!("error: scan failed: {e}");
return ExitCode::from(2);
}
};
if let Some(cat) = category_filter {
filter_report_by_category(&mut scan_report, cat);
}
let audit::orchestrate::AuditBundle {
audit: audit_report,
category: category_report,
deferred: deferred_report,
convergent: convergent_report,
recurrent: recurrent_report,
lineage_fidelity: lineage_fidelity_report,
coverage: coverage_report,
prescriptive: prescriptive_report,
} = audit::orchestrate::run(&scan_report, &args.root);
let enveloped = ReportEnvelope::new(
&args.root,
JsonAuditReport {
scan: &scan_report,
audit: &audit_report,
category: &category_report,
deferred_defense_audit: &deferred_report,
convergent_evidence_audit: &convergent_report,
recurrent_audit: &recurrent_report,
lineage_fidelity_audit: &lineage_fidelity_report,
coverage_audit: &coverage_report,
prescriptive_audit: &prescriptive_report,
},
);
if let Some(path) = args.output.as_ref() {
if let Err(e) = write_report_render(path, &enveloped) {
eprintln!("error: {e}");
return ExitCode::from(2);
}
}
match args.format {
OutputFormat::Human => {
print_audit_human(&scan_report, &audit_report);
print_deferred_defenses_loud(&deferred_report);
print_convergent_evidence_concerns(&convergent_report);
print_recurrent_concerns(&recurrent_report);
print_lineage_fidelity_advisory(&lineage_fidelity_report);
print_category_audit_human(&category_report);
print_coverage_frontier(&coverage_report);
print_prescriptive_board(&prescriptive_report);
}
OutputFormat::Json => match serde_json::to_string_pretty(&enveloped) {
Ok(s) => println!("{s}"),
Err(e) => {
eprintln!("error: failed to serialize report: {e}");
return ExitCode::from(2);
}
},
}
let strict_state7_fails = args.strict && !audit_report.inherited_unaddressed.is_empty();
let strict_witness_fails =
args.strict && !audit_report.all_meet_tier(audit::WitnessTier::Reachability);
let strict_undefended_fails = args.strict && !audit_report.undefended_verdicts().is_empty();
let strict_orphaned_tolerances = args.strict && !scan_report.orphaned_tolerances().is_empty();
let strict_lineage_broken = args.strict
&& (!scan_report.orphaned_lineage_edges().is_empty()
|| !scan_report.dangling_child_lineage_edges().is_empty());
if strict_state7_fails
|| strict_witness_fails
|| strict_undefended_fails
|| strict_orphaned_tolerances
|| strict_lineage_broken
{
ExitCode::from(1)
} else {
ExitCode::SUCCESS
}
}
fn run_attest(cli: AttestCli) -> ExitCode {
match cli.command {
AttestSubcommand::Scaffold(args) => {
run_attest_scaffold(args, antigen_attestation::RatificationKind::Immunity)
}
AttestSubcommand::Sign(args) => run_attest_sign(args),
AttestSubcommand::Check(args) => run_attest_check(args),
AttestSubcommand::Delta(args) => run_attest_delta(args),
AttestSubcommand::List(args) => run_attest_list(args),
AttestSubcommand::Gc(args) => run_attest_gc(args),
AttestSubcommand::Oracle => {
eprintln!(
"`attest oracle mark` is not yet implemented in v0.1-rc.\n\
Renamed from `attest oracle complete` per ADR-021 F28-R2 to \
disambiguate from the top-level `cargo antigen oracle complete` \
state-machine verb. Implementation pending: must write \
`OracleCompletionMarker {{ oracle_state_at_attestation, .. }}` \
at sign time per ADR-021 §D4 + ATK-021-19 (without the \
sign-time-state anchor, sign-time-validity cannot be \
structurally enforced — the audit cannot distinguish post-\
sign-time deprecation from pre-sign-time deprecation).\n\
Operator scripts MUST NOT rely on this exit code as success."
);
ExitCode::FAILURE
}
}
}
fn run_tolerate(cli: TolerateCli) -> ExitCode {
match cli.command {
TolerateSubcommand::Scaffold(args) => {
run_attest_scaffold(args, antigen_attestation::RatificationKind::Tolerance)
}
TolerateSubcommand::Sign(args) => run_attest_sign(args),
TolerateSubcommand::Check(args) => run_attest_check(args),
TolerateSubcommand::List(mut args) => {
args.tolerance_only = true;
run_attest_list(args)
}
}
}
fn autofill_fingerprint(
source_file: &Path,
antigen_stem: &str,
item_path: &str,
) -> Result<Option<String>, String> {
let scan_root = source_file.parent().unwrap_or_else(|| Path::new("."));
let Ok(report) = scan::scan_workspace(scan_root, None) else {
return Ok(None);
};
let item_filter = (!item_path.is_empty()).then_some(item_path);
let matches = |antigen_type: &str, target: &scan::ItemTarget| {
antigen_type == antigen_stem && item_filter.is_none_or(|p| target.label() == p)
};
let mut fingerprints: Vec<String> = report
.immunities
.iter()
.filter(|i| matches(&i.antigen_type, &i.item_target))
.map(|i| i.structural_fingerprint.clone())
.filter(|fp| !fp.is_empty())
.collect();
if fingerprints.is_empty() {
fingerprints = report
.presentations
.iter()
.filter(|p| matches(&p.antigen_type, &p.item_target))
.map(|p| p.structural_fingerprint.clone())
.filter(|fp| !fp.is_empty())
.collect();
}
fingerprints.sort();
fingerprints.dedup();
match fingerprints.as_slice() {
[] => Ok(None),
[single] => Ok(Some(single.clone())),
many => Err(format!(
"auto-fill found {} distinct fingerprints for antigen `{antigen_stem}`\
{} — pass `--item-path` to disambiguate, or `--fingerprint` explicitly.",
many.len(),
if item_filter.is_some() {
format!(" at item `{item_path}`")
} else {
String::new()
}
)),
}
}
fn run_attest_scaffold(
args: AttestScaffoldArgs,
kind_override: antigen_attestation::RatificationKind,
) -> ExitCode {
use antigen_attestation::{
AntigenIdentifier, ItemRatification, Ratification, RatificationKind, SchemaVersion,
};
use std::collections::BTreeMap;
let kind = if matches!(kind_override, RatificationKind::Immunity) {
args.kind.into()
} else {
kind_override
};
let stem = args.antigen.rsplit("::").next().unwrap_or(&args.antigen);
let Some(source_dir_ref) = args.source_file.parent() else {
eprintln!("error: source-file has no parent directory");
return ExitCode::from(2);
};
let source_dir = source_dir_ref.to_path_buf();
let attest_dir = source_dir.join(".attest");
let sidecar_path = attest_dir.join(format!("{stem}.json"));
if sidecar_path.exists() && !args.force {
eprintln!(
"error: sidecar already exists: {}\n\
Use --force to overwrite.",
sidecar_path.display()
);
return ExitCode::from(1);
}
if let Err(e) = std::fs::create_dir_all(&attest_dir) {
eprintln!("error: failed to create .attest/ directory: {e}");
return ExitCode::from(2);
}
let mut fingerprint = args.fingerprint.clone();
let mut autofilled = false;
if fingerprint.is_empty() {
match autofill_fingerprint(&args.source_file, stem, &args.item_path) {
Ok(Some(fp)) => {
fingerprint = fp;
autofilled = true;
}
Ok(None) => {}
Err(reason) => {
eprintln!("error: {reason}");
return ExitCode::from(1);
}
}
}
if !autofilled {
warn_if_empty_fingerprint(&fingerprint);
}
let ratification = Ratification {
schema_version: SchemaVersion::V1,
kind,
antigen: AntigenIdentifier {
name: stem.to_string(),
defined_in: None,
},
source_file: args.source_file.clone(),
items: vec![ItemRatification {
item_path: args.item_path.clone(),
current_fingerprint: fingerprint.clone(),
doc_ref: None,
signers: vec![],
oracles: vec![],
fresh_through: None,
extensions: BTreeMap::new(),
}],
};
let json = match serde_json::to_string_pretty(&ratification) {
Ok(s) => s,
Err(e) => {
eprintln!("error: failed to serialize sidecar: {e}");
return ExitCode::from(2);
}
};
if let Err(e) = std::fs::write(&sidecar_path, &json) {
eprintln!("error: failed to write sidecar: {e}");
return ExitCode::from(2);
}
eprintln!("Created sidecar: {}", sidecar_path.display());
if autofilled {
eprintln!(" fingerprint auto-filled from scan: {fingerprint}");
} else if fingerprint.is_empty() {
eprintln!(
" note: fingerprint is empty — auto-fill found no matching scanned site, so \
update `current_fingerprint` before signing.\n\
\n\
Get the item fingerprint from:\n\
\n cargo antigen scan --format json | jq '.report.immunities[] | select(.antigen_type==\"{}\") | .structural_fingerprint'\n",
stem
);
}
let next_fp = if fingerprint.is_empty() {
"<fp>".to_string()
} else {
fingerprint
};
eprintln!(
"\nNext: `cargo antigen attest sign --sidecar {} --item-path \"{}\" --signer <name> --fingerprint {next_fp}`",
sidecar_path.display(),
args.item_path
);
ExitCode::SUCCESS
}
fn looks_like_structural_digest(fingerprint: &str) -> bool {
let Some(hex) = fingerprint.strip_prefix("fnv1a64:") else {
return false;
};
hex.len() == 16
&& hex
.bytes()
.all(|b| b.is_ascii_hexdigit() && !b.is_ascii_uppercase())
}
fn warn_if_empty_fingerprint(fingerprint: &str) {
if fingerprint.is_empty() {
eprintln!(
"warning: signing against an empty fingerprint. A substrate-witness \
predicate bound to `against = \"current\"` (or `fresh_within_days`) \
will fail at audit time because the signed-against fingerprint `` \
cannot match the item's real structural digest. Obtain the item's \
fingerprint from `cargo antigen scan --format json` (the \
`structural_fingerprint` field on the immunity/presentation entry) \
and pass it via `--fingerprint`."
);
} else if !looks_like_structural_digest(fingerprint) {
eprintln!(
"warning: `--fingerprint {fingerprint}` does not look like a structural \
digest (expected `fnv1a64:` + 16 lowercase-hex chars). It will be \
recorded as the signed-against fingerprint, but a substrate-witness \
predicate bound to `against = \"current\"` will never match the item's \
real digest at audit time — the signature is dead-on-arrival. Obtain \
the digest from `cargo antigen fingerprint` (or `scan --format json`) \
and pass that value."
);
}
}
fn run_attest_sign(args: AttestSignArgs) -> ExitCode {
use antigen_attestation::{Ratification, Signer, SignerBasis};
let content = match std::fs::read_to_string(&args.sidecar) {
Ok(c) => c,
Err(e) => {
eprintln!(
"error: failed to read sidecar {}: {e}",
args.sidecar.display()
);
return ExitCode::from(2);
}
};
let mut ratification: Ratification = match serde_json::from_str(&content) {
Ok(r) => r,
Err(e) => {
eprintln!("error: sidecar is not valid JSON (Ratification schema): {e}");
return ExitCode::from(2);
}
};
let item = ratification
.items
.iter_mut()
.find(|i| i.item_path == args.item_path);
let Some(item) = item else {
eprintln!(
"error: no item with path `{}` in sidecar.\n\
Available item paths: {}",
args.item_path,
ratification
.items
.iter()
.map(|i| i.item_path.as_str())
.collect::<Vec<_>>()
.join(", ")
);
return ExitCode::from(1);
};
let already_signed = item
.signers
.iter()
.any(|s| s.name == args.signer && s.signed_against_fingerprint == args.fingerprint);
if already_signed {
eprintln!(
"warning: signer `{}` has already signed this item against fingerprint `{}`.\n\
No entry added.",
args.signer, args.fingerprint
);
return ExitCode::SUCCESS;
}
warn_if_empty_fingerprint(&args.fingerprint);
if !item.current_fingerprint.is_empty() && args.fingerprint != item.current_fingerprint {
eprintln!(
"warning: --fingerprint `{}` does not match sidecar's stored \
current_fingerprint `{}`. The new signer entry will be immediately \
stale at audit time. Update the sidecar's current_fingerprint first, \
or re-run `cargo antigen scan --format json` to get the current fingerprint.",
args.fingerprint, item.current_fingerprint
);
}
let today = chrono::Local::now().date_naive();
item.signers.push(Signer {
name: args.signer.clone(),
role: args.role.clone(),
date: today,
signed_against_fingerprint: args.fingerprint.clone(),
basis: SignerBasis::Fresh {
reasoning: args.reasoning.clone(),
},
strength: antigen_attestation::SignatureStrength::from(args.strength),
signature: None,
});
let json = match serde_json::to_string_pretty(&ratification) {
Ok(s) => s,
Err(e) => {
eprintln!("error: failed to serialize updated sidecar: {e}");
return ExitCode::from(2);
}
};
if let Err(e) = std::fs::write(&args.sidecar, &json) {
eprintln!("error: failed to write updated sidecar: {e}");
return ExitCode::from(2);
}
eprintln!(
"Signed: {} added to `{}` item `{}` against fingerprint `{}`",
args.signer,
args.sidecar.display(),
args.item_path,
args.fingerprint
);
if let Some(role) = &args.role {
eprintln!(" role: {role}");
}
ExitCode::SUCCESS
}
fn run_attest_delta(args: AttestDeltaArgs) -> ExitCode {
use antigen_attestation::schema::HARD_DELTA_CHAIN_CAP_MAX;
use antigen_attestation::{Ratification, Signer, SignerBasis};
const DEFAULT_DELTA_CAP: u32 = 3;
let content = match std::fs::read_to_string(&args.sidecar) {
Ok(s) => s,
Err(e) => {
eprintln!(
"error: could not read sidecar `{}`: {e}",
args.sidecar.display()
);
return ExitCode::from(2);
}
};
let mut ratification: Ratification = match serde_json::from_str(&content) {
Ok(r) => r,
Err(e) => {
eprintln!("error: sidecar is not valid Ratification JSON: {e}");
return ExitCode::from(2);
}
};
let signer_name = match resolve_steward_name(args.signer.as_deref()) {
Ok(n) => n,
Err(e) => {
eprintln!("{e}");
return ExitCode::from(1);
}
};
if let Err(code) = validate_delta_rationale(&args.rationale) {
return code;
}
let item = ratification
.items
.iter_mut()
.find(|i| i.item_path == args.item_path);
let Some(item) = item else {
eprintln!(
"error: no item with path `{}` in sidecar `{}`",
args.item_path,
args.sidecar.display()
);
return ExitCode::from(1);
};
let cumulative_root = item
.signers
.iter()
.rfind(|s| s.name == signer_name && s.basis.is_fresh())
.map(|s| s.signed_against_fingerprint.clone());
let Some(cumulative_root_fingerprint) = cumulative_root else {
eprintln!(
"error: no prior Fresh-basis signature found for signer `{signer_name}` \
at item `{}`. Run `attest sign` first to establish a fresh attestation \
before using `attest delta`.",
args.item_path
);
return ExitCode::from(1);
};
let chain_depth = compute_delta_chain_depth(&item.signers, &signer_name);
if chain_depth > DEFAULT_DELTA_CAP {
eprintln!(
"error: delta chain depth {chain_depth} exceeds default cap \
{DEFAULT_DELTA_CAP} (hard max = {HARD_DELTA_CHAIN_CAP_MAX}). \
Run `attest sign` to re-anchor with a Fresh basis."
);
return ExitCode::from(1);
}
warn_if_empty_fingerprint(&args.fingerprint);
warn_if_empty_fingerprint(&args.prior_fingerprint);
let today = chrono::Local::now().date_naive();
item.signers.push(Signer {
name: signer_name.clone(),
role: args.role,
date: today,
signed_against_fingerprint: args.fingerprint.clone(),
basis: SignerBasis::DeltaFrom {
prior_fingerprint: args.prior_fingerprint.clone(),
cumulative_root_fingerprint,
chain_depth,
rationale: args.rationale.clone(),
},
strength: antigen_attestation::SignatureStrength::from(args.strength),
signature: None,
});
let json = match serde_json::to_string_pretty(&ratification) {
Ok(s) => s,
Err(e) => {
eprintln!("error: failed to serialize updated sidecar: {e}");
return ExitCode::from(2);
}
};
if let Err(e) = std::fs::write(&args.sidecar, &json) {
eprintln!("error: failed to write sidecar: {e}");
return ExitCode::from(2);
}
eprintln!(
"Delta: `{}` signed `{}` item `{}` at depth {chain_depth}",
signer_name,
args.sidecar.display(),
args.item_path,
);
ExitCode::SUCCESS
}
fn run_attest_list(args: AttestListArgs) -> ExitCode {
use antigen_attestation::Ratification;
let sidecars = collect_sidecars(&args.root);
if sidecars.is_empty() {
eprintln!(
"No .attest/ sidecars found under `{}`.",
args.root.display()
);
return ExitCode::SUCCESS;
}
let mut printed = 0usize;
for path in &sidecars {
let content = match std::fs::read_to_string(path) {
Ok(s) => s,
Err(e) => {
eprintln!("warning: could not read `{}`: {e}", path.display());
continue;
}
};
let rat: Ratification = match serde_json::from_str(&content) {
Ok(r) => r,
Err(e) => {
eprintln!(
"warning: `{}` is not valid Ratification JSON: {e}",
path.display()
);
continue;
}
};
if args.tolerance_only && rat.kind != antigen_attestation::RatificationKind::Tolerance {
continue;
}
match args.format {
OutputFormat::Human => {
println!(
"{} [{:?}] — {} item(s)",
path.display(),
rat.kind,
rat.items.len()
);
for item in &rat.items {
println!(" {} ({} signer(s))", item.item_path, item.signers.len());
}
}
OutputFormat::Json => {
let obj = serde_json::json!({
"path": path.display().to_string(),
"kind": format!("{:?}", rat.kind),
"antigen": rat.antigen.name,
"item_count": rat.items.len(),
});
println!("{obj}");
}
}
printed += 1;
}
if args.orphan_scan {
eprintln!("\n-- Orphan scan (--orphan-scan): comparing sidecar item_paths against source macros --");
eprintln!("(Note: full bidirectional scan requires `cargo antigen scan` integration; v0.2 adds gc bidirectional traversal)");
for path in &sidecars {
let Ok(content) = std::fs::read_to_string(path) else {
continue;
};
if let Ok(rat) = serde_json::from_str::<Ratification>(&content) {
for item in &rat.items {
if item.signers.is_empty() {
println!(
"ORPHAN-CANDIDATE: {} item `{}` has no signers",
path.display(),
item.item_path
);
}
}
}
}
}
eprintln!("{printed} sidecar(s) listed.");
ExitCode::SUCCESS
}
fn run_attest_gc(args: AttestGcArgs) -> ExitCode {
use antigen_attestation::Ratification;
let sidecars = collect_sidecars(&args.root);
let mut orphans: Vec<std::path::PathBuf> = Vec::new();
for path in &sidecars {
let content = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(e) => {
eprintln!("warning: could not read `{}`: {e}", path.display());
continue;
}
};
let rat: Ratification = match serde_json::from_str(&content) {
Ok(r) => r,
Err(e) => {
eprintln!(
"warning: `{}` is not valid Ratification JSON: {e} \
(corrupt sidecar — cannot evaluate for gc; surfaced rather than \
silently skipped)",
path.display()
);
continue;
}
};
let source = args.root.join(&rat.source_file);
if !source.exists() {
orphans.push(path.clone());
}
}
if orphans.is_empty() {
eprintln!(
"No orphaned sidecars found under `{}`.",
args.root.display()
);
return ExitCode::SUCCESS;
}
eprintln!("{} orphaned sidecar(s) found:", orphans.len());
for path in &orphans {
println!("{}", path.display());
}
if args.force {
let mut removed = 0usize;
for path in &orphans {
match std::fs::remove_file(path) {
Ok(()) => {
eprintln!("Removed: {}", path.display());
removed += 1;
}
Err(e) => eprintln!("error removing `{}`: {e}", path.display()),
}
}
eprintln!("{removed} sidecar(s) removed.");
} else {
eprintln!("(Run with --force to delete. Report-only in v0.1.)");
}
ExitCode::SUCCESS
}
fn collect_sidecars(root: &std::path::Path) -> Vec<std::path::PathBuf> {
let mut result = Vec::new();
let Ok(entries) = std::fs::read_dir(root) else {
return result;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
if path
.file_name()
.is_some_and(|n| n.to_string_lossy().ends_with(".attest"))
{
if let Ok(inner) = std::fs::read_dir(&path) {
for inner_entry in inner.flatten() {
let inner_path = inner_entry.path();
if inner_path.extension().is_some_and(|e| e == "json") {
result.push(inner_path);
}
}
}
} else if path.file_name().is_none_or(|n| {
let s = n.to_string_lossy();
!s.starts_with('.') && s != "target"
}) {
result.extend(collect_sidecars(&path));
}
}
}
result
}
struct CheckContext;
impl antigen_attestation::EvaluationContext for CheckContext {
fn today(&self) -> chrono::NaiveDate {
chrono::Local::now().date_naive()
}
fn read_doc(&self, path: &std::path::Path) -> Option<String> {
std::fs::read_to_string(path).ok()
}
fn read_oracle(&self, path: &std::path::Path) -> Option<String> {
std::fs::read_to_string(path).ok()
}
fn read_git_trailers(
&self,
item_source_file: &std::path::Path,
_item_path: &str,
) -> Vec<String> {
let log_output = std::process::Command::new("git")
.args([
"log",
"--format=%B",
"--",
item_source_file.to_str().unwrap_or(""),
])
.output();
let log_bytes = match log_output {
Ok(o) if o.status.success() => o.stdout,
_ => return Vec::new(),
};
let trailer_output = std::process::Command::new("git")
.args(["interpret-trailers", "--parse"])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.spawn()
.and_then(|mut child| {
use std::io::Write;
if let Some(stdin) = child.stdin.take() {
let mut stdin = stdin;
let _ = stdin.write_all(&log_bytes);
}
child.wait_with_output()
});
match trailer_output {
Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout)
.lines()
.map(|l| l.trim().to_owned())
.filter(|l| !l.is_empty())
.collect(),
_ => Vec::new(),
}
}
}
fn run_attest_check(args: AttestCheckArgs) -> ExitCode {
use antigen_attestation::{evaluate::evaluate_predicate_with_kind, Predicate, Ratification};
let content = match std::fs::read_to_string(&args.sidecar) {
Ok(c) => c,
Err(e) => {
eprintln!(
"error: failed to read sidecar {}: {e}",
args.sidecar.display()
);
return ExitCode::from(2);
}
};
let ratification: Ratification = match serde_json::from_str(&content) {
Ok(r) => r,
Err(e) => {
eprintln!("error: sidecar schema invalid: {e}");
return ExitCode::from(2);
}
};
let predicate: Predicate = match serde_json::from_str(&args.predicate) {
Ok(p) => p,
Err(e) => {
eprintln!("error: predicate JSON invalid: {e}");
return ExitCode::from(2);
}
};
let item = if let Some(ref path) = args.item_path {
if let Some(i) = ratification.items.iter().find(|i| i.item_path == *path) {
i
} else {
eprintln!("error: no item with path `{path}` in sidecar.");
return ExitCode::from(1);
}
} else if let Some(i) = ratification.items.first() {
i
} else {
eprintln!("error: sidecar has no items.");
return ExitCode::from(1);
};
if args.fingerprint.is_none() {
eprintln!(
"warning: --fingerprint not supplied; using sidecar's stored \
current_fingerprint for stale-signer detection.\n\
If the item's code has changed since the sidecar was written, stale \
signers will appear current. Supply --fingerprint from \
`cargo antigen scan --format json` for accurate stale detection.\n\
(CLI-SF-1: self-referential fingerprint cannot detect real staleness)"
);
} else if let Some(fp) = args.fingerprint.as_deref() {
warn_if_empty_fingerprint(fp);
}
let current_fingerprint = args
.fingerprint
.as_deref()
.unwrap_or(&item.current_fingerprint);
let result = match evaluate_predicate_with_kind(
&predicate,
item,
current_fingerprint,
&args.sidecar,
ratification.kind,
&CheckContext,
) {
Ok(r) => r,
Err(e) => {
eprintln!("error: evaluation error: {e}");
return ExitCode::from(2);
}
};
eprintln!("Sidecar: {}", args.sidecar.display());
eprintln!("Item path: {}", item.item_path);
eprintln!("Kind: {:?}", ratification.kind);
eprintln!();
eprintln!("Result:");
eprintln!(" witness_tier: {:?}", result.witness_tier);
eprintln!(" audit_hint: {:?}", result.audit_hint);
eprintln!(" evidence_kind: {:?}", result.evidence_kind);
eprintln!(" signature_strength:{:?}", result.signature_strength);
if !result.leaf_outcomes.is_empty() {
eprintln!();
eprintln!("Per-leaf:");
for leaf in &result.leaf_outcomes {
let mark = if leaf.passed { "PASS" } else { "FAIL" };
eprintln!(" {}: {} — {}", leaf.label, mark, leaf.reason);
}
}
if result.witness_tier == antigen_attestation::WitnessTier::None {
ExitCode::from(1)
} else {
ExitCode::SUCCESS
}
}
#[derive(serde::Serialize)]
struct JsonAuditReport<'a> {
scan: &'a scan::ScanReport,
audit: &'a audit::AuditReport,
category: &'a audit::CategoryAuditReport,
deferred_defense_audit: &'a audit::DeferredDefenseAuditReport,
convergent_evidence_audit: &'a audit::ConvergentEvidenceAuditReport,
recurrent_audit: &'a audit::RecurrentAuditReport,
lineage_fidelity_audit: &'a audit::LineageFidelityAuditReport,
coverage_audit: &'a audit::CoverageAuditReport,
prescriptive_audit: &'a audit::PrescriptiveAuditReport,
}
fn print_audit_human(scan_report: &scan::ScanReport, audit_report: &audit::AuditReport) {
println!();
print_audit_summary(audit_report);
println!();
print_confirmed_immunity_claims(audit_report);
let problematic = audit_report.problematic_audits();
if problematic.is_empty() {
println!("✓ All immunity claims meet the Execution tier or higher.");
println!(
" Note: semantic verification (does the witness actually test this failure class?)"
);
println!(" requires fingerprint-aware audit, planned for Sweep A4-A5.");
if scan_report.immunities.is_empty() {
println!(" (No immunity declarations found in the workspace.)");
}
} else {
println!(
"⚠ {} immunity claim(s) below Execution tier:",
problematic.len()
);
println!();
for a in &problematic {
let i = &a.immunity;
println!(
" {}:{} {} (witness = `{}`)",
i.file.display(),
i.line,
i.antigen_type,
i.witness
);
println!(" tier = {:?}, hint = {:?}", a.witness_tier, a.audit_hint);
for leaf in &a.leaf_outcomes {
let mark = if !leaf.evaluated {
"NOT-EVALUATED"
} else if leaf.passed {
"PASS"
} else {
"FAIL"
};
println!(" {}: {} — {}", leaf.label, mark, leaf.reason);
}
match &a.witness_status {
audit::WitnessStatus::NotFound { reason } => {
println!(" → broken: {reason}");
}
audit::WitnessStatus::Missing => {
println!(
" → missing: declaration has no witness identifier; \
a marker without proof is not a claim (per ADR-005)"
);
}
audit::WitnessStatus::Ambiguous { candidates } => {
println!(
" → ambiguous: witness name matches {} workspace functions",
candidates.len(),
);
for c in candidates {
println!(" - {}", c.display());
}
println!(
" Fix: rename one of the colliding functions, or \
qualify the witness path"
);
}
audit::WitnessStatus::External { tool_hint } => {
println!(
" → external ({tool_hint}): tool prefix recognized but not invoked. \
A3+ will run the tool to promote this witness to Execution tier."
);
}
audit::WitnessStatus::Resolved { .. } => {
}
}
if a.code_witness_sidecar_ignored {
println!(
" → sidecar ignored: a `.attest/` substrate-witness sidecar exists \
for this antigen, but this site uses `witness = ...`, not \
`requires = ...`. Substrate-witness sidecars are credited only for \
`requires =` immunities, so the sidecar can never be counted here. \
Either switch this site to `requires = <predicate>` to use the \
sidecar, or remove the orphan sidecar."
);
}
}
println!();
println!(
"Resolve below-Execution claims by either:\n \
a) Adding test invocation that exercises the witness path (A4-A5 feature)\n \
b) Pointing the witness at a runnable test (#[test] without #[ignore])\n \
c) Renaming colliding functions or qualifying ambiguous witness paths\n \
d) Adding the witness function to the workspace if it's missing\n \
e) Tolerating the gap with `#[antigen_tolerance(...)]` if intentional"
);
}
print_state7_diagnostics(audit_report);
print_immune_state_verdicts(audit_report);
}
fn print_immune_state_verdicts(audit_report: &audit::AuditReport) {
use audit::ImmuneVerdict;
if audit_report.presentation_verdicts.is_empty() {
return;
}
println!();
println!("Immune-state verdicts (ADR-029 — observed, not declared):");
let undefended = audit_report.undefended_verdicts();
let defended_count = audit_report
.presentation_verdicts
.iter()
.filter(|v| matches!(v.verdict, ImmuneVerdict::Defended { .. }))
.count();
let gap_count = audit_report
.presentation_verdicts
.iter()
.filter(|v| matches!(v.verdict, ImmuneVerdict::SubstrateGap))
.count();
println!(
" {} defended, {} undefended, {} substrate-gap \
(across {} presents-site(s))",
defended_count,
undefended.len(),
gap_count,
audit_report.presentation_verdicts.len()
);
for v in &audit_report.presentation_verdicts {
let site = format!("{}:{}", v.presentation.file.display(), v.presentation.line);
match &v.verdict {
ImmuneVerdict::Defended { tier } => {
let witnesses = if v.defended_by.is_empty() {
String::new()
} else {
format!(" by {}", v.defended_by.join(", "))
};
println!(
" ✓ {site} {} — defended at {tier:?}{witnesses}",
v.antigen_type
);
}
ImmuneVerdict::Undefended => {
println!(
" ✗ {site} {} — undefended (no #[defended_by] witness, \
no passing requires= predicate)",
v.antigen_type
);
}
ImmuneVerdict::SubstrateGap => {
println!(
" ⚠ {site} {} — substrate-gap (defense intent present; \
current substrate does not satisfy the requires= predicate)",
v.antigen_type
);
}
}
}
}
fn print_category_audit_human(category_report: &audit::CategoryAuditReport) {
if !category_report.all_explicit() {
println!();
println!(
"antigen-category: {} declaration(s) with absent category \
(defaulted to FunctionalCorrectness):",
category_report.defaulted_count
);
for ca in &category_report.audits {
if ca
.hints
.contains(&audit::AuditHint::AntigenCategoryDefaultedImplicitFunctional)
{
println!(
" - {} ({}:{}) — antigen-category-defaulted-implicit-functional",
ca.antigen_type,
ca.file.display(),
ca.line
);
}
}
println!(
" Add `category = AntigenCategory::...` per ADR-028. (v0.2: \
scan-time hint; v0.2.x: parse-time hard-error for new decls.)"
);
}
if !category_report.no_category_witness_mismatch() {
println!();
println!(
"antigen-category: {} declaration(s) whose category is not \
backed by a matching witness type:",
category_report.mismatch_count
);
for ca in &category_report.audits {
if ca
.hints
.contains(&audit::AuditHint::AntigenCategoryClaimInconsistentWithPredicateType)
{
println!(
" - {} ({}:{}) — antigen-category-claim-inconsistent-with-predicate-type",
ca.antigen_type,
ca.file.display(),
ca.line
);
} else if ca
.hints
.contains(&audit::AuditHint::AntigenCategoryHybridIncompleteEvidence)
{
println!(
" - {} ({}:{}) — antigen-category-hybrid-incomplete-evidence",
ca.antigen_type,
ca.file.display(),
ca.line
);
}
}
println!(
" SubstrateAlignment needs a `requires = ...` immunity; \
FunctionalCorrectness needs a `witness = ...` immunity; \
hybrid needs both (one axis present → \
hybrid-incomplete-evidence; ADR-028 §Schema)."
);
}
print_silence_witness_advisory(category_report);
}
fn print_silence_witness_advisory(category_report: &audit::CategoryAuditReport) {
if category_report.no_silence_witness_mismatch() {
return;
}
println!();
println!(
"antigen-category: SubstrateAlignment declaration(s) whose witness \
shape cannot detect silence (advisory):"
);
for ca in &category_report.audits {
if ca
.hints
.contains(&audit::AuditHint::AntigenWitnessShapeMismatchForSilenceNoWitness)
{
println!(
" - {} ({}:{}) — antigen-witness-shape-mismatch-for-silence-no-witness",
ca.antigen_type,
ca.file.display(),
ca.line
);
}
if ca
.hints
.contains(&audit::AuditHint::AntigenWitnessShapeMismatchForSilenceWrongTier)
{
println!(
" - {} ({}:{}) — antigen-witness-shape-mismatch-for-silence-wrong-tier",
ca.antigen_type,
ca.file.display(),
ca.line
);
}
}
println!(
" A SubstrateAlignment failure is silence-by-absence: it surfaces \
only when a mechanism asserts the closure exists. no-witness → wire \
a parity/bijection witness (assert the mechanism exists, not just \
that the two representations agree now). wrong-tier → a code-tier \
test detects behavioral failures; reach for a `requires = ...` \
substrate predicate or a bijection-parity test (exception: the \
wrong-weighting generator legitimately uses a code-tier \
confidence test — confirm the intended generator first)."
);
}
fn print_audit_summary(audit_report: &audit::AuditReport) {
let formal_proof_count = audit_report
.audits
.iter()
.filter(|a| a.witness_tier == audit::WitnessTier::FormalProof)
.count();
let execution_count = audit_report
.audits
.iter()
.filter(|a| a.witness_tier == audit::WitnessTier::Execution)
.count();
let reachability_resolved_count = audit_report
.resolved_count
.saturating_sub(formal_proof_count + execution_count);
println!("Audited {} immunity claim(s):", audit_report.audits.len());
if formal_proof_count > 0 {
println!(
" - {formal_proof_count} formal-proof (phantom-type or formal-verification \
tool — compile-time evidence)"
);
}
if execution_count > 0 {
println!(" - {execution_count} execution (test/proptest run confirmed by audit)");
}
println!(
" - {reachability_resolved_count} declared (witness identifier found in \
workspace — not yet semantically verified)"
);
println!(
" - {} external (delegated to clippy/kani/prusti/etc. — not yet executed by antigen)",
audit_report.external_count
);
println!(
" - {} ambiguous (witness name resolves to multiple workspace functions)",
audit_report.ambiguous_count
);
println!(
" - {} broken (witness identifier not found)",
audit_report.broken_count
);
println!(
" - {} missing (no witness identifier)",
audit_report.missing_count
);
}
fn print_confirmed_immunity_claims(audit_report: &audit::AuditReport) {
let confirmed: Vec<&audit::ImmunityAudit> = audit_report
.audits
.iter()
.filter(|a| a.witness_tier >= audit::WitnessTier::Execution)
.collect();
if confirmed.is_empty() {
return;
}
println!(
"✓ {} immunity claim(s) at Execution tier or higher:",
confirmed.len()
);
println!();
for a in &confirmed {
let i = &a.immunity;
println!(
" {}:{} {} (witness = `{}`)",
i.file.display(),
i.line,
i.antigen_type,
i.witness
);
println!(" tier = {:?}, hint = {:?}", a.witness_tier, a.audit_hint);
}
println!();
}
fn print_state7_diagnostics(audit_report: &audit::AuditReport) {
if audit_report.inherited_unaddressed.is_empty() {
return;
}
println!();
println!(
"⚠ {} inherited presentation(s) not re-attested on the descendant \
(state 7 of the 7-state interaction matrix):",
audit_report.inherited_unaddressed.len()
);
println!();
for iu in &audit_report.inherited_unaddressed {
let p = &iu.presentation;
let ancestors: Vec<String> = p
.inherited_from
.as_ref()
.map(|chain| chain.iter().map(|pe| pe.antigen_type.clone()).collect())
.unwrap_or_default();
println!(
" warning: inherited presentation: `{}` flowed from {:?} \
to `{}` via `#[descended_from]`;",
p.antigen_type, ancestors, p.item_kind
);
println!(
" the witness inherited from the ancestor has not been \
re-attested on the descendant."
);
println!(
" Add `#[defended_by({})]` on a test (code-tier), or \
`#[presents({}, requires = ...)]` for substrate-witness evidence, or \
`#[antigen_tolerance({}, rationale = \"...\")]` on the descendant.",
p.antigen_type, p.antigen_type, p.antigen_type
);
println!(" --> {}:{}", p.file.display(), p.line);
println!();
}
println!(
" Note: behavioral re-validation (does the ancestor's witness \
apply to the descendant?) is A4-A5 work; reachability-tier \
audit cannot perform this check."
);
println!(
" Use `cargo antigen audit --strict` to promote state-7 \
warnings to errors for CI gating."
);
}
#[cfg(test)]
mod tests {
use super::cratesio_index_path;
#[test]
fn cratesio_index_path_follows_cargo_length_convention() {
assert_eq!(cratesio_index_path("a").as_deref(), Some("1/a"));
assert_eq!(cratesio_index_path("ab").as_deref(), Some("2/ab"));
assert_eq!(cratesio_index_path("abc").as_deref(), Some("3/a/abc"));
assert_eq!(cratesio_index_path("serde").as_deref(), Some("se/rd/serde"));
assert_eq!(cratesio_index_path("ureq").as_deref(), Some("ur/eq/ureq"));
}
#[test]
fn cratesio_index_path_lowercases_the_name() {
assert_eq!(cratesio_index_path("Serde").as_deref(), Some("se/rd/serde"));
assert_eq!(cratesio_index_path("AB").as_deref(), Some("2/ab"));
}
#[test]
fn cratesio_index_path_empty_name_is_none() {
assert_eq!(cratesio_index_path(""), None);
}
}