Record the guards, the narrowing, and the two things I could not do
gates / gates (push) Successful in 30s

R-273's owed guards are both built and closed. R-291 records what CI stopped
covering and why, so it can be widened deliberately rather than discovered.
R-292 is new and was found by a test failing for the wrong reason:
artifact_sha_invalid conflates "version missing", "registry unreachable" and
"bad sha" into one message. v0.102.0 works around it by ORDERING -- the probes
run first, so an unreachable registry is reported as unreachable -- but the
message itself is untouched.

CONTEXT gains the rule this session is about: a check and the policy it enforces
must read the same number from the same place, or they drift and the drift looks
like a defect in something else. Two corollaries, both of which cost something:
a bounded check must print what it stopped covering on every run, and an
unreadable policy is INCONCLUSIVE rather than unbounded.

Stated in the report rather than glossed: Part 4 (finding receipts for the twelve
downgraded claims) was NOT done and is a shortfall, not a decision -- splitting
it would have produced exactly the half-checked green the exercise exists to
prevent. Part 5 was droppable and dropped. The tag-push green is not re-proved
tonight and is not claimed; the evidence offered is runs 190 and 216.
This commit is contained in:
2026-08-09 19:28:45 +02:00
parent 36bcd12543
commit 985f0ba63c
4 changed files with 184 additions and 167 deletions
+22
View File
@@ -15,6 +15,28 @@
> would make one of the two audiences stop reading. `STATUS.md` is also a **view of `OPEN-ITEMS.md`**
> and holds nothing of its own; this file does hold its own content, namely the standing rulings below.
## A check and the policy it enforces must read the same number from the same place
**Earned 2026-08-09.** The container registry retains N versions per package; `check-published-versions.py`
demanded that **every** git tag still be downloadable. Both rules are individually sensible and together
they are impossible. Nothing connected them, so the day the (N+1)th agent was published the oldest was
evicted and CI went red **at a commit whose own run had been green the day before** — on a true finding
nobody could act on, and one that would have returned at the very next publish.
**The drift looks like a defect in something else.** It was filed as CI being "sensitive to tag refs",
which it is not; two earlier tag pushes had passed, and the gate reads its version list from an API that
does not know what ref is checked out. A whole session went into the wrong explanation before the two
numbers were even compared.
So: **one file owns the number, and every consumer reads it from there** — today
`felhom-agent/scripts/retention-policy.json`, read by the check and referenced by the prune procedure.
Two corollaries, both of which cost something to learn:
- **A bounded check must print what it stopped covering, every run.** A narrowing that goes quiet becomes
permanent by accident.
- **An unreadable policy is INCONCLUSIVE, never "unbounded".** A check that silently loses its bound is
worse than one that fails.
## Standing rulings
**S-39 — "WE DO NOT KNOW" IS NEVER DRAWN AS "FINE", AND THE CODEBASE HAS ONE WAY OF SAYING IT
+127 -166
View File
@@ -1,191 +1,152 @@
# REPORT — making the picture true (2026-08-09, unattended)
# REPORT — the guards, and one thing I could not establish (2026-08-09 evening)
Read-only against all live infrastructure. **Both demo machines were powered off and in transit; no
box was probed, woken or waited on.** Claims that only a running box could settle are marked
`needs-hardware`, which is a verdict, not a gap.
No machine touched. Both demo boxes stayed offline and in transit; nothing here needed them.
**Parts 1, 2 and 3 are done. Part 4 and Part 5 are NOT done** — see §6 and §8. Part 5 was marked
droppable and was dropped; **Part 4 was not marked droppable and I ran out of session before it**,
which is a shortfall rather than a decision.
---
## 1. The verdict table
## 1. Part 1.1 — the deleter: **NOT ESTABLISHED**
**55 claims. Statuses moved on 12 of them — all downwards.** `walked 32 → 20`, `built 5 → 17`;
`partial 14` and `missing 4` unchanged. Full per-claim detail with sources is in
`documentation/architecture/where-felhom-stands.yaml`.
**This prompt's account is not corroborated by any source I can reach**, and the prompt itself said
not to take its word for it.
| claim | was | now | why |
| claim in the prompt | what I found |
|---|---|
| "the register records … pruned to the newest ten … 33 deletions, all HTTP 204" | **No such row exists.** The only prune-adjacent row is **R-210**, which is `WAITING-ON-OPERATOR`, says in terms *"Nothing was deleted; this is a list, not an action"*, and concerns **local Docker images on DooPlex**, not the Gitea registry |
| "newest ten per package" | **Not visible in the current state.** `felhom-agent` generic holds 10, but `felhom-controller` and `felhom-hub` container packages hold **19 each** |
Everything else I could reach is silent, and silence here is not evidence of anything:
`package_cleanup_rule` is **empty**; `package_version` has **no soft-delete column**, so a deletion
leaves no row; Gitea's `action` feed carries **no package operation at all** across 2026-08-08 →
2026-08-09 and nothing whatever on the evening of 08-08; the Gitea pod has **53 days uptime, 0
restarts**.
**And I withdrew one of my own claims.** R-287 said *"no DELETE on the packages API appears in 48 h of
Gitea router logs"*. Re-checked: `kubectl logs --since=72h` returns nothing older than **2026-08-09
16:35** and contains **zero** `api/packages` lines even for requests I made myself. **The log never
covered the window, so its silence was never evidence.** Withdrawn in the register.
**What remains established, unchanged:** `v0.120.0` downloadable at 2026-08-08 14:29 UTC (run 267
printed `ok v0.120.0`), 404 by 2026-08-09 09:30 UTC (run 284), and `0.128.0` published at 14:47 UTC —
eighteen minutes after the good run. **It may be unestablishable from this side: Gitea keeps no
package-deletion trail.**
Per §8 I halted that part's attribution and carried on; the rest is independent.
## 2. The number, and the two places that read it
`felhom-agent/scripts/retention-policy.json``generic_versions_kept: 10`.
Read by **`scripts/check-published-versions.py`** (bounds its assertion) and referenced by the prune
procedure. The file states, in its own header, that ten is an **observed state and not a located
ruling**, and that the principled bound is the hub's vouched `min_agent` floor — nothing can install
below it — which needs network the gate does not have.
**What CI no longer covers:** *a released version older than the retention window is no longer
asserted downloadable.* Its **git tag and config tree are still asserted**; only the binary's presence
is dropped. The check **prints the dropped versions every run**:
```
11 released version(s); retention policy keeps the newest 10
NOT ASSERTED (older than the retention window …): 0.120.0
^ these versions still have git TAGS … what is no longer asserted is the BINARY's presence.
```
Three controls: green at 10 naming what it dropped (**exit 0**); widened to 11 the evicted version
re-enters and convicts (**exit 1**, `FAIL v0.120.0`); the policy file removed → **exit 2 INCONCLUSIVE**,
naming the path it tried — never silently unbounded.
## 3. `felhom-agent` green
```
reuse-refs OK · instructions OK · published OK · release-complete OK · all agent gates OK
```
**At `main`: green**, pushed as `53d047a`, and the pre-push hook ran the same entry point.
**At a tag: not re-proved tonight, and I will not claim it.** The available evidence is that the gate
is ref-independent — it enumerates from the Gitea tags API, and runs **190** (`v0.126.0`) and **216**
(`v0.127.0`) were tag pushes that passed. Minting a tag purely to prove it would have published a
release, which this session forbids.
## 4. Red-proofs
| # | mutation | asserted applied | outcome |
|---|---|---|---|
| `install.installer-by-tag` | walked | **built** | gate 6 asserts the manifest names an installer tag; no walk of a rollback on file |
| `use.lifecycle` | walked | **built** | no walk document cited |
| `drives.enrol` | walked | **built** | the 08-09 walk exercised RE-attach (which failed, R-280); first enrolment of a NEW drive has no walk |
| `drives.migrate` | walked | **built** | no walk document cited |
| `backup.tier1` | walked | **built** | no walk document cited |
| `backup.whole-machine` | walked | **built** | no walk document cited |
| `backup.restore-proof` | walked | **built** | no walk cited, **and the last recorded restore-test on demo-hp FAILED** (2026-08-05) |
| `fault.selfheal` | walked | **built** | no walk document cited |
| `fault.operator-email` | walked | **built** | source-verified as correct, but no run observed delivering |
| `fail.drive-filling` | walked | **built** | no walk document cited |
| `fail.lost-recovery-code` | walked | **built** | by-design refusal; no walk document cited |
| `fail.hub-down` | walked | **built** | no walk document cited |
| 1 | **tag check removed** | `grep -c` → 1 | **scenario A FAILED, reporting `flash = "artifacts_set"`** — Friday's exact defect returned: the manifest saved with no tag |
| 2 | package check removed | `grep -c` → 1 | scenario B FAILED, `flash = "artifacts_set"` |
| 3 | inconclusive branch mapped onto success | `grep -c` → 2 | scenario E FAILED |
**Upgraded: 1.** `install.byo` — the page said *"the first real one has not happened"*. A real
`--mode byo` install completed on demo-hp on 2026-08-09 (`Day-0 provision SUCCESS`, 3 m 49 s). Still
not a customer's own hardware, so not *walked*, but the sentence was false.
All reverted; `grep -c MUTATION`**0**; all seven tests green again.
**`needs-hardware`: 4** — `use.lan-fallback`, `backup.restore-proof`, `fail.disk-failing`,
`fail.internet-down`. Each needs an observation on a running box; each says which.
**Red-proof 3, stated precisely rather than flatteringly:** with the inconclusive branch mapped onto
success the save did **not** complete — it fell through to `artifact_sha_invalid`. So the mutation
proves the guard is load-bearing for *the message the operator sees*, not for the save itself. That is
the honest reading, and it is exactly the defect scenario E exists to prevent: the operator being told
"missing or invalid" when the truth is "I could not reach the registry".
**Confirmed: 38.** Seven of those were re-confirmed against live source or the live hub tonight rather
than against paperwork: the tripwire, the off-site repository, the claim path, the catalogue, the
tunnel, the reset code and the operator-email digest.
## 5. The five scenarios, as the operator sees them
## 2. Every downgrade, with the coupling that broke
| | outcome | the message |
|---|---|---|
| **A** tag missing | REFUSED | *"Refused: that version has no usable git tag. The installer fetches an agent's config files from `raw/tag/v<version>/configs/`, so a version published without its tag makes every fresh install and reinstall fail at step 5 of 8 — as root, on a virgin machine. … Fix it by pushing the tag: `git tag -a v<version> <released-commit> && git push origin v<version>`"* |
| **B** package pruned | REFUSED | *"Refused: that version's artifact is not downloadable. The version is tagged but its package is not in the registry, so a box would 404 fetching the binary itself. … Publish it — `bash scripts/release-agent.sh <version>`"* |
| **C** no checksum | REFUSED | the pre-existing *"Couldn't set the checksum …"* — see the note below |
| **D** both good | **SAVES**, `artifacts_set`, byte-identical behaviour | — |
| **E** registry unreachable | REFUSED | *"Refused: could not verify — this does not mean anything is missing. … It refuses rather than saving with a warning, because a warning beside a success reads as a success. … There is deliberately no override: the registry is on your own server, so if it is unreachable the vouch can wait."* |
The rule is *"a proof is about the code that existed when it ran"*. **It did not fire the way the task
expected.** Not one downgrade came from code moving under an old proof. **All twelve came from step 1
of the same rule — the cited evidence does not exist.**
**Scenario C changed shape because my first draft modelled nothing real,** and that is worth recording.
With a Gitea client configured, `resolveArtifactSHA` fetches the sha **authoritatively and ignores what
was submitted** — so "submit an empty sha" cannot produce an empty stored sha. The genuine shape is
Gitea answering with no `sha256`, and the **existing** refusal already owns it. The test now pins the
guarantee (*the manifest is unchanged*) rather than a mechanism I had invented.
Measured: of the 28 capability-map rows behind the page's claims, **8 carry a `tests/` or `audits/`
path in their evidence column and 20 carry prose only.** The green dots were being drawn from rows
that cite an argument, not a walk. Filed as **R-290**.
**No override was built, and none is wanted.** §8's halt condition did not trigger.
**And the decay ran the other way once.** `fault.operator-email` — *"one mail per run, every failing
app named"* — I first took to be contradicted by R-182 (open, *"tells the operator about ONE app and
silently swallows every other"*). Reading live source: the digest `backup_run_failures` is allowlisted
(`hub/internal/api/handler.go:1837`), operator-only (`notify/dispatcher.go:423`) and templated
(`notify/templates.go:48`); `recovery_unit_capture_failed` is record-only (`dispatcher.go:376`); a
cooldown drop now logs a `suppressed` row (`dispatcher.go:314-330`). **The claim is right and the
register row is stale** — filed as **R-289**. The session went looking for stale proofs and found a
stale defect.
## 6. Part 4 — NOT DONE
## 3. The positive control
The twelve downgraded claims were not re-examined and no receipts were searched for. **The honest-grey
count is therefore still the twelve from last night, unverified in either direction**, and
`where-felhom-stands.*` is untouched. This is the session's shortfall: Parts 13 took the budget, and
splitting Part 4 in half would have produced exactly the kind of half-checked green the whole exercise
exists to prevent.
```
1 BASELINE real dataset -> OK, exit 0
2 PLANT scratch copy: use.dlna missing -> walked -> CONVICTED, exit 1
"use.dlna: status 'walked' but NO evidence document cited"
3 REMOVE scratch copy deleted; committed dataset never touched
4 RE-RUN real dataset -> OK, exit 0
```
**Whether the "code moved under the proof" rule can fire at all** is therefore still open from last
night, where it fired **zero** times out of twelve — every downgrade came from missing evidence, not
from decayed evidence. My reading remains that it *can* fire but will stay rare until rows cite
evidence at all, which is R-290.
Plant → convicted → removed → clean. The gate also convicted **51 problems in my own first draft** of
the dataset (bad anchors, register ids that are not in `OPEN-ITEMS.md`, an evidence path that does not
exist) before any of this — which is the more convincing demonstration, because it was not staged.
## 7. Hub deployed
## 4. The two known disagreements — both settled, and neither document was wrong
**v0.102.0**, live and verified: deploy image `gitea.dooplex.hu/admin/felhom-hub:0.102.0`, rollout
complete, and the page footer reads `0.102.0`. Manifest commit **`36bcd12`**; code commit `b55fc17`.
The image was verified **served by the registry before** the manifest was bumped, not after. ArgoCD's
`felhom` app has `automated.enabled: false`, so the sync was triggered explicitly — **no
`kubectl set image` at any point.**
**"A customer restores their own data with no help."** The map says **MISSING (as evidence)**; the
2026-08-07 walk records a customer route completed with no shell. **Not a contradiction.** The map's
row is *"A customer (**not the operator**) performs a restore via UI alone"* — it is about *who*. The
walk proves the *route*. No non-operator has ever done it, which is what the page's own neighbouring
claim already says.
## 8. Part 5 — dropped, as marked
**The reinstall story.** The map's `PROVEN-LIVE (2026-08-04 night drill)` row is scoped in its own text
to *"a controller-data-volume rebuild — NOT a total host loss"*. The 2026-08-09 rehearsal was a
whole-host uninstall and reinstall. **The map has no row for that case at all** — a gap, not a
disagreement.
Not started. It was explicitly droppable and it is the only part that touches customer-facing wording,
which is where a rushed edit does most harm.
**Would anything here have caught either one? No — and it could not have, because neither was false.**
Both are collisions of vocabulary: "customer" meaning *the route* or *a person*, "rebuild" meaning
*the guest* or *the host*. No gate detects an ambiguity that makes two true sentences look
contradictory. They surfaced only when someone tried to state them side by side. **That is the
argument for the dataset** — one id, one scope, one status — and against prose rows.
## 9. Register
## 5. The data file
`documentation/architecture/where-felhom-stands.yaml`, 55 entries. **Every entry cites at least one
source and the gate proves it** (`check_stands.py` rule 1). YAML rather than JSON because statuses move
one line at a time and a YAML diff shows which claim moved; a JSON re-dump reflows.
Rules honoured: it is a **view** (every entry cites map / register / evidence); **no status was raised
in it** — the one upgrade is recorded against evidence and the map is named as the thing that must
change; and it is **regenerated, not hand-edited** for the page.
## 6. The page
- `where-felhom-stands.html`**generated, 54 KB, zero `<script>` tags**, same palette
(`#0b1220` / `#121b2c` / `#34d399` `#60a5fa` `#fbbf24` `#64748b`), 1600 px, A3 landscape print rules.
- Every moved status carries a visible **`changed 2026-08-09, was walked`** chip plus a *why it moved*
line — 12 of them, no diffing required.
- `Where Felhom Stands.html`**`where-felhom-stands-2026-08-09-snapshot.html`** (`git mv`, so the
space is out of every shell path), with a line in `documentation/README.md` calling it a dated
snapshot that is not maintained.
- **What the old bundle actually was**, since it aimed the fix: not merely minified — the content sat
**gzip+base64 inside a JS module map**, and the three blobs decompress to the bundler and React, with
the document itself in a JSON-escaped string on line 393. It rendered and nothing else.
## 7. The corrected silence rows
**R-281 is WITHDRAWN. It was wrong in both directions**, and the operator's mailbox is what settled it.
- **The tripwire DID fire**: `escrow_blob_served` at 10:19:41 UTC = **12:19 CEST**, eight minutes before
the verified restore.
- **False alarms fired too**: `host_down` 09:28 UTC and `node_down` 09:30 UTC, both error severity, both
`sent`, for deliberate attended work — eight operator mails in all. → **R-285**, the opposite gap
from the one filed.
**The measurement's cause IS established.** The P7 query copied `/data/hub.db` **without
`hub.db-wal`**; the hub runs SQLite in WAL mode, so everything after the last checkpoint was invisible.
**Signature, exact:** P7 reported *"2 events all day, newest `db_dump_completed` 00:30:07"*, and the
number of rows on 08-09 at or before 00:30:07 is **exactly 2**.
The two obvious alternatives were **tested and refuted**, not waved away: a **timezone offset** — all
nine mailbox stamps equal the hub's UTC + 2 h exactly, so the window was right; and a **wrong key or
wrong store** — the same table and key return the correct rows now. A live re-run **cannot** reproduce
the fault because the WAL has since been checkpointed, and that is stated rather than dressed up as a
reproduction.
**The lesson, filed as R-286:** the control was drawn from the *same stale snapshot* as the
measurement, so it agreed. **A control must come from a different channel.** The independent channel —
the mailbox — was available the whole time. This is also a trap `operations/nodes.md` already
documents, and which I had avoided correctly earlier in the same session.
## 8. Register
**Ceiling moved R-284 → R-290.** Opened: **R-285** (planned reinstall pages the operator), **R-286**
(same-channel control), **R-287** (the CI red is true), **R-288** (the capability map is unreadable),
**R-289** (R-182's row is stale), **R-290** (map rows cite no evidence). Withdrawn: **R-281**.
## 9. Part 4 — and I did not change the workflow, on purpose
**Which gate:** `published` / `scripts/check-published-versions.py`.
**The premise is wrong in every particular. It is not ref-sensitive.** The gate enumerates releases
from the **Gitea tags API** (`main()`, `/api/v1/repos/admin/felhom-agent/tags?limit=200`), so the
checked-out ref is irrelevant — and the two previous tag pushes **passed** (run 190 `v0.126.0`, run 216
`v0.127.0`).
**What is true:** run 267 (main, `28ba8593b8`, 08-08 14:29 UTC) printed `ok v0.120.0: binary
downloadable`. Run 284 (**the same commit**, on the tag, 08-09 09:30 UTC) printed
`FAIL v0.120.0 — HTTP 404`. A published release became uninstallable between the two. The registry now
holds exactly the ten newest versions; `0.128.0` was published **14:47 UTC, eighteen minutes after run
267**.
**Who removed `0.120.0` is NOT established, and I will not guess:** `package_cleanup_rule` is empty
(queried in Postgres), `app.ini` sets no limit, `publish-agent.sh:77` only pre-deletes the version it
is publishing, the Gitea pod has 53 days uptime and 0 restarts, and **no `DELETE` on the packages API
appears in 48 h of router logs**. The internal `[cron.cleanup_packages]` `@midnight` job falls in the
window and would leave no router line — **a leading candidate, not a conclusion.**
**So nothing was silenced and no workflow file was changed.** The red is a **true positive** — a tagged
version that cannot be installed is the exact R-115 defect the gate exists to catch, and muting it
would hide the next one. **What it therefore still does not check: nothing. Nothing was disabled.**
The honest fixes — bound the gate to versions at or above the vouched `min_agent` floor (0.127.0 today;
nothing can install 0.120.0), or retire tags whose packages go — are release decisions, and §8 forbids
fixing findings here. Filed as **R-287**.
**Also measured, and it is good news:** the failure alarm did send —
`RESEND-ACCEPTED id=fa1a7a83-714f-4357-b0ca-d3c4bb7ae73f`.
**Ceiling R-290 → R-292.** Opened **R-291** (the narrowing, with its reason, so it can be widened
deliberately) and **R-292** (`artifact_sha_invalid` conflates three facts — found by scenario E
failing, worked around by ordering, message untouched). Closed: **R-273's owed-guards tail**, both
guards built. Corrected: **R-287**, twice.
## 10. Observations — noticed, not acted on
- **`fail.app-crash` and `fault.operator-email` are the same unobserved thing** seen from two sides:
the digest is wired and correct in source, and no one has watched it arrive.
- **The verification depth is recorded per claim** (`depth: source-read | register+map |
needs-hardware`). 23 of 55 got a source or evidence read; the rest were checked against the register
and map only. That is on the face of the data rather than implied by a green tick.
- **The capability map is the thing that actually needs fixing.** The dataset now disagrees with it for
twelve rows, and the dataset is only a view — **the map still says PROVEN-LIVE for all twelve** (R-290).
- `documentation/audits/` holds 131 files and `tests/` 37; the evidence exists in quantity. The gap is
that the map does not point at it.
- **Out of scope and left alone, as instructed:** every finding above, the capability-map restructure
(R-288), and the two guards owed from yesterday.
- **R-292 is the interesting one.** A test I wrote to check a new guard failed for a reason that had
nothing to do with the guard, and that reason was a real pre-existing defect. The five scenarios
earned their keep before the feature shipped.
- **The golden gets no tag probe.** It is fetched by version and has no config tree, so a tag probe
would assert something the installer never does. Deliberate, and stated in the code.
- **The gate is skipped entirely when no Gitea client is configured**, or a hub without registry
credentials could never vouch anything. Pinned by its own test, and worth knowing: the guard is only
as present as the client is.
- `check-release-complete` runs in `--fast`, so a missing tag is now caught by the **pre-push hook**,
earlier than CI.
+33 -1
View File
@@ -1,6 +1,6 @@
# STATUS — what works, what's broken, what's next
**Updated 2026-08-09.**
**Updated 2026-08-09 (evening).**
> **A view, not a source.** `documentation/backlog/OPEN-ITEMS.md` is the authority; this page restates
> part of it in plain words, and **nothing may exist only here**. Not `CONTEXT.md`, which is technical
@@ -42,6 +42,38 @@ destroyed on purpose and its files came back byte for byte identical — four ti
their recovery code got everything back with **no command line inside the machine at any point**,
in 72 seconds. The two rough edges that walk found are also gone. *(R-201, R-252, R-253 — closed.)*
## The guard is in. An approval that cannot be installed is now refused.
**Yesterday morning every install in existence failed for hours, and nothing would have stopped it
happening again. Now something does.** When you press Save on the Day-0 artifacts, the hub checks —
before it writes — that each version you are vouching actually has its git label **and** that its file
can actually be downloaded. If either is missing it refuses and tells you which, for which version,
and the one command that fixes it. **Hub v0.102.0 is live.**
Three details worth your knowing:
- **It checks the exact file the installer fetches first** — the one whose absence broke Friday — not
some other file that happens to exist. A test pins that, because probing the wrong file is precisely
how the failure stayed invisible.
- **"Could not check" also refuses**, with a different message. Saving with a warning would read as a
success, and we have the scars. **There is no override**: the registry is on your own server, so if
it is unreachable the approval can wait.
- **A second machine now catches it one step earlier** — the agent repo refuses to consider a release
complete unless its label and its package both exist.
## The red repository is green, and the two rules now share one number
The tidy-up keeps the newest ten versions; the check demanded every version ever labelled still be
downloadable. Both are sensible and together impossible, so the red would have returned on your next
publish. They now read the same number from one file. **What the check no longer covers, plainly: a
version older than the ten is no longer asserted downloadable** — its label and its config files still
are — and it prints which ones it dropped on every run so this cannot go quiet.
**One thing I could not establish, and I am not guessing.** Who actually deleted the old versions is
**still unknown**. Gitea keeps no deletion trail: no cleanup rule is configured, the version table has
no deleted-marker, the activity feed shows no package operation, and the server log no longer reaches
back that far. I also **withdrew my own claim from yesterday** that the logs showed no deletion — the
logs did not cover the window, so they never said anything.
## The rehearsal finished. The data came back byte for byte; the journey did not.
**We wiped a working demo machine and put it back. All four test files returned identical — including
+2
View File
@@ -519,6 +519,8 @@ applied.** The one that matters: Scenario A **fails against today's tree** with
| **R-288** | **The capability map is too long to be read, and that is why it stops being true.** `architecture/00-capability-map.md` is **134 642 bytes / 19 456 words across 99 table rows in only 159 lines** — because the rows ARE the length. Measured, longest first: the unaided-recovery-journey row is **3 024 words**, the offsite-password-recovery row **1 087**, the unattended-restore-proof row **971**, the app/guest-network-failure row **904**. That single longest row is a novella of nested corrections, each appended rather than resolved. Its own verification stamp reads **2026-07-16 against evidence corpus @ felhom.eu tip `4b18cc5`** (line 23) — three weeks stale, which is the measurable consequence: nobody re-reads a row they cannot finish. **This is the project's memory, so restructuring it is surgery and wants daylight** — filed, deliberately not attempted in the 2026-08-09 session. **What the shape should probably be:** one line of status per capability plus a dated evidence pointer, with the argument moved to the audit it came from | **READY (M) — NEW 2026-08-09** | — | Do not fold this into another session; it needs its own | Viktor |
| **R-289** | **R-182's register row describes a defect the code no longer has — an OPEN row that is a false alarm.** The row reads *"A full disk tells the operator about ONE app and silently swallows every other app's refusal for an hour"*, cited at `hub/internal/notify/dispatcher.go:268`. **Read against live source 2026-08-09, that is fixed:** the per-run digest `backup_run_failures` is allowlisted (`hub/internal/api/handler.go:1837`), operator-only (`dispatcher.go:423`) and templated (`notify/templates.go:48`); `recovery_unit_capture_failed` is now a **record-only** event (`dispatcher.go:376`) whose notification IS the digest, listing every failed app in one mail; and a cooldown drop now writes a `suppressed` row instead of vanishing (`dispatcher.go:314-330`). The capability map already records the fixed shape (*"EVERY failing app, in ONE mail per run"*). **So the register is behind the code, which is the mirror of the decay this session was looking for** — the session expected stale PROOFS and found a stale DEFECT. **Not closed here, deliberately:** the digest's *delivery* has never been observed end to end (the page's own "an app crashes — the email leg has never been confirmed" card), so the honest move is to re-scope R-182 to that residue rather than tick it | **READY (XS) — NEW 2026-08-09** | — | Re-scope R-182 to "the digest has never been seen delivering", or close it and open that | CC |
| **R-290** | **Most capability-map rows that back a green dot cite no evidence document at all — measured, 20 of 28 probed.** The page's *Walked* means *"done end to end on real hardware, evidence on file"*. Extracting the evidence column for the 28 rows behind the page's claims found a `tests/` or `audits/` path in **8**; the other 20 carry prose only. **Consequence, applied this session:** of 32 claims the page drew as Walked, **12 were downgraded to Built** because no walk document exists for them — `install.installer-by-tag`, `use.lifecycle`, `drives.enrol`, `drives.migrate`, `backup.tier1`, `backup.whole-machine`, `backup.restore-proof`, `fault.selfheal`, `fault.operator-email`, `fail.drive-filling`, `fail.lost-recovery-code`, `fail.hub-down`. **This is not a claim that those twelve are false** — several are near-certainly fine — it is a claim that nothing on file distinguishes them from an opinion, which is exactly what the status word promises. **The gate now enforces it going forward:** `scripts/check_stands.py` fails on `status: walked` with no `evidence:` source. **What is owed:** either a walk document per row, or an honest demotion in the map itself (the map is the source; the dataset only follows it) | **READY (M) — NEW 2026-08-09** | R-288 | The dataset was corrected; **the capability map itself still says PROVEN-LIVE for these rows** and is the thing to fix | Viktor |
| **R-291** | **CI's installability assertion is now BOUNDED by a retention number, and the narrowing is recorded here so it can be widened deliberately rather than discovered.** `check-published-versions.py` demanded that **every** `v<semver>` tag still be downloadable while the registry demonstrably does not retain every version — two sensible rules that cannot both hold, which is why CI went red at a commit whose own run had been green the day before, and would have gone red again at the next publish. **The fix couples them:** `felhom-agent/scripts/retention-policy.json` is THE number (`generic_versions_kept: 10`) and the check reads it. **WHAT CI NO LONGER COVERS, stated plainly: a released version older than the retention window is no longer asserted downloadable.** Its git TAG and its config tree are still asserted — only the binary's presence is dropped — and the check **prints the dropped versions on every run**, so the narrowing cannot go quiet. Controls run: widened to 11 the evicted version re-enters and convicts (exit 1); the policy file removed gives INCONCLUSIVE (exit 2), never silently unbounded. **The number is an OBSERVED state, not a located ruling** (R-287) and the file says so. **The better bound, recorded rather than built:** the hub's vouched `min_agent` floor — nothing can install an agent below it, so a sub-floor version being un-downloadable costs nothing real; it needs the gate to read the hub, which is network it does not have today | **READY (S) — NEW 2026-08-09** | R-287 | Widen or replace the number when the deleter is established, or move the bound to the floor | CC |
| **R-292** | **The artifact-save flash conflates three different facts, and a failing test found it rather than a reading.** `artifact_sha_invalid` reads *"the Gitea sha lookup failed (version missing / Gitea unreachable) or the manually-entered sha is invalid"* — three causes, one message, and the operator acts differently on each. It surfaced because scenario E of the new installability gate kept reporting `artifact_sha_invalid` where it expected `artifact_unverifiable`: `resolveArtifactSHA` ran first and swallowed the distinction. **Worked around in v0.102.0 by ORDERING** — the installability probes now run before the sha resolution, so an unreachable registry is reported as unreachable — **but the underlying message is untouched and still conflates on its own paths** | **READY (XS) — NEW 2026-08-09** | — | Split it into "version not found", "registry unreachable" and "invalid sha" | CC |
**Explicitly still open, untouched by this session:** R-246, R-255, R-256, R-257, R-261, R-262,
R-263, **R-264** (the twenty-one undecided facts — a design session of its own), R-240, R-243,