blotter-cli 1.1.0

Append-only friction ledger for AI agents: log the cuts that got in the way and the findings worth writing up, find what recurs, promote it into durable fixes, and verify the fixes held.
Documentation
use crate::cli::DogearArgs;
use crate::commands::add::{read_text, redact_evidence, validate_text};
use crate::error::{AppError, AppResult};
use crate::output::{self, Meta};
use crate::redact::rewrite_home_paths;
use crate::store;
use crate::{LogEvent, Origin, compute_dogear_id, format_timestamp, resolve_agent_checked};
use jiff::Timestamp;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;

#[derive(Debug, Serialize, Deserialize)]
pub struct DogearData {
    pub changed: bool,
    pub record: LogEvent,
}

pub fn run(
    args: DogearArgs,
    file: Option<PathBuf>,
    pretty: bool,
    now: Timestamp,
) -> AppResult<i32> {
    let resolved = store::discover(file)?;
    let text = read_text(args.text, "dogear", "dogear")?;
    let home = store::home_dir(&resolved.cwd);
    let text = rewrite_home_paths(&text, home.as_deref());
    validate_text(&text, "dogear")?;
    let (agent, source) = resolve_agent_checked(args.agent, true)?;
    let mut tags = args.tags;
    tags.sort();
    tags.dedup();
    let ts = format_timestamp(now);
    let record = LogEvent::Dogear {
        id: compute_dogear_id(&ts, &agent, &text, &tags),
        ts,
        agent,
        text,
        tags,
        evidence: args
            .evidence
            .as_deref()
            .map(|value| redact_evidence(value, home.as_deref())),
        cwd: store::record_cwd(&resolved.cwd, resolved.cwd_repo(), home.as_deref()),
        origin: Some(Origin::agent()),
    };
    let (changed, record) = store::append_unique(&resolved.path, record, args.dry_run)?;
    let mut meta = Meta::new();
    meta.file = Some(resolved.path.to_string_lossy().into_owned());
    meta.agent_source = Some(source.into());
    meta.warnings = resolved.warnings.clone();
    if args.dry_run {
        meta.warnings.push("dry run; no record appended".into());
    } else if !changed {
        meta.warnings
            .push("duplicate dogear; existing record returned".into());
    }
    output::write_success(DogearData { changed, record }, pretty, meta)
        .map_err(|error| AppError::from_io(error, std::path::Path::new("stdout")))?;
    Ok(0)
}