HOMELAB-1929: LD5 measurement rules (M0) and single report query — draft for freeze #7

Open
aaron wants to merge 0 commits from plane/HOMELAB-1929-m0 into main
Owner

What

Two files under design/ld5/, no existing file touched.

  • design/ld5/measurement.md — the LD5 measurement rules (M0), written as
    citable rules M0-R1…M0-R41: the sample and the sign, the five per-ticket
    success conditions, the four gate clauses, the preconditions to sign, the two
    informational labels, the running rules (caps, leases, landing, interventions,
    frozen AC, fixed denominator, drain rule), the void rule and the closed
    infra-cause list, the report contract, the stop rule, and §8's recorded source
    tensions X1–X15.
  • design/ld5/report.sql — the single report query. Long-format output
    (report_scope, ticket, rule, metric, subkey, numerator, denominator, value, unit, note) so every reported number names the M0 rule that requires it and
    the source table / event kind it comes from. Per ticket and in total, and
    it ends with the verdict rows: clause 1 against 70%, clause 2, clause 3,
    clause 4, and the overall PASS / FAIL / VOID / NOT_CERTIFIABLE.

Scope

This encodes already-approved rulings; it decides nothing new. Every rule
traces to a source named in measurement.md §9.

It supersedes 07 §7.2's gate (rule 8) per the operator's 2026-10-21
re-ruling in B1 (HOMELAB-1929): the gate is the 4 clauses, revision 5's 11
safety tests and the 100% injection-canary proof move to E1's exit, the canary
clause is replaced by preconditions-to-sign (planted-defect bar 6 of 8, auditor
recall 4 of 6, 6 safety tests, shakedown, N0), and the two informational labels
("fail — auditor-dependent", "panel drift") are recorded.

design/ld5/measurement.md §8 lists the places where the sources genuinely
disagree, numbered X1–X15 (renamed from T1–T12 so they cannot be confused
with the T1–T6 safety tests of M0-R17). X1–X12 are readings this PR
encodes. X13–X15 are OPEN items for the operator
, implemented fail-closed so
a sample cannot be gamed while the question is open:

  • X13 — condition 4 can pass by default when the auditor's bundle-only
    checks come back inconclusive (M0-R41). The report certifies the condition only
    on a conclusive check and prints the literal reading beside it, so the cost of
    the cap is visible.
  • X14 — a failed live probe inside the 14-day window. The report fails
    condition 5 on it and prints the failures separately.
  • X15 — the target repo's image-tag file vs Q17 guard 2 ("nothing merges on
    an oversight path while a sample runs"): if the tag file is under deploy/**,
    every pin bump is blocked. Build/ruling question, no query change.

Fix round 3 (Opus check, 2026-10-22)

This revision closes every blocker and every fix-before-freeze item from the
Opus check of head 3e0ba08:

  • integer division — every rate divides as ::numeric; 14/20 prints 0.70,
    not 0, and dfpy_confirmed is correct below 100%.
  • the two commits — landing.merged carries head_sha and
    merge_commit_sha; condition 2 reads the reviewed head, condition 3 requires
    the probe to name the merge commit (a passing probe_ok alone no longer
    counts).
  • clause-4 re-baseline — each node is compared against the baseline in force
    at its own recorded_at; drains carry a drain_id and pair on it, never by
    position; only operator-actor drains are declared, and a re-baseline is only
    used when the operator recorded it inside a declared drain (a runtime can no
    longer mark its own drift as declared).
  • sample scoping — sample_id on every record and every CTE; the signed
    sample is capped at the 20 slots; decided_unsigned counts only events after
    the sign.
  • condition 4 — the replay joins the goal.check event's own id.
  • also: ac_sha re-specs counted as misses (M0-R25); escapes per M0-R12 (a
    revert or red CI on live needs no flag); image digests compared by containment
    (@>); replacements must be a signed spare, released by the operator, not a
    spare of a spare, before the claim; a zero-slot sample cannot print a clause-4
    pass; CI stated as implied by decision='pass'.

[needs Opus check before freeze]

This is a draft for freeze. It must pass the Opus re-check before it is
frozen at the LD5 sign (B20). At the sign, the blob SHAs of
measurement.md and report.sql are snapshotted with the gate text, image
digests and config (M0-R1 / M0-R16), so a query or ruleset edited after the sign
cannot quietly change the number.

report.sql is not executable against the real DB yet: B5 (HOMELAB-1933,
the CNPG record DB) has not landed and its schema review is still open. The
header comment states every schema and event-kind assumption. When B5 lands,
only the source CTEs change; the rules do not.

Checks

  • report.sql executes on a local Postgres 15 against a B5-shaped fixture
    (3 tables: workflows, node_instances, events) — not just parses. 24
    scenarios were run: 14/20 at the 70% boundary, declared drain + re-baseline,
    undeclared runtime re-baseline, in-window vs out-of-window mismatch, open
    drain, drain without an id, gate decision on the wrong commit, probe on the
    wrong commit, re-spec, five replacement-rejection paths, revert / fix-ticket /
    failed-probe escapes, five infra re-runs (void), off-list infra cause,
    all-inconclusive checks, a fresh sample, duplicate slot orders, replay
    reproduced / not reproduced, and the empty schema.
  • Single Postgres statement (one semicolon outside comments and string
    literals, balanced parentheses, sqlglot parses 1 statement).
  • All 41 rules defined (M0-R1…M0-R41); every M0-Rn cited in report.sql
    exists in measurement.md; all 15 tensions X1–X15 defined.
  • Two files changed, no existing file touched.

Ticket: HOMELAB-1929 (LD5-B1), epic HOMELAB-1714.

## What Two files under `design/ld5/`, no existing file touched. - **`design/ld5/measurement.md`** — the LD5 measurement rules (M0), written as citable rules `M0-R1`…`M0-R41`: the sample and the sign, the five per-ticket success conditions, the four gate clauses, the preconditions to sign, the two informational labels, the running rules (caps, leases, landing, interventions, frozen AC, fixed denominator, drain rule), the void rule and the closed infra-cause list, the report contract, the stop rule, and §8's recorded source tensions **X1–X15**. - **`design/ld5/report.sql`** — the single report query. Long-format output (`report_scope, ticket, rule, metric, subkey, numerator, denominator, value, unit, note`) so every reported number names the M0 rule that requires it and the source table / event kind it comes from. Per ticket **and** in total, and it ends with the **verdict rows**: clause 1 against 70%, clause 2, clause 3, clause 4, and the overall `PASS` / `FAIL` / `VOID` / `NOT_CERTIFIABLE`. ## Scope This **encodes already-approved rulings**; it decides nothing new. Every rule traces to a source named in `measurement.md` §9. It **supersedes 07 §7.2's gate** (rule 8) per the operator's 2026-10-21 re-ruling in B1 (HOMELAB-1929): the gate is the 4 clauses, revision 5's 11 safety tests and the 100% injection-canary proof move to E1's exit, the canary clause is replaced by preconditions-to-sign (planted-defect bar 6 of 8, auditor recall 4 of 6, 6 safety tests, shakedown, N0), and the two informational labels ("fail — auditor-dependent", "panel drift") are recorded. `design/ld5/measurement.md` §8 lists the places where the sources genuinely disagree, numbered **X1–X15** (renamed from T1–T12 so they cannot be confused with the **T1–T6 safety tests** of M0-R17). **X1–X12 are readings this PR encodes. X13–X15 are OPEN items for the operator**, implemented fail-closed so a sample cannot be gamed while the question is open: - **X13** — condition 4 can pass by default when the auditor's bundle-only checks come back inconclusive (M0-R41). The report certifies the condition only on a conclusive check and prints the literal reading beside it, so the cost of the cap is visible. - **X14** — a failed live probe inside the 14-day window. The report fails condition 5 on it and prints the failures separately. - **X15** — the target repo's image-tag file vs Q17 guard 2 ("nothing merges on an oversight path while a sample runs"): if the tag file is under `deploy/**`, every pin bump is blocked. Build/ruling question, no query change. ## Fix round 3 (Opus check, 2026-10-22) This revision closes every blocker and every fix-before-freeze item from the Opus check of head `3e0ba08`: - **integer division** — every rate divides as `::numeric`; 14/20 prints `0.70`, not `0`, and `dfpy_confirmed` is correct below 100%. - **the two commits** — `landing.merged` carries `head_sha` *and* `merge_commit_sha`; condition 2 reads the reviewed head, condition 3 requires the probe to name the merge commit (a passing `probe_ok` alone no longer counts). - **clause-4 re-baseline** — each node is compared against the baseline in force at its own `recorded_at`; drains carry a `drain_id` and pair on it, never by position; only operator-actor drains are declared, and a re-baseline is only used when the operator recorded it inside a declared drain (a runtime can no longer mark its own drift as declared). - **sample scoping** — `sample_id` on every record and every CTE; the signed sample is capped at the 20 slots; `decided_unsigned` counts only events after the sign. - **condition 4** — the replay joins the `goal.check` event's own id. - **also**: ac_sha re-specs counted as misses (M0-R25); escapes per M0-R12 (a revert or red CI on live needs no flag); image digests compared by containment (`@>`); replacements must be a signed spare, released by the operator, not a spare of a spare, before the claim; a zero-slot sample cannot print a clause-4 pass; CI stated as implied by `decision='pass'`. ## [needs Opus check before freeze] This is a **draft for freeze**. It must pass the **Opus re-check** before it is frozen at the **LD5 sign (B20)**. At the sign, the blob SHAs of `measurement.md` and `report.sql` are snapshotted with the gate text, image digests and config (M0-R1 / M0-R16), so a query or ruleset edited after the sign cannot quietly change the number. `report.sql` is **not executable against the real DB yet**: B5 (HOMELAB-1933, the CNPG record DB) has not landed and its schema review is still open. The header comment states every schema and event-kind assumption. When B5 lands, only the source CTEs change; the rules do not. ## Checks - `report.sql` **executes** on a local Postgres 15 against a B5-shaped fixture (3 tables: `workflows`, `node_instances`, `events`) — not just parses. 24 scenarios were run: 14/20 at the 70% boundary, declared drain + re-baseline, undeclared runtime re-baseline, in-window vs out-of-window mismatch, open drain, drain without an id, gate decision on the wrong commit, probe on the wrong commit, re-spec, five replacement-rejection paths, revert / fix-ticket / failed-probe escapes, five infra re-runs (void), off-list infra cause, all-inconclusive checks, a fresh sample, duplicate slot orders, replay reproduced / not reproduced, and the empty schema. - Single Postgres statement (one semicolon outside comments and string literals, balanced parentheses, `sqlglot` parses 1 statement). - All 41 rules defined (`M0-R1`…`M0-R41`); every `M0-Rn` cited in `report.sql` exists in `measurement.md`; all 15 tensions `X1`–`X15` defined. - Two files changed, no existing file touched. Ticket: HOMELAB-1929 (LD5-B1), epic HOMELAB-1714.
Author
Owner

Review verdict (landing worker, fresh context)

Reviewed the diff at head 00fb54a against primary sources only: HOMELAB-1929 (description + all 3 comments), HOMELAB-1714 (via the 1929 epic frame), design/where-we-are/after.html §03–§05, design/where-we-are/next.html B1/B12/B16/B19/B20, design/fleet-design-claude/07-layered-fleet-steer.md §7.2 + D40 + §4.6. I did not write this code and had no implementer summary.

Verdict: REQUEST CHANGES — 4 blockers. The rule text is faithful in most places, but two query defects change the number itself, clause 4 is not answerable from the report, and one approved ruling is missing from the frozen text.

Blockers

#FindingEvidence
B1Condition 2 is evaluated against the wrong panel round, so ordinary fix rounds are scored as misses. gate_first keeps the earliest gate.decision row (round ASC) and cond2 then requires merged_sha = that row's head_sha. Every ticket that had an in-workflow fix round (M0-R8: "in-workflow fix rounds inside that first workflow do count as first-pass") or a landing-conflict fix round (M0-R23: "the new SHA needs a fresh panel round. It does not start a second workflow") has a later round for the merged SHA and therefore fails cond2 — a documented non-miss becomes a miss and DFPY is understated. Needs two separate reads: the merged-SHA decision for cond2, the first-workflow first verdict for M0-R30.report.sql:214-224 (ORDER BY round ASC), :300-303 (cond2); rules measurement.md:83-89 (M0-R8), :207-210 (M0-R23)
B2Spare replacement is read from the wrong event kind and the exclusion runs backwards, so the fixed denominator is wrong. M0-R5 says a replacement "is recorded as an event (release with replaces)", but sign_ticket derives is_spare/replaces_ticket from sign.snapshot and sample_ticket filters WHERE NOT s.is_spare. With doc-compliant data nothing is marked: the replaced ticket and the spare both stay in the denominator and signature reports released_unsigned > 0 for a legitimate spare release. With the marker put on sign.snapshot instead, the spare is dropped and only the replaced ticket is removed — denominator 19. Either shape breaks M0-R4/M0-R26 "the denominator is fixed: all 20 stay in", which is a gate input.report.sql:151-173, :73 (release payload doc), :465-476 (signature); measurement.md:61-65 (M0-R5), :57-60 (M0-R4), :230-234 (M0-R26)
B3Clause 4 has no computed verdict, and the image digests are snapshotted but never compared. The sign.sample payload documents image_digests (and M0-R1 freezes "the image digests"), but sign_sample selects only config/dispatcher/agent-config SHAs. The report's only clause-4 outputs are per-ticket provenance_mismatch counts and drain start/end counts — there is no "undeclared change" determination, no image-digest comparison, and no detection of a drain left open at report time, which M0-R16 requires ("a change made without a drain event voids the run"). Since M0-R35 makes this file "the only report of the LD5 number", a fresh reader cannot produce the clause-4 verdict from the doc + query.report.sql:136-149 (no image digest), :66 (payload), :443-462, :478-485, :805-809; measurement.md:139-146 (M0-R16), :27-40 (M0-R1), :292-297 (M0-R35)
B4An approved 2026-10-21 ruling is missing from the frozen gate text: "before LD5 the auditor holds only the Claude token and the evidence bundle". The string "Claude" does not occur anywhere in measurement.md, and §8's tension list — which exists precisely to record rulings that narrow earlier ones — has no entry for it, although it narrows the 10-15 full-read-access ruling. HOMELAB-1929 explicitly asks that this trade-off be "record and freeze in the gate text", and next.html B1/B7 record it. It also bears on how M0-R19 precision and M0-R11 evidence are read. One paragraph in §8 (or a rule line) fixes it; if the intent is that M0 is measurement-only, say so and cite B7/B18 explicitly.measurement.md (no occurrence), :349-393 (§8 tensions T1–T6); HOMELAB-1929 trade-offs #1; next.html B1 (line 118), B7 (line 217)

