{
  "summary": "Strongest production mechanism: the apparent shared registry mutex is process-local, but attachment registration/List execute in the broker and TTL reaping executes in its separate brain child. Consequently both successful Attach replies can coexist with a later stale reaper publication that loses entries. Source inspection establishes the synchronization gap, not the failing run's exact interleaving.",
  "files": [
    {
      "path": ".worktrees/297-enforcement-shape/crates/spt/src/attach.rs",
      "description": "resolve_sources and sequential register loop, lines 41–116; one independently acknowledged Attach per source."
    },
    {
      "path": ".worktrees/297-enforcement-shape/crates/spt/src/cli.rs",
      "description": "Repeatable attachment Vec at 155–158; attachment TTL selection and registration/ATTACHED emission at 11602–11676."
    },
    {
      "path": ".worktrees/297-enforcement-shape/crates/spt-daemon/src/servehost.rs",
      "description": "Process-local REGISTRY_WRITE at 20; locking and load at 141–180; Attach publication before reply at 193–235; unfiltered List at 275–277; handle/control exchange at 414–481; thread-only regression coverage at 703–754."
    },
    {
      "path": ".worktrees/297-enforcement-shape/crates/spt-daemon/src/seedmap.rs",
      "description": "Broker seed-control connection threads at 110–148 directly invoke servehost::handle at 182–183."
    },
    {
      "path": ".worktrees/297-enforcement-shape/crates/spt-daemon/src/daemon.rs",
      "description": "Broker starts separate brain supervisor at 461–472 and runs seed-control foreground at 482; separate brain entry at 505–519."
    },
    {
      "path": ".worktrees/297-enforcement-shape/crates/spt-daemon/src/brainproc.rs",
      "description": "Brain starts livehost at 358; actual Command::new/Command::spawn child boundary at 1260–1297 and daemon brain argv at 1316–1325."
    },
    {
      "path": ".worktrees/297-enforcement-shape/crates/spt-daemon/src/livehost.rs",
      "description": "TTL load/reap/unlink/save at 1281–1336; brain-hosted pulse calls reaper at 1420 and sleeps 5000ms plus work duration; 50, 1346–1429."
    },
    {
      "path": ".worktrees/297-enforcement-shape/crates/spt-store/src/serving.rs",
      "description": "load/save at 138–173; attachment append at 362–408; expired/reap at 441–466; snapshot UUID copying at 697–710."
    },
    {
      "path": ".worktrees/297-enforcement-shape/crates/spt-store/src/atomic.rs",
      "description": "Durable unique temporary-file publication at 108–109, 132–153; atomic rename retry at 163–218 does not serialize read-modify-write."
    },
    {
      "path": ".worktrees/297-enforcement-shape/crates/spt/src/serveverb.rs",
      "description": "List request/control call at 160–202; JSON output maps every returned entry at 265–278, without TTL/name filtering."
    },
    {
      "path": ".worktrees/297-enforcement-shape/CONTEXT.md",
      "description": "Broker/brain domain distinction at 35–39; registry, name and attachment meanings at 730–757."
    },
    {
      "path": ".worktrees/297-enforcement-shape/docs/adr/0018-broker-brain-process-isolation-restoration.md",
      "description": "Decision explicitly requires separate broker and brain processes, broker-owned seed-control, child brain logic."
    }
  ],
  "architecture": "CLI -> broker seed-control thread -> servehost::apply_at -> load/mutate/durable atomic replace. Separate brain process -> livehost pulse -> with_registry_write -> load/reap/durable atomic replace. Each process has its own static Mutex, so individual publications are atomic but cross-process transactions are not. List reloads persisted state in broker and does not reconcile/filter.",
  "report": "Scope/evidence: read-only source investigation, no commands, builds, tests, edits, or processes launched. Worktree .git -> .git/worktrees/297-enforcement-shape/HEAD -> refs/heads/fix/297-enforcement-shape resolves to 7890ead39bb7f14ed44aaae44b0951f098ffe9ac. This verifies the branch ref, not executable provenance or absence of uncommitted changes. No LSP device was exposed (xd:// lists only spt_checkpoint), so navigation used broad code search and targeted sections. Did not open the failing integration test; initial broad search incidentally returned test references. Read CONTEXT serving/process glossary plus ADR-0057, ADR-0058, ADR-0018 before interpreting ownership.\n\nCALL GRAPH / SUCCESS CONTRACT\n1. cli.rs:155–158 accepts repeatable --attachment into Vec<PathBuf>, dispatched into SendExtras at 2341. cmd_send_verdict resolves every source before irreversible work; TTL is parsed per send or DEFAULT_ATTACHMENT_TTL_MS = 2,592,000,000ms (serving.rs:19–24). cli.rs:11602–11617 selects the TTL, and 11648–11676 registers then emits ATTACHED lines.\n2. attach::register (attach.rs:72–116) ensures daemon running, obtains node/port, and loops sequentially over sources. Each call waits for ServeResult::Attached before continuing. It is not one batch transaction; a partial failure leaves already minted entries and returns an error. Both ATTACHED lines are emitted only after the entire register() returned Ok. The lines prove two positive registration replies, not that entries remain present later; message delivery is later and independent.\n3. servehost::call -> call_with_timeout (servehost.rs:436–481) opens seed-control IPC, sends the request, waits for the result; default timeout 10s, no automatic retry. seedmap::serve_seed_control and handle_conn (110–183) execute handlers on broker connection threads.\n4. servehost::handle captures SystemTime BEFORE acquiring REGISTRY_WRITE (414–424). apply_at locks, reloads registry, snapshots source to UUID file, appends entry, saves, then returns Attached (170–235). Save failures return Refused and clean snapshot; no success-before-save shortcut exists.\n5. ServingRegistry::load_at reads the whole file, validates, and returns empty only on actual missing file (138–161); corruption is an error. save_at validates then serializes the entire snapshot and calls atomic_write_bytes_durable (165–173). No version check/CAS/advisory file lock occurs. atomic.rs writes unique .tmp.pid-seq, sync_all, then rename with transient Windows retries (132–218). Atomic rename prevents torn reads, NOT stale whole-file replacement.\n6. ServeCmd::List calls broker through the same socket (serveverb.rs:160–202). apply_at reloads under broker-local lock and returns all entries at 275–277. JSON maps all entries at serveverb.rs:265–278. There is no TTL, audience, file-existence, or name filter, and no cache. HTTP reads are separate snapshots (webserve.rs:335–338); their serve-time validity policy cannot remove List rows.\n7. Broker Daemon::run starts supervisor (daemon.rs:461–472), then serves seed-control at 482. brainproc::spawn_brain_child constructs and spawns a DIFFERENT process (1260–1297), args daemon brain (1316–1325). That child runs brainproc::run_brain, spawning livehost at 358. livehost invokes reap_expired_attachments at 1420 after reconcile, then sleeps in slices for LIVE_RECONCILE_INTERVAL_MS=5000 (50,1422–1428). Tick is work + sleep, not an exact wall-clock 5-second deadline.\n8. Reaper acquires with_registry_write but only in brain address space; loads persisted registry (livehost.rs:1287–1299); calculates expired entries (serving.rs:441–466: registered_at_ms.saturating_add(ttl) <= now_ms); if none, returns WITHOUT save (1300–1302). If nonempty, deletes owned snapshot files first, then saves full retained snapshot (1303–1326), logging SERVE_REAP count and bytes (1327–1331). File/Dir paths are never unlinked. Reap retains name_owners/next_suffix history for entries it actually observed (serving.rs:452–466).\n\nRANKED MECHANISMS (hypotheses, not incident classification)\n1. CROSS-PROCESS STALE REAPER PUBLICATION — strongest, direct structural defect. servehost.rs:20 static Mutex<()> is not shared across broker and brain. The comments at 135–149 and livehost.rs:1282–1286 describe same-process serialization, but production is explicitly two-process. Example [INFERENCE]: brain loads S containing an expired corpse but no alpha; broker saves S+alpha and acknowledges; brain removes corpse and publishes stale S−corpse, erasing alpha; broker loads that state, appends next attachment and acknowledges. Both ATTACHED lines then print, yet alpha is absent. Another [INFERENCE] ordering: brain loads a state already containing report but before alpha; broker adds alpha; brain publishes afterward retaining report and dropping alpha. Which schedule matches depends on attachment order and identity of the surviving report (Main owns this test/log comparison). If a reaper snapshot predates BOTH new entries and saves after BOTH, both new entries disappear; an old report can remain. The exact surviving report UUID/path/registered_at_ms is therefore materially stronger evidence than its basename alone.\nPredictions/discriminators: at least one expired entry must be present in the reaper's loaded snapshot so it reaches save; expect a successful SERVE_REAP in the overlapping interval. Failure should cluster where an expired-corpse sweep overlaps registration; clean homes/no eligible expiry remove THIS writer's save opportunity. The lost alpha UUID snapshot may remain orphaned because the stale reaper never saw/deleted it; alpha may also disappear from name_owners/next_suffix because whole history is replaced. A trace of PID, load entry IDs, save entry IDs and timing would prove interleaving, but current SERVE_REAP only reports count/bytes, not IDs or transaction timestamps. A passing 10x cohort does not disprove this scheduling race. Reads/captures proposed here are for already authorized evidence collection, not a request to launch/change anything.\nCounterevidence: if complete, correctly scoped logs show no successful reaper publication from before alpha registration through the relevant list, or no possible expired entry existed, this specific route is weakened/eliminated. A SERVE_REAP_LOAD_FAIL/LOCK_FAIL/SAVE_FAIL alone cannot account for a successful stale publication. A successful reap nearby supports opportunity but is NOT proof it dropped alpha.\nImportant existing coverage gap: servehost.rs:703–754 a_reap_shaped_pass_cannot_clobber_a_concurrent_add creates two threads in ONE test process using the same mutex. It proves thread exclusion, not production broker/brain exclusion. No test was executed or edited.\n2. GENUINE ALPHA EXPIRATION — possible only with applicable TTL/time evidence; substantially weaker if this send uses default 30 days. Both attachments receive the same ttl_ms from one send (attach.rs:86–93), but separate request timestamps. A short TTL and a delay between requests/list can let alpha expire while report is still alive. servehost::handle stamps before mutex acquisition/copy/save, so queued/copy time consumes lifetime before acknowledgment. A large forward SystemTime jump can also expire entries; backwards movement retains them (serving.rs:436–448). Reaper must publish before List; mere expiry does not hide a row from List. Predictions: alpha's persisted registration+TTL <= sweep clock; history for alpha stays reserved; its owned snapshot is normally deleted. Discriminator: actual ATTACHED ttl, timestamps, wall-clock discontinuities, reaped-byte sizes and retained name history. Default 30-day TTL and no clock jump decisively oppose ordinary seconds-scale expiry.\n3. OTHER CROSS-PROCESS/OUT-OF-BAND FULL-REGISTRY REWRITE OR HOME MIX-UP — lower-ranked, no incident evidence. The store exposes unguarded public load/save, and no cross-process transaction guard prevents another process from publishing an older object. Production search found registry mutations principally in servehost and livehost; webserve is read-only outside tests. A manual/external writer or a second process can also erase entries, but should not be assumed. Socket names hash SPT_HOME (endpoint.rs:18–33); registry uses SPT_HOME/serve/registry (perch.rs:118–125). Different home/env between send/list could yield unrelated report-only state, but fixed home derivation plus sequential calls is counterevidence. Discriminate using exact homes, broker/brain PIDs and executable provenance, surviving report entry identity, registry metadata/history. Do not infer a rogue process merely because the mutex is process-local: the NORMAL broker/brain pair already suffices for #1.\n\nMECHANISMS NOT SUPPORTED\n- Same-broker Attach-vs-Attach lost update: serialized by the broker mutex, and sender requests are sequential.\n- List stale in-memory cache or TTL filtering: no cache/filter exists; it reloads disk. It can return a momentary snapshot while the other PROCESS writes, but cannot spontaneously invent a report-only state without a publication that contains it.\n- Attachment dedup/name collision: add_attachment always allocates UUID entry and pushes; snapshots use fresh UUID paths (serving.rs:362–408,697–710). Distinct ASCII alpha.md/report.md have no URL decoding complication.\n- Reconcile removing attachments deliberately: reconcile_registry only retires entries with adapter ownership (servehost.rs:353–373); attachment adapter=None (serving.rs:398–400). Reconcile can participate as another full-file writer against the unlocked brain, not as a semantic alpha filter.\n- Ordinary Windows rename failure silently acknowledged: failed save propagates into Refused before Attached. Unique temporary siblings avoid fixed-temp collision, and rename does not deliberately delete/reset destination first. Latency can widen the stale-RMW window, but error suppression is not the found mechanism.\n\nLogs/cohort handoff: preserve exact ATTACHED TTLs/URLs and order, raw serve-list rows including id/path/registered_at_ms, broker vs brain log provenance, and SERVE_REAP/FAIL lines around the send. If saved home artifacts already exist and ownership permits reading, entry history plus orphan UUID snapshots separates stale overwrite from legitimate expiry. Main retains final classification; no live evidence collection/cohort run was started."
}
