# #109 — the purged-perch predicate, in writing + the measured HFENDULEAM population

todlando, 2026-08-04. doyle's step 1 and step 4 ONLY. No sweep is written, no REQ
is minted, no perch was mutated. Both gate-ready lanes (#6 `80a36a7`, #75
`3cf720c`) are untouched.

---

## 1. The predicate

### 1.1 The store named in the constraint does not hold that answer

doyle's constraint phrases the positive predicate as *"this endpoint no longer
exists in the registry"*. Source-grounded, on `main` @ `da82748`:

- `<owlery>/.registry` (`crates/spt-store/src/registry.rs`) is an **address
  discovery table**, not an endpoint set. Its documented invariant
  (KNOWN-HAZARDS 4.3 / `REQ-HAZARD-REGISTRY-STALE-CLEAN`) is that
  `clean_stale_entries` **deletes** rows whose owning pid is dead, and
  `resolve_address` runs that clean **before** every lookup. `unregister_address`
  also fires on an ordinary stop, and on the purge path itself
  (`cli.rs` step (2)).
- Therefore **absence from `.registry` is the normal steady state of every
  offline endpoint.** On this box right now that is 6 of 14 endpoints (chert,
  emphasys, liam, sptc-poll-int, swan, webbie). A GC keyed on registry absence
  would reap all six — the exact inversion of constraint 1.

### 1.2 What IS authoritative

There is **no independent endpoint-existence store on a node.** Every endpoint
surface derives the endpoint set from the owlery tree itself:

- `crates/spt/src/roster.rs::enumerate` — `read_dir(owlery)`, and a dir with
  **no `info.json` is not a perch and is skipped**.
- `crates/spt-daemon/src/projwriter.rs::enumerate_perches` — same shape.
- `spt_store::perch::list_self_perch_ids_checked` / `list_all_perch_dirs` — same.

**The perch directory plus its parseable `info.json` IS the endpoint record.**
So the authoritative store is the owlery tree, read through `spt_store::perch` +
`spt_store::info` — the same readers every consumer already uses. Nothing else
on the node knows the endpoint set, which is why the predicate has to be a
present-tense fact about the directory rather than an inference from another
store's silence.

### 1.3 The predicate

> A perch directory is **residue** iff it carries **no endpoint record** — no
> `info.json` present on any read attempt — so no id it might name resolves to an
> endpoint on any surface. Anything else is **occupied**.

Positive, present-tense, and checkable in one read. It is not an age heuristic
and never consults mtime. It is also exactly the class the purge path leaves
behind: `purge_endpoint_core_with` removes records in order — context store →
ready marker → registry row → trust rows → **then** `remove_dir_all(perch)` —
so a purge blocked at the final step (a held handle; the `PerchRemoveFailed`
arm) leaves a directory whose record is already gone. An abandoned bringup that
opened the spool before `spt api bind` wrote `info.json` leaves the same shape.

### 1.4 The unavailable-store answer: do nothing

- `read_dir(owlery)` fails ⇒ **the sweep does nothing at all** and says so. The
  store already distinguishes this from "no perches" —
  `list_self_perch_ids_checked` returns `Option` precisely so an unreadable root
  is not read as an empty one.
- One directory unreadable ⇒ that directory is **skipped**, never reaped.
- `info.json` reads **CORRUPT** (present, unparseable across all 5 attempts) ⇒
  **occupied, never residue** (3.4 verbatim). A destroyed record belongs to an
  endpoint that exists; it is a restore case, not garbage.
- Refusal is loud and distinct, never silent.

### 1.5 The trap the lane must not walk into

`spt_store::liveness::is_perch_alive` returns **true** for a directory with no
`info.json` (`RawRead::Absent ⇒ true`, `liveness.rs:147`, interim parity).

So **every one of the 24 residue directories on this box reads ALIVE under the
shipped liveness predicate, and all 6 offline real endpoints read DEAD.**
`!is_perch_alive` is therefore not merely weak as the reap authorization — it
authorizes exactly the wrong set. It fails safe (residue is never reaped) but it
can never be the sweep's authority. Its only sound role here is a redundant
second refusal on record-carrying dirs.

---

## 2. The measured HFENDULEAM population

Read-only. Nothing under the owlery was written, moved or deleted.

Root: `C:\Users\decid\AppData\Local\spt-core\owlery` (`perch::owlery_dir()` with
`SPT_HOME` unset). The legacy `%LOCALAPPDATA%\spt\owlery` also still exists on
this box and is **out of scope** — any sweep scopes to `perch::owlery_dir()` and
must never walk the dead root.

| | count |
|---|---|
| top-level perch directories | **38** |
| nested children under them | 12 |
| resolve to an endpoint (carry `info.json`) | **14** |
| resolve to a LIVE endpoint (`is_perch_alive` true on a record) | **8** |
| resolve to NOTHING (no record — invisible to every surface) | **24** |

The 14 record-carrying dirs are **exactly** the 14 rows the shipped enumerator
prints for this node (`spt endpoint list`, spt 0.53.0) — the filesystem census
and the shipped tool agree on the population with no residual.