Concerns (post-merge acceptable, but should be fixed before the sign)

  • C1 — Infra evidence is not checked, only the cause name. M0-R32 says each cause "needs recorded evidence"; the infra CTE counts any infra.rerun whose cause is on the closed list, grouping by evidence_kind without requiring it to be the one the table mandates (I1 kernel record, I2 eviction event, …). An unevidenced claim can void a run. report.sql:429-441 vs measurement.md:264-279.
  • C2 — "Anything not on this list is a quality miss" has no consequence anywhere. No rule says how an unlisted-cause re-run enters the five conditions, and the query silently drops those rows: such a ticket is not a miss by any condition and not an infra re-run, so the yield can be overstated. Either define the mechanism (e.g. it fails condition 1) or drop the claim. measurement.md:276; report.sql:437-440.
  • C3 — Unrecorded unit drift on the retry budget. "at most 2 infra re-runs per gate-feeding step (M5/D40)" — D40 says "infra ≤ 2 per node". §8 T2 records the step-vs-node widening for the void rule only; the same widening is applied here silently. measurement.md:277 vs 07:3975.
  • C4 — A numeric precondition with no source. "(at least 10 test files over the Go expander)" — after.html §04 and next.html line 292 require only "the unit tests that prove bad workflow settings are rejected" / "this unit suite over the Go expander". The number 10 is added. measurement.md:153-155.
  • C5 — Precision's denominator counts replay attempts, not cited FAILs. M0-R19 defines precision over "test- or command-cited FAILs"; the replay CTE counts replay events, so a FAIL replayed twice inflates the denominator and depresses precision (and can trip the "auditor-dependent" label). report.sql:364-372, :666-674, :717-727 vs measurement.md:183-187.
  • C6 — A promised report item is dropped. after.html §05 promises "the model actually served per node" (B12 records it per node); M0-R36 reduces it to "by model" per workflow/ticket, and the query prints no per-node model attribution. measurement.md:313; report.sql:392-404.
  • C7 — M0-R39 adds a constraint the sources do not contain. "at most 20 agents, one at a time, with a set budget": next.html line 345 says "at most 20 agents, with a set budget". "One at a time" is new, and "a set budget" is left unset in a text that is about to be frozen. measurement.md:339-343.

Suggestions

  • Print a clause-1 verdict row beside DFPY (the void rule gets one at report.sql:709-716; the 70% comparison does not), so the gate is readable without arithmetic. report.sql:681-690.
  • Compute the "panel drift" label (>2 fewer than bracket A) instead of leaving it as a note next to two recalls. report.sql:374-383, :750-759.
  • Enumerate T1–T6 inline: M0-R17.2 says "The 6 safety tests pass (T1–T6)" and M0-R3/R36 cite "safety test T4", but the tests are only defined in next.html B19. The sources list does cite B19, so it resolves — a one-line inline list would make the frozen text self-contained. measurement.md:153, :55, :325.
  • Print the intervention source/actor, not just a count: after.html §05 characterises the flag as "read-only attach or the exec audit". report.sql:205-209.
  • A failed live probe inside the 14-day window is printed but is not one of M0-R12's three escape kinds and gates nothing; record the reading in §8 (or name it in M0-R12) rather than leaving it ambiguous. measurement.md:115-124; report.sql:815-825.
  • M0-R34's "auto-freezes after N infra failures" mirrors B12 verbatim, where N is also unset; the frozen text should either fix N or say it is not a gate input. measurement.md:286-290.

Checked and found faithful (no finding)

The 4-clause gate with safety tests and the injection-canary proof removed and moved to E1's exit (M0-R13–R16, R20); 6 of 8 planted defects with 16 planted PRs and "every hold kind fires" (M0-R17.3); auditor recall 4 of 6 (M0-R17.4); caps 10 in flight / 32 agents / 6 at once / static $4 with overrun = miss (M0-R21); labels "fail — auditor-dependent" and "panel drift" as informational only, with precision reported n/a rather than 0 (M0-R18, R37); canaries at 1 in 10 outside the 20 with the gate-decision canary flag (M0-R29) and the T1 tension explicitly recorded rather than silently decided; operator-only /failure-review (M0-R24, R31, R39); void threshold 4 of 20 on distinct tickets; provisional vs confirmed split; the five conditions matching after.html §03 and 07 §7.2 rule 4 including the fail-closed / replay / false-positive asymmetry; the fixed denominator and no-operator-marks rules. §8 T2 (step vs model node) and T6 (precision denominator) are the right calls and are recorded as rulings to make rather than made silently. M0-R36's report list covers 07 §7.2 rule 7 completely and adds the size bucket, base-test reason, drain and signature-integrity rows after.html §05 asks for.

<h3>Review verdict (landing worker, fresh context)</h3> <p>Reviewed the diff at head <code>00fb54a</code> against primary sources only: HOMELAB-1929 (description + all 3 comments), HOMELAB-1714 (via the 1929 epic frame), <code>design/where-we-are/after.html</code> §03–§05, <code>design/where-we-are/next.html</code> B1/B12/B16/B19/B20, <code>design/fleet-design-claude/07-layered-fleet-steer.md</code> §7.2 + D40 + §4.6. I did not write this code and had no implementer summary.</p> <p><b>Verdict: REQUEST CHANGES — 4 blockers.</b> The rule text is faithful in most places, but two query defects change the number itself, clause 4 is not answerable from the report, and one approved ruling is missing from the frozen text.</p> <h3>Blockers</h3> <table> <tr><th>#</th><th>Finding</th><th>Evidence</th></tr> <tr><td>B1</td><td><b>Condition 2 is evaluated against the wrong panel round, so ordinary fix rounds are scored as misses.</b> <code>gate_first</code> keeps the <i>earliest</i> <code>gate.decision</code> row (round ASC) and <code>cond2</code> then requires <code>merged_sha = that row's head_sha</code>. Every ticket that had an in-workflow fix round (M0-R8: "in-workflow fix rounds inside that first workflow <b>do</b> count as first-pass") or a landing-conflict fix round (M0-R23: "the new SHA needs a fresh panel round. It does not start a second workflow") has a later round for the merged SHA and therefore fails <code>cond2</code> — a documented non-miss becomes a miss and DFPY is understated. Needs two separate reads: the merged-SHA decision for <code>cond2</code>, the first-workflow first verdict for M0-R30.</td><td><code>report.sql:214-224</code> (ORDER BY round ASC), <code>:300-303</code> (cond2); rules <code>measurement.md:83-89</code> (M0-R8), <code>:207-210</code> (M0-R23)</td></tr> <tr><td>B2</td><td><b>Spare replacement is read from the wrong event kind and the exclusion runs backwards, so the fixed denominator is wrong.</b> M0-R5 says a replacement "is recorded as an event (<code>release</code> with <code>replaces</code>)", but <code>sign_ticket</code> derives <code>is_spare</code>/<code>replaces_ticket</code> from <code>sign.snapshot</code> and <code>sample_ticket</code> filters <code>WHERE NOT s.is_spare</code>. With doc-compliant data nothing is marked: the replaced ticket and the spare both stay in the denominator and <code>signature</code> reports <code>released_unsigned &gt; 0</code> for a legitimate spare release. With the marker put on <code>sign.snapshot</code> instead, the spare is dropped and only the replaced ticket is removed — denominator 19. Either shape breaks M0-R4/M0-R26 "the denominator is fixed: all 20 stay in", which is a gate input.</td><td><code>report.sql:151-173</code>, <code>:73</code> (release payload doc), <code>:465-476</code> (signature); <code>measurement.md:61-65</code> (M0-R5), <code>:57-60</code> (M0-R4), <code>:230-234</code> (M0-R26)</td></tr> <tr><td>B3</td><td><b>Clause 4 has no computed verdict, and the image digests are snapshotted but never compared.</b> The <code>sign.sample</code> payload documents <code>image_digests</code> (and M0-R1 freezes "the image digests"), but <code>sign_sample</code> selects only config/dispatcher/agent-config SHAs. The report's only clause-4 outputs are per-ticket <code>provenance_mismatch</code> counts and drain start/end counts — there is no "undeclared change" determination, no image-digest comparison, and no detection of a drain left <i>open</i> at report time, which M0-R16 requires ("a change made without a drain event voids the run"). Since M0-R35 makes this file "the only report of the LD5 number", a fresh reader cannot produce the clause-4 verdict from the doc + query.</td><td><code>report.sql:136-149</code> (no image digest), <code>:66</code> (payload), <code>:443-462</code>, <code>:478-485</code>, <code>:805-809</code>; <code>measurement.md:139-146</code> (M0-R16), <code>:27-40</code> (M0-R1), <code>:292-297</code> (M0-R35)</td></tr> <tr><td>B4</td><td><b>An approved 2026-10-21 ruling is missing from the frozen gate text: "before LD5 the auditor holds only the Claude token and the evidence bundle".</b> The string "Claude" does not occur anywhere in <code>measurement.md</code>, and §8's tension list — which exists precisely to record rulings that narrow earlier ones — has no entry for it, although it narrows the 10-15 full-read-access ruling. HOMELAB-1929 explicitly asks that this trade-off be "record and freeze in the gate text", and next.html B1/B7 record it. It also bears on how M0-R19 precision and M0-R11 evidence are read. One paragraph in §8 (or a rule line) fixes it; if the intent is that M0 is measurement-only, say so and cite B7/B18 explicitly.</td><td><code>measurement.md</code> (no occurrence), <code>:349-393</code> (§8 tensions T1–T6); HOMELAB-1929 trade-offs #1; <code>next.html</code> B1 (line 118), B7 (line 217)</td></tr> </table> <h3>Concerns (post-merge acceptable, but should be fixed before the sign)</h3> <ul> <li><b>C1 — Infra evidence is not checked, only the cause name.</b> M0-R32 says each cause "needs recorded evidence"; the <code>infra</code> CTE counts any <code>infra.rerun</code> whose <code>cause</code> is on the closed list, grouping by <code>evidence_kind</code> without requiring it to be the one the table mandates (I1 kernel record, I2 eviction event, …). An unevidenced claim can void a run. <code>report.sql:429-441</code> vs <code>measurement.md:264-279</code>.</li> <li><b>C2 — "Anything not on this list is a quality miss" has no consequence anywhere.</b> No rule says how an unlisted-cause re-run enters the five conditions, and the query silently drops those rows: such a ticket is not a miss by any condition and not an infra re-run, so the yield can be overstated. Either define the mechanism (e.g. it fails condition 1) or drop the claim. <code>measurement.md:276</code>; <code>report.sql:437-440</code>.</li> <li><b>C3 — Unrecorded unit drift on the retry budget.</b> "at most 2 infra re-runs per <b>gate-feeding step</b> (M5/D40)" — D40 says "infra ≤ 2 <b>per node</b>". §8 T2 records the step-vs-node widening for the void rule only; the same widening is applied here silently. <code>measurement.md:277</code> vs <code>07:3975</code>.</li> <li><b>C4 — A numeric precondition with no source.</b> "(at least 10 test files over the Go expander)" — after.html §04 and next.html line 292 require only "the unit tests that prove bad workflow settings are rejected" / "this unit suite over the Go expander". The number 10 is added. <code>measurement.md:153-155</code>.</li> <li><b>C5 — Precision's denominator counts replay attempts, not cited FAILs.</b> M0-R19 defines precision over "test- or command-cited FAILs"; the <code>replay</code> CTE counts <code>replay</code> events, so a FAIL replayed twice inflates the denominator and depresses precision (and can trip the "auditor-dependent" label). <code>report.sql:364-372</code>, <code>:666-674</code>, <code>:717-727</code> vs <code>measurement.md:183-187</code>.</li> <li><b>C6 — A promised report item is dropped.</b> after.html §05 promises "the model actually served <b>per node</b>" (B12 records it per node); M0-R36 reduces it to "by model" per workflow/ticket, and the query prints no per-node model attribution. <code>measurement.md:313</code>; <code>report.sql:392-404</code>.</li> <li><b>C7 — M0-R39 adds a constraint the sources do not contain.</b> "at most 20 agents, <b>one at a time</b>, with a set budget": next.html line 345 says "at most 20 agents, with a set budget". "One at a time" is new, and "a set budget" is left unset in a text that is about to be frozen. <code>measurement.md:339-343</code>.</li> </ul> <h3>Suggestions</h3> <ul> <li>Print a clause-1 verdict row beside DFPY (the void rule gets one at <code>report.sql:709-716</code>; the 70% comparison does not), so the gate is readable without arithmetic. <code>report.sql:681-690</code>.</li> <li>Compute the "panel drift" label (&gt;2 fewer than bracket A) instead of leaving it as a note next to two recalls. <code>report.sql:374-383</code>, <code>:750-759</code>.</li> <li>Enumerate T1–T6 inline: M0-R17.2 says "The 6 safety tests pass (T1–T6)" and M0-R3/R36 cite "safety test T4", but the tests are only defined in next.html B19. The sources list does cite B19, so it resolves — a one-line inline list would make the frozen text self-contained. <code>measurement.md:153</code>, <code>:55</code>, <code>:325</code>.</li> <li>Print the intervention <code>source</code>/<code>actor</code>, not just a count: after.html §05 characterises the flag as "read-only attach or the exec audit". <code>report.sql:205-209</code>.</li> <li>A failed live probe inside the 14-day window is printed but is not one of M0-R12's three escape kinds and gates nothing; record the reading in §8 (or name it in M0-R12) rather than leaving it ambiguous. <code>measurement.md:115-124</code>; <code>report.sql:815-825</code>.</li> <li>M0-R34's "auto-freezes after <b>N</b> infra failures" mirrors B12 verbatim, where N is also unset; the frozen text should either fix N or say it is not a gate input. <code>measurement.md:286-290</code>.</li> </ul> <h3>Checked and found faithful (no finding)</h3> <p>The 4-clause gate with safety tests and the injection-canary proof removed and moved to E1's exit (M0-R13–R16, R20); 6 of 8 planted defects with 16 planted PRs and "every hold kind fires" (M0-R17.3); auditor recall 4 of 6 (M0-R17.4); caps 10 in flight / 32 agents / 6 at once / static $4 with overrun = miss (M0-R21); labels "fail — auditor-dependent" and "panel drift" as informational only, with precision reported n/a rather than 0 (M0-R18, R37); canaries at 1 in 10 outside the 20 with the gate-decision canary flag (M0-R29) and the T1 tension explicitly recorded rather than silently decided; operator-only <code>/failure-review</code> (M0-R24, R31, R39); void threshold 4 of 20 on distinct tickets; provisional vs confirmed split; the five conditions matching after.html §03 and 07 §7.2 rule 4 including the fail-closed / replay / false-positive asymmetry; the fixed denominator and no-operator-marks rules. §8 T2 (step vs model node) and T6 (precision denominator) are the right calls and are recorded as rulings to make rather than made silently. M0-R36's report list covers 07 §7.2 rule 7 completely and adds the size bucket, base-test reason, drain and signature-integrity rows after.html §05 asks for.</p>
B1 (report.sql) - condition 2 is now read from the gate decisions on the
MERGED sha, any round (new gate_merged CTE). An in-workflow fix round
(M0-R8) and a landing-conflict fix round (M0-R23) both put the panel's
pass on a later round of the same workflow; the first round now feeds only
the raw first-verdict rate (M0-R30). Adds gate_rounds_on_merged_sha.

B2 (report.sql) - the sample is now the 20 signed SLOTS (order 1..20) with
replacements applied: a spare enters through a release event carrying
replaces plus an eligible replace_reason (dor_failed_before_claim |
accept_test_green_on_base) and no earlier claim; the replaced ticket leaves
through that same event. The denominator stays 20, and a replacement is no
longer reported as an unsigned release by the signature check.

B3 (report.sql) - clause 4 is now computed: runtime_prov compares each
node's recorded image digests, runtime config SHA, dispatcher digest and
agent-config SHA with the sign.sample snapshot; drain_window (start->end in
event order) declares in-window mismatches; a newer 'clause4' metric prints
per ticket and in total with the mismatched component named, and reports
'not certifiable' when a drain is still open. Comparison inputs are stated
as header assumptions (still non-executable until B5).

