use chrono::Utc;
use serde::{Deserialize, Serialize};
use crate::cli::ObligationKind;
use crate::error::AppError;
use crate::ledger::{marker, obligations, obligations::Obligation, scope::Scope};
use crate::output::{self, Ctx};
#[derive(Debug, Deserialize)]
struct Manifest {
outcome: String,
#[serde(default)]
obligations: Vec<ManifestObligation>,
}
#[derive(Debug, Deserialize)]
struct ManifestObligation {
claim: String,
proof: String,
#[serde(default = "default_kind")]
kind: ObligationKind,
#[serde(default = "default_critical")]
critical: bool,
#[serde(default)]
depends_on: Vec<String>,
}
fn default_kind() -> ObligationKind {
ObligationKind::Other
}
fn default_critical() -> bool {
true
}
#[derive(Serialize)]
struct SeedResult {
outcome: String,
obligations_seeded: usize,
state_dir: String,
}
pub fn run(ctx: Ctx, manifest_path: String, force: bool) -> Result<(), AppError> {
let content = std::fs::read_to_string(&manifest_path)
.map_err(|e| AppError::InvalidInput(format!("cannot read {manifest_path}: {e}")))?;
let manifest: Manifest = if manifest_path.ends_with(".toml") {
toml::from_str(&content)
.map_err(|e| AppError::InvalidInput(format!("invalid TOML in {manifest_path}: {e}")))?
} else if manifest_path.ends_with(".yaml") || manifest_path.ends_with(".yml") {
serde_yaml::from_str(&content)?
} else {
toml::from_str(&content)
.or_else(|_| serde_yaml::from_str(&content).map_err(|e| e.into()))
.map_err(|e: AppError| {
AppError::InvalidInput(format!("cannot parse {manifest_path}: {e}"))
})?
};
let mut validated: Vec<Obligation> = Vec::with_capacity(manifest.obligations.len());
for (i, mob) in manifest.obligations.iter().enumerate() {
let claim = mob.claim.trim().to_string();
if claim.is_empty() {
return Err(AppError::InvalidInput(format!(
"obligation {} has an empty claim",
i + 1
)));
}
let proof_cmd = mob.proof.trim().to_string();
if proof_cmd.is_empty() {
return Err(AppError::InvalidInput(format!(
"obligation {} ({claim}) has an empty proof command",
i + 1
)));
}
let depends_on = obligations::normalize_depends_on(&mob.depends_on)?;
validated.push(Obligation {
id: format!("O-{:03}", i + 1),
claim,
kind: mob.kind,
critical: mob.critical,
proof_cmd,
created_at: Utc::now(),
depends_on,
});
}
let cwd = std::env::current_dir()?;
let (dir, _shadows) = crate::commands::init::target_state_dir(&cwd);
let _guard = crate::ledger::lock_state(&dir)?;
let has_contract = dir.join("scope.yaml").exists() || dir.join("obligations.jsonl").exists();
if has_contract && !force {
return Err(AppError::InvalidInput(
"contract already exists — use --force to overwrite".into(),
));
}
if force && has_contract {
let _ = std::fs::remove_file(dir.join("obligations.jsonl"));
let _ = std::fs::remove_file(dir.join("evidence.jsonl"));
}
let scope = Scope::new(manifest.outcome.clone());
scope.write(&dir)?;
let marker_msg = format!(
"ritalin: outcome = {}\nSeeded from: {}\n",
manifest.outcome, manifest_path
);
marker::create(&dir, &marker_msg)?;
for ob in &validated {
obligations::append(&dir, ob)?;
}
let result = SeedResult {
outcome: manifest.outcome,
obligations_seeded: manifest.obligations.len(),
state_dir: dir.display().to_string(),
};
output::print_success_or(ctx, &result, |r| {
use owo_colors::OwoColorize;
println!(
"{} seeded {} obligations from manifest",
"+".green().bold(),
r.obligations_seeded
);
println!(" outcome: {}", r.outcome);
println!(" state: {}", r.state_dir.dimmed());
});
Ok(())
}