use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
const PATTERNS: &[&str] = &[
"fs::write",
"fs::rename",
"fs::remove_file",
"fs::remove_dir_all",
"fs::copy",
"File::create",
"fs::hard_link",
"OpenOptions",
];
struct Site {
path: &'static str,
pattern: &'static str,
count: usize,
reason: &'static str,
}
#[rustfmt::skip]
const ALLOWED: &[Site] = &[
Site { path: "src/storage.rs", pattern: "fs::write", count: 1, reason: "the journaled write primitives (`write_file`, `move_issue_file`, `delete`), their rollback, the atomic asset copy they call, and the out-of-corpus number-claim ledger" },
Site { path: "src/storage.rs", pattern: "fs::rename", count: 4, reason: "the journaled write primitives (`write_file`, `move_issue_file`, `delete`), their rollback, the atomic asset copy they call, and the out-of-corpus number-claim ledger" },
Site { path: "src/storage.rs", pattern: "fs::remove_file", count: 8, reason: "the journaled write primitives (`write_file`, `move_issue_file`, `delete`), their rollback, the atomic asset copy they call, and the out-of-corpus number-claim ledger" },
Site { path: "src/storage/lifecycle.rs", pattern: "fs::rename", count: 5, reason: "the journaled write primitives (`write_file`, `move_issue_file`, `delete`) and their rollback, after the storage module split (tix #1723)" },
Site { path: "src/storage/lifecycle.rs", pattern: "fs::remove_file", count: 1, reason: "the journaled write primitives (`write_file`, `move_issue_file`, `delete`) and their rollback, after the storage module split (tix #1723)" },
Site { path: "src/storage/lifecycle.rs", pattern: "fs::remove_dir_all", count: 1, reason: "the journaled write primitives (`write_file`, `move_issue_file`, `delete`) and their rollback, after the storage module split (tix #1723)" },
Site { path: "src/storage.rs", pattern: "fs::copy", count: 1, reason: "the journaled write primitives (`write_file`, `move_issue_file`, `delete`), their rollback, the atomic asset copy they call, and the out-of-corpus number-claim ledger" },
Site { path: "src/storage.rs", pattern: "File::create", count: 1, reason: "the journaled write primitives (`write_file`, `move_issue_file`, `delete`), their rollback, the atomic asset copy they call, and the out-of-corpus number-claim ledger" },
Site { path: "src/storage.rs", pattern: "fs::hard_link", count: 1, reason: "the journaled write primitives (`write_file`, `move_issue_file`, `delete`), their rollback, the atomic asset copy they call, and the out-of-corpus number-claim ledger" },
Site { path: "src/storage.rs", pattern: "OpenOptions", count: 1, reason: "the out-of-corpus number-claim ledger, which is deliberately outside the issue journal" },
Site { path: "src/app/editor_input.rs", pattern: "fs::write", count: 1, reason: "the TUI's editor temp file; the edited issue is applied through `Store`" },
Site { path: "src/app/repair.rs", pattern: "fs::write", count: 1, reason: "the TUI's retained editor copy and pending-edit temp; the corpus write is applied through `Store`" },
Site { path: "src/app/repair.rs", pattern: "fs::remove_file", count: 5, reason: "the TUI's retained editor copy and pending-edit temp; the corpus write is applied through `Store`" },
Site { path: "src/app/session.rs", pattern: "fs::write", count: 1, reason: "the TUI's editor-session temp file; the corpus write is applied through `Store` and the backup through `crate::backup`" },
Site { path: "src/app/session.rs", pattern: "fs::remove_file", count: 1, reason: "the TUI's editor-session temp file; the corpus write is applied through `Store` and the backup through `crate::backup`" },
Site { path: "src/backup.rs", pattern: "fs::write", count: 1, reason: "the centralized `.tix-backups/` copy (#1581) — deliberately out-of-corpus, never the issue file itself" },
Site { path: "src/backup.rs", pattern: "fs::remove_file", count: 1, reason: "`tix clean --keep`'s prune of old centralized backups — deliberately out-of-corpus" },
Site { path: "src/commands/clean.rs", pattern: "fs::remove_file", count: 1, reason: "`tix clean`'s migration sweep of legacy stray `.tixbak`/`.md.bak` files — the command's whole purpose; the centralized `.tix-backups/` prune lives in `src/backup.rs`" },
Site { path: "src/commands/edit.rs", pattern: "fs::write", count: 1, reason: "the `$EDITOR` temp file; the edited issue is applied through `Store` afterwards" },
Site { path: "src/commands/edit.rs", pattern: "fs::remove_file", count: 1, reason: "the `$EDITOR` temp file; the edited issue is applied through `Store` afterwards" },
Site { path: "src/commands/init.rs", pattern: "File::create", count: 1, reason: "repository bootstrap (`tix.toml`, `.gitignore`) — not the issue corpus" },
Site { path: "src/commands/init.rs", pattern: "fs::remove_file", count: 2, reason: "repository bootstrap (`tix.toml`, `.gitignore`) — not the issue corpus" },
Site { path: "src/commands/init.rs", pattern: "fs::rename", count: 1, reason: "repository bootstrap (`tix.toml`, `.gitignore`) — not the issue corpus" },
Site { path: "src/commands/mod.rs", pattern: "fs::write", count: 1, reason: "`atomic_write` for config/metadata files — not the issue corpus" },
Site { path: "src/commands/mod.rs", pattern: "fs::rename", count: 1, reason: "`atomic_write` for config/metadata files — not the issue corpus" },
Site { path: "src/config/io.rs", pattern: "File::create", count: 1, reason: "saving `tix.toml` — not the issue corpus" },
Site { path: "src/config/io.rs", pattern: "fs::remove_file", count: 2, reason: "saving `tix.toml` — not the issue corpus" },
Site { path: "src/config/io.rs", pattern: "fs::rename", count: 1, reason: "saving `tix.toml` — not the issue corpus" },
Site { path: "src/ics.rs", pattern: "fs::write", count: 1, reason: "the user-named `.ics` output — an export artifact, not the corpus" },
Site { path: "src/ics.rs", pattern: "fs::rename", count: 1, reason: "the user-named `.ics` output — an export artifact, not the corpus" },
Site { path: "src/records.rs", pattern: "fs::write", count: 1, reason: "the distinct named-record store (milestones/cycles/…) — separate from the issue corpus and single-file per operation" },
Site { path: "src/records.rs", pattern: "fs::rename", count: 1, reason: "the distinct named-record store (milestones/cycles/…) — separate from the issue corpus and single-file per operation" },
Site { path: "src/records.rs", pattern: "fs::remove_file", count: 1, reason: "the distinct named-record store (milestones/cycles/…) — separate from the issue corpus and single-file per operation" },
Site { path: "src/mentions/store.rs", pattern: "fs::remove_file", count: 1, reason: "removing an empty per-target mention file. It is **not** an un-journaled write: the removal records its pre-image with `storage::journal_record_write` first, and the ordinary mention write goes through `storage::atomic_write`, so a rolled-back transaction restores it" },
Site { path: "src/sync/beads.rs", pattern: "fs::write", count: 2, reason: "one stages the `bd import` JSONL under `std::env::temp_dir()` (removed after the call), the other is `save_state`'s temp half of the atomically-renamed `.tix-beads-state.json` — the beads identity ledger, not the issue corpus" },
Site { path: "src/sync/beads.rs", pattern: "fs::remove_file", count: 1, reason: "removes the staged `bd import` temp file after `bd` has read it — never the issue corpus" },
Site { path: "src/sync/beads.rs", pattern: "fs::rename", count: 1, reason: "`save_state`'s rename of `.tix-beads-state.json` into place — the beads identity ledger, not the issue corpus" },
];
fn production_source(path: &Path) -> String {
let text = std::fs::read_to_string(path)
.unwrap_or_else(|e| panic!("cannot read {}: {e}", path.display()));
let mut out = String::new();
for line in text.lines() {
let trimmed = line.trim_start();
if trimmed.starts_with("#[cfg(test)]") {
break;
}
if trimmed.starts_with("//") {
continue;
}
out.push_str(line);
out.push('\n');
}
out
}
fn rs_files(dir: &Path, out: &mut Vec<PathBuf>) {
for entry in
std::fs::read_dir(dir).unwrap_or_else(|e| panic!("read_dir {}: {e}", dir.display()))
{
let path = entry.expect("dir entry").path();
if path.is_dir() {
if path.file_name().is_some_and(|name| name == "tests") {
continue;
}
rs_files(&path, out);
} else if path.extension().is_some_and(|e| e == "rs") {
out.push(path);
}
}
}
#[test]
fn no_unlisted_production_write() {
let manifest = Path::new(env!("CARGO_MANIFEST_DIR"));
let mut files = Vec::new();
rs_files(&manifest.join("src"), &mut files);
assert!(!files.is_empty(), "the audit found no source files to scan");
let mut observed: BTreeMap<(String, String), usize> = BTreeMap::new();
for file in &files {
let rel = file
.strip_prefix(manifest)
.expect("source under the manifest")
.to_string_lossy()
.replace('\\', "/");
let source = production_source(file);
for pattern in PATTERNS {
let count = source.matches(pattern).count();
if count > 0 {
*observed
.entry((rel.clone(), (*pattern).to_string()))
.or_default() += count;
}
}
}
let mut allowed: BTreeMap<(String, String), usize> = BTreeMap::new();
for site in ALLOWED {
assert!(
!site.reason.is_empty(),
"every site states why it is journaled or exempt: {} {}",
site.path,
site.pattern
);
assert!(site.count > 0, "a site records a real count: {}", site.path);
*allowed
.entry((site.path.to_string(), site.pattern.to_string()))
.or_default() += site.count;
}
assert_eq!(
observed, allowed,
"a production write is unclassified, or a classified count changed. \
Route the write through a journaled primitive in `src/storage.rs`, or add \
it to ALLOWED with the surface it does not touch"
);
}
#[test]
fn the_audit_covers_the_named_mutators() {
for required in ["fs::write", "File::create", "fs::rename", "fs::remove_file"] {
assert!(
PATTERNS.contains(&required),
"the audit must track `{required}` (#1579)"
);
}
assert!(
ALLOWED.iter().any(|s| s.path == "src/storage.rs"),
"the journaled primitives must be a classified site"
);
}