883401309a
Registry::append_family and append_campaign_run did unsynchronized OpenOptions::append + writeln! to their shared JSONL stores; concurrent writers through one shared &Registry interleaved one append's content and newline writes with another's, merging records onto one physical line and corrupting the store (the #276 RED test failed 3/3 with a "trailing characters" parse error). The planned parallel campaign cell loop (#277) makes exactly that caller shape reachable, so this is its prerequisite. Mechanism: one internal append_write_lock: Mutex<()> on Registry, acquired by both append functions and held across the whole read-the-run-counter-then-write critical section — serializing the write also serializes the read-before-write counter derivation, so concurrent appends under one name cannot double-assign a run index. Mutual exclusion around a per-cell-per-stage microsecond write, not a barrier: cells never rendezvous, and the sim event loop stays lock-free (C1's "without locking" governs the sims; this lock sits at the results-persistence edge outside the hot path). A poisoned lock is entered anyway (PoisonError::into_inner): every store line is independent, so one panicked writer must not wedge all later appends. Scope, per the decision log on #276: in-process synchronization owned by the Registry type itself; cross-process locking stays out of contract (the corrected doc comments now state the actual guarantee). The serial per-cell-buffer alternative stays open to #277 as an ordering decision on top — this lock does not preclude it. Adds the sibling test pinning append_campaign_run under the same concurrent load. Verified: the #276 headline test green 3/3 (was red 3/3), sibling green, cargo test --workspace no failures, clippy --workspace --all-targets -D warnings clean. closes #276