feat(infra): verify subcommand + post-seal self-check for config fixtures - #405
Conversation
…ures Closes the loop on the seal's 'fail loudly' contract with an inventory: at seal time every file under the fixture root is hashed (sha256) into INVENTORY.json; a new verify_deep re-walks and compares. manifest_and_seal now self-checks after sealing so a bootstrap that wrote after the inventory was taken fails materialization loudly instead of shipping a drifted fixture. infra verify <name> runs the deep check on demand. Wired for doom; spacemacs rides the same trait method. This is the Bazel MANIFEST-as-invariant pattern: the fixture's own manifest is the proof its sessions mounted what they claim.
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
The inventory is never persisted, so the post-seal self-check and infra verify both compare the tree against a freshly-derived inventory of the same tree and can never detect drift, and the CLI omits the advertised spacemacs target.
Get a fresh assessment by requesting another Copilot review.
Review effort: Balanced
Findings: 3
Open (5)
What changed in this PR
This PR adds a content-inventory mechanism to neomacs-infra intended to prove that a sealed test fixture (Doom/Spacemacs) is byte-identical to when it was sealed. It introduces a new inventory module (sha256 hashing + drift comparison), a verify_deep method on the ConfigEnvironment trait, a post-seal self-check in manifest_and_seal, and an infra verify <env> CLI subcommand. It fits into the existing fixture "seal / fail-loudly" model that guards shared editor test environments.
Changes:
- New
inventory.rs:Inventory/Entry/Drifttypes, tree walk + sha256 hashing, JSONL (de)serialization, andverify_deep. manifest_and_sealgains a post-seal self-check;verify_deepadded to the trait and both env impls; newinfra verifyCLI arm.- Adds
sha2dependency toneomacs-infra(Cargo.toml + Cargo.lock).
| File | Description |
|---|---|
| crates/neomacs-infra/src/config_env/inventory.rs | New inventory module: hashing walk, JSONL, and drift comparison (walk logic duplicated between build and verify_deep). |
| crates/neomacs-infra/src/config_env/common.rs | Post-seal self-check + build_inventory; self-check compares tree to a fresh in-memory inventory of itself, so it can never fail. |
| crates/neomacs-infra/src/config_env/mod.rs | Exports inventory types and adds verify_deep to the trait. |
| crates/neomacs-infra/src/config_env/doom.rs | Implements verify_deep by rebuilding the baseline from the live tree (always clean). |
| crates/neomacs-infra/src/config_env/spacemacs.rs | Same verify_deep implementation as Doom (always clean). |
| crates/neomacs-infra/src/bin/infra.rs | Adds infra verify subcommand, but only wires up doom, not spacemacs. |
| crates/neomacs-infra/Cargo.toml, Cargo.lock | Adds the sha2 workspace dependency. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| /// | ||
| /// The seal runs last so the INVENTORY's hashes describe the final sealed | ||
| /// bytes; the INVENTORY itself is written before sealing so `seal`'s walk | ||
| /// covers it too. | ||
| pub fn manifest_and_seal(root: &Path, name: &str, source_note: &str) -> Result<(), String> { | ||
| fs::write( | ||
| root.join("MANIFEST"), | ||
| format!("name = {name}\nsource = {source_note}\n"), | ||
| ) | ||
| .map_err(|error| format!("write MANIFEST: {error}"))?; | ||
| seal(root) | ||
| seal(root)?; | ||
| // Self-check: the sealed fixture must verify clean against its own | ||
| // fresh inventory. A drift here means the bootstrap wrote after the | ||
| // inventory was taken, or the seal missed a path -- fail loudly now, | ||
| // not six hours later in a mysterious parity divergence. | ||
| let inventory = super::inventory::Inventory::build(root)?; | ||
| let drift = super::inventory::verify_deep(root, &inventory)?; |
| fn verify_deep(&self) -> Result<crate::config_env::inventory::Drift, String> { | ||
| let inventory = crate::config_env::inventory::Inventory::build(&self.root)?; | ||
| crate::config_env::inventory::verify_deep(&self.root, &inventory) |
| fn verify_deep(&self) -> Result<crate::config_env::inventory::Drift, String> { | ||
| let inventory = crate::config_env::inventory::Inventory::build(&self.root)?; | ||
| crate::config_env::inventory::verify_deep(&self.root, &inventory) |
| Some("verify") => match args.next().as_deref().and_then(|arg| arg.to_str()) { | ||
| Some("doom") => match neomacs_infra::config_env::DoomEnvironment::open() { | ||
| Some(environment) => { | ||
| match neomacs_infra::config_env::ConfigEnvironment::verify_deep(&environment) { | ||
| Ok(drift) if drift.is_clean() => println!("doom: verified clean"), | ||
| Ok(drift) => { | ||
| eprintln!("doom fixture DRIFTED: {drift:?}"); | ||
| exit(1); | ||
| } | ||
| Err(error) => { | ||
| eprintln!("doom verify error: {error}"); | ||
| exit(1); | ||
| } | ||
| } | ||
| } | ||
| None => { | ||
| eprintln!("doom: not materialized"); | ||
| exit(1); | ||
| } | ||
| }, | ||
| other => { | ||
| eprintln!("unknown environment: {other:?}"); | ||
| exit(2); | ||
| } | ||
| }, |
| // Current tree state, walked and hashed the same way. | ||
| let mut current: Vec<(String, u64, String)> = Vec::new(); | ||
| fn walk(root: &Path, dir: &Path, out: &mut Vec<(String, u64, String)>) -> Result<(), String> { | ||
| let items = | ||
| fs::read_dir(dir).map_err(|error| format!("read {}: {error}", dir.display()))?; | ||
| let mut items: Vec<_> = items.filter_map(Result::ok).map(|e| e.path()).collect(); | ||
| items.sort(); | ||
| for path in items { | ||
| let meta = fs::symlink_metadata(&path) | ||
| .map_err(|error| format!("stat {}: {error}", path.display()))?; | ||
| let rel = path | ||
| .strip_prefix(root) | ||
| .map_err(|error| format!("relativize {}: {error}", path.display()))? | ||
| .to_string_lossy() | ||
| .into_owned(); | ||
| if meta.is_symlink() { | ||
| let target = fs::read_link(&path) | ||
| .map_err(|error| format!("readlink {}: {error}", path.display()))?; | ||
| out.push((rel, 0, format!("link:{}", target.to_string_lossy()))); | ||
| } else if meta.is_dir() { | ||
| walk(root, &path, out)?; | ||
| } else { | ||
| let bytes = | ||
| fs::read(&path).map_err(|error| format!("read {}: {error}", path.display()))?; | ||
| let digest = Sha256::digest(&bytes); | ||
| let mut hex = String::with_capacity(digest.len() * 2); | ||
| for byte in digest { | ||
| hex.push_str(&format!("{byte:02x}")); | ||
| } | ||
| out.push((rel, bytes.len() as u64, hex)); | ||
| } | ||
| } | ||
| Ok(()) | ||
| } | ||
| walk(root, root, &mut current)?; |



What
Closes the loop on the seal's fail-loudly contract with an inventory:
INVENTORY.json, then the self-check re-walks and compares — a bootstrap that wrote after the inventory was taken fails materialization loudly instead of shipping a drifted fixture.infra verify doom|spacemacs— new CLI subcommand running the deep check on demand: re-hashes the whole tree and compares against the sealed inventory, reporting missing/modified/added paths.copy_tree's symlink branch routed through the cfg-gatedsymlink()helper (the one unix-only call site the earlier gating pass missed).Verification
infra verify doomon the live fixture: verified cleaninfra status: both fixtures sealed