---
name: a-new-guards-outcome-must-be-minted-not-borrowed
description: "hertz 2026-09-08 — when you add a bound/guard/refusal, give its result its OWN name; folding it into an existing token that already conflates causes buys the guard and destroys the diagnosis, and the overloaded token then reads as the old cause"
metadata: 
  node_type: memory
  type: feedback
  originSessionId: 468c4c6e-2bd4-4c75-bb45-2cdfaa9a32d5
  modified: 2026-09-08T16:05:35.200Z
---

Three times in one afternoon, in three unrelated subsystems, the same shape: a guard's result had to be a NEW value rather than an existing one.

1. **Exit file.** A killed clippy chain left NO `clippy.exit`, and a watcher's own exit 0 over an empty diagnostics section read as a clean lint. Fix was not "trap the kill" (a force kill is TerminateProcess and runs no trap) but PRE-SEEDING `VOID:unfinished` before the lint starts, so the killed case has its own on-disk name and the reader refuses anything but a bare integer.
2. **Holders census.** WMI blanks `ExecutablePath` for a process you cannot open, so "unreadable" was arriving as "" and being read as "no holders". Fix was three-valued: readable-match REFUSES, unreadable is its own reported value with membership UNDETERMINED, and empty on the readable line means "none I could read".
3. **`WanRequestOutcome::NoReply`** (`crates/spt-daemon/src/wan.rs`, read at 2f422990). Two producers already: the `NetStreamEof` arm, and `from_token`'s `_ =>` catch-all — a unit at :1396 pins `from_token("") == NoReply`. The source is self-aware (":341 NoReply is the honest reading of two very different situations we cannot distinguish", ":443 NoReply must NEVER collapse into Refused"). The obvious fix for the unbounded reply-read hang is a read budget — and a naive one makes NoReply mean a THIRD thing, "I stopped waiting", inside a token that already admits it cannot separate the two it has.

**Why:** a guard exists to make a bad state VISIBLE. Reusing an existing outcome to report it buys the protection and pays for it with the diagnosis: every occurrence now reads as whichever cause the token already meant, and the reader cannot tell the guard fired from the old thing happening. Worse, the old meaning is usually the benign one ("no holders", "clean lint", "old or silent receiver"), so the overloaded token fails SAFE-LOOKING. This is the same family as "an absence needs a way to appear", one step later: the absence has a way to appear, and you gave it somebody else's name.

**How to apply:** when adding a bound, refusal, timeout, or census arm, ask what the caller will SEE when it fires, and whether that value already means something else. If it does, mint a new one — a distinct token, sentinel, or field — even when the handling is identical today, because the handling is not what you will need in six weeks; the grep is. Check the target's doc comments first: a type that says it "deliberately covers two causes we cannot distinguish" is telling you it is already at capacity. Corollary for reading, not just writing: before concluding from a token, count its PRODUCERS at the sha you are reasoning about (`grep -n` the constructor), because a token with several producers cannot by itself name a cause — here the clock did, B's NoReply landing five minutes after A's process died named the EOF arm. Related: [[an-absence-is-data-only-if-it-has-a-way-to-appear]], [[pre-seed-the-exit-file-with-a-void-sentinel-before-the-leg-starts]], [[a-positive-control-must-cross-the-boundary-the-guard-faces]], [[a-path-predicate-builder-census-is-intermittently-blind]], [[a-guard-bijected-to-the-wrong-enum-is-silent-by-design]].
