burn-down Part B: 14 small rows fixed and closed across four repos (306 -> 292); no :latest in the hub build; gate list pinned; closed-id duplicates refused; R-262 subset pinned
gates / gates (push) Failing after 1m40s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-05 17:10:38 +02:00
parent f5a0aeb0b8
commit b26d292a64
19 changed files with 220 additions and 35 deletions
@@ -0,0 +1,6 @@
### R262-a: drop skipped from knownUnmodelled
--- FAIL: TestR262_RestoreTestFieldsAreAKnownSubset (0.00s)
FAIL
### R262-b: the hub stops decoding source_tier
--- FAIL: TestR262_RestoreTestFieldsAreAKnownSubset (0.00s)
FAIL
@@ -0,0 +1,9 @@
### R263-a: ClearBackupTarget writes true
1700: s.StoragePaths[i].BackupTarget = true
--- FAIL: TestR263_OnlySetBackupTargetGrantsTheRole (0.24s)
r263_backup_target_writers_test.go:73: BackupTarget may be granted outside SetBackupTarget at: [../settings/settings.go:1700:3]
FAIL
### R263-b: a composite literal BackupTarget: true in another package
--- FAIL: TestR263_OnlySetBackupTargetGrantsTheRole (0.28s)
r263_backup_target_writers_test.go:73: BackupTarget may be granted outside SetBackupTarget at: [../web/zz_r263_decoy.go:5:50]
FAIL
+14
View File
@@ -62,6 +62,20 @@ The full text of every row below: `git show ab2b3049:documentation/backlog/OPEN-
| **R-818** | **Two changelogs cite register ids for other findings.** (P4) | CLOSED 2026-10-05 — CORRECTED | Dated correction notes under hub v0.109.0 (`hub/CHANGELOG.md`) and controller v0.224.0 + v0.225.0 (`felhom-controller/CHANGELOG.md`): those two findings never had register rows of their own — the triage's „the real ids are in CLOSED-ITEMS" was itself wrong (no closed row names hub v0.109.0 or controller v0.224.0/v0.225.0). Nothing renumbered. | | **R-818** | **Two changelogs cite register ids for other findings.** (P4) | CLOSED 2026-10-05 — CORRECTED | Dated correction notes under hub v0.109.0 (`hub/CHANGELOG.md`) and controller v0.224.0 + v0.225.0 (`felhom-controller/CHANGELOG.md`): those two findings never had register rows of their own — the triage's „the real ids are in CLOSED-ITEMS" was itself wrong (no closed row names hub v0.109.0 or controller v0.224.0/v0.225.0). Nothing renumbered. |
| **R-755** | **[P3-LOW] wger runs Django's DEVELOPMENT server in production: `manage.py runserver`, because the template does not set `WGER_USE_GUNICORN=True`.** (P3) | CLOSED 2026-10-05 — DUPLICATE of R-762 (its unique fact moved there) | Still true: templates/wger/docker-compose.yml has no WGER_USE_GUNICORN (grep empty). R-762 (open, read) states 'Owner decides together with R-755 (same server question)' and its fix names 'the gunicorn switch of R-755'. | | **R-755** | **[P3-LOW] wger runs Django's DEVELOPMENT server in production: `manage.py runserver`, because the template does not set `WGER_USE_GUNICORN=True`.** (P3) | CLOSED 2026-10-05 — DUPLICATE of R-762 (its unique fact moved there) | Still true: templates/wger/docker-compose.yml has no WGER_USE_GUNICORN (grep empty). R-762 (open, read) states 'Owner decides together with R-755 (same server question)' and its fix names 'the gunicorn switch of R-755'. |
| **R-446** | **[P2-MEDIUM] „Naprakész" can be FALSE, and the badge that says it cannot tell.** (P3) | CLOSED 2026-10-05 — DUPLICATE of R-440 (its unique fact moved there) | felhom-controller/controller/internal/stacks/updateorder.go:96: `if len(s.CatalogDigests) == 0 // s.CatalogTestedAt.IsZero() { return false }` — blind only for apps with no ladder entry, i.e. the same 15 templates R-440 lists (app-catalog has no update_ladder for them). Both rows close by the same act: each app's first proven ladder step (R-462). | | **R-446** | **[P2-MEDIUM] „Naprakész" can be FALSE, and the badge that says it cannot tell.** (P3) | CLOSED 2026-10-05 — DUPLICATE of R-440 (its unique fact moved there) | felhom-controller/controller/internal/stacks/updateorder.go:96: `if len(s.CatalogDigests) == 0 // s.CatalogTestedAt.IsZero() { return false }` — blind only for apps with no ladder entry, i.e. the same 15 templates R-440 lists (app-catalog has no update_ladder for them). Both rows close by the same act: each app's first proven ladder step (R-462). |
| **R-799** | **[P3-LOW] The MeTube fixture's `POST /add` leaves out `download_type`, which upstream's validator lists as required.** (P4) | CLOSED 2026-10-05 — FIXED (burn-down Part B) | app-catalog `29ac711`: `MeTube.add_body()` sends `download_type: video`; `scripts/test_upgrade_fixtures_metube.py` (red-proof: the field removed → FAIL). Not exercised on a box (the next MeTube step will). |
| **R-761** | **[P3-LOW] The canonical example template tells a new app's author the logo is `<slug>-logo.webp`; the controller loads `<slug>-logo.svg`, then `.png`.** (P4) | CLOSED 2026-10-05 — FIXED (burn-down Part B) | app-catalog `29ac711`: the canonical template comment, `REUSE.md` and `NEW-APP-CHECKLIST.md` name `{slug}-logo.svg` then `.png` (controller `config.go` AppLogoURL/AppLogoPNGURL). Comment-only. |
| **R-391** | **Gate 11 (observations) is registered in three of the four runners; `app-catalog-felhom.eu` is the exception.** (P4) | CLOSED 2026-10-05 — FIXED (burn-down Part B) | app-catalog `29ac711`: `CLAUDE.md` states its `REPORT.md` carries no observations section by convention (the row's second option); the shared gate was not copied. |
| **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.** (P4) | CLOSED 2026-10-05 — FIXED (burn-down Part B) | felhom-agent `d833163`: `scripts/retention-policy.json` names the source of its 10 — the R-267 newest-10 prune, established 2026-08-10 (R-287) — and drops the non-existent `registry-retention.md` reader; `check-published-versions.py` still reads 10 (checked). The min_agent-floor bound stays recorded in the file as the better bound. |
| **R-348** | **Every agent restart blanks the reported backup list for up to ~18 hours, and the comment that covers it says "unaffected".** (P4) | CLOSED 2026-10-05 — FIXED (burn-down Part B) | felhom-agent `d833163`: `internal/backup/store.go` says a restart blanks the reported backup list until the next run; only the hub's verdict (7-day look-back) is unaffected. Comment-only. |
| **R-263** | **C7 — „This is the ONLY writer of `StoragePath.BackupTarget`" is false, and nothing pins it.** (P4) | CLOSED 2026-10-05 — FIXED (burn-down Part B) | felhom-controller `114ff27`: comment „the only writer that GRANTS"; `internal/settings/r263_backup_target_writers_test.go` scans every non-test file under internal/ and cmd/ (assignments and composite-literal keys). Red-proofs: ClearBackupTarget writing true; a `BackupTarget: true` literal in internal/web — both convict (`audits/burndown-2026-10-05/r263-red-proof.txt`). |
| **R-368** | **The storage default DOES apply at deploy time — the earlier claim that it never does was wrong, and the residual defect is smaller and different.** (P4) | CLOSED 2026-10-05 — FIXED (burn-down Part B) | felhom-controller `114ff27`: the `IsDefault` comment says the deploy FORM pre-selects it and the deploy API applies no default (the row's second option; behaviour unchanged on purpose). |
| **R-418** | **`repo_gates.py`'s docstring listed ELEVEN gates while THIRTEEN were registered** (P4) | CLOSED 2026-10-05 — FIXED (burn-down Part B) | felhom.eu this commit: `repo_gates.py` docstring lists all 17 gates; `scripts/test_repo_gates_docstring.py` asserts list == GATES in order (red-proof: one line removed → FAIL), run on every push by the script-tests gate. |
| **R-345** | **`hub/Makefile` tags and pushes `:latest`, which the project's own rules forbid in two places.** (P4) | CLOSED 2026-10-05 — FIXED (burn-down Part B) | felhom.eu this commit: `hub/Makefile` AND the real release script `scripts/build-hub.sh` (3 lines; the build dir links to it) no longer tag or push `felhom-hub:latest` — nothing pulls it (grep of all repos + homelab-manifests). `scripts/test_no_latest_push.py` (walk of hub/ + scripts/; red-proofs: the old Makefile and the old build-hub.sh each FAIL). Whether a stale `:latest` sits on the registry was not checked. |
| **R-416** | **`closed_register_gate.py` still has no within-register duplicate-id rule.** (P4) | CLOSED 2026-10-05 — FIXED (burn-down Part B) | felhom.eu this commit: `closed_register_gate.py` RULE 4 refuses an id twice in CLOSED-ITEMS.md (OPEN duplicates were already `register_shape_gate.py` RULE 3); 0 duplicates existed, so it registered green. Decoy `closed-register/duplicate-closed-id` (red-proof: RULE 4 off → LIVE HOLE). |
| **R-261** | **C6 — `CountSelfBindTokens` exists so that callers can assert an invariant, and no production caller asserts it.** (P4) | CLOSED 2026-10-05 — FIXED (burn-down Part B) | felhom.eu this commit: `hub/internal/store/selfbind.go` names `CountSelfBindTokens` a test accessor and the two tests that pin the auto-mint invariant. Comment-only; no hub release needed. |
| **R-262** | **C7 — a comment claims a cross-repo contract is mirrored „field-for-field" and „the key-set tests guard drift"; it is two fields short, AND THE FIXTURE THE TEST READS OMITS THE SAME TWO FIELDS.** (P3) | CLOSED 2026-10-05 — FIXED (burn-down Part B) | felhom.eu this commit: the hub comment says hostRestoreTest is a deliberate SUBSET; `hub/internal/api/r262_restoretest_subset_test.go` pins the hub fields and the known-unmodelled agent fields and cross-checks the agent source beside it — which found a THIRD unmodelled field, `skipped` (agent v0.133.0, R-672; by design a skipped test reads as failed with its reason). Red-proofs: drop `skipped` from the list / stop decoding source_tier → FAIL. |
| **R-286** | **A control drawn from the same channel as the measurement cannot detect a defect in that channel — and this one passed while the measurement was wrong.** (P4) | CLOSED 2026-10-05 — FIXED (burn-down Part B) | felhom.eu this commit: workspace standing rule 3 (both `CLAUDE.md` copies, identical) adds: a control must come from a DIFFERENT channel than the measurement; a hub-state check copies `hub.db-wal` or asks the running pod. |
| **R-588** | **[P3-LOW] ISO release records live in two different places, so "was the gate run for this image?" cannot be answered by looking.** (P4) | CLOSED 2026-10-05 — FIXED (burn-down Part B) | felhom.eu this commit: `runbooks/iso-release-gate.md` names `documentation/tests/iso-release-<ver>-<date>/` as the one home; `tests/iso-release-1.28.0-2026-09-16/README.md` points at the 1.28.0 record inside the 2026-09-16 audit. |
--- ---
File diff suppressed because one or more lines are too long
@@ -320,3 +320,8 @@ record.
Record each criterion as PASS/FAIL **with the observed value and what was scanned for**, in the Record each criterion as PASS/FAIL **with the observed value and what was scanned for**, in the
release report. A criterion with no recorded observation is a criterion that was not run. release report. A criterion with no recorded observation is a criterion that was not run.
**The ONE home for a release's record is `documentation/tests/iso-release-<version>-<date>/`** (R-588,
2026-10-05) — one directory per published ISO, so „was the gate run for this image?" is answered by looking,
not by a full-text search for a checksum. A record made elsewhere (inside an audit) gets a directory here
whose `README.md` points at it. Today: 1.27.0, 1.27.1, 1.28.0 (pointer), 1.29.0.
+7 -1
View File
@@ -54,7 +54,13 @@ roles. **A file being open in the editor is NOT an instruction. If no task is st
attempts. attempts.
3. **An absent log line is not evidence of correct behaviour.** Verify with a POSITIVE observable — 3. **An absent log line is not evidence of correct behaviour.** Verify with a POSITIVE observable —
something that MUST appear when the system is healthy. An empty log is equally consistent with something that MUST appear when the system is healthy. An empty log is equally consistent with
"working" and "stopped entirely". "working" and "stopped entirely". **And the control must come from a DIFFERENT channel than the measurement** (R-286):
same query, same snapshot, same API or same clock all share the defect they are meant to catch — a
stale hub snapshot once "confirmed" itself (2 events all day) while the operator's mailbox held eight
alarms. A hub-state check copies `hub.db-wal` too, or asks the running pod. **And the control must come from a DIFFERENT channel than the measurement** (R-286):
same query, same snapshot, same API or same clock all share the defect they are meant to catch — a
stale hub snapshot once "confirmed" itself (2 events all day) while the operator's mailbox held eight
alarms. A hub-state check copies `hub.db-wal` too, or asks the running pod.
4. **A recommendation that is not followed gets one line saying why.** Silence reads as agreement and 4. **A recommendation that is not followed gets one line saying why.** Silence reads as agreement and
the disagreement is lost. the disagreement is lost.
5. **Evidence is copied off the machine at the end of the phase that produced it — before any revert, 5. **Evidence is copied off the machine at the end of the phase that produced it — before any revert,
@@ -0,0 +1,9 @@
# ISO release 1.28.0 — 2026-09-16 (pointer)
The gate record for this image was made inside an audit about something else, before the one-home rule
(`runbooks/iso-release-gate.md` „Result recording", R-588):
- `documentation/audits/evidence-backup-promise-2026-09-16/phaseD-iso-gate.txt` — run 2026-09-16T15:07:32Z against
`felhom-installer-1.28.0-pve9.2-1.iso`, sha256 `a4cd9b6ddcb55bae3700ab307084d2b699330cc20710688d5816318f04f6d635`.
Nothing was re-run for this pointer; it only makes the record findable by looking.
+11
View File
@@ -1,3 +1,14 @@
## unreleased — comments, a pinned subset and the build without `:latest`; no image change (burn-down 2026-10-05: R-261, R-262, R-345)
The next hub release carries these lines into its own entry. Nothing here changes the running hub.
- **R-262:** `hostRestoreTest` is a deliberate SUBSET of the agent's `RestoreTest` — `mount_parity`, `mount_inventory`
and `skipped` are not modelled (a skipped test reads as failed with its reason, by the agent's design).
`internal/api/r262_restoretest_subset_test.go` pins both lists and cross-checks the agent source when it sits beside
this repo; it found `skipped`, which the comment had not named.
- **R-261:** `CountSelfBindTokens` is documented as the test accessor it is.
- **R-345:** `Makefile` `docker-push` no longer tags or pushes `:latest` (nor does `scripts/build-hub.sh`).
## v0.136.0 — the hub writes a nightly, checked copy of its own database (R-173, decision A) (2026-10-05) ## v0.136.0 — the hub writes a nightly, checked copy of its own database (R-173, decision A) (2026-10-05)
**Operator action on deploy: none.** The hub's volume grows from 1 GiB to 2 GiB and joins Longhorn's nightly backup **Operator action on deploy: none.** The hub's volume grows from 1 GiB to 2 GiB and joins Longhorn's nightly backup
+1 -2
View File
@@ -18,8 +18,7 @@ docker:
docker-push: docker docker-push: docker
docker push $(IMAGE):$(VERSION) docker push $(IMAGE):$(VERSION)
docker tag $(IMAGE):$(VERSION) $(IMAGE):latest # never :latest (R-345) — the manifest pins a version; a moving tag is how a restart upgrades by surprise
docker push $(IMAGE):latest
clean: clean:
rm -rf bin/ rm -rf bin/
+3 -2
View File
@@ -724,8 +724,9 @@ type hostPBSSnapshot struct {
VerifyUPID string `json:"verify_upid,omitempty"` VerifyUPID string `json:"verify_upid,omitempty"`
} }
// hostBackup / hostRestoreTest mirror the agent's hub.Backup / hub.RestoreTest wire // hostBackup mirrors the agent's hub.Backup wire contract field-for-field; hostRestoreTest is a deliberate
// contract field-for-field (slice 6, doc 03 §8). DUPLICATED contract — the golden stays // SUBSET of hub.RestoreTest — the agent's mount_parity, mount_inventory and skipped are NOT modelled here (R-262,
// pinned by TestR262_RestoreTestFieldsAreAKnownSubset) (slice 6, doc 03 §8). DUPLICATED contract — the golden stays
// byte-identical with felhom-agent's copy and the key-set tests guard drift. The hub // byte-identical with felhom-agent's copy and the key-set tests guard drift. The hub
// persists these via report_json (no new columns this slice) and surfaces a FAILED // persists these via report_json (no new columns this slice) and surfaces a FAILED
// restore-test prominently (the loudest DR signal). The rich backup policy is slice 10. // restore-test prominently (the loudest DR signal). The rich backup policy is slice 10.
@@ -0,0 +1,58 @@
package api
import (
"os"
"reflect"
"regexp"
"sort"
"strings"
"testing"
)
// R-262: hostRestoreTest is a deliberate SUBSET of the agent's hub.RestoreTest. This pins WHICH subset: the hub decodes
// exactly `hubRestoreTestFields`, and the agent fields it leaves out are exactly `knownUnmodelled`. When the agent's
// source is beside this repo (the workspace), its RestoreTest json tags must equal the union — so an agent field added
// later fails here until someone decides to model it or to list it as unmodelled. RED-PROOF: drop a field from the
// struct, or delete an entry from knownUnmodelled → this test fails.
func TestR262_RestoreTestFieldsAreAKnownSubset(t *testing.T) {
// skipped (agent v0.133.0, R-672): a skipped test arrives as Pass=false with Error "skipped: …", which the hub
// reads as a failed test with that reason — the agent's own comment calls that the honest reading.
knownUnmodelled := []string{"mount_inventory", "mount_parity", "skipped"}
var got []string
rt := reflect.TypeOf(hostRestoreTest{})
for i := 0; i < rt.NumField(); i++ {
got = append(got, strings.Split(rt.Field(i).Tag.Get("json"), ",")[0])
}
sort.Strings(got)
want := []string{"duration_seconds", "error", "pass", "scratch_vmid", "source_archive", "source_tier",
"tested_at", "verified", "warnings", "warnings_recognized"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("hostRestoreTest json fields = %v, want %v", got, want)
}
for _, f := range knownUnmodelled {
for _, g := range got {
if g == f {
t.Fatalf("%s is listed as unmodelled but hostRestoreTest decodes it — update knownUnmodelled", f)
}
}
}
src, err := os.ReadFile("../../../../felhom-agent/internal/hub/report.go")
if err != nil {
t.Logf("agent source not beside this repo (%v) — the cross-repo half is not checked here", err)
return
}
body := regexp.MustCompile(`(?s)type RestoreTest struct \{(.*?)\n\}`).FindSubmatch(src)
if body == nil {
t.Fatal("type RestoreTest struct not found in felhom-agent/internal/hub/report.go")
}
var agent []string
for _, m := range regexp.MustCompile("json:\"([a-z_]+)").FindAllSubmatch(body[1], -1) {
agent = append(agent, string(m[1]))
}
sort.Strings(agent)
union := append(append([]string{}, want...), knownUnmodelled...)
sort.Strings(union)
if !reflect.DeepEqual(agent, union) {
t.Fatalf("agent RestoreTest fields %v != hub fields + knownUnmodelled %v — model the new field or list it", agent, union)
}
}
+5 -4
View File
@@ -104,10 +104,11 @@ func (s *Store) DeleteSelfBindTokens(customerID string) error {
} }
// CountSelfBindTokens reports how many capability tokens exist for a customer (v0.67.0). Minting is // CountSelfBindTokens reports how many capability tokens exist for a customer (v0.67.0). Minting is
// single-active (delete-then-insert), so this is 0 or 1 in practice; it exists so callers can assert // single-active (delete-then-insert), so this is 0 or 1 in practice. It is a TEST ACCESSOR (R-261): no
// the "after this runs, the only live link is one we just issued — or none" invariant that the // production code calls it. The "after this runs, the only live link is one we just issued — or none"
// auto-mint at customer-create / RESET-completion depends on. Read-only, no oracle risk: it is keyed // invariant the auto-mint at customer-create / RESET-completion depends on is pinned by
// by customer id, which the operator already knows. // web/selfbind_automint_test.go and web/customer_delete_test.go, which call it. Read-only, no oracle risk:
// it is keyed by customer id, which the operator already knows.
func (s *Store) CountSelfBindTokens(customerID string) (int, error) { func (s *Store) CountSelfBindTokens(customerID string) (int, error) {
var n int var n int
err := s.db.QueryRow(`SELECT COUNT(*) FROM selfbind_tokens WHERE customer_id = ?`, customerID).Scan(&n) err := s.db.QueryRow(`SELECT COUNT(*) FROM selfbind_tokens WHERE customer_id = ?`, customerID).Scan(&n)
+8
View File
@@ -1,3 +1,11 @@
## build + gates — no `:latest`; the gate list pinned; duplicate closed ids refused (2026-10-05, burn-down)
- **R-345:** `build-hub.sh` no longer tags or pushes `felhom-hub:latest` (`--push`, `--multiarch`, local); nothing
pulls it. `test_no_latest_push.py` walks `hub/` and `scripts/` build files (red-proofs: the old Makefile, the old
build script).
- **R-418:** `repo_gates.py`'s docstring lists all 17 gates; `test_repo_gates_docstring.py` keeps list == `GATES`.
- **R-416:** `closed_register_gate.py` RULE 4 — an id twice in `CLOSED-ITEMS.md`; decoy in `test_gate_decoys.py`.
## gates — `script-tests`: every Python test suite under scripts/ runs on every push (R-885) (2026-10-05) ## gates — `script-tests`: every Python test suite under scripts/ runs on every push (R-885) (2026-10-05)
- `scripts/script_tests_gate.py` (registered in `repo_gates.py`, fast): walks `scripts/` for `test_*.py` and runs each; - `scripts/script_tests_gate.py` (registered in `repo_gates.py`, fast): walks `scripts/` for `test_*.py` and runs each;
+1 -4
View File
@@ -147,12 +147,11 @@ case "${ACTION}" in
info "Building for current platform + pushing..." info "Building for current platform + pushing..."
docker build "${BUILD_ARGS[@]}" \ docker build "${BUILD_ARGS[@]}" \
-t "${IMAGE}:${VERSION}" \ -t "${IMAGE}:${VERSION}" \
-t "${IMAGE}:latest" \
. .
info "Pushing..." info "Pushing..."
docker push "${IMAGE}:${VERSION}" docker push "${IMAGE}:${VERSION}"
docker push "${IMAGE}:latest" # never :latest (R-345): the manifest pins a version, and nothing pulls hub:latest (checked 2026-10-05)
;; ;;
--multiarch) --multiarch)
@@ -169,7 +168,6 @@ case "${ACTION}" in
docker buildx build "${BUILD_ARGS[@]}" \ docker buildx build "${BUILD_ARGS[@]}" \
--platform linux/amd64,linux/arm64 \ --platform linux/amd64,linux/arm64 \
-t "${IMAGE}:${VERSION}" \ -t "${IMAGE}:${VERSION}" \
-t "${IMAGE}:latest" \
--push \ --push \
. .
;; ;;
@@ -178,7 +176,6 @@ case "${ACTION}" in
info "Building for current platform (local only)..." info "Building for current platform (local only)..."
docker build "${BUILD_ARGS[@]}" \ docker build "${BUILD_ARGS[@]}" \
-t "${IMAGE}:${VERSION}" \ -t "${IMAGE}:${VERSION}" \
-t "${IMAGE}:latest" \
. .
;; ;;
esac esac
+13 -3
View File
@@ -50,9 +50,9 @@ WHAT THIS GATE CANNOT SEE — the residual holes, named rather than implied:
printed as a WARNING. printed as a WARNING.
3. **A closed-sounding verdict that is not true escapes.** `PARTLY CLOSED` leads with no open word. 3. **A closed-sounding verdict that is not true escapes.** `PARTLY CLOSED` leads with no open word.
This gate checks where a row FILED, never whether the verdict is honest. This gate checks where a row FILED, never whether the verdict is honest.
4. **A duplicate id WITHIN one register escapes.** `OPEN-ITEMS.md` carries two unrelated findings 4. ~~A duplicate id WITHIN one register escapes.~~ **Closed 2026-10-05 (R-416):** in `OPEN-ITEMS.md`
both numbered R-133 (filed as R-406). Adding that rule would fail the gate on a pre-existing `register_shape_gate.py` RULE 3 refuses it; in `CLOSED-ITEMS.md` this gate's RULE 4 does (no duplicates
defect, and a registered-but-failing gate refuses every push, so it was deliberately left out. existed when it was added, so it was registered green).
5. Nothing here reads audits, spikes or inventories. A finding that never reaches either register 5. Nothing here reads audits, spikes or inventories. A finding that never reaches either register
is invisible to this gate, as it is to `one_register_gate.py`. is invisible to this gate, as it is to `one_register_gate.py`.
@@ -153,6 +153,16 @@ def main():
for rid in sorted(closed_ids & set(open_ids), key=lambda r: (int(re.sub(r"\D", "", r)), r)): for rid in sorted(closed_ids & set(open_ids), key=lambda r: (int(re.sub(r"\D", "", r)), r)):
convicted.append((open_ids[rid], rid, "", "has a row in BOTH registers")) convicted.append((open_ids[rid], rid, "", "has a row in BOTH registers"))
# RULE 4 — a duplicate id WITHIN CLOSED-ITEMS.md (2026-10-05, R-416). The open register's duplicates are
# register_shape_gate.py's RULE 3; this file had no such rule, so two closed rows under one id (the R-133/R-406
# shape) would make `git show` of "the row that closed R-n" ambiguous. Suffixed ids (R-88a, R-88b) are distinct.
first_seen = {}
for n, rid, _, _ in rows(CLOSED):
if rid in first_seen:
convicted.append((n, rid, "", "CLOSED-ITEMS.md has this id twice (first at line %d)" % first_seen[rid]))
else:
first_seen[rid] = n
# RULE 3 — a finished row left in the OPEN register (2026-10-03) # RULE 3 — a finished row left in the OPEN register (2026-10-03)
finished_in_open = [] finished_in_open = []
for n, rid, columns, cells, _ in register_table.rows(OPEN): for n, rid, columns, cells, _ in register_table.rows(OPEN):
+2
View File
@@ -24,6 +24,8 @@ Gates, in order (all must pass; **non-zero exit on any failure**):
13. observations a REPORT.md observation with no register row behind it (R-389) 13. observations a REPORT.md observation with no register row behind it (R-389)
14. register-shape a register row whose state cell was eaten, a duplicated id, or a blank line 14. register-shape a register row whose state cell was eaten, a duplicated id, or a blank line
splitting the table — the register mis-stating how many findings exist (R-627) splitting the table — the register mis-stating how many findings exist (R-627)
15. script-tests every Python test suite under scripts/ (found by a walk), by exit code (R-885)
16. decoy-coverage every registered gate in all four repos has a decoy, or a named exemption (R-421)
**THE `GATES` TABLE BELOW IS THE LIST; THIS IS A POINTER TO IT.** It drifted once already — **THE `GATES` TABLE BELOW IS THE LIST; THIS IS A POINTER TO IT.** It drifted once already —
it read eleven while thirteen were registered, from 2026-08-24 until 2026-09-01, so it read eleven while thirteen were registered, from 2026-08-24 until 2026-09-01, so
+15
View File
@@ -25,6 +25,7 @@ Run from the repo root: python3 scripts/test_gate_decoys.py
Exit 0 all decoys rejected · 1 a decoy passed (a live hole). Exit 0 all decoys rejected · 1 a decoy passed (a live hole).
""" """
import io import io
import re
import json import json
import os import os
import shutil import shutil
@@ -217,6 +218,20 @@ decoy("closed-register/unreadable-row", "closed_register_gate.py",
append_to(os.path.join(ROOT, "documentation", "backlog", "CLOSED-ITEMS.md"), append_to(os.path.join(ROOT, "documentation", "backlog", "CLOSED-ITEMS.md"),
u"\n| **R-905** | A row with no state cell at all. |\n")) u"\n| **R-905** | A row with no state cell at all. |\n"))
# --- closed-register RULE 4 (2026-10-05, R-416): the same id twice in CLOSED-ITEMS.md ---------------
# The id is read from the file's FIRST closed row at run time, so the decoy duplicates a real id rather than an
# invented one that could never collide.
def _first_closed_id():
for line in io.open(os.path.join(ROOT, "documentation", "backlog", "CLOSED-ITEMS.md"), encoding="utf-8"):
m = re.match(r"^\| \*\*(R-\d+[a-z]?)\*\* \|", line)
if m:
return m.group(1)
decoy("closed-register/duplicate-closed-id", "closed_register_gate.py",
append_to(os.path.join(ROOT, "documentation", "backlog", "CLOSED-ITEMS.md"),
u"\n| **%s** | The same id again. | CLOSED 2026-10-05 | none |\n" % _first_closed_id()))
# --- closed-register RULE 3 (2026-10-03): a FINISHED row left in the OPEN register --------------- # --- closed-register RULE 3 (2026-10-03): a FINISHED row left in the OPEN register ---------------
# On 2026-10-03 the open register held 113 rows whose leading verdict was finished — a quarter of the # On 2026-10-03 the open register held 113 rows whose leading verdict was finished — a quarter of the
# file. The decoy is that exact shape: a row a session closed in place and never moved. The genuine # file. The decoy is that exact shape: a row a session closed in place and never moved. The genuine
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env python3
"""R-345: nothing in this repo's build tooling tags or pushes an image as `:latest`.
Scans hub/Makefile, scripts/build-hub.sh and every Dockerfile/Makefile/*.sh under hub/ and scripts/ (a walk).
Run: python3 scripts/test_no_latest_push.py"""
import os
import re
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
BAD = re.compile(r"^(?![ \t]*#)[^\n]*?(\bdocker\s+(tag|push)\b[^#\n]*:latest\b|-t\s+\S*:latest\b)", re.M)
hits, scanned = [], 0
for top in ("hub", "scripts"):
for dp, dns, fns in os.walk(os.path.join(ROOT, top)):
dns[:] = [d for d in dns if d not in (".git", "__pycache__", "node_modules")]
for f in fns:
if f in ("Makefile", "Dockerfile") or f.endswith(".sh"):
p = os.path.join(dp, f)
scanned += 1
for m in BAD.finditer(open(p, encoding="utf-8", errors="replace").read()):
hits.append("%s: %s" % (os.path.relpath(p, ROOT), m.group(0).strip()))
if scanned < 5:
print("FAIL: scanned only %d build files — the scope is wrong" % scanned)
sys.exit(1)
if hits:
print("FAIL: a :latest tag/push in build tooling (R-345):\n " + "\n ".join(hits))
sys.exit(1)
print("OK: %d build files, no docker tag/push of :latest" % scanned)
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env python3
"""R-418: repo_gates.py's docstring list of gates names exactly the gates in GATES, in the same order.
It drifted twice (eleven listed while thirteen ran, 2026-08-24..09-01; then fifteen while seventeen ran). Run:
python3 scripts/test_repo_gates_docstring.py"""
import os
import re
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import repo_gates # noqa: E402
listed = re.findall(r"^ {1,3}\d+b?\. +([a-z][a-z0-9-]+) ", repo_gates.__doc__, re.M)
registered = [g[0] for g in repo_gates.GATES]
if listed != registered:
print("FAIL: the docstring lists %d gate(s), GATES registers %d" % (len(listed), len(registered)))
print(" only listed: %s" % sorted(set(listed) - set(registered)))
print(" only registered: %s" % sorted(set(registered) - set(listed)))
if set(listed) == set(registered):
print(" (same set, different order)")
sys.exit(1)
print("OK: the docstring lists the %d registered gates, in order" % len(registered))