kobold-json 0.1.0

Forensic JSON evidence packets for COBOL record migration: raw-byte custody, copybook/record hashes, field findings, round-trip proof. Clean-room; independent of GnuCOBOL/libcob.
Documentation
//! # kobold-json -- forensic JSON evidence packets for COBOL record migration
//!
//! Export a raw COBOL record + its copybook into a deterministic JSON packet that preserves **both the
//! semantic value AND the storage truth** (`offset` / `length` / `pic` / `raw_hex` / `encoding` /
//! `copybook_hash` / `record_hash` / `findings` / `roundtrip`), and import it back to the exact original
//! bytes. Parsing and re-encoding are **fail-closed**: a value too long for its field, or a non-numeric
//! value into a numeric field, yields a [`model::Finding`] -- never a silent truncation or coercion.
//!
//! ## Non-claim
//!
//! **kobold-json is clean-room and zero-libcob.** It links no COBOL runtime, contains no libcob-derived or
//! GnuCOBOL-derived code, and depends on no `gnucobol-rs*` crate. It is **not a generic JSON library** and
//! **not a GnuCOBOL 3.2 parity claim.** It answers *"what should a forensic JSON evidence packet for a
//! COBOL record migration look like?"* Evidence here is the [`COURT_NAMESPACE`] (`KOBOLD.JSON.*`) court.
//!
//! ## Courts
//!
//! * `KOBOLD.JSON.EXPORT.1` -- [`export::export`]: record + copybook -> audit packet.
//! * `KOBOLD.JSON.PARSE.1` -- [`parse::parse_into`]: packet + copybook -> reconstructed bytes (fail-closed).
//! * `KOBOLD.JSON.ROUNDTRIP.1` -- bytes -> Evidence packet -> [`parse::parse_into`] -> identical bytes.
//! * `KOBOLD.JSON.REDACTION.1` -- [`redact::redact`]: mask / hash / remove named fields.
//! * `KOBOLD.JSON.DIFF.1` -- [`diff::diff`]: per-path differences between two packets.
//!
//! No crate dependencies; SHA-256 is the pure-Rust [`sha256`] module.

#![forbid(unsafe_code)]

pub mod diff;
pub mod export;
pub mod json;
pub mod model;
pub mod parse;
pub mod redact;
pub mod sha256;

/// The court namespace this crate's evidence is filed under.
pub const COURT_NAMESPACE: &str = "KOBOLD.JSON";

// Re-exports for the common surface.
pub use diff::{diff, DiffEntry, DiffKind};
pub use export::{export, Mode};
pub use json::{parse, serialize, to_string, JsonValue, ParseError, SerializeOptions};
pub use model::{Copybook, DecodedField, FieldDecl, FieldKind, Finding};
pub use parse::parse_into;
pub use redact::{redact, Redaction};

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn end_to_end_roundtrip_and_diff() {
        // KOBOLD.JSON.ROUNDTRIP.1 end to end through the crate's public surface.
        let cb = Copybook {
            record_name: "ACCT".into(),
            encoding: "ascii".into(),
            fields: vec![
                FieldDecl::alnum("HOLDER", "X(6)", 0, 6),
                FieldDecl::numeric("BAL", "S9(4)V99", 6, 6, 2, true),
            ],
        };
        // BAL = +0123.45 zoned: "012345" with overpunch of last '5' positive -> 'E'
        let rec = b"ALICE 01234E";
        let packet = export(&cb, rec, Mode::Evidence);
        let back = parse_into(&cb, &packet).expect("roundtrip");
        assert_eq!(&back, rec);

        // A packet diffs cleanly against itself.
        assert!(diff(&packet, &packet).is_empty());
        assert_eq!(COURT_NAMESPACE, "KOBOLD.JSON");
    }
}