Breakdown of the 14:

- 7 daemon-hosted `status:online` — deployah, doyle, flynn, hertz, lia, perri,
  todlando (me).
- 5 daemon-hosted `status:offline` — chert, emphasys, liam, sptc-poll-int, webbie.
- 2 status-less external (pid-probe) — **mobile-gw, pid 42900, process `spt`,
  ALIVE**; swan, pid 42156, dead.

Live count is 8 = the 7 hosted-online plus mobile-gw. mobile-gw is the sharp
negative-control subject: a live endpoint with **no daemon status**, held up only
by a pid probe.

Breakdown of the 24 recordless dirs (474 KB total):

- **10** carry a `spool.db` (4 KB–20 KB each) — hertz-rca-shutdown,
  omp-spt-diag-dup, omp-spt-diag-fixed, omp-spt-release-021,
  omp-spt-v0315-resume, probeheal, probeheal2, probeheal3, probever, wakep9.
- **6** are entirely EMPTY (0 entries) — f017-norhome-43544, sptc-classify-probe,
  sptc-diag-2, sptc-pin-1, sptc-pin-2, sptc-pin-3. Oldest 2026-06-22.
- **8** carry only `.info.lock` and/or `digest.log` — omp-spt-v0316-smoke,
  omp-spt-v0317-smoke, omp-spt-v0318-smoke, omp-spt-wake-repro, perri-probe-6779,
  sptc-diag-1, sptc-whoami-probe-13523, wakerc1.
- 1 of them (wakep9) also holds a `nested/wakep9-psyche` child and a
  `sessions.log`.

Bytes: recordless **485,497 B** of **15,732,658 B** owlery total — **3.1%**. The
14 record-carrying dirs hold the other 15.2 MB.

**The value and the blast radius are not the same number here.** The value is 24
directory names and 474 KB. The cost of a wrong predicate is 14 endpoint records
including 8 live agents' infrastructure and 15.2 MB.

### Commands that produced the numbers

- Census (classification + counts):
  `powershell -File <scratchpad>/perch-census.ps1` — one `Get-ChildItem -Directory
  -Force` over the owlery, one `Get-Content info.json | ConvertFrom-Json` per dir,
  `Get-Process -Id <pid>` for status-less records. Script is verbatim in the
  session scratchpad and is pure-read.
- Cross-check against the shipped enumerator: `spt endpoint list` (spt 0.53.0,
  `C:\Users\decid\AppData\Local\spt-core\bin\spt.exe`). NOTE: the CLI's own
  documented invariant runs `clean_stale_entries` on the `.registry` during
  resolution, so the CLI is the cross-check and the filesystem read is the
  authority; no perch is touched by either.
- Bytes: `Get-ChildItem -Recurse -File -Force | Measure-Object Length -Sum` per
  dir, partitioned on `Test-Path info.json`.

---

## 3. The KNOWN-HAZARDS 3.4 tension — doyle's ruling, not mine

3.4 (`REQ-HAZARD-RING-PERCH-ADOPTION`) rules for `ring`, whose direction is
"never adopt"; the GC's direction is "delete". Its reasoning transfers, its
verdicts do not transfer automatically — and applied verbatim it very nearly
forbids the sweep outright. Measured against this box:

- **"A dir carrying a record OR A SPOOL is OCCUPIED."** 10 of the 24 residue
  dirs carry a `spool.db`. Verbatim, those are occupied and unreapable.
- **"A pre-existing EMPTY dir is refused"** (emphasys revoked ring's empty-dir
  self-heal, mid-create race). 6 of the 24 are empty. Verbatim, refused.
- That leaves **at most 8 of 38** reapable, and on a strict reading possibly 0.

The question I am not answering in-lane:

1. Does a positive *"no endpoint record exists in this directory"* **outrank**
   3.4's spool-occupancy rule? A spool whose id has no record is undeliverable by
   construction — delivery resolves the perch record first — but it is also the
   only surviving copy of whatever was queued there.
2. Does the empty-dir refusal transfer to the GC verbatim? The mid-create race
   3.4 protects is real for `ring` (which races a bringup). A GC could be given a
   different, still-positive discriminator, but that is a rule change to 3.4's
   reasoning and it is yours to make, not mine to assume.

Shapes, so the ruling is a choice and not an essay:

- **(a) conservative** — reap only recordless AND spool-less AND non-empty:
  8 dirs. Nothing that ever held a message is deleted.
- **(b) full residue** — reap every recordless dir including spools and empties:
  24 dirs; deletes queued messages for ids that can never receive them.
- **(c) recordless + spool-less, empties included** — 14 dirs.
- **(d) nothing without an explicit operator qualifier** — the sweep only reports.

I will build whichever you rule, negative control first (mobile-gw's shape — a
live endpoint held up by a pid probe alone — is the arm I would use for it,
constructed in a temp owlery, never against the real one). No REQ is minted until
the ruling lands.
