---
name: a-predicate-without-its-tool-is-not-evidence
description: "BINDING: quote a measurement with its TOOL, DIALECT and SCOPE — nine same-day incidents where two correct observers manufactured a false discrepancy because the predicate omitted its environment"
metadata: 
  node_type: memory
  type: feedback
  originSessionId: f5d0d603-387b-4bcf-bcbe-273b1452dc5c
  modified: 2026-08-18T03:46:41.620Z
---

**BINDING. Nine incidents in one day (2026-07-29/30, milestone A), every one the same species: the number was real, the observers were competent, and the DISCREPANCY WAS MANUFACTURED because the predicate did not name its tool, dialect, or scope.**

**Why:** two agents each run a correct instrument, publish only the number, and then argue about the number. The missing piece is never the number — it is the environment the number was measured in. Worse, a rule written from the wrong root cause teaches the fleet to distrust a command that works.

**How to apply — when you state a measurement, state the predicate AND its environment:** scan root · literal-vs-regex and which dialect · the tool and its version · the populations you did NOT measure. And prefer the dialect-independent form when one exists.

The nine, kept because the pattern only becomes obvious as a list:

1. **"19 files"** from a `grep` with no `-E`, so `.pid` matched any-char+"pid" (struct fields, comments), and never intersected with the set it claimed to filter. A peer's smaller ENUMERATED list was right. **An enumerated list is falsifiable file by file; a bare count is not.**
2. **`grep -v "0 passed"`** also ate `310 passed` — a substring filter over numeric output. Anchor it (`ok\. 0 passed`).
3. **A condition quoted off a stale checkout** — the file had moved `ci.yml`→`golden.yml`. See [[name-the-file-and-sha-a-condition-came-from]].
4. **A margin computed off a log's `epoch_ms`** instead of the clock — 1.6h out, sizing an irreversible date call. See [[measure-the-free-quantity-dont-derive-it]].
5. **`grep -c 'spt_home()'` returned 10 for one agent and 5 for another — BOTH REAL.** `()` is LITERAL in POSIX BRE (GNU grep's default) and an EMPTY GROUP in ERE/PCRE/ripgrep/.NET, where the pattern degrades to bare `spt_home` and also matches imports/comments (5 real calls + 5 bare = 10). ⚖ **doyle's ruling: adopt `grep -F` (fixed-string, dialect-independent) BUT do NOT write "grep -c is regex not literal" into the ADR — it teaches something false about the tool most agents run.** The peer who reported it had measured with a *correct* escaped .NET regex and published a *different* command, so nothing in their own results could have warned them.
6. **A case-INSENSITIVE `FAIL` grep** over nextest output returned a dozen "casualties" that were PASS lines for tests with `fail` in their NAMES (`escalation_choice_parses_fail_closed`). Anchor to the tool's real marker: `^\s+(FAIL|TIMEOUT|ABORT)\s*\[`. Nearly reported a red suite off my own filter.
7. **A 23-test cohort standing in for CI's 2170-test pool.** My hand-built leg B "did not reproduce" — but **a 23-test pool cannot express the contention being hunted, so its clean zero was never evidence.** N of the wrong measurement. See [[verdict-from-probe-competence]].
8. **A commit count from arithmetic instead of `rev-list --count`** — I ff'd twice and added one, said 10, measured 11. Two peers caught it independently. **The count was one command away.** See [[measure-what-costs-one-command]].
9. **"On hfenduleam, `sed -i` is a silent no-op."** Real root cause: `sed -i` stages a temp file and silently exits 0 writing NOTHING when its temp dir is unavailable, and that agent's bash had no `/tmp` (`bash.exe: warning: could not find /tmp`). **Falsified on the same box the same night:** my shell has `/tmp`, GNU sed 4.9 at `/usr/bin/sed`, a live `sed -i '2d'` landed, and I used `sed -i` to resolve the shipping candidate's merge conflict successfully. A property of the SHELL, not the box. Correct form: *`sed -i` exits 0 having written nothing when its temp dir is missing — check `ls -d /tmp`; verify the EDIT, never the exit code.*

**10. ⭐⭐ "THE FILE" IS NOT "THE IDS" — scan the population the claim QUANTIFIES OVER (deployah, 2026-07-30).** doyle's claim was *"zero REQUIREMENT IDS match `MONIC|MNEMONIC|KNOCK|TRUST|WARN|CAUTION`"*. deployah checked it by grepping the WHOLE `traceable-reqs.toml` and got **109.5KB of output** — `TRUST` matches prose like "trust store"/"trusted origin" in titles and comments. The flood looked like a contradiction of doyle's zero when it was a **category error in the probe**: right file, wrong population. Fixed by extracting `^id = ` lines FIRST and matching within them: 631 ids, zero for MONIC/MNEMONIC/KNOCK/CAUTION, exactly two incidental TRUST/WARN hits. **A predicate must scan the set the claim is about; pointing a file-wide probe at an id-scoped claim manufactures a discrepancy out of nothing.** Same species as his unanchored `proc\.rs` (matched `brainproc.rs`, 2 not 1) and #6 above. I ran the same leg independently and added the piece that makes a zero trustworthy: **a competence control — the identical pattern shape against `ACL` returns 11 ids, so the probe demonstrably CAN match at this tip.** A zero from an uncontrolled probe is [[verdict-from-probe-competence]] waiting to happen.

**11. A POINT-IN-TIME COUNT OVER A CHURNING POPULATION IS A SAMPLE, NOT A STATE (chert, 2026-07-30, release night).** I read `java+rustc+cargo+nextest` = **11** on hfenduleam at 02:00:36Z and **1** at 02:00:52Z — same box, same probe, 16 seconds, nothing changed but which short-lived `rustc` invocations existed in that instant. During a build **both a zero and a large number are equally uninformative**, so a quiet-window check cannot be "the count is low". deployah took it as a predicate change, not an instrument note: **"no CI job in flight" (a run-level STATE) is the primary predicate; parent-chain attribution applies only to what survives it.** ⚠ Corollary from my own failed follow-up — I walked the parent chain of an `rustc.exe` after counting it and got **empty output**, because the process exited between the count and the walk, and `Get-CimInstance` matching nothing is indistinguishable from "unattributable". **Snapshot the pid list ONCE and walk chains within that snapshot; never re-query by name between the count and the walk.** Named as a failure-to-measure, never recorded as a zero.

**12. ⭐⭐ PRINT THE COUNT, NEVER THE INTERPRETATION — a hardcoded label will contradict its own output, and the eye reads the label (deployah, 2026-07-30, his third instance in one night).** He ran `git log --oneline --branches --not --remotes` and printed `(empty = nothing of mine unpushed)` **underneath ten lines of output** — the label was an assertion written before the command ran, not a reading of its result. Same defect as his earlier `(empty = daemon never invokes the ring path)`, also printed under non-empty output. Had he trusted it he would have reported "nothing pending" while holding an unpushed branch during a push freeze. **Corrective, adopted: `n=$(cmd | wc -l)` and let the number speak — a count cannot disagree with itself.**
⚠ **SELF-AUDIT, unasked, and it was live in my own gate check.** My quiet-window check immediately before leg 5 of the W-SPEC sweep printed `echo "(no non-terminal runs above = clear)"` after the `gh run list` call — a hardcoded "clear" under output I had not counted. The data really was empty, so the verdict was right, **but the label was structurally incapable of telling me otherwise**, and that check was the gate on whether I was allowed to run at all. Contrast my *first* quiet check, which did it correctly: it printed the actual list plus a measured `599 processes, 0 targets` from one CIM snapshot — numbers, not narration. **The defect is not laziness; it is that a label placed by the author encodes the expected result, so it survives exactly the case it exists to catch.** Kin: [[verdict-from-exit-not-from-silence]] (an exit answers *did it fail*, never *did the effect land*) — a printed label is that error one layer up, asserting the effect in prose.

**13. ⭐⭐ I PROPOSED BUILDING A TOOL THAT ALREADY SHIPPED, AND MY HAND-ROLLED ENUMERATION WAS 39% SHORT (hertz, 2026-08-03, the 44-worktree reap).** Doyle dispatched a stale-worktree sweep. I hand-rolled the pool inventory with the predicate *"a directory named `target` inside a worktree"*, reported it as the population, and drafted an INFRA-REGISTER entry whose remedy paragraph said *"nothing walks `.worktrees\` for pool-shaped dirs and reports them."* **Both halves were false, and one command refuted both.** `xtask pool-sweep` already existed, shipped in `c6515f1` (an ancestor of `origin/main`, verified by `merge-base` not by reading a branch list), tagged `REQ-POOL-GC-ORPHAN-RECLAIM` — it walks the root for dirs AND links, counts inbound links across the whole sweep *before* forming any verdict, classifies InUse/Owned/Orphaned, and dry-runs by default. Running it took seconds and surfaced **`<root>\target-seam`, 52.06 GiB**, a second root-level pool under a different name that my predicate structurally could not express. My "population" was ~61% of the box's pool bytes, and I had already sent doyle a superlative ("the 72 GB root pool is the biggest object by 9×") that the missing pool falsified.
**Why it happened:** the gap-claim and the enumeration shared one root — I reasoned about what the tooling *must* be missing instead of invoking it. A capability claim ("nothing does X") is a measurement like any other and needs its tool; asserting one *inside a proposal to build X* is the shape that survives review, because the proposal reads as evidence of the gap.
**How to apply:** before writing "nothing does X" or hand-rolling an enumeration, **grep the CLI's subcommand table and run the candidate dry** — `pool-sweep`-class tools are dry-run by default and cost nothing. And when a hand-rolled filter and a shipped tool disagree on a population, the tool's answer is the one with a maintained predicate. ⚠ Second-order: a name-pattern filter (`*\target`) cannot find a sibling pool named differently — [[verdict-from-probe-competence]] with the competence control never run. Related: [[measure-what-costs-one-command]].

**14. ⭐⭐ A BACKGROUND TASK'S OUTPUT FILE IS NOT EVIDENCE UNTIL THE TASK REPORTS COMPLETION — empty-because-unfinished and empty-because-zero are THE SAME BYTES (deployah, 2026-08-17, v0.56.0 shape night).** Four clean zeros in one session, all mine, all the same artifact: I ran probes as BACKGROUND tasks and read the output file immediately, before the completion notification. An in-flight file reads as `contents are empty`, which is indistinguishable from a probe that ran and found nothing. It produced (a) "no workflow trigger block" from an `awk` range, (b) "this memory is orphaned, no index line" — it *was* indexed in `RELEASE-INDEX.md`, (c) "both deleted footer phrases absent tree-wide" — one survives at `traceable-reqs.toml:3313` by design, and (d) a failure to reproduce doyle's own citation, which had I trusted it would have sent me back to tell a correct gater his finding did not reproduce.
**Contributing factor, NOT the cause:** plain `grep -r` walks `target/` (tens of GB here), so these probes ran long enough to make the race near-certain; the ripgrep-backed Grep tool skips it via gitignore and returned instantly. That asymmetry is exactly what made the INSTRUMENT look like the variable.
⚠ **I misdiagnosed this in public and had to withdraw it.** I told doyle "bash grep with `--include` returns false zeros — demote it for absence claims", he logged it as fleet craft, and I retracted the cause within the hour. My glob hypothesis (unquoted `--include=*` being shell-expanded before grep sees it) was **refuted by its own negative control**: a three-file rig with a planted hit found it under unquoted `--include=*`, quoted `--include='*'`, and no `--include` alike, rc=0 — an unmatched glob stays literal, so the argument reaches grep intact. **A rule that is right in EFFECT can be wrong in CAUSE, and the wrong cause is what gets copied**, teaching the fleet to distrust a working tool — the same defect as #5's ruling. **The hole is CLOSED, in grep's favour** — I held it open only until the bounded re-run finished, then filled it rather than leaving a suspicion attached to a working tool. `grep -rln` with `target/`, `.git/` and `.worktrees/` excluded FOUND `traceable-reqs.toml`, rc=0, in **real 7m29.674s**. There is no bash-grep defect of any kind, and that runtime is itself the proof of cause: reads at ~0s, ~45s and ~90s were each guaranteed premature. **Filling the hole made the claim STRONGER, not weaker** — the harness race goes from leading candidate to sole cause. A hole you can close for the price of waiting is not a hole to keep.
⚖ **NOT A PERSONAL SLIP — doyle disclosed the SAME pattern twice in the same session** (sleep-then-cat on task output instead of waiting for the notification) when he processed this retraction, and dropped it on his side. **Both his reads happened to land after completion, and that is exactly why the pattern persists: it usually wins the race, right up until it manufactures a clean zero on the probe that mattered.** A habit that is correct 90% of the time is invisible until the 10% is load-bearing — which is the argument for the rule, not against it.
**How to apply:** read a background task's output ONLY on its completion notification, never on a timer and never right after launching it. Do not `cat` task A's output file from inside task B — B will happily print A's half-written buffer as a result (that is how (d) happened). If you must bound a sweep, exclude `target/`/`.git/` or use the ripgrep-backed tool, so the probe is short enough that the question never arises. Kin: [[cancelled-measurement-leaves-labelled-hole]], [[verdict-from-probe-competence]], [[one-sample-is-not-a-negative]], and #12 above — a label written before the command ran, and a file read before the command finished, are the same error at two different layers.

**15. ⭐⭐ SCOPING-OF-A-GREP = SCOPING-OF-A-CENSUS — two agents stated a conclusion about a POPULATION neither had enumerated, in opposite directions, within one hour (2026-08-29, CONDUIT #236 red #2).** deployah cut a CI job log at the panic line, searched DOWNWARD only, found no `stdout` section, and reported *"not in the artifact, no section containing the string stdout at all"* — the scoping was a sub-range, the claim was the whole artifact. The `=== ER177 BRIEF-ONCE ===` verdict block sits UPSTREAM of the panic and had rendered fine, carrying rc2's stdout, which contained the ANSWER: `'engine-room' is controlled by another window on this machine`. Same hour, doyle reasoned from the error path (`RC_FAIL` prints to stderr) to *"your capture patch is the ONLY instrument that can name the author of this failure"* — also a population claim, also unenumerated, and also wrong, because the refusal was a GRACEFUL `println!` + `return Ok(())` (rc.rs:1991), not an error path at all. **Both pointed a lane at capturing something already captured; a ten-run A/B was pre-registered, ratified, and about to fire against a question the artifact had already answered.** One grep, thirty seconds, beat it — and it only ran because the claim was handed back as re-checkable ("worth one grep for ER177 before we call them absent") rather than accepted.
⭐⭐ **The generalisation deployah and doyle both banked, and it is this file's thesis at the level of ARGUMENT rather than TOOL: a conclusion about a population you did not enumerate is unmeasured no matter how sound the reasoning that produced it.** #10 is the same defect with a grep (right file, wrong population); this is the same defect with a *rationale* — reasoning from where an error SHOULD print is not a census of where output DID land. Kin: [[dont-take-a-diagnosis-as-measured]], and #13's capability claim ("nothing does X") which is the same shape a third way.
⚠ **SAME CLASS, IN MY OWN SCRIPT, TWICE IN TEN MINUTES — a census over SOURCE has two traps a grep does not (2026-08-29, the emission-site census doyle wanted for hertz's IR entry).** I wrote a Rust-source census and ran it three times, each answer plausible, each wrong: **286 → 412 → 487 sites**, and my remembered figure going in had been 277.
- **(a) A LINE-BASED SCAN MISSES MULTI-LINE MACROS.** Matching the macro and the token literal on the SAME line drops every `eprintln!(` whose format string starts on the next line — which is most of the long ones, i.e. exactly the fragment-rich emitters the lane is about. Fixed with a lookback window: +126 sites.
- **(b) TRUNCATING AT THE FIRST `#[cfg(test)]` DISCARDS LIVE PRODUCT CODE.** I cut each file at its first `#[cfg(test)]` to drop the unit-test module. `broker.rs` carries one at line **2771** on an inline helper and its real test module at **9641**, so I threw away ~6,800 lines of shipping code. Fix: truncate only at a `#[cfg(test)]` that OPENS A MODULE: +75 sites.
⭐⭐ **THE DETECTOR, and it is the transferable part: name a member you already KNOW is in the population and assert its presence in your own output.** I caught both only because `SUBSCRIBE_DECISION` — the emitter the whole morning turned on — was missing from my results. Without that sentinel every count was unfalsifiable and each looked as reasonable as the next. Same standing as #10's competence control, but pointed at your own instrument instead of at the tip: **a census with no known-positive is a number, not a measurement.** hertz adopted it as a mandatory line in the IR entry. ⚠ And the retraction that had to travel with it: I had stated "277 sites, 243 tokens, spt-daemon + spt-live" to a peer TWICE as fact; the real spread is six crates with `spt` holding 195 and `spt-live` holding **five**. A remembered figure repeated confidently is [[dont-take-a-diagnosis-as-measured]] with the measurement one command away.

⚠ **And the cheap-check asymmetry that decides whether this class ever gets caught:** the re-check cost thirty seconds; the plan it cancelled cost ten runs on a Linux box. **When a peer's absence claim gates expensive work, re-running their probe is always the cheaper half of the decision** — doyle's binding clause above ("a peer's READING of an artifact is a claim ABOUT the artifact") with a cost argument attached, which is what makes it get done rather than merely agreed with.

⭐ **The two corollaries that actually catch these:**
- **A LINE-COUNT TOOL IS A DELIMITER COUNTER, NOT A RECORD ORACLE (hertz's wording, 2026-08-29).** A file whose last record has no terminal newline undercounts by one under `wc -l`, silently and unnoticeably — my `census_tokens.txt` reported 382 for 383 real tokens because I wrote it with a single joined string while its two sibling files wrote per-row terminators. Assert the LOGICAL record count from the generator, independently of `wc`; and never mutate an artifact two parties have already hashed to fix a cosmetic property — state the quirk in evidence notes and fix the GENERATOR for the next run.
- **Verify by falsifiable count, not by reading the file back.** Both the merge-union proof (630 vs **631**) and a peer's awk-escape catch (`* expects:` with no comment marker, which would have hard-errored all 8 configs) came from counting matches rather than re-reading.
- **Get the REAL exit code, never a pipeline's.** `cmd | tail; echo $?` reports `tail`. Use `cmd > f; RC=$?`. I nearly reported `tail`'s 0 as a cherry-pick's and as `traceable-reqs`' twice in one hour. See [[verdict-from-exit-not-from-silence]].

⭐ **And verify hardest where you'd most like to be right** — the correction that FLATTERS your thesis is the one to check. ⚠ **`gh run view --jq '.conclusion // .status'` IS A BROKEN IDIOM — it bit me three times in one session (2026-07-30).** For an incomplete job `gh` emits `conclusion: ""`, an **empty string, not `null`**, and jq's `//` only falls through on `null`/`false`. So every running job renders as blank and reads as *"not started."* I nearly opened a "runner online but deaf" incident off it — both runners showed `busy=true` while my own query insisted no job had started. **Ask for `status` and `conclusion` as separate fields.** Also note the run's own top-level `status` stays `queued` while its jobs are `in_progress`, so the run-level and job-level answers legitimately disagree — [[opt-in-ci-legs-skip-silently]] is about jobs, and so is this: read at JOB level.

Related: [[load-claim-needs-measured-axes]], [[absence-needs-sibling-probe]], [[assemble-onto-the-repair-tip-not-a-sibling]].