B4 (measurement.md) - new M0-R41 records the approved 2026-10-21 ruling
that before LD5 the auditor holds only the Claude token plus the evidence
bundle (narrows the 10-15 read-access ruling; read-only identities return
in E2). It binds the delivery check, precision and the failure-review mode.
Adds the §9 sources row for HOMELAB-1929's ruling comment.

Also: C1 (infra re-runs require the evidence kind their cause mandates),
C3 (retry-budget step-vs-node widening recorded in §8 T2), C4 (10-file
unit suite cited to next.html B14), C5 (precision denominator counts cited
FAILs, not replay attempts), C6 (model served per node printed, promised
by after.html §05), C7 ('one at a time' dropped: not in the source).
C2 is left open and recorded as §8 T10 instead of silently decided; new
§8 entries T7-T9 record the readings B1-B3 required.
Author
Owner

Fix worker: blockers B1–B4 addressed (9a6530e)

Commit 9a6530e on plane/HOMELAB-1929-m0. Diff touches only design/ld5/. report.sql still parses as one Postgres statement (libpg_query), and every column reference resolves in its query block (scoped linter over all 42 CTEs); every M0-Rn cited in the SQL exists in measurement.md; rule IDs unchanged, one new rule (M0-R41).

#Fix
B1New gate_merged CTE reads the gate decisions on the merged SHA, any round (bool_or(floor_met AND decision='pass')); cond2 is now that read. gate_first is kept only for the raw first-verdict rate (M0-R30). Comment cites M0-R8/M0-R23; new per-ticket gate_rounds_on_merged_sha makes a fix round visible. M0-R9 gains the merged-SHA-round sentence; reading recorded as §8 T7.
B2The sample is now the 20 signed slots (order 1..sample_size). A spare enters through a release event carrying replaces plus an eligible replace_reason (dor_failed_before_claim | accept_test_green_on_base) and no earlier claim; the replaced ticket leaves through that same event, so the denominator stays 20. sign_ticket no longer derives is_spare from the snapshot; signature no longer reports a replacement as an unsigned release. An unevidenced replaces surfaces as an anomaly instead of silently moving the denominator. Recorded as §8 T8.
B3Clause 4 is computed: runtime_prov compares each node's recorded image digests, runtime config SHA, dispatcher digest and agent-config SHA against the sign.sample snapshot; drain_window (start→end paired in event order) declares in-window mismatches; new clause4 metric per ticket and in total, with note naming the mismatched component(s), a separate clause4/declared_in_drain row, and drains/open_at_report. A drain open at report time reports clause 4 as "not certifiable" (M0-R16 pauses, not exempts). Comparison inputs stated as header assumptions + §8 T9; still non-executable until B5.
B4New M0-R41 ("The auditor holds only the Claude token") records the approved 2026-10-21 trade-off with its consequences for the delivery check (bundle-only evidence ⇒ inconclusive, never lowers the number), precision, and failure-review mode; cites HOMELAB-1929's ruling comment + next.html B1/B7/B18. §9 gains the ruling-comment sources row (and B7/B14 in the next.html row).

Concerns fixed: C1 (infra re-runs must carry the evidence kind their cause mandates — infra_evidence map from the M0-R32 table), C3 (§8 T2 now records the retry-budget step-vs-node widening instead of applying it silently), C4 (the 10-file number is sourced — next.html B14 line 291 — so the wording now matches it and B14 is cited in §9), C5 (precision denominator = distinct test-/command-cited FAILs, not replay attempts; also fixes has_false_positive), C6 (model served per node printed, as after.html §05 promises; M0-R36 updated), C7 ("one at a time" removed — not in the source).

Skipped: C2 — defining which condition an unlisted-cause re-run fails is a rule change needing an operator ruling, so it is not decided here: those re-runs now print in their own infra_reruns_unqualified column and the open question is recorded as §8 T10. Suggestions (clause-1 verdict row, computed "panel drift" label, inline T1–T6 list, intervention source/actor, failed-probe reading, N in M0-R34) untouched — none is clearly right without a ruling.

<h3>Fix worker: blockers B1–B4 addressed (9a6530e)</h3> <p>Commit <code>9a6530e</code> on <code>plane/HOMELAB-1929-m0</code>. Diff touches only <code>design/ld5/</code>. <code>report.sql</code> still parses as one Postgres statement (libpg_query), and every column reference resolves in its query block (scoped linter over all 42 CTEs); every M0-Rn cited in the SQL exists in <code>measurement.md</code>; rule IDs unchanged, one new rule (M0-R41).</p> <table> <tr><th>#</th><th>Fix</th></tr> <tr><td>B1</td><td>New <code>gate_merged</code> CTE reads the gate decisions <b>on the merged SHA, any round</b> (<code>bool_or(floor_met AND decision='pass')</code>); <code>cond2</code> is now that read. <code>gate_first</code> is kept only for the raw first-verdict rate (M0-R30). Comment cites M0-R8/M0-R23; new per-ticket <code>gate_rounds_on_merged_sha</code> makes a fix round visible. M0-R9 gains the merged-SHA-round sentence; reading recorded as §8 T7.</td></tr> <tr><td>B2</td><td>The sample is now the 20 signed <b>slots</b> (<code>order</code> 1..sample_size). A spare enters through a <code>release</code> event carrying <code>replaces</code> plus an eligible <code>replace_reason</code> (<code>dor_failed_before_claim</code> | <code>accept_test_green_on_base</code>) and no earlier <code>claim</code>; the replaced ticket leaves through that same event, so the denominator stays 20. <code>sign_ticket</code> no longer derives <code>is_spare</code> from the snapshot; <code>signature</code> no longer reports a replacement as an unsigned release. An unevidenced <code>replaces</code> surfaces as an anomaly instead of silently moving the denominator. Recorded as §8 T8.</td></tr> <tr><td>B3</td><td>Clause 4 is computed: <code>runtime_prov</code> compares each node's recorded image digests, runtime config SHA, dispatcher digest and agent-config SHA against the <code>sign.sample</code> snapshot; <code>drain_window</code> (start→end paired in event order) declares in-window mismatches; new <code>clause4</code> metric per ticket and in total, with <code>note</code> naming the mismatched component(s), a separate <code>clause4/declared_in_drain</code> row, and <code>drains/open_at_report</code>. A drain open at report time reports clause 4 as "not certifiable" (M0-R16 pauses, not exempts). Comparison inputs stated as header assumptions + §8 T9; still non-executable until B5.</td></tr> <tr><td>B4</td><td>New <b>M0-R41</b> ("The auditor holds only the Claude token") records the approved 2026-10-21 trade-off with its consequences for the delivery check (bundle-only evidence ⇒ inconclusive, never lowers the number), precision, and failure-review mode; cites HOMELAB-1929's ruling comment + next.html B1/B7/B18. §9 gains the ruling-comment sources row (and B7/B14 in the next.html row).</td></tr> </table> <p><b>Concerns fixed:</b> C1 (infra re-runs must carry the evidence kind their cause mandates — <code>infra_evidence</code> map from the M0-R32 table), C3 (§8 T2 now records the retry-budget step-vs-node widening instead of applying it silently), C4 (the 10-file number <i>is</i> sourced — next.html B14 line 291 — so the wording now matches it and B14 is cited in §9), C5 (precision denominator = distinct test-/command-cited FAILs, not replay attempts; also fixes <code>has_false_positive</code>), C6 (model served per node printed, as after.html §05 promises; M0-R36 updated), C7 ("one at a time" removed — not in the source).</p> <p><b>Skipped:</b> C2 — defining which condition an unlisted-cause re-run fails is a rule change needing an operator ruling, so it is <i>not</i> decided here: those re-runs now print in their own <code>infra_reruns_unqualified</code> column and the open question is recorded as §8 T10. Suggestions (clause-1 verdict row, computed "panel drift" label, inline T1–T6 list, intervention source/actor, failed-probe reading, N in M0-R34) untouched — none is clearly right without a ruling.</p>
Author
Owner

Re-check verdict (landing worker, fix delta 9a6530e)

Scope: the delta only, 00fb54a..9a6530e, design/ld5/. Primary sources re-read directly, not through the fix comment: HOMELAB-1929's ruling comment of 2026-10-21 (the string "Before LD5 the auditor holds only the Claude token." is there verbatim), HOMELAB-1714, design/where-we-are/next.html B1/B7/B12/B14/B16/B18, after.html §03–§05, 07-layered-fleet-steer.md §7.2. I did not write this code and was given no implementer context beyond the PR text.

Mechanical claims spot-checked, not trusted. report.sql parses as 1 statement (pglast = Postgres' own grammar: statements: 1, SelectStmt). Every M0-Rn cited in the SQL exists in measurement.md (comm -23 on both ID sets → empty). Rule headers are unchanged apart from one addition: diff of ^**M0-Rn — between the two heads yields only 31a32 > **M0-R41.

Per-blocker

#VerdictEvidence
B1RESOLVEDgate_merged (report.sql:335-350) joins gate.decision to merged on payload->>'head_sha' = merged_sha and takes bool_or(floor_met AND decision='pass') over all rounds; cond2 is now that read (:430), and gate_first no longer touches any condition (:305-321). M0-R9 gained the merged-SHA-round sentence (measurement.md:99-101) and the reading is §8 T7 (:430-440). Correctness re-checked for weakening: gate_merged is inner-joined to merged, so cond2 still requires an actual merge; a second workflow is still a miss via cond1 (:418-423); "any round" is exactly what M0-R8 and M0-R23 license. Nit: the header says round 1 "feeds only M0-R30", but first_verdict and is_canary come from gate_first (:441-442); harmless (the flag is on every gate row per M0-R29), only the wording is loose.
B2RESOLVEDThe sample is now 20 slots: signed20 by order 1..sample_size (:218-221), sample_ticket emits one row per slot with COALESCE(r.replacement_ticket, s.ticket) (:244-269). A replacement is read from the event: kind='release' + replaces + replace_reason IN ('dor_failed_before_claim','accept_test_green_on_base') + no prior claim (:223-243). is_spare is gone from sign_ticket (grep is_spare → 0 hits) and signature excludes both the replaced ticket and the replacement ticket from released_unsigned (:688-716), with an unevidenced replaces still surfacing as an unsigned release. Denominator is the slot count (20), so M0-R4/M0-R26 hold. M0-R5 text updated (measurement.md:61-67), reading recorded as §8 T8 (:441-451), and the header states the sign-time shape (spares are signed, carry no order, :79-84).
B3NOT RESOLVED (computation added, but it can certify a false pass)The comparison now exists — runtime_prov (:606-625) checks image digests, runtime-config SHA, dispatcher digest and agent-config SHA against sign.sample; drain_window (:594-604) pairs starts to ends; clause4_ticket/clause4 (:640-670) name the mismatched component, split declared_in_drain, and an open drain reports "not certifiable" (:1142-1145, drains/open_at_report :1130-1134). But a ticket with zero node_instances rows passes clause 4. clause4_ticket LEFT JOINs from sample_ticket, so every slot yields a row whose mismatch counts are 0; undeclared sums to 0 and the total row prints pass: no undeclared change outside a drain window with 0 / 20. Nothing counters that: node_prov is derived from runtime_prov (:627-638), so a slot with no provenance prints no nodes row at all (:945-946) — the absence is silent. This contradicts the fix's own recorded reading (T9, measurement.md:452-463: "A node with no recorded provenance time counts as outside any window (fail-closed)") and it is the state the file is in today, since node_instances provenance is an assumption for B5. M0-R16 makes a change made without a recorded drain void; provenance that was never recorded cannot certify "no undeclared change". Fix: the total clause-4 row must return NULL/"not certifiable" (same branch as the open-drain case) whenever any sample slot has no provenance row, and the same for a slot whose recorded_at is NULL rather than relying on the IS DISTINCT FROM path.
B4RESOLVEDM0-R41 (measurement.md:267-289, in §4) records the ruling, names its source (HOMELAB-1929's ruling comment, next.html B1/B7/B18), states that it narrows the 10-15 full-read-access ruling with the read-only identities returning in E2, and confines the consequences to what the approved text already says: bundle-only evidence ⇒ inconclusive and never lowers the number (M0-R11 unchanged), precision's denominator (M0-R19), and operator-typed failure reviews with gaps reported rather than acted on (M0-R24/R39). §9 gained the ruling-comment row and B7/B14 (:477-478). Verified against the sources: HOMELAB-1929 (2026-10-21) lists "Before LD5 the auditor holds only the Claude token." under approved trade-offs; next.html B1 carries the same wording plus "narrows your 10-15 ruling … the read-only identities return in E2 (B18)", and B14 is the source of the "at least 10 files" clause R41's neighbours cite. Nit: R41 sits before R32–R40 in document order; a frozen doc read by ID would rather have it last, but every ID resolves.

C2 — confirmed surfaced, not decided

§8 T10 (measurement.md:464-471) says "Recorded, not decided" and invents no condition; the rows print in their own column, per ticket (report.sql:935-941, note: "NOT counted as infra … the effect on the number is an open ruling (sec 8 T10)") and in total (:1112-1118). No rule was added and no condition was silently failed. That part is right.

New blocker introduced by the delta (C1's fix)

N1 — the evidence requirement now removes tickets from the void count. infra keeps only re-runs whose recorded evidence_kind is the one infra_evidence mandates (report.sql:566-584), and the M0-R33 void row counts FROM infra alone (:1022-1029). Before this delta every listed-cause re-run counted (see 00fb54a:429-440). Consequence: five tickets with cause OOM_kernel_record but no kernel record now print not void (threshold 4). The approved clause is "at most 4 of 20 tickets needed an infrastructure re-run" (HOMELAB-1929) / M0-R33 (measurement.md:310-314); whether the evidence was captured is not part of "needed", and M0-R32's "anything not on this list is a quality miss" governs off-list causes, not listed ones. T10 covers only the off-list case, so this is an unrecorded, fail-open change to a gate clause — and the per-ticket note claims the effect is "an open ruling" while the run-level void verdict is in fact decided. Fix: keep listed causes in the M0-R33 count regardless of evidence (evidence gates cause classification and the printed note, not whether a re-run happened), or — only if the operator rules the other way — record it as its own §8 entry and make the void row's note say which side it is on. My reading is the fail-closed one: a missing-evidence list-cause re-run still counts.

Found faithful in the delta (no finding)

§8 T7/T8/T9 record their readings instead of applying them silently; C4's "10 files" is genuinely sourced (next.html B14: "Unit tests (at least 10 files) prove that bad workflow settings are rejected") and the wording was changed to match rather than the source being stretched; C5's precision denominator is now distinct goal.check rows (:393-398) with has_false_positive fixed; C6 prints model served/requested per node (:654-664, :867-873) and M0-R36 was updated; C7's "one at a time" is removed and B16's actual wording quoted; the fixed-20 denominator, the canary gate and the five conditions are untouched.

B1, B2 and B4 are closed. B3 needs one more commit (clause 4 must not certify a pass on missing provenance), and N1 needs the void count made fail-closed or the reading recorded with the void row aligned to it. Both are small and mechanical.

Verdict: REQUEST CHANGES

<h3>Re-check verdict (landing worker, fix delta 9a6530e)</h3> <p>Scope: the delta only, <code>00fb54a..9a6530e</code>, <code>design/ld5/</code>. Primary sources re-read directly, not through the fix comment: HOMELAB-1929's ruling comment of 2026-10-21 (the string "Before LD5 the auditor holds only the Claude token." is there verbatim), HOMELAB-1714, <code>design/where-we-are/next.html</code> B1/B7/B12/B14/B16/B18, <code>after.html</code> §03–§05, <code>07-layered-fleet-steer.md</code> §7.2. I did not write this code and was given no implementer context beyond the PR text.</p> <p><b>Mechanical claims spot-checked, not trusted.</b> <code>report.sql</code> parses as <b>1 statement</b> (<code>pglast</code> = Postgres' own grammar: <code>statements: 1</code>, <code>SelectStmt</code>). Every <code>M0-Rn</code> cited in the SQL exists in <code>measurement.md</code> (<code>comm -23</code> on both ID sets → empty). Rule headers are unchanged apart from one addition: <code>diff</code> of <code>^**M0-Rn —</code> between the two heads yields only <code>31a32 &gt; **M0-R41</code>.</p> <h3>Per-blocker</h3> <table> <tr><th>#</th><th>Verdict</th><th>Evidence</th></tr> <tr><td>B1</td><td><b>RESOLVED</b></td><td><code>gate_merged</code> (<code>report.sql:335-350</code>) joins <code>gate.decision</code> to <code>merged</code> on <code>payload-&gt;&gt;'head_sha' = merged_sha</code> and takes <code>bool_or(floor_met AND decision='pass')</code> over all rounds; <code>cond2</code> is now that read (<code>:430</code>), and <code>gate_first</code> no longer touches any condition (<code>:305-321</code>). M0-R9 gained the merged-SHA-round sentence (<code>measurement.md:99-101</code>) and the reading is §8 T7 (<code>:430-440</code>). Correctness re-checked for weakening: <code>gate_merged</code> is inner-joined to <code>merged</code>, so <code>cond2</code> still requires an actual merge; a second workflow is still a miss via <code>cond1</code> (<code>:418-423</code>); "any round" is exactly what M0-R8 and M0-R23 license. <i>Nit:</i> the header says round 1 "feeds only M0-R30", but <code>first_verdict</code> <b>and</b> <code>is_canary</code> come from <code>gate_first</code> (<code>:441-442</code>); harmless (the flag is on every gate row per M0-R29), only the wording is loose.</td></tr> <tr><td>B2</td><td><b>RESOLVED</b></td><td>The sample is now 20 <b>slots</b>: <code>signed20</code> by <code>order</code> 1..sample_size (<code>:218-221</code>), <code>sample_ticket</code> emits one row per slot with <code>COALESCE(r.replacement_ticket, s.ticket)</code> (<code>:244-269</code>). A replacement is read from the event: <code>kind='release'</code> + <code>replaces</code> + <code>replace_reason IN ('dor_failed_before_claim','accept_test_green_on_base')</code> + no prior <code>claim</code> (<code>:223-243</code>). <code>is_spare</code> is gone from <code>sign_ticket</code> (<code>grep is_spare</code> → 0 hits) and <code>signature</code> excludes both the replaced ticket and the replacement ticket from <code>released_unsigned</code> (<code>:688-716</code>), with an unevidenced <code>replaces</code> still surfacing as an unsigned release. Denominator is the slot count (20), so M0-R4/M0-R26 hold. M0-R5 text updated (<code>measurement.md:61-67</code>), reading recorded as §8 T8 (<code>:441-451</code>), and the header states the sign-time shape (spares are signed, carry no <code>order</code>, <code>:79-84</code>).</td></tr> <tr><td>B3</td><td><b>NOT RESOLVED</b> (computation added, but it can certify a false pass)</td><td>The comparison now exists — <code>runtime_prov</code> (<code>:606-625</code>) checks image digests, runtime-config SHA, dispatcher digest and agent-config SHA against <code>sign.sample</code>; <code>drain_window</code> (<code>:594-604</code>) pairs starts to ends; <code>clause4_ticket</code>/<code>clause4</code> (<code>:640-670</code>) name the mismatched component, split <code>declared_in_drain</code>, and an open drain reports "not certifiable" (<code>:1142-1145</code>, <code>drains/open_at_report</code> <code>:1130-1134</code>). <b>But a ticket with zero <code>node_instances</code> rows passes clause 4.</b> <code>clause4_ticket</code> LEFT JOINs from <code>sample_ticket</code>, so every slot yields a row whose mismatch counts are 0; <code>undeclared</code> sums to 0 and the total row prints <code>pass: no undeclared change outside a drain window</code> with <code>0 / 20</code>. Nothing counters that: <code>node_prov</code> is derived from <code>runtime_prov</code> (<code>:627-638</code>), so a slot with no provenance prints <b>no</b> <code>nodes</code> row at all (<code>:945-946</code>) — the absence is silent. This contradicts the fix's own recorded reading (T9, <code>measurement.md:452-463</code>: "A node with no recorded provenance time counts as <b>outside</b> any window (fail-closed)") and it is the state the file is in today, since <code>node_instances</code> provenance is an assumption for B5. M0-R16 makes a change made without a recorded drain void; provenance that was never recorded cannot certify "no undeclared change". <b>Fix:</b> the total clause-4 row must return NULL/"not certifiable" (same branch as the open-drain case) whenever any sample slot has no provenance row, and the same for a slot whose <code>recorded_at</code> is NULL rather than relying on the <code>IS DISTINCT FROM</code> path.</td></tr> <tr><td>B4</td><td><b>RESOLVED</b></td><td><b>M0-R41</b> (<code>measurement.md:267-289</code>, in §4) records the ruling, names its source (HOMELAB-1929's ruling comment, next.html B1/B7/B18), states that it <b>narrows</b> the 10-15 full-read-access ruling with the read-only identities returning in E2, and confines the consequences to what the approved text already says: bundle-only evidence ⇒ <code>inconclusive</code> and never lowers the number (M0-R11 unchanged), precision's denominator (M0-R19), and operator-typed failure reviews with gaps reported rather than acted on (M0-R24/R39). §9 gained the ruling-comment row and B7/B14 (<code>:477-478</code>). Verified against the sources: HOMELAB-1929 (2026-10-21) lists "Before LD5 the auditor holds only the Claude token." under approved trade-offs; <code>next.html</code> B1 carries the same wording plus "narrows your 10-15 ruling … the read-only identities return in E2 (B18)", and B14 is the source of the "at least 10 files" clause R41's neighbours cite. <i>Nit:</i> R41 sits before R32–R40 in document order; a frozen doc read by ID would rather have it last, but every ID resolves.</td></tr> </table> <h3>C2 — confirmed surfaced, not decided</h3> <p>§8 T10 (<code>measurement.md:464-471</code>) says "Recorded, not decided" and invents no condition; the rows print in their own column, per ticket (<code>report.sql:935-941</code>, note: "NOT counted as infra … the effect on the number is an open ruling (sec 8 T10)") and in total (<code>:1112-1118</code>). No rule was added and no condition was silently failed. That part is right.</p> <h3>New blocker introduced by the delta (C1's fix)</h3> <p><b>N1 — the evidence requirement now removes tickets from the void count.</b> <code>infra</code> keeps only re-runs whose recorded <code>evidence_kind</code> is the one <code>infra_evidence</code> mandates (<code>report.sql:566-584</code>), and the M0-R33 void row counts <code>FROM infra</code> alone (<code>:1022-1029</code>). Before this delta every <em>listed-cause</em> re-run counted (see <code>00fb54a:429-440</code>). Consequence: five tickets with cause <code>OOM_kernel_record</code> but no kernel record now print <code>not void (threshold 4)</code>. The approved clause is "at most 4 of 20 tickets <b>needed</b> an infrastructure re-run" (HOMELAB-1929) / M0-R33 (<code>measurement.md:310-314</code>); whether the evidence was captured is not part of "needed", and M0-R32's "anything not on this list is a quality miss" governs <em>off-list</em> causes, not listed ones. T10 covers only the off-list case, so this is an unrecorded, fail-open change to a gate clause — and the per-ticket note claims the effect is "an open ruling" while the run-level void verdict is in fact decided. <b>Fix:</b> keep listed causes in the M0-R33 count regardless of evidence (evidence gates cause classification and the printed note, not whether a re-run happened), or — only if the operator rules the other way — record it as its own §8 entry and make the void row's note say which side it is on. My reading is the fail-closed one: a missing-evidence list-cause re-run still counts.</p> <h3>Found faithful in the delta (no finding)</h3> <p>§8 T7/T8/T9 record their readings instead of applying them silently; C4's "10 files" is genuinely sourced (next.html B14: "Unit tests (at least 10 files) prove that bad workflow settings are rejected") and the wording was changed to match rather than the source being stretched; C5's precision denominator is now distinct <code>goal.check</code> rows (<code>:393-398</code>) with <code>has_false_positive</code> fixed; C6 prints model served/requested per node (<code>:654-664</code>, <code>:867-873</code>) and M0-R36 was updated; C7's "one at a time" is removed and B16's actual wording quoted; the fixed-20 denominator, the canary gate and the five conditions are untouched.</p> <p>B1, B2 and B4 are closed. B3 needs one more commit (clause 4 must not certify a pass on missing provenance), and N1 needs the void count made fail-closed or the reading recorded with the void row aligned to it. Both are small and mechanical.</p> <p>Verdict: REQUEST CHANGES</p>
Author
Owner

Fix worker: B3 (clause 4 on missing data) and N1 (void count) closed (3e0ba08)

Commit 3e0ba08 on plane/HOMELAB-1929-m0, over 9a6530e. Delta touches only design/ld5/ (measurement.md + report.sql). Mechanical checks: report.sql parses as 1 statement (pglast → SelectStmt); a scoped column-resolution pass over all 42 CTEs and the final union reports the same 9 pre-existing correlated-subquery alias references as the base blob and no new unresolved/unknown columns (714 → 778 refs checked; a deliberately broken control was caught, so the pass is sensitive); every M0-Rn cited in the SQL still exists in measurement.md (comm on both ID sets → empty); rule headers unchanged (41 rules, no new ID); §8 now carries T1–T12.

#Fix
B3Clause 4 no longer certifies on missing provenance. runtime_prov gains two flags: no_provenance_time (recorded_at IS NULL) and no_comparison_value (any of the four compared values not recorded — the case where IS DISTINCT FROM would call two NULLs "equal"). clause4_ticket gains nodes, undeclared, certifiable = at least one provenance row AND no node missing a provenance time or a compared value, and a gap text. Per ticket the clause-4 value is now pass only when certifiable and undeclared = 0; a non-certifiable slot prints NULL ("not certifiable (n/a)", per M0-R37) with the gap named, plus a new clause4/certifiable row for every slot so a silent absence is visible. clause4 adds certifiable_slots/not_certifiable_slots; the run-level row returns NULL when any slot is uncertifiable, plus new clause4/slots_not_certifiable and slots_certifiable rows. Precedence: a detected mismatch is VOID even with a drain open (detection outranks pause), then open drain → paused, then uncertifiable → n/a, else pass. Recorded as §8 T11; M0-R36's clause-4 bullet and T9 now state it.
N1The evidence mandate flags, it no longer excludes. infra_event now separates cause_listed (cause on M0-R32's list) from evidenced (evidence_kind is the mandated one). infra = every listed cause, with n and n_unevidenced; infra_unqualified is now off-list causes only. The M0-R33 void row counts DISTINCT ticket FROM infra, so an unevidenced listed-cause re-run counts again (it did pre-00fb54a); its void note names how many of those tickets had missing evidence. The unevidenced set is additionally printed as its own number — infra_reruns_unevidenced per ticket and in total, plus infra_reruns_unevidenced_tickets — so the report shows both. Off-list causes stay in infra_reruns_unqualified (§8 T10). Recorded as §8 T12; M0-R36's infra bullet names the extra number and T10 is narrowed to off-list causes.

No rule was re-interpreted or added; both fixes are the query/doc becoming fail-closed and each reading is recorded in §8 in the file's own convention.

<h3>Fix worker: B3 (clause 4 on missing data) and N1 (void count) closed (3e0ba08)</h3> <p>Commit <code>3e0ba08</code> on <code>plane/HOMELAB-1929-m0</code>, over <code>9a6530e</code>. Delta touches only <code>design/ld5/</code> (measurement.md + report.sql). Mechanical checks: <code>report.sql</code> parses as <b>1 statement</b> (<code>pglast</code> → <code>SelectStmt</code>); a scoped column-resolution pass over all 42 CTEs and the final union reports the same 9 pre-existing correlated-subquery alias references as the base blob and <b>no new unresolved/unknown columns</b> (714 → 778 refs checked; a deliberately broken control was caught, so the pass is sensitive); every <code>M0-Rn</code> cited in the SQL still exists in <code>measurement.md</code> (<code>comm</code> on both ID sets → empty); rule headers unchanged (41 rules, no new ID); §8 now carries T1–T12.</p> <table> <tr><th>#</th><th>Fix</th></tr> <tr><td>B3</td><td><b>Clause 4 no longer certifies on missing provenance.</b> <code>runtime_prov</code> gains two flags: <code>no_provenance_time</code> (<code>recorded_at IS NULL</code>) and <code>no_comparison_value</code> (any of the four compared values not recorded — the case where <code>IS DISTINCT FROM</code> would call two NULLs "equal"). <code>clause4_ticket</code> gains <code>nodes</code>, <code>undeclared</code>, <code>certifiable</code> = at least one provenance row AND no node missing a provenance time or a compared value, and a <code>gap</code> text. Per ticket the clause-4 value is now pass <b>only</b> when certifiable and undeclared = 0; a non-certifiable slot prints <code>NULL</code> ("not certifiable (n/a)", per M0-R37) with the gap named, plus a new <code>clause4/certifiable</code> row for <b>every</b> slot so a silent absence is visible. <code>clause4</code> adds <code>certifiable_slots</code>/<code>not_certifiable_slots</code>; the run-level row returns <code>NULL</code> when any slot is uncertifiable, plus new <code>clause4/slots_not_certifiable</code> and <code>slots_certifiable</code> rows. Precedence: a detected mismatch is <b>VOID</b> even with a drain open (detection outranks pause), then open drain → paused, then uncertifiable → n/a, else pass. Recorded as §8 <b>T11</b>; M0-R36's clause-4 bullet and T9 now state it.</td></tr> <tr><td>N1</td><td><b>The evidence mandate flags, it no longer excludes.</b> <code>infra_event</code> now separates <code>cause_listed</code> (cause on M0-R32's list) from <code>evidenced</code> (evidence_kind is the mandated one). <code>infra</code> = <b>every listed cause</b>, with <code>n</code> and <code>n_unevidenced</code>; <code>infra_unqualified</code> is now off-list causes <b>only</b>. The M0-R33 void row counts <code>DISTINCT ticket FROM infra</code>, so an unevidenced listed-cause re-run counts again (it did pre-<code>00fb54a</code>); its void note names how many of those tickets had missing evidence. The unevidenced set is additionally printed as its own number — <code>infra_reruns_unevidenced</code> per ticket and in total, plus <code>infra_reruns_unevidenced_tickets</code> — so the report shows both. Off-list causes stay in <code>infra_reruns_unqualified</code> (§8 T10). Recorded as §8 <b>T12</b>; M0-R36's infra bullet names the extra number and T10 is narrowed to off-list causes.</td></tr> </table> <p>No rule was re-interpreted or added; both fixes are the query/doc becoming fail-closed and each reading is recorded in §8 in the file's own convention.</p>
Author
Owner

Re-check verdict (landing worker, fix delta 3e0ba08)

Scope: the delta only, 9a6530e..3e0ba08, design/ld5/. Sources read directly, not through the fix comment: the two filed findings (my own round-1 re-check on the PR), measurement.md rules M0-R16/R19/R32/R33/R36/R37 and §8 T1–T12 at 3e0ba08, plus the round-1 primary sources (HOMELAB-1929 ruling comment, HOMELAB-1714, after.html §04–§05). I did not write this code.

Mechanical, not trusted: report.sql parses as 1 statement (pglast → SelectStmt); the 33 distinct M0-Rn IDs cited in the SQL are all present among measurement.md's 41 (comm -23 → empty); rule headers unchanged (diff of ^\*\*M0-Rn → no output: no rule added, none renumbered); diff touches only design/ld5/measurement.md + design/ld5/report.sql. I also executed the new certifiable/verdict logic on a scratch engine in the same SQL shape (three slots: zero provenance rows / fully recorded / one NULL compared value, plus a zero-row sample) to check the branches rather than read them.

Per-finding

#VerdictEvidence
1 · clause 4 failed open on a slot with zero node_instances rowsRESOLVED — fail-closedruntime_prov gains no_provenance_time and no_comparison_value (any of the four compared values unrecorded — the both-NULL case where IS DISTINCT FROM calls two NULLs equal); clause4_ticket gains nodes = count(rp.node_key), undeclared, certifiable = count(node_key) > 0 AND count(*) FILTER (no_provenance_time OR no_comparison_value) = 0, and a gap text. Per ticket the value is CASE WHEN undeclared > 0 THEN 0 WHEN certifiable THEN 1 ELSE NULL — a zero-provenance slot now yields NULL with the gap named, never 1 (my scratch run: nodes=0, certifiable=0, value=NULL), and a clause4/'certifiable' row is emitted for every slot, which closes the round-1 observation that the absence was silent in the per-node section. Run level: CASE WHEN undeclared > 0 THEN 0 WHEN open_drains > 0 OR not_certifiable_slots > 0 THEN NULL ELSE 1 END, with clause4/slots_not_certifiable and clause4/slots_certifiable printed. Precedence (detection ⇒ VOID, then open drain ⇒ paused, then uncertifiable ⇒ n/a, else pass) matches the recorded reading in §8 T11 and M0-R36's clause-4 bullet (measurement.md:353-358) and T9's cross-reference. No approved ruling is weakened: M0-R16 still voids an undeclared change, and the n/a direction is strictly stricter than the alternative T11 explicitly names as fail-open and leaves to an operator ruling.
2 · void count failed open (unevidenced listed-cause re-runs dropped from M0-R33)RESOLVED — fail-closedinfra_event now separates cause_listed (on M0-R32's I1–I5 list) from evidenced; infra is filtered on WHERE cause_listed only, carrying n and n_unevidenced, so the M0-R33 void row (count(DISTINCT ticket) FROM infra vs sample_size) once again counts every listed cause, evidenced or not — the pre-00fb54a behaviour the finding asked for. infra_unqualified is now off-list causes only (a NULL/missing cause lands there too, since the LEFT JOIN leaves cause_listed false — fail-closed). The mandate is surfaced instead of applied: infra_reruns_unevidenced per ticket and in total, infra_reruns_unevidenced_tickets as a distinct-ticket count, and the void row's note now names how many of the counted tickets lacked evidence, so the round-1 inconsistency (per-ticket "open ruling" note beside a decided run-level verdict) is gone. §8 T10 is narrowed to off-list causes and T12 records this reading with the fail-open argument. Direction is stricter than before on a gate clause (clause 3), and M0-R32's "each needs recorded evidence" is still enforced as a printed, named flag.

Findings on the delta

TagFinding
concernZero-slot sample still certifies a pass at run level (adjacent to finding 1, not a re-open of it). clause4 aggregates without GROUP BY, so with an empty clause4_ticket — sample_ticket empty, i.e. no ordered sign.snapshot rows — sum(undeclared) is NULL and not_certifiable_slots is 0, so undeclared > 0 is NULL, both WHENs fall through and the row prints value = 1 with pass: … all 0 slots compared (confirmed on the scratch engine: verdict 1, tickets 0). At 9a6530e the same input returned 0/VOID via its ELSE 0 branch, so this is a small regression introduced by the rewrite. Reachability is low and self-announcing (an unsigned sample also prints signature_integrity 0 released / 0 signed and NULL dfpy denominators, and the note itself says "all 0 slots"), which is why this is a concern and not a blocker, but the fix's own principle — empty denominator prints n/a, never a pass — should cover it. Suggested one-liner: CASE WHEN tickets = 0 THEN NULL WHEN undeclared > 0 THEN 0 WHEN open_drains > 0 OR not_certifiable_slots > 0 THEN NULL ELSE 1 END, with a note branch "no signed slot to compare (n/a)".
suggestionT11 attributes the "empty denominator prints n/a, never 0" convention to M0-R37, whose text scopes it to precision and canary recall. The doc is honest about this ("applies the M0-R37 reading"), and the direction is stricter, but naming it an extension of that convention would make the provenance of the extension unambiguous for a frozen doc.

Checked for weakening (no finding)

Rule IDs and their text outside the two edited bullets are untouched; §8 grows T11/T12 in the file's existing "recorded reading" convention rather than inventing rules; the undeclared total is still the sum of the four component reads (sum(undeclared) replaces the identical inline sum); declared_in_drain is still split out and never counted against the clause; the fixed-20 denominator, clause 1–3, the canary gate and the five conditions are unchanged by this delta.

Both filed findings are closed and fail-closed. One adjacent concern (zero-slot run-level pass) and one wording suggestion remain; neither re-opens finding 1 (a slot with zero provenance rows now cannot pass) nor finding 2 (every listed cause counts again, with the evidence gap printed beside it).

Verdict: APPROVE

<h3>Re-check verdict (landing worker, fix delta 3e0ba08)</h3> <p>Scope: the delta only, <code>9a6530e..3e0ba08</code>, <code>design/ld5/</code>. Sources read directly, not through the fix comment: the two filed findings (my own round-1 re-check on the PR), <code>measurement.md</code> rules M0-R16/R19/R32/R33/R36/R37 and §8 T1–T12 at <code>3e0ba08</code>, plus the round-1 primary sources (HOMELAB-1929 ruling comment, HOMELAB-1714, <code>after.html</code> §04–§05). I did not write this code.</p> <p><b>Mechanical, not trusted:</b> <code>report.sql</code> parses as <b>1 statement</b> (<code>pglast</code> → <code>SelectStmt</code>); the 33 distinct <code>M0-Rn</code> IDs cited in the SQL are all present among <code>measurement.md</code>'s 41 (<code>comm -23</code> → empty); rule headers unchanged (<code>diff</code> of <code>^\*\*M0-Rn</code> → no output: no rule added, none renumbered); diff touches only <code>design/ld5/measurement.md</code> + <code>design/ld5/report.sql</code>. I also executed the new <code>certifiable</code>/verdict logic on a scratch engine in the same SQL shape (three slots: zero provenance rows / fully recorded / one NULL compared value, plus a zero-row sample) to check the branches rather than read them.</p> <h3>Per-finding</h3> <table> <tr><th>#</th><th>Verdict</th><th>Evidence</th></tr> <tr><td>1 · clause 4 failed open on a slot with zero <code>node_instances</code> rows</td><td><b>RESOLVED — fail-closed</b></td><td><code>runtime_prov</code> gains <code>no_provenance_time</code> and <code>no_comparison_value</code> (any of the four compared values unrecorded — the both-NULL case where <code>IS DISTINCT FROM</code> calls two NULLs equal); <code>clause4_ticket</code> gains <code>nodes = count(rp.node_key)</code>, <code>undeclared</code>, <code>certifiable = count(node_key) &gt; 0 AND count(*) FILTER (no_provenance_time OR no_comparison_value) = 0</code>, and a <code>gap</code> text. Per ticket the value is <code>CASE WHEN undeclared &gt; 0 THEN 0 WHEN certifiable THEN 1 ELSE NULL</code> — a zero-provenance slot now yields <code>NULL</code> with the gap named, never 1 (my scratch run: <code>nodes=0, certifiable=0, value=NULL</code>), and a <code>clause4/'certifiable'</code> row is emitted for <b>every</b> slot, which closes the round-1 observation that the absence was silent in the per-node section. Run level: <code>CASE WHEN undeclared &gt; 0 THEN 0 WHEN open_drains &gt; 0 OR not_certifiable_slots &gt; 0 THEN NULL ELSE 1 END</code>, with <code>clause4/slots_not_certifiable</code> and <code>clause4/slots_certifiable</code> printed. Precedence (detection ⇒ VOID, then open drain ⇒ paused, then uncertifiable ⇒ n/a, else pass) matches the recorded reading in §8 T11 and M0-R36's clause-4 bullet (<code>measurement.md:353-358</code>) and T9's cross-reference. No approved ruling is weakened: M0-R16 still voids an undeclared change, and the <code>n/a</code> direction is strictly stricter than the alternative T11 explicitly names as fail-open and leaves to an operator ruling.</td></tr> <tr><td>2 · void count failed open (unevidenced listed-cause re-runs dropped from M0-R33)</td><td><b>RESOLVED — fail-closed</b></td><td><code>infra_event</code> now separates <code>cause_listed</code> (on M0-R32's I1–I5 list) from <code>evidenced</code>; <code>infra</code> is filtered on <code>WHERE cause_listed</code> only, carrying <code>n</code> and <code>n_unevidenced</code>, so the M0-R33 void row (<code>count(DISTINCT ticket) FROM infra</code> vs <code>sample_size</code>) once again counts every listed cause, evidenced or not — the pre-<code>00fb54a</code> behaviour the finding asked for. <code>infra_unqualified</code> is now <b>off-list causes only</b> (a NULL/missing cause lands there too, since the <code>LEFT JOIN</code> leaves <code>cause_listed</code> false — fail-closed). The mandate is surfaced instead of applied: <code>infra_reruns_unevidenced</code> per ticket and in total, <code>infra_reruns_unevidenced_tickets</code> as a distinct-ticket count, and the void row's note now names how many of the counted tickets lacked evidence, so the round-1 inconsistency (per-ticket "open ruling" note beside a decided run-level verdict) is gone. §8 T10 is narrowed to off-list causes and T12 records this reading with the fail-open argument. Direction is stricter than before on a gate clause (clause 3), and M0-R32's "each needs recorded evidence" is still enforced as a printed, named flag.</td></tr> </table> <h3>Findings on the delta</h3> <table> <tr><th>Tag</th><th>Finding</th></tr> <tr><td><b>concern</b></td><td><b>Zero-slot sample still certifies a pass at run level</b> (adjacent to finding 1, not a re-open of it). <code>clause4</code> aggregates without <code>GROUP BY</code>, so with an empty <code>clause4_ticket</code> — <code>sample_ticket</code> empty, i.e. no ordered <code>sign.snapshot</code> rows — <code>sum(undeclared)</code> is NULL and <code>not_certifiable_slots</code> is 0, so <code>undeclared &gt; 0</code> is NULL, both <code>WHEN</code>s fall through and the row prints <code>value = 1</code> with <code>pass: … all 0 slots compared</code> (confirmed on the scratch engine: verdict 1, tickets 0). At <code>9a6530e</code> the same input returned <code>0</code>/VOID via its <code>ELSE 0</code> branch, so this is a small regression introduced by the rewrite. Reachability is low and self-announcing (an unsigned sample also prints <code>signature_integrity 0 released / 0 signed</code> and NULL dfpy denominators, and the note itself says "all 0 slots"), which is why this is a concern and not a blocker, but the fix's own principle — empty denominator prints <code>n/a</code>, never a pass — should cover it. Suggested one-liner: <code>CASE WHEN tickets = 0 THEN NULL WHEN undeclared &gt; 0 THEN 0 WHEN open_drains &gt; 0 OR not_certifiable_slots &gt; 0 THEN NULL ELSE 1 END</code>, with a note branch "no signed slot to compare (n/a)".</td></tr> <tr><td><b>suggestion</b></td><td>T11 attributes the "empty denominator prints <code>n/a</code>, never <code>0</code>" convention to M0-R37, whose text scopes it to precision and canary recall. The doc is honest about this ("applies the M0-R37 reading"), and the direction is stricter, but naming it an <i>extension</i> of that convention would make the provenance of the extension unambiguous for a frozen doc.</td></tr> </table> <h3>Checked for weakening (no finding)</h3> <p>Rule IDs and their text outside the two edited bullets are untouched; §8 grows T11/T12 in the file's existing "recorded reading" convention rather than inventing rules; the <code>undeclared</code> total is still the sum of the four component reads (<code>sum(undeclared)</code> replaces the identical inline sum); <code>declared_in_drain</code> is still split out and never counted against the clause; the fixed-20 denominator, clause 1–3, the canary gate and the five conditions are unchanged by this delta.</p> <p><b>Both filed findings are closed and fail-closed.</b> One adjacent concern (zero-slot run-level pass) and one wording suggestion remain; neither re-opens finding 1 (a slot with zero provenance rows now cannot pass) nor finding 2 (every listed cause counts again, with the evidence gap printed beside it).</p> <p>Verdict: APPROVE</p>
Fixes every blocker and every fix-before-freeze item from the Opus check of
2026-10-22 (head 3e0ba08). report.sql was never executed before this commit;
it is now run against a real Postgres 15 with a B5-shaped fixture.

Blockers:
- B1 integer division: every rate divides as ::numeric (14/20 now prints 0.70,
  not 0).
- B2 the two commits: landing.merged carries head_sha AND merge_commit_sha;
  condition 2 reads the reviewed head, condition 3 requires the probe to name
  the merge commit (probe_ok alone is not enough).
- B3 clause 4 re-baseline: each node is compared against the baseline in force
  at its recorded_at; drains carry a drain_id and pair on it, never by
  position; only operator-actor drains are declared, and a re-baseline is only
  used when the operator recorded it inside a declared drain.
- B4 sample scoping: sample_id on every record and every CTE; signed20 capped
  at the signed slots; decided_unsigned counts only events after the sign.
- B5 condition 4: the replay joins the goal.check event's own id.

Fix-before-freeze: ac_sha re-specs counted (M0-R25); escapes per M0-R12 (revert
and red CI on live need no flag); image digests compared by containment (@>);
replacements must be signed spares, released by the operator, not spare of a
spare, before the claim; a zero-slot sample cannot print a clause-4 pass;
verdict rows (clause 1 vs 70%, clause 2, clause 3, clause 4, overall
PASS/FAIL/VOID/NOT_CERTIFIABLE); CI stated as implied by decision='pass';
rules text fixed (M0-R26 vs R16, M0-R2 vs X2, §8 tensions renamed X1-X12).

Three new §8 items are OPEN for the operator and implemented fail-closed so the
sample cannot be gamed while they are open: X13 condition-4 default-pass cap,
X14 failed live probe inside the window, X15 target repo tag file vs Q17 guard 2.
Author
Owner

Fix round 3 pushed (a4e0176) — Opus-check blockers closed

Every blocker and every fix-before-freeze item from the 2026-10-22 Opus check of 3e0ba08 is addressed in this head. report.sql now actually executes — it is run against a local Postgres 15 with a B5-shaped fixture (3 tables), not merely parsed. The old head had never been executed and carried real defects a parser cannot see (a bare t.ticket in the predecessors UNION arm, forward CTE references, integer division).

Blockers

Finding Fix
Integer division every rate divides as ::numeric; 14/20 prints 0.70000000000000000000, dfpy_confirmed correct below 100%
"Merged SHA" = two commits landing.merged → head_sha and merge_commit_sha (legacy merged_sha read as the merge commit). Condition 2 reads the reviewed head; condition 3 requires probe_sha = merge_commit_sha — probe_ok alone no longer passes
Drain cannot re-baseline each node is compared against the baseline in force at its recorded_at; drains carry a drain_id and pair on it, never by position; only operator-actor drains are declared; a re-baseline counts only when the operator wrote it inside a declared drain (baselines/rejected prints refusals)
Events not tied to a sample sample_id on every record and every CTE; signed20 capped by the order bound and LIMIT sample_size; decided_unsigned counts only at >= signed_at
Condition 4 fails open goal exposes g.id; the replay joins r.payload->>'goal_check_id' = g.id::text

Fix before freeze

  • Re-spec counted (M0-R25): ac_sha_mismatch fails condition 1 and prints re_spec + miss reason re_spec_ac_sha.
  • Escapes per M0-R12: a revert or red CI on live counts with no flag; a fix ticket needs evidence_ok; printed by kind.
  • Image digests by containment (node @> baseline); NULL on either side = not certifiable, not a mismatch.
  • Replacements must be a signed spare (own snapshot, no order, own AC sha), released by the operator, not another signed slot, not a spare of a spare, before the claim. Five rejection paths each verified.
  • Zero-slot sample: clause 4 prints n/a ("no signed slots"), verdict NOT_CERTIFIABLE; an empty sample can never print a clause-4 pass.
  • Verdict rows: clause1_vs_70, clause2, clause3_void, clause4, gate_verdict ∈ {PASS, FAIL, VOID, NOT_CERTIFIABLE}.
  • CI: stated in M0-R9, in the report header and in the condition-2 row — decision='pass' implies green CI on that head; CI is never fetched separately.
  • Rules text: M0-R26 no longer contradicts M0-R16; §8 tensions renamed X1–X15 (safety tests stay T1–T6); M0-R2 now says rule 3 carries over as widened by X2; M0-R5/R9/R10/R11/R12/R16/R25/R36 restated to match the query.

New §8 open items (implemented fail-closed, not decided)

  • X13 — condition 4 default-pass cap when bundle-only checks are inconclusive (M0-R41). The gate reads the fail-closed reading (needs one conclusive check) and the literal M0-R11 reading is printed beside it plus cond4_uncertifiable_slots. This is the one open item that moves the gate number.
  • X14 — a failed live probe inside the 14-day window fails condition 5 and prints live_probe_failures.
  • X15 — the target repo's image-tag file vs Q17 guard 2: if it lives under deploy/**, every pin bump is blocked during the sample. Build/ruling question, no query change.

Verification (assertions, not eyeballing)

45 assertions across 24 scenarios, all green: empty schema, 20/20, 14/20 at the boundary, declared drain + re-baseline, undeclared runtime re-baseline, in-window vs out-of-window mismatch, open declared drain, drain without an id, gate decision on the merge commit, probe on the wrong commit, legacy merged_sha, re-spec, 5 replacement-rejection paths, revert / unevidenced fix / evidenced fix / failed-probe escapes, 5 infra re-runs (void), off-list cause, all-inconclusive checks, fresh sample S2, duplicate slot order (still 20), replay reproduced / not reproduced, precision 0/1.

Mechanical: one statement (1 semicolon outside comments/strings, 590 balanced parens, sqlglot parses 1), 41 rules defined, every M0-Rn cited exists, X1–X15 defined.

Do not merge. This freezes only after the operator's Opus re-check; X13/X14/X15 need rulings before the sign.

## Fix round 3 pushed (`a4e0176`) — Opus-check blockers closed Every blocker and every fix-before-freeze item from the 2026-10-22 Opus check of `3e0ba08` is addressed in this head. **`report.sql` now actually executes** — it is run against a local Postgres 15 with a B5-shaped fixture (3 tables), not merely parsed. The old head had never been executed and carried real defects a parser cannot see (a bare `t.ticket` in the `predecessors` UNION arm, forward CTE references, integer division). ### Blockers | Finding | Fix | |---|---| | Integer division | every rate divides as `::numeric`; 14/20 prints `0.70000000000000000000`, `dfpy_confirmed` correct below 100% | | "Merged SHA" = two commits | `landing.merged` → `head_sha` **and** `merge_commit_sha` (legacy `merged_sha` read as the merge commit). Condition 2 reads the **reviewed head**; condition 3 requires `probe_sha = merge_commit_sha` — `probe_ok` alone no longer passes | | Drain cannot re-baseline | each node is compared against the baseline **in force at its `recorded_at`**; drains carry a `drain_id` and pair on it, never by position; only operator-actor drains are `declared`; a re-baseline counts only when the operator wrote it inside a declared drain (`baselines/rejected` prints refusals) | | Events not tied to a sample | `sample_id` on every record and every CTE; `signed20` capped by the order bound **and** `LIMIT sample_size`; `decided_unsigned` counts only `at >= signed_at` | | Condition 4 fails open | `goal` exposes `g.id`; the replay joins `r.payload->>'goal_check_id' = g.id::text` | ### Fix before freeze - **Re-spec counted** (M0-R25): `ac_sha_mismatch` fails condition 1 and prints `re_spec` + miss reason `re_spec_ac_sha`. - **Escapes per M0-R12**: a revert or red CI on live counts with no flag; a fix ticket needs `evidence_ok`; printed by kind. - **Image digests by containment** (`node @> baseline`); NULL on either side = not certifiable, not a mismatch. - **Replacements** must be a signed spare (own snapshot, no `order`, own AC sha), released by the operator, not another signed slot, not a spare of a spare, before the claim. Five rejection paths each verified. - **Zero-slot sample**: clause 4 prints `n/a` ("no signed slots"), verdict `NOT_CERTIFIABLE`; an empty sample can never print a clause-4 pass. - **Verdict rows**: `clause1_vs_70`, `clause2`, `clause3_void`, `clause4`, `gate_verdict` ∈ {PASS, FAIL, VOID, NOT_CERTIFIABLE}. - **CI**: stated in M0-R9, in the report header and in the condition-2 row — `decision='pass'` implies green CI on that head; CI is never fetched separately. - **Rules text**: M0-R26 no longer contradicts M0-R16; §8 tensions renamed **X1–X15** (safety tests stay T1–T6); M0-R2 now says rule 3 carries over **as widened by X2**; M0-R5/R9/R10/R11/R12/R16/R25/R36 restated to match the query. ### New §8 open items (implemented fail-closed, **not decided**) - **X13** — condition 4 default-pass cap when bundle-only checks are inconclusive (M0-R41). The gate reads the fail-closed reading (needs one conclusive check) and the literal M0-R11 reading is printed beside it plus `cond4_uncertifiable_slots`. This is the one open item that moves the gate number. - **X14** — a failed live probe inside the 14-day window fails condition 5 and prints `live_probe_failures`. - **X15** — the target repo's image-tag file vs Q17 guard 2: if it lives under `deploy/**`, every pin bump is blocked during the sample. Build/ruling question, no query change. ### Verification (assertions, not eyeballing) 45 assertions across 24 scenarios, all green: empty schema, 20/20, 14/20 at the boundary, declared drain + re-baseline, undeclared runtime re-baseline, in-window vs out-of-window mismatch, open declared drain, drain without an id, gate decision on the merge commit, probe on the wrong commit, legacy `merged_sha`, re-spec, 5 replacement-rejection paths, revert / unevidenced fix / evidenced fix / failed-probe escapes, 5 infra re-runs (void), off-list cause, all-inconclusive checks, fresh sample S2, duplicate slot order (still 20), replay reproduced / not reproduced, precision 0/1. Mechanical: one statement (1 semicolon outside comments/strings, 590 balanced parens, sqlglot parses 1), 41 rules defined, every `M0-Rn` cited exists, X1–X15 defined. **Do not merge.** This freezes only after the operator's Opus re-check; X13/X14/X15 need rulings before the sign.
Author
Owner

Verdict: APPROVE — head a4e0176 (no blockers; 1 item to land before freeze)

Fresh-context review (deepseek landing worker), primary sources only: the diff at a4e0176, the 2026-10-22T05:13 Opus check and the ticket's rulings on HOMELAB-1929, design/ld5/measurement.md, design/ld5/report.sql. Reviewer ≠ implementer; I did not write this change.

Not merged. After this APPROVE the operator runs the Opus re-check before freeze — that review, not this one, decides whether M0 is frozen at the LD5 sign (B20).

I executed the query myself, not the claim

Fresh Postgres 15 cluster on my own port (initdb → /tmp/pgdata, socket /tmp, port 55433; the fix worker's instance on 55432 was not reachable and its TCP port is held by another process). Re-ran the worker's harness unmodified except for the port:

  • 45/45 assertions pass, 0 fail (24 scenarios, incl. empty schema, 14/20 boundary, drain/re-baseline, replacement rejections, replay reproduced/not, fresh-sample isolation).
  • I then wrote 5 scenarios of my own (not in their suite): 13/20 → 0.65000000000000000000, clause 1 FAIL: 65.0% < 70%, verdict FAIL · probe_ok=true with probe_sha=NULL → cond3 false, clause 2 19/20, verdict FAIL · 14/20 → PASS: 70.0% >= 70% (boundary is >=, correct) · operator-start/runtime-end drain → not declared → clause 4 VOID · one slot with no node_instances row → clause 4 not certifiable (n/a), verdict NOT_CERTIFIABLE.
  • Control for the division bug: select 14/20 prints 0; the query prints 0.70, so the cast is real and load-bearing.

Opus blockers — all five closed (code read + executed)

  1. Integer division — CLOSED. gate_numbers casts every scalar to ::numeric (report.sql:1187-1205); no bigint/bigint division remains (I machine-scanned every / outside comments/strings: report.sql:1558,1587,1594,1602,1624-1633,1641,1676,1691,1731,1739,1961-1990 — all numeric-operand). Verified 14/20 = 0.7000…, and 13/20 = 0.65 below the line.
  2. Two commits — CLOSED. landing.merged exposes head_sha and merge_commit_sha (:716-742, legacy merged_sha read as the merge commit only). Condition 2 reads gate_on_head joined on m.head_sha — the reviewed head, any round (:744-761, :884). Condition 3 is pin_moved AND probe_ok AND probe_sha IS NOT NULL AND probe_sha = merge_commit_sha (:886-890), so probe_ok alone no longer passes. Verified both ways (d1, d2, and my own probe_sha=NULL scenario).
  3. Drain re-baseline — CLOSED. Per-node as-of join: latest legal baseline with in_force_from <= recorded_at (:555-563). Starts/ends paired on drain_id, FULL JOIN never by position (:324-362). declared requires operator actor at the start and at the end and a real drain_id (:359-361); a re-baseline is a baseline only when the operator wrote it inside a declared drain (:367-403), refusals counted and printed (:404-422, baselines/rejected). Verified: in-drain declared vs out-of-window VOID, open drain → NOT_CERTIFIABLE, no-drain_id → not declared, operator-start/runtime-end → not declared and VOID.
  4. Sample scoping — CLOSED. sample_id filters on every events/workflows CTE; the one CTE that reads node_instances is scoped through sample_ticket (:555-557), so no cross-sample leak (asserted by a fresh S2 → 0 slots). signed20 capped twice — order bound and LIMIT sample_size (:438-448), duplicate order flagged while the denominator stays 20. decided_unsigned counts only at >= signed_at and excludes canary/replacement tickets (:1157-1166).
  5. Condition 4 fail-open — CLOSED. goal exposes g.id::text and both the fail_stands and conclusive lookups join replays on r.payload->>'goal_check_id' = g.id::text (:762-813). The payload key lives on the replay event, not on the header of goal.check, so a header-faithful runtime now gets its replay counted. Verified reproduced → lowers DFPY; not reproduced → does not, and enters precision 0/1.

Fix-before-freeze list — closed

  • Re-spec counted (M0-R25): ac_sha_mismatch in cond1 (:877), miss reason re_spec_ac_sha (:958), re_spec metric (:1310-1319). One gap below.
  • Escapes per M0-R12: revert/red_ci_live/ci_red_on_live count with no flag; every other kind needs evidence_ok (:831-847), each row printed with counts marked.
  • Image digests by containment: NOT (n.image_digests @> b.image_digests) (:549); NULL on either side → no_comparison_value → not certifiable, not a mismatch (:544-553).
  • Replacements: signed spare with its own snapshot and no order, operator actor, not a signed slot, no spare-of-spare, before the claim (:450-491); all five rejection paths verified, rejections counted (sample_replacements_rejected), and the spare's own AC sha is used, so nothing inherits the replaced ticket's sha (:500-511).
  • Zero-slot sample: tickets = 0 → clause 4 value NULL + "not certifiable (n/a): the sample has no signed slots" (:1825-1851), overall NOT_CERTIFIABLE (:1953-1980). Also holds per-slot when provenance is missing (my own scenario).
  • Verdict rows: clause1_vs_70 (:1621), clause2 (:1638), clause3_void (:1655), clause4 (:1825), gate_verdict PASS/FAIL/VOID/NOT_CERTIFIABLE (:1953-2005).
  • CI stance: stated in M0-R9, in the header (:119-122, :242-244) and on the condition-2 row (:1250) — decision='pass' implies green CI on the reviewed head because landing refuses the merge first.
  • Rules text: M0-R26 no longer says "excused" — a drain pauses (measurement.md:285-290); §8 renamed X1–X15 with the explicit "cannot be confused with the T1–T6 safety tests" note and M0-R3's "safety test T4" intact (:452-461); M0-R2 says rule 3 does not carry over unchanged, restated as widened by X2 (:45-53); 41 rules defined, every M0-Rn cited in the SQL exists, one statement, balanced parentheses (I re-checked all of these mechanically; sqlglot is not installed here, but the file executed in Postgres, which is a stronger check).

Finding to land before the freeze (1)

  1. re_spec has no total row — measurement.md:282 and measurement.md:411 both promise "the re-spec count per ticket and in total", but the query emits re_spec only in per_ticket (report.sql:1310); there is no per_total row. Gate impact: none (cond1 counts the re-spec per ticket, so a re-specced ticket is already a miss) — but the report contradicts its own contract, which is the class of defect this check exists to catch. Cheapest closure: one per_total row summing respec_nodes. My verdict covers a4e0176; a commit adding this needs only a delta re-check, not a new full review.

Nits (non-blocking)

  • report.sql:4 still reads "NOT EXECUTABLE YET". True against the real DDL, but it now reads as "never run"; say instead that it was executed against a B5-shaped fixture on 2026-10-22 and is not yet runnable against the real B5 schema — that is the honest status and it is what the PR body says.
  • The fixture + 45-assertion harness live only in /tmp (worker's tree and mine). It is not in the diff, so the "45 assertions pass" claim cannot be re-run after /tmp is cleared — the Opus re-check will have to take it on trust. Recommend committing the fixture, the runner and the assertions (e.g. design/ld5/verify/) before freeze. I have re-run them here, so the claim is true today; it just isn't reproducible.
  • The d3 assertion labelled "legacy merged_sha ok" keeps head_sha. A payload with only merged_sha now fails cond2 (correct, fail-closed), but the notes read "legacy payload → pass" without that caveat; worth one line in the notes rather than a test.

Open for the operator — not decided by the implementer (verified)

  • X13 — condition-4 default-pass cap: implementer took the fail-closed reading and prints dfpy_provisional_default_pass + cond4_uncertifiable_slots beside the gate reading (measurement.md X13, report.sql:1602-1613). This is the one open item that moves the gate number.
  • X14 — failed live probe inside the 14-day window fails condition 5, printed as live_probe_failures (report.sql:848-863, :1566-1572).
  • X15 — target repo's tag file vs Q17 guard 2: recorded as a build/ruling question, no query change (X15).
  • Unchanged from earlier rounds: X10 (off-list infra cause) and X12 (unevidenced listed cause) remain open.
  • Verified: X1–X12 are readings the document encodes; X13–X15 are marked "OPEN for the operator" in §8 and in the PR body, and none of them is silently decided.

Next: operator Opus re-check → freeze (snapshot both blob SHAs at the sign, M0-R1/R35). I did not merge.

## Verdict: **APPROVE** — head `a4e0176` (no blockers; 1 item to land before freeze) Fresh-context review (deepseek landing worker), primary sources only: the diff at `a4e0176`, the 2026-10-22T05:13 Opus check and the ticket's rulings on HOMELAB-1929, `design/ld5/measurement.md`, `design/ld5/report.sql`. Reviewer ≠ implementer; I did not write this change. **Not merged.** After this APPROVE the operator runs the **Opus re-check before freeze** — that review, not this one, decides whether M0 is frozen at the LD5 sign (B20). ### I executed the query myself, not the claim Fresh Postgres 15 cluster on my own port (`initdb` → `/tmp/pgdata`, socket `/tmp`, port 55433; the fix worker's instance on 55432 was not reachable and its TCP port is held by another process). Re-ran the worker's harness unmodified except for the port: - **45/45 assertions pass, 0 fail** (`24 scenarios`, incl. empty schema, 14/20 boundary, drain/re-baseline, replacement rejections, replay reproduced/not, fresh-sample isolation). - I then wrote **5 scenarios of my own** (not in their suite): 13/20 → `0.65000000000000000000`, clause 1 `FAIL: 65.0% < 70%`, verdict FAIL · `probe_ok=true` with `probe_sha=NULL` → cond3 false, clause 2 `19/20`, verdict FAIL · 14/20 → `PASS: 70.0% >= 70%` (boundary is `>=`, correct) · operator-start/runtime-end drain → not declared → clause 4 VOID · one slot with no `node_instances` row → clause 4 `not certifiable (n/a)`, verdict `NOT_CERTIFIABLE`. - Control for the division bug: `select 14/20` prints `0`; the query prints `0.70`, so the cast is real and load-bearing. ### Opus blockers — all five closed (code read + executed) 1. **Integer division** — CLOSED. `gate_numbers` casts every scalar to `::numeric` (`report.sql:1187-1205`); no bigint/bigint division remains (I machine-scanned every `/` outside comments/strings: `report.sql:1558,1587,1594,1602,1624-1633,1641,1676,1691,1731,1739,1961-1990` — all numeric-operand). Verified 14/20 = `0.7000…`, and 13/20 = `0.65` below the line. 2. **Two commits** — CLOSED. `landing.merged` exposes `head_sha` **and** `merge_commit_sha` (`:716-742`, legacy `merged_sha` read as the merge commit only). Condition 2 reads `gate_on_head` joined on `m.head_sha` — the reviewed head, any round (`:744-761`, `:884`). Condition 3 is `pin_moved AND probe_ok AND probe_sha IS NOT NULL AND probe_sha = merge_commit_sha` (`:886-890`), so `probe_ok` alone no longer passes. Verified both ways (`d1`, `d2`, and my own `probe_sha=NULL` scenario). 3. **Drain re-baseline** — CLOSED. Per-node as-of join: latest legal baseline with `in_force_from <= recorded_at` (`:555-563`). Starts/ends paired on `drain_id`, `FULL JOIN` never by position (`:324-362`). `declared` requires operator actor at the start **and** at the end **and** a real `drain_id` (`:359-361`); a re-baseline is a baseline only when the operator wrote it inside a declared drain (`:367-403`), refusals counted and printed (`:404-422`, `baselines/rejected`). Verified: in-drain declared vs out-of-window VOID, open drain → NOT_CERTIFIABLE, no-`drain_id` → not declared, operator-start/runtime-end → not declared and VOID. 4. **Sample scoping** — CLOSED. `sample_id` filters on every events/workflows CTE; the one CTE that reads `node_instances` is scoped through `sample_ticket` (`:555-557`), so no cross-sample leak (asserted by a fresh S2 → 0 slots). `signed20` capped twice — order bound **and** `LIMIT sample_size` (`:438-448`), duplicate `order` flagged while the denominator stays 20. `decided_unsigned` counts only `at >= signed_at` and excludes canary/replacement tickets (`:1157-1166`). 5. **Condition 4 fail-open** — CLOSED. `goal` exposes `g.id::text` and both the `fail_stands` and `conclusive` lookups join replays on `r.payload->>'goal_check_id' = g.id::text` (`:762-813`). The payload key lives on the *replay* event, not on the header of `goal.check`, so a header-faithful runtime now gets its replay counted. Verified reproduced → lowers DFPY; not reproduced → does not, and enters precision 0/1. ### Fix-before-freeze list — closed - **Re-spec counted** (M0-R25): `ac_sha_mismatch` in cond1 (`:877`), miss reason `re_spec_ac_sha` (`:958`), `re_spec` metric (`:1310-1319`). One gap below. - **Escapes per M0-R12**: `revert`/`red_ci_live`/`ci_red_on_live` count with **no** flag; every other kind needs `evidence_ok` (`:831-847`), each row printed with `counts` marked. - **Image digests by containment**: `NOT (n.image_digests @> b.image_digests)` (`:549`); NULL on either side → `no_comparison_value` → not certifiable, not a mismatch (`:544-553`). - **Replacements**: signed spare with its own snapshot and no `order`, operator actor, not a signed slot, no spare-of-spare, before the claim (`:450-491`); all five rejection paths verified, rejections counted (`sample_replacements_rejected`), and the spare's own AC sha is used, so nothing inherits the replaced ticket's sha (`:500-511`). - **Zero-slot sample**: `tickets = 0` → clause 4 value NULL + "not certifiable (n/a): the sample has no signed slots" (`:1825-1851`), overall `NOT_CERTIFIABLE` (`:1953-1980`). Also holds per-slot when provenance is missing (my own scenario). - **Verdict rows**: `clause1_vs_70` (`:1621`), `clause2` (`:1638`), `clause3_void` (`:1655`), `clause4` (`:1825`), `gate_verdict` PASS/FAIL/VOID/NOT_CERTIFIABLE (`:1953-2005`). - **CI stance**: stated in M0-R9, in the header (`:119-122`, `:242-244`) and on the condition-2 row (`:1250`) — `decision='pass'` implies green CI on the reviewed head because landing refuses the merge first. - **Rules text**: M0-R26 no longer says "excused" — a drain **pauses** (`measurement.md:285-290`); §8 renamed **X1–X15** with the explicit "cannot be confused with the T1–T6 safety tests" note and M0-R3's "safety test T4" intact (`:452-461`); M0-R2 says rule 3 does not carry over unchanged, restated as widened by X2 (`:45-53`); 41 rules defined, every `M0-Rn` cited in the SQL exists, one statement, balanced parentheses (I re-checked all of these mechanically; `sqlglot` is not installed here, but the file executed in Postgres, which is a stronger check). ### Finding to land before the freeze (1) 1. **`re_spec` has no total row** — `measurement.md:282` and `measurement.md:411` both promise "the re-spec count per ticket **and in total**", but the query emits `re_spec` only in `per_ticket` (`report.sql:1310`); there is no `per_total` row. Gate impact: none (cond1 counts the re-spec per ticket, so a re-specced ticket is already a miss) — but the report contradicts its own contract, which is the class of defect this check exists to catch. Cheapest closure: one `per_total` row summing `respec_nodes`. My verdict covers `a4e0176`; a commit adding this needs only a delta re-check, not a new full review. ### Nits (non-blocking) - `report.sql:4` still reads "NOT EXECUTABLE YET". True against the real DDL, but it now reads as "never run"; say instead that it was executed against a B5-shaped fixture on 2026-10-22 and is not yet runnable against the real B5 schema — that is the honest status and it is what the PR body says. - The fixture + 45-assertion harness live only in `/tmp` (worker's tree and mine). It is **not** in the diff, so the "45 assertions pass" claim cannot be re-run after `/tmp` is cleared — the Opus re-check will have to take it on trust. Recommend committing the fixture, the runner and the assertions (e.g. `design/ld5/verify/`) before freeze. I have re-run them here, so the claim is true today; it just isn't reproducible. - The `d3` assertion labelled "legacy `merged_sha` ok" keeps `head_sha`. A payload with **only** `merged_sha` now fails cond2 (correct, fail-closed), but the notes read "legacy payload → pass" without that caveat; worth one line in the notes rather than a test. ### Open for the operator — not decided by the implementer (verified) - **X13** — condition-4 default-pass cap: implementer took the fail-closed reading and prints `dfpy_provisional_default_pass` + `cond4_uncertifiable_slots` beside the gate reading (`measurement.md` X13, `report.sql:1602-1613`). This is the one open item that **moves the gate number**. - **X14** — failed live probe inside the 14-day window fails condition 5, printed as `live_probe_failures` (`report.sql:848-863`, `:1566-1572`). - **X15** — target repo's tag file vs Q17 guard 2: recorded as a build/ruling question, no query change (X15). - Unchanged from earlier rounds: **X10** (off-list infra cause) and **X12** (unevidenced listed cause) remain open. - Verified: X1–X12 are readings the document encodes; X13–X15 are marked "OPEN for the operator" in §8 and in the PR body, and none of them is silently decided. **Next:** operator Opus re-check → freeze (snapshot both blob SHAs at the sign, M0-R1/R35). I did not merge.
NIT 1: report.sql printed re_spec (M0-R25) per ticket only, while M0-R36
promises every number per ticket and in total. Add the total arm summing
respec_nodes over the signed 20, denominator = nodes recorded (same reading
as the per-ticket row). Verified to add exactly one row and change no other
arm's numbers (full-output diff over base and three scenarios).

NIT 2: the fixture and assertion runner lived only in /tmp. Commit them under
design/ld5/testdata/ (schema, base fixture, 33 one-delta scenarios, 63-check
runner, README with stand-up/load/run instructions) so the pre-freeze check
and B5 can re-execute. Ports the five re-check boundaries (exactly 70% PASS,
65% FAIL, probe_ok with NULL probe_sha fails condition 3, operator-start /
runtime-end drain is not declared -> VOID, zero-node slot -> clause 4 n/a).

Assertions: 63/63 pass on a fresh PostgreSQL 15 cluster (initdb, port 55434,
TCP) and on the pre-existing socket instance (port 55433). report.sql remains
one parseable Postgres statement (sqlglot) and cites only rules defined in
measurement.md.
Author
Owner

Both pre-freeze nits closed (38fd00f)

NIT 1 — re_spec had no total row

report.sql now emits the re_spec total arm (M0-R25) alongside the per-ticket one, so the M0-R36 promise ("per ticket and in total") is met by the query rather than by the reader:

total||M0-R25|re_spec|ac_sha_mismatch|0|20|0|count|no node in the signed 20 recorded an AC sha other than the signed one
  • numerator/value = sum(respec_nodes) over the signed slots; denominator = NULLIF(sum(nodes),0) — the same reading as the per-ticket row. No COUNT-over-subquery pattern exists among the other total arms, so the plain aggregate mirror (like interventions / budget_exhausted) is what was used.
  • No other arm changed. Full-output diff (pre vs post) over the base fixture and three scenarios (a, e1, m1) shows exactly one added line per run and zero modified lines.
  • With e1-respec-ac-sha the row reads 1|20|1 and the mechanical miss reason re_spec_ac_sha is 1.

NIT 2 — the suite is committed

design/ld5/testdata/ (source only — no data dirs, no dumps, no captured output):

schema.sql              drop+create the three tables report.sql assumes
fixture.sql             base sample S1: 20 signed slots + 2 spares, clean 20/20
scenarios/              33 one-delta scenarios (a..m2 plus the five re-check boundaries)
assert.py               63 assertions, exits non-zero on failure
README.md               stand up PG, load, run, expected output, scenario catalogue

Run:

initdb -D /tmp/ld5-pgdata -U postgres --auth=trust
pg_ctl -D /tmp/ld5-pgdata -o "-p 55433 -k /tmp -c listen_addresses=''" -l /tmp/ld5-pg.log start
PGHOST=/tmp PGPORT=55433 python3 design/ld5/testdata/assert.py
# -> all 63 assertions passed

Connection comes from PGHOST/PGPORT/PGUSER/PGDATABASE with the documented defaults; REPORT_SQL= points it at a different copy of the query.

Assertion run

63/63 pass, on two independent clusters:

  • a fresh initdb PostgreSQL 15.19 on port 55434 (TCP, throwaway data dir, since removed);
  • the pre-existing socket instance on port 55433.

The five boundaries from the re-check are now permanent assertions, not one-off probes:

Scenario Reading asserted
x1-exactly-70-percent 6 in-slot tickets get a second workflow → 14/20 = 70.0% PASS
x4-clause1-65-percent-fail 7 in-slot tickets → 13/20 = 65.0% FAIL
x2-probe-ok-null-sha probe_ok true with probe_sha NULL → condition 3 fails, clause 2 FAIL, gate FAIL
x3-operator-start-runtime-end-drain drain opened by operator, closed by runtime → not declared → VOID (clause 4)
x5-zero-node-slot slot with no node_instances row → clause 4 n/a, NOT_CERTIFIABLE, never a pass

(Note for anyone re-deriving these: T20 is legally replaced by SPARE1 in the base fixture, so T14..T20 moves only 6 in-slot tickets — that is why the 70%-exact case is T14..T20 and the 65% case is T13..T19.)

Mechanical checks

  • sqlglot.parse(read='postgres') → 1 statement (Select).
  • Every M0-Rn cited in report.sql exists in measurement.md (41 rules defined, no undefined citations).

Not merged — the operator runs the Opus re-check before freeze.

## Both pre-freeze nits closed (38fd00f) ### NIT 1 — `re_spec` had no total row `report.sql` now emits the `re_spec` total arm (M0-R25) alongside the per-ticket one, so the M0-R36 promise ("per ticket and in total") is met by the query rather than by the reader: ``` total||M0-R25|re_spec|ac_sha_mismatch|0|20|0|count|no node in the signed 20 recorded an AC sha other than the signed one ``` - numerator/value = `sum(respec_nodes)` over the signed slots; denominator = `NULLIF(sum(nodes),0)` — the same reading as the per-ticket row. No `COUNT`-over-subquery pattern exists among the other total arms, so the plain aggregate mirror (like `interventions` / `budget_exhausted`) is what was used. - **No other arm changed.** Full-output diff (pre vs post) over the base fixture and three scenarios (`a`, `e1`, `m1`) shows exactly one added line per run and zero modified lines. - With `e1-respec-ac-sha` the row reads `1|20|1` and the mechanical miss reason `re_spec_ac_sha` is 1. ### NIT 2 — the suite is committed `design/ld5/testdata/` (source only — no data dirs, no dumps, no captured output): ``` schema.sql drop+create the three tables report.sql assumes fixture.sql base sample S1: 20 signed slots + 2 spares, clean 20/20 scenarios/ 33 one-delta scenarios (a..m2 plus the five re-check boundaries) assert.py 63 assertions, exits non-zero on failure README.md stand up PG, load, run, expected output, scenario catalogue ``` Run: ```bash initdb -D /tmp/ld5-pgdata -U postgres --auth=trust pg_ctl -D /tmp/ld5-pgdata -o "-p 55433 -k /tmp -c listen_addresses=''" -l /tmp/ld5-pg.log start PGHOST=/tmp PGPORT=55433 python3 design/ld5/testdata/assert.py # -> all 63 assertions passed ``` Connection comes from `PGHOST/PGPORT/PGUSER/PGDATABASE` with the documented defaults; `REPORT_SQL=` points it at a different copy of the query. ### Assertion run **63/63 pass**, on two independent clusters: - a **fresh** `initdb` PostgreSQL 15.19 on port 55434 (TCP, throwaway data dir, since removed); - the pre-existing socket instance on port 55433. The five boundaries from the re-check are now permanent assertions, not one-off probes: | Scenario | Reading asserted | |---|---| | `x1-exactly-70-percent` | 6 in-slot tickets get a second workflow → 14/20 = **70.0% PASS** | | `x4-clause1-65-percent-fail` | 7 in-slot tickets → 13/20 = **65.0% FAIL** | | `x2-probe-ok-null-sha` | `probe_ok` true with `probe_sha` NULL → **condition 3 fails**, clause 2 FAIL, gate FAIL | | `x3-operator-start-runtime-end-drain` | drain opened by operator, closed by runtime → not declared → **VOID** (clause 4) | | `x5-zero-node-slot` | slot with no `node_instances` row → clause 4 **n/a, NOT_CERTIFIABLE**, never a pass | (Note for anyone re-deriving these: `T20` is legally replaced by `SPARE1` in the base fixture, so `T14..T20` moves only 6 in-slot tickets — that is why the 70%-exact case is `T14..T20` and the 65% case is `T13..T19`.) ### Mechanical checks - `sqlglot.parse(read='postgres')` → **1 statement** (`Select`). - Every `M0-Rn` cited in `report.sql` exists in `measurement.md` (41 rules defined, no undefined citations). Not merged — the operator runs the Opus re-check before freeze.
The header said "NOT EXECUTABLE YET" while the query now runs against the
committed fixture. Say what is true: it executes against design/ld5/testdata
today and cannot run against the real record DB until B5 lands. No SQL change
(one parseable statement, 63/63 assertions still pass).
Author
Owner

Addendum (6b1c188) — the third nit from the re-check

The re-check also noted the header still said "NOT EXECUTABLE YET" while the query had been executed. Fixed as a comment-only change (no SQL change): the header now says it executes against design/ld5/testdata/ today and cannot run against the real record DB until B5 lands — and that the suite proves the query is coherent, not that its schema assumptions match the landed DDL.

Re-verified after the edit: sqlglot → 1 statement; PGHOST=/tmp PGPORT=55433 python3 design/ld5/testdata/assert.py → all 63 assertions passed.

PR head is now 6b1c188. Still not merged.

### Addendum (6b1c188) — the third nit from the re-check The re-check also noted the header still said "NOT EXECUTABLE YET" while the query had been executed. Fixed as a **comment-only** change (no SQL change): the header now says it executes against `design/ld5/testdata/` today and cannot run against the real record DB until B5 lands — and that the suite proves the query is coherent, not that its schema assumptions match the landed DDL. Re-verified after the edit: `sqlglot` → 1 statement; `PGHOST=/tmp PGPORT=55433 python3 design/ld5/testdata/assert.py` → **all 63 assertions passed**. PR head is now **6b1c188**. Still not merged.
Author
Owner

Verdict: APPROVE — delta only (head 6b1c188, over the approved a4e0176)

Fresh-context landing worker (deepseek), primary sources only: git diff a4e0176..6b1c188 fetched from origin/plane/HOMELAB-1929-m0, the committed files themselves, and design/ld5/measurement.md as the frozen ruleset. Reviewer ≠ implementer; I wrote none of this and was given no implementer summary. Scope is the delta only — the approval of a4e0176 (comment above) is not re-litigated here, and nothing in this verdict covers it.

Not merged. The operator's Opus re-check remains the gate before the freeze at the LD5 sign (B20).

(1) report.sql — only the re_spec total arm moved

Two commits, two hunks in this file, nothing else:

Commit Change Verified
38fd00f +11 / −0, inserted immediately above the M0-R21 usd_total total arm (report.sql:1717-1727) the single re_spec total arm; no other arm touched (the diff has no other hunk)
6b1c188 +9 / −2, header comment block (report.sql:1-16) comment only — zero SQL change

The arm itself is what was asked for: sum(t.respec_nodes)::numeric as numerator and value, NULLIF(sum(t.nodes), 0)::numeric as denominator, FROM scored t. I checked scored is not a fan-out: scored reads FROM ticket, and ticket reads FROM sample_ticket s — one row per signed slot (report.sql:943-991). So the sum is over the signed 20 exactly, with no double counting, and respec_nodes/nodes are the same per-ticket columns the per-ticket arm uses (report.sql:926-927, 1317-1321). Reading is identical to the per-ticket row ("nodes recorded"), which is the right choice for a denominator.

I executed the boundary rather than reading it:

two tickets re-specced (T03, T07)   -> total row: value=2 denom=20 ; per-ticket rows: 1/1 each
zero node_instances rows recorded   -> total row: value=0 denom=NULL(empty)

so the aggregate is a real sum (not a count(*), which would have printed 20) and the NULLIF denominator collapses to NULL when no node was recorded, matching the per-ticket arm's behaviour.

(2) The committed testdata is source-only

37 files, design/ld5/testdata/ + nothing outside it: schema.sql, fixture.sql, assert.py, README.md, scenarios/*.sql (33). git diff --numstat reports no binary entries; file on every added path returns UTF-8/ASCII text; a name scan for pgdata|base/|global/|*.dump|*.tar|*.gz|*.zip|*.bin|*.pdf|*.png returns nothing; the tree is 176 KB total. schema.sql is 10 lines and starts with drop table if exists events, node_instances, workflows — the runner cannot write anywhere except the DB named by PGDATABASE, and it refuses to silently pass because assert.py sets -v ON_ERROR_STOP=1 on every psql call and exits non-zero on any failure.

(3) I ran the suite on my own cluster

Fresh initdb PostgreSQL 15.19, own throwaway data dir, own free port, unix socket only (listen_addresses='') — I did not reuse the fix worker's instance. Then:

PGHOST=/tmp/ld5sock PGPORT=<free> PGUSER=postgres PGDATABASE=postgres \
  python3 design/ld5/testdata/assert.py
-> all 63 assertions passed   (exit 0, 0 fail)

63/63. The three assertions the delta exists for pass and are the right assertions: base: re_spec total present, base: re_spec total value = 0, base: re_spec total denominator = 20; plus e1-respec-ac-sha → per-ticket T05 = 1, total = 1, and miss_reason_mechanical/re_spec_ac_sha = 1.

Mechanical checks, re-run by me and not taken from the commit message:

  • sqlglot.parse(read='postgres') → 1 statement (sqlglot 30.24.0). 6b1c188's claim ("one parseable statement") holds.
  • Every M0-Rn cited in report.sql exists in measurement.md: 36 distinct IDs cited, comm -23 against the 41 defined → empty.
  • 6b1c188's claim ("63/63 assertions still pass") holds on an independent cluster.
  • measurement.md was not touched in this delta — git diff --name-only a4e0176..6b1c188 -- design/ld5/measurement.md is empty. Correct: a nit fix must not edit the frozen ruleset, and it didn't. The rules did not move, so no assertion was "fixed to match the query" (the README's own updating rule).

Concerns (non-blocking, for the freeze record, not for this merge)

  • report.sql:1720 — sum() is exercised by only one planted mismatch in the committed suite. e1 re-specs one ticket, so 1 of 1 is indistinguishable from max(). My two-ticket probe confirmed the summation is correct, but the committed suite would not catch a future max()/count(*) regression. Suggest adding a second update ... where key in ('n-T05','n-T07') scenario asserting 2/20.
  • PR body is now stale on counts. The ## Checks section still says "24 scenarios were run"; the committed suite is 33 scenarios / 63 assertions (README and report.sql:4 both say 63). Worth correcting before the description is read as the freeze record.
  • Out of delta scope, noted once and not counted against this head: M0-R41 is cited in measurement.md and in scenario j1 but nowhere in report.sql (the X13 rows cite sec 8 X13 instead). Pre-existing at a4e0176, not introduced here.

No blockers. No open question is decided by this delta, no rule text moved, and the header change is honest about what has and has not been proven (executes against the fixture; not executable against the real record DB until B5 lands).

## Verdict: **APPROVE** — delta only (head `6b1c188`, over the approved `a4e0176`) Fresh-context landing worker (deepseek), primary sources only: `git diff a4e0176..6b1c188` fetched from `origin/plane/HOMELAB-1929-m0`, the committed files themselves, and `design/ld5/measurement.md` as the frozen ruleset. Reviewer ≠ implementer; I wrote none of this and was given no implementer summary. **Scope is the delta only** — the approval of `a4e0176` (comment above) is not re-litigated here, and nothing in this verdict covers it. **Not merged.** The operator's Opus re-check remains the gate before the freeze at the LD5 sign (B20). ### (1) `report.sql` — only the `re_spec` total arm moved Two commits, two hunks in this file, nothing else: | Commit | Change | Verified | |---|---|---| | `38fd00f` | **+11 / −0**, inserted immediately above the `M0-R21 usd_total` total arm (`report.sql:1717-1727`) | the single `re_spec` total arm; no other arm touched (the diff has no other hunk) | | `6b1c188` | **+9 / −2**, header comment block (`report.sql:1-16`) | comment only — zero SQL change | The arm itself is what was asked for: `sum(t.respec_nodes)::numeric` as numerator **and** value, `NULLIF(sum(t.nodes), 0)::numeric` as denominator, `FROM scored t`. I checked `scored` is not a fan-out: `scored` reads `FROM ticket`, and `ticket` reads `FROM sample_ticket s` — one row per signed slot (`report.sql:943-991`). So the sum is over the signed 20 exactly, with no double counting, and `respec_nodes`/`nodes` are the same per-ticket columns the per-ticket arm uses (`report.sql:926-927`, `1317-1321`). Reading is identical to the per-ticket row ("nodes recorded"), which is the right choice for a denominator. I executed the boundary rather than reading it: ``` two tickets re-specced (T03, T07) -> total row: value=2 denom=20 ; per-ticket rows: 1/1 each zero node_instances rows recorded -> total row: value=0 denom=NULL(empty) ``` so the aggregate is a real sum (not a `count(*)`, which would have printed 20) and the `NULLIF` denominator collapses to NULL when no node was recorded, matching the per-ticket arm's behaviour. ### (2) The committed testdata is source-only 37 files, `design/ld5/testdata/` + nothing outside it: `schema.sql`, `fixture.sql`, `assert.py`, `README.md`, `scenarios/*.sql` (33). `git diff --numstat` reports **no binary entries**; `file` on every added path returns UTF-8/ASCII text; a name scan for `pgdata|base/|global/|*.dump|*.tar|*.gz|*.zip|*.bin|*.pdf|*.png` returns nothing; the tree is 176 KB total. `schema.sql` is 10 lines and starts with `drop table if exists events, node_instances, workflows` — the runner cannot write anywhere except the DB named by `PGDATABASE`, and it refuses to silently pass because `assert.py` sets `-v ON_ERROR_STOP=1` on every `psql` call and exits non-zero on any failure. ### (3) I ran the suite on my own cluster Fresh `initdb` PostgreSQL **15.19**, own throwaway data dir, own free port, unix socket only (`listen_addresses=''`) — I did not reuse the fix worker's instance. Then: ``` PGHOST=/tmp/ld5sock PGPORT=<free> PGUSER=postgres PGDATABASE=postgres \ python3 design/ld5/testdata/assert.py -> all 63 assertions passed (exit 0, 0 fail) ``` **63/63.** The three assertions the delta exists for pass and are the right assertions: `base: re_spec total present`, `base: re_spec total value` = `0`, `base: re_spec total denominator` = `20`; plus `e1-respec-ac-sha` → per-ticket `T05` = 1, total = 1, and `miss_reason_mechanical/re_spec_ac_sha` = 1. Mechanical checks, re-run by me and not taken from the commit message: - `sqlglot.parse(read='postgres')` → **1 statement** (sqlglot 30.24.0). `6b1c188`'s claim ("one parseable statement") holds. - Every `M0-Rn` cited in `report.sql` exists in `measurement.md`: 36 distinct IDs cited, `comm -23` against the 41 defined → **empty**. - `6b1c188`'s claim ("63/63 assertions still pass") holds on an independent cluster. - **`measurement.md` was not touched** in this delta — `git diff --name-only a4e0176..6b1c188 -- design/ld5/measurement.md` is empty. Correct: a nit fix must not edit the frozen ruleset, and it didn't. The rules did not move, so no assertion was "fixed to match the query" (the README's own updating rule). ### Concerns (non-blocking, for the freeze record, not for this merge) - **`report.sql:1720` — `sum()` is exercised by only one planted mismatch in the committed suite.** `e1` re-specs one ticket, so `1 of 1` is indistinguishable from `max()`. My two-ticket probe confirmed the summation is correct, but the committed suite would not catch a future `max()`/`count(*)` regression. Suggest adding a second `update ... where key in ('n-T05','n-T07')` scenario asserting `2/20`. - **PR body is now stale on counts.** The `## Checks` section still says "24 scenarios were run"; the committed suite is 33 scenarios / 63 assertions (README and `report.sql:4` both say 63). Worth correcting before the description is read as the freeze record. - Out of delta scope, noted once and not counted against this head: `M0-R41` is cited in `measurement.md` and in scenario `j1` but nowhere in `report.sql` (the X13 rows cite `sec 8 X13` instead). Pre-existing at `a4e0176`, not introduced here. No blockers. No open question is decided by this delta, no rule text moved, and the header change is honest about what has and has not been proven (executes against the fixture; not executable against the real record DB until B5 lands).
This branch is already included in the target branch. There is nothing to merge.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin plane/HOMELAB-1929-m0:plane/HOMELAB-1929-m0
git switch plane/HOMELAB-1929-m0

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch main
git merge --no-ff plane/HOMELAB-1929-m0
git switch plane/HOMELAB-1929-m0
git rebase main
git switch main
git merge --ff-only plane/HOMELAB-1929-m0
git switch plane/HOMELAB-1929-m0
git rebase main
git switch main
git merge --no-ff plane/HOMELAB-1929-m0
git switch main
git merge --squash plane/HOMELAB-1929-m0
git switch main
git merge --ff-only plane/HOMELAB-1929-m0
git switch main
git merge plane/HOMELAB-1929-m0
git push origin main
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aaron/lab-director!7
No description provided.