Skip to content

backlog: file the AG-rig validation, and date the AOAG outage it is blocking - #105

Closed
wshallwshall wants to merge 2 commits into
mainfrom
backlog-agrig-validation
Closed

backlog: file the AG-rig validation, and date the AOAG outage it is blocking#105
wshallwshall wants to merge 2 commits into
mainfrom
backlog-agrig-validation

Conversation

@wshallwshall

Copy link
Copy Markdown
Collaborator

Two changes. [store].multi_subnet_failover shipped on 2026-07-10 (#100) and has never been pointed at a real cross-subnet availability group. That gap existed only as a line in a plan-11 doc reading "needs the SQL AG rig" — not as tracked work — so for three weeks AOAG-DEPLOYMENT.md §4.5 went on telling DBAs to take a planned DB outage on a premise that had stopped being true.

1. BACKLOG #319 — file the validation as real work

A two-subnet WSFC with an AG listener, a forced failover, and five measurements: does it reconnect at all; time-to-first-write against the DNS-workaround path; what happens to in-flight messages; failback as well as failover; and a control run with the setting off, without which the result cannot be attributed to it.

It leads with the trap: if the rig's listener is already at RegisterAllProvidersIP = 0 because someone followed §4.5, you are measuring the workaround rather than the keyword. It also fences off the db_lookup gap explicitly, so a green result cannot be read as "the workaround is obsolete" — that connector builds its own connection string and emits nothing.

On the number: allocated with scripts/coord/alloc.ps1 (post-#88 floor, ~550 refs). Not hand-picked, and not taken from my own scan — that scan showed 315/317/318 free and the allocator returned 319, which is precisely the race the atomic allocator exists to win. I have no attachment to it; if the #231+ reconciliation renumbers, renumber it.

2. §4.5 — keep the outage, date the doubt

The listener configuration and its brief outage remain the recommendation. Added is a dated, version-pinned note (2026-07-31, engine 0.3.2) saying it may not be necessary, and pointing at #319.

The asymmetry is why the instruction stays. Leaving it in costs a maintenance window someone may not have needed — annoying, scheduled, harmless. Taking it out while the setting is unproven risks converting a scheduled outage into an unplanned one during a real failover, on a system carrying patient data.

The note also pins the two things that will not change even if #319 passes — the keyword covers the store connection only, and a listener already at RegisterAllProvidersIP = 0 will likely still need a restart to undo it, so an existing deployment may not escape the outage either way — and tells a later reader to check #319 rather than assume the note is still current.

Verification

Backlog-status, ledger, runbook and doc-ref suites: 49 passed. The new 🔢 is the item's only status banner, so the "item contradicts itself" invariant holds. #319 appears exactly once and is the last item in file order. The anchor link resolves.

🤖 Generated with Claude Code

…locking

`[store].multi_subnet_failover` shipped on 2026-07-10 (#100) and has never been
pointed at a real cross-subnet availability group. That gap existed only as a
line in a plan-11 doc saying "needs the SQL AG rig" — not as tracked work — so
for three weeks AOAG-DEPLOYMENT.md 4.5 went on telling DBAs to take a planned DB
outage on a premise that had stopped being true.

Two changes, deliberately conservative:

1. BACKLOG #319 files the validation as real work: a two-subnet WSFC with an AG
   listener, a forced failover, and five measurements — reconnect at all,
   time-to-first-write against the DNS-workaround path, in-flight message fate,
   failback as well as failover, and a control run with the setting OFF, without
   which the result cannot be attributed to it. It carries the trap up front:
   if the rig's listener is already at RegisterAllProvidersIP=0 you are
   measuring the workaround, not the keyword.

   Number allocated with scripts/coord/alloc.ps1 (post-#88 floor, ~550 refs).
   Not hand-picked and not taken from a local scan — my own namespace scan
   showed 315/317/318 free and the allocator returned 319, which is exactly the
   race the atomic allocator exists to win.

2. AOAG-DEPLOYMENT.md 4.5 keeps the outage as the base recommendation, and now
   carries a dated, version-pinned note saying it may not be necessary. The
   asymmetry is the reason the instruction stays: leaving it in costs a
   maintenance window someone may not have needed; taking it out while the
   setting is unproven risks converting a scheduled outage into an unplanned one
   during a real failover, on a system carrying patient data.

   The note also pins two things that will not change even if #319 passes — the
   keyword covers the store connection only (db_lookup emits nothing), and a
   listener already at RegisterAllProvidersIP=0 will likely still need a restart
   to undo it — and tells a later reader to check #319 rather than assume the
   note is current.

Verified: backlog-status, ledger, runbook and doc-ref suites 49 passed; the new
🔢 banner is the item's only status banner, so the "contradicts itself"
invariant holds; #319 appears exactly once; the anchor link resolves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
wshallwshall added a commit that referenced this pull request Aug 1, 2026
§A.3's roll-up reads "In scope: 14 chapters active today (V1–V8, V11–V16)"
— it said 13. V1–V8 is 8 and V11–V16 is 6.

The table directly above it is the authority and agrees with 14: exactly 14
rows carry an in-scope "Yes" (V1–V8, V11–V16), V17 is the sole "No", and V9
and V10 carry their own qualified verdicts.

This is the same off-by-one that was already in the line before #111 — it
read "12 chapters active (V1, V2, V4–V8, V11–V16)", which is 13. #111
correctly brought V3 into scope but carried the arithmetic error forward
rather than recomputing it, so the count moved 12→13 when the list moved
13→14.

Found because the website mirrors this file as
`docs/secure-development-standards.md` and publishes it as
`assets/MessageFoundry-Secure-Development-Standards.pdf`. That copy was
corrected to 14 in website PR #105, so the two have been disagreeing since
#111 merged. This makes them agree, with the engine as the correct one.

The "17 chapters" statements at :243 and :248 are right and untouched — that
is the total in ASVS 5.0, not the in-scope count.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@wshallwshall
wshallwshall enabled auto-merge (squash) August 3, 2026 13:06
Resolves the append/append conflict in docs/BACKLOG.md. Both sides only appended
new items at EOF, so the resolution is a union: main's items are kept verbatim
and #319 is appended after them.

#319 was verified to be a genuine hole in main's sequence (main runs 318, then
320..353), so it is this branch's own allocation and the union creates no
duplicate item number. Appending rather than inserting in numeric order matches
the file's actual convention -- BACKLOG.md is ordered by merge, not by number.

Verified: no conflict markers; 278 items = main's 277 + #319; no duplicate
numbers; no item lost from either side; #319 byte-identical to the branch's
version; #114's banner preserved from main (it moved in #162).
wshallwshall added a commit that referenced this pull request Aug 4, 2026
…6-08-03)

Every open item now carries a current value x difficulty score. Before this, 23 had
none at all and the other 69 were from the frozen 2026-07-10 pass, which predates the
2026-07-28 reconcile that closed 31 items -- and a stale score reads exactly like a
fresh one.

Method, unchanged from the pass it supersedes: scored from each item's own Scope /
Why / Trigger / Nearest-existing-mechanism text rather than rescaled from the old
number, then adversarially verified against the code -- a second reader per batch
attacking build state first, then verdict/tier, then value and difficulty. 26 of 92
scores were overturned by that pass and carry the refuter's number.

The banner is the live record and the table is a view of it; both are written here and
a mechanical check confirms 92 banners and 92 rows agree on every triple.

THE RATIONALE IS REPLACED, NOT JUST THE NUMBERS. Carrying an old justification under a
new score is how a banner comes to argue against itself:
  - #114's surviving "clean workaround via the on-demand test probe" is a claim PR #162
    explicitly retracted -- both destinations' test_connection CREATE the target dir, so
    the probe cannot answer the question the toggle asks. That is what lifts it off the
    parity-with-a-workaround band to 6/3. Its replacement rationale was ALSO stale (it
    described the silent-ignore #162 had just fixed) and is hand-corrected.
  - #105's "large greenfield 71-action mapper needing its own ADR" describes an importer
    that has since shipped under ADR 0086.

Scheduling barely moved, which is the reassuring result: only TWO tiers changed --
#64 DEMAND-GATE -> P3 (an index over levers that live in #62/#63/#47/#34, so it ships
nothing runnable of its own) and #105 P3 -> DEMAND-GATE. Neither contradicts an
explicit demand-gate/on-trigger ruling in its own body; that was checked for all 51
items carrying a prior tier.

Distribution is RECOMPUTED with the table rather than carried forward, and all four
lines sum to 92. The superseded table keeps its own frozen lines and now says so.

  Tiers: P1 4, P2 19, P3 17, DEMAND-GATE 52
  Quadrants: quick win 22, big bet 5, fill-in 56, money pit 9

The four P1s: #341 (9/3, a handler returning a tuple/set of Sends delivers nothing
silently -- an accept-and-drop CLAUDE.md §12 forbids), #324 (7/2), #325 (6/2), #327 (6/2).

NOT in this commit: 24 items were found to misdescribe their own build state -- prose
asserting a gap that has since shipped, or citing messagefoundry/console/, a package
retired with #103. Those are banner corrections and land separately; the scores here
already price the remainder rather than the original scope.

Two mechanical faults were caught by reading the output rather than trusting the run:
the quadrant regex omitted the hyphen in "fill-in", so 57 of 69 items took the fallback
branch and got a SECOND score inserted beside the first; and the synthesizer's own
distribution lines did not follow from its own table (11 quadrant mismatches, 8
ordering violations, difficulty summing to 95 of 92). The script now refuses to write
when any line carries two score spans or the scored count is not 92.

backlog_status_check.py --min-items 277: OK, 277 items across both files.
wshallwshall added a commit that referenced this pull request Aug 4, 2026
The 2026-08-03 re-score flagged 24 open items as misdescribing what the code does.
Re-verified each against the tree as it stands -- after the archive move and after
PR #162, both of which post-date the findings -- and 10 survived. The other 14 did
not, and are recorded here rather than silently dropped:

  #84 #95 #99 #105 #114 #124 #125 #127 #133 #137 #167 #169 #214 #228

Most of those already carry an amendment that covers the stale sentence (#95, #99,
#105, #114, #124, #125, #127, #133, #228), and stacking a second ruling saying the
same thing is noise. The rest did not survive verification: the finding was itself
wrong or overstated, and a wrong correction in a ledger is worse than a stale one.

CORRECTIONS ARE ADDED AS DATED AMENDMENTS, NOT PROSE REWRITES. This file's convention
is to leave the original claim standing and rule against it, so the record shows what
was believed and what replaced it. Silently editing the stale sentence would destroy
the evidence that makes the correction checkable.

Applied to #62 #64 #131 #166 #179 #182 #237 #321 #329 #336. Representative:

  - #329 "Five MEFOR_ALLOW_INSECURE_TLS cells": the census is FOUR. #323 landed and
    routed transports/direct.py through the clamp; it now holds no call to the raw
    predicate at all (:63, :197, :215).
  - #321 "no test asserts the detectors can see a site code": false --
    tests/test_scan_forbidden.py has per-class hit tests for at least the site code
    (:126), a customer name (:83), a case-sensitive code (:91) and a routable IP
    (:107). The detector-coverage half of its Proposed 2 is already in the tree.
  - #62 plans a dual-read over "existing mfenc:v1 rows", but cell-bound mfenc:v2 is
    the default writer (settings.py:383 -> base.py:1841; crypto.py:36), and v2 folds
    (table, column, pk) into the GCM tag -- so a body landing under a different column
    must be RE-ENCRYPTED, not merely re-encoded. That tightens the catch.
  - #64's ordered plan still reads live ("Nothing builds before it"), but the
    measure-first phase completed 2026-07-12 (ADR 0051) and its step-2 lever is
    refused outright (ADR 0055 withdrawn; ADR 0107 "Do not build F2 or F3").

The refuters removed two overclaims before they landed: #62's draft asserted a live
store holds both mfenc markers (a fresh store under the shipped default holds only
v2 -- the defensible claim is that a MIGRATION must expect both), and #64's asserted
the multi-DB log split still remains, which could not be verified against ADR 0098 and
would have been a fresh false claim.

No item closes here: in every case the correction narrows the remainder rather than
discharging it, and the 2026-08-03 scores already price the remainder.

backlog_status_check.py --min-items 277: OK, 277 items, one status banner each.
wshallwshall added a commit that referenced this pull request Aug 4, 2026
…ones (#163)

* docs(backlog): close BACKLOG #226 — the estate Hybrid-layout sweep is done, off-repo

The per-feed Hybrid split (connections.toml / <INBOUND>_router.py /
<INBOUND>_handler.py / _<feed>_transforms.py) landed across the ported estate in
the maintainer-internal migration repository. Owner-attested; nothing in this
repository changes, which is also why leaving the item open could never have
closed it.

Both "Also" clauses are recorded as NOT delivered, with the reason each is not a
residual of this item:

  - "align the IDE Corepoint-import / scaffold path to emit the Hybrid layout" —
    there is no Corepoint-import path in ide/ to align. That tooling is #105,
    still open, so the clause is a constraint on #105's design rather than work
    #226 can perform. The scaffold half is misaddressed too: Insert Element (#48)
    drops per-file idioms into the current buffer (ide/src/insertElement.ts:1-5)
    and emits no multi-file feed layout.

  - "consider a recursive-glob / folder-per-feed loader enhancement" — filed as a
    consider, and not taken: load_config still globs *.py non-recursively
    (config/wiring.py:4162), the flat-merge behaviour the Hybrid layout is built
    around.

Follows the #227 precedent: close the primary, state the off-repo/misaddressed
residuals explicitly so the item is not re-opened for them.

backlog_status_check.py: OK — 277 items, each declaring exactly one status.

* fix(ledger): teach the number-space gates to span an archive, and fix two holes found proving it

Prerequisite for moving the 185 closed BACKLOG items into docs/archive/backlog/.
No item has moved yet; this only makes the guards able to see one when it does.

The item namespace will span two paths, so every guard now reads their UNION:

  - backlog_status_check.py: scan() takes (label, text) pairs and parses them as ONE
    namespace. A number re-used across BACKLOG.md and the archive was structurally
    undetectable before -- `seen` was per-parse -- which is the erratum's own shape.
  - ledger_check.py: triggers on any backlog-bearing path, not the one literal, and
    builds head/base as the union. Reading the union on both sides also removes a
    false positive: the move relocates 185 items, so head-union == base-union and
    `head - base` stays empty, where a per-file view would report 185 vanished
    numbers with a remedy that renumbers cited items.
  - alloc.ps1: sweeps both paths in the all-refs term and the working-tree term.
  - backlog-hygiene.yml: accepts a banner updated in either location.

Two pre-existing defects surfaced only because the gates were made to fail on
purpose first, neither of which is about the archive:

  1. alloc.ps1's working-tree term has NEVER worked. `[regex]'^...'` anchors at the
     start of the STRING; the term feeds it `Get-Content -Raw`, one string starting
     "# Backlog". Measured: 0 of 277 headings matched without Multiline, 277 with.
     The all-refs term hid it by covering every number committed somewhere -- i.e.
     every case except the uncommitted one this term exists for.
  2. backlog-hygiene.yml diffed BASE_SHA..HEAD_SHA (two-dot), which credits a PR for
     main-side changes to paths it never touched. One main-side edit to BACKLOG.md
     -- the move being a large one -- would let every PR with an older base pass the
     "must update BACKLOG.md" required check while enforcing nothing. Now three-dot,
     matching ci.yml's form for the same question.

Anti-narrowing, because a green gate over a shrunken corpus is the failure mode:
  - `--min-items N` fails when fewer items are found than required, and CI pins 277.
    Without it, 277 -> 92 fails nothing.
  - The scanned files are always printed with the count; a bare integer cannot
    distinguish "items closed" from "a file stopped being read".
  - A liveness receipt in the test suite asserts the same floor.
  - An explicitly-named --backlog path that does not exist is an error, not a skip.

alloc.ps1 gains `-ShowFloor`: print the floor and the swept paths, allocate nothing.
Allocation is a one-way door, so before this the only way to ask what the floor could
see was to spend a number on the question -- which is how it ran a whole release
reading two refs while its header promised all of them. Get-Floor takes -Peek so the
inspection cannot advance the high-water ratchet; the first -ShowFloor run against a
planted number moved this clone's watermark 316 -> 990 before that was fixed.

Proofs run, each observed failing BEFORE the fix:
  - archive-only unallocated #1007 staged: old gate rc=0, new gate BLOCKED.
  - #990 planted in the archive: old sweep floor 353 (blind), new sweep 990.
  - cross-file duplicate #118: detected, naming the other file.
  - banner violations inside the archive only: detected.
  - --min-items over a narrowed corpus: rc=1 with the scanned-file list.
  - -ShowFloor twice against a plant: watermark unchanged at 316.

ruff + mypy --strict clean; 43 gate tests pass.

* docs(backlog): move the 185 closed items into docs/archive/backlog/BACKLOG-CLOSED.md

docs/BACKLOG.md becomes the ~92 items someone can act on: 8,742 -> 3,648 lines.
The closed items are not deleted, summarised, or rewritten -- they are relocated
verbatim, so the file that gets opened, grepped and edited daily is the open set.

MOVED, NOT REWRITTEN. Every relocated block is byte-identical to the one that left
BACKLOG.md, headings included. Verified mechanically against a pre-move copy:

  - 277 items before = 92 after + 185 archived, no overlap, union identical
  - every OPEN block byte-identical to its source
  - every ARCHIVED block byte-identical to its source
  - all non-item prose in BACKLOG.md preserved verbatim

Byte-identical headings are load-bearing, not tidiness: GitHub derives anchor slugs
from heading text, so all 64 archived->archived cross-references keep resolving with
no edit at all. That is the whole argument for one archive file rather than a split
by status, year, or cluster -- #52 alone receives 99 of the 110 in-file anchors, and
its citers span #65 to #184, so no cut isolates them.

Cutting item blocks at the next '## ' heading of EITHER kind, not the next numbered
item: 4 blocks in this file are followed by a section header, which a naive cut would
have dragged into the archive along with the prose beneath it.

Anchors, all 127 re-resolved against real headings after the edit:
  - 44 rewritten in BACKLOG.md   -> archive/backlog/BACKLOG-CLOSED.md#<same-slug>
  -  1 rewritten in the archive  -> ../../BACKLOG.md#<same-slug>  (#226 -> #105)
  -  3 cross-file links repointed: AOAG-DEPLOYMENT.md (#100, #101), ADR 0026 (#30)
  - 64 archived->archived untouched, by design

13 anchors still do not resolve, and ALL 13 WERE ALREADY DEAD BEFORE THIS COMMIT --
confirmed by running the same check over the pre-move file, which returns the
identical multiset (11 bare-number self-anchors: #40 x4, #323 x3, #28, #29, #329,
#333; plus 2 links to #13 in COUNSEL-ENGAGEMENT-BRIEF.md, a number this sequence
never had). They are left dead and documented in the archive header rather than
repointed at a plausible neighbour: a citation resolving to the WRONG item is the
erratum's failure mode, and unlike a dead link it looks like success.

The archive carries its retirement banner inline rather than in a sibling README --
docs/archive/throughput/ needs a README because it indexes five documents; one file
does not, and two documents that must agree is a drift surface. It states the rules
that keep the namespace honest: never renumber, re-open by moving the block back
(never by copying, which creates the cross-file duplicate the status check now
fails), and add any future archive file to alloc.ps1's $backlogPaths AND
backlog_status_check.py's DEFAULT_SOURCES in the same commit -- a file named in
neither is policed by nothing.

Gates verified post-move:
  - backlog_status_check.py --min-items 277: OK, 277 items, and it now PRINTS
    "scanned: docs/BACKLOG.md (92), docs/archive/backlog/BACKLOG-CLOSED.md (185)"
  - ledger_check.py on the staged move: rc=0 (relocation adds no numbers, because
    head-union == base-union -- the exact false positive the union view removes)
  - alloc.ps1 -ShowFloor: floor 353 across both paths, next 1000
  - 43 gate tests pass

Note the floor is unchanged at 353 because the highest item (#353) is open and stays
in BACKLOG.md. The archive-sweep fix is therefore PROSPECTIVE, not a save: it starts
mattering the first time a top-of-range item closes and moves.

* docs(backlog): re-score all 92 open items on the ten-level scale (2026-08-03)

Every open item now carries a current value x difficulty score. Before this, 23 had
none at all and the other 69 were from the frozen 2026-07-10 pass, which predates the
2026-07-28 reconcile that closed 31 items -- and a stale score reads exactly like a
fresh one.

Method, unchanged from the pass it supersedes: scored from each item's own Scope /
Why / Trigger / Nearest-existing-mechanism text rather than rescaled from the old
number, then adversarially verified against the code -- a second reader per batch
attacking build state first, then verdict/tier, then value and difficulty. 26 of 92
scores were overturned by that pass and carry the refuter's number.

The banner is the live record and the table is a view of it; both are written here and
a mechanical check confirms 92 banners and 92 rows agree on every triple.

THE RATIONALE IS REPLACED, NOT JUST THE NUMBERS. Carrying an old justification under a
new score is how a banner comes to argue against itself:
  - #114's surviving "clean workaround via the on-demand test probe" is a claim PR #162
    explicitly retracted -- both destinations' test_connection CREATE the target dir, so
    the probe cannot answer the question the toggle asks. That is what lifts it off the
    parity-with-a-workaround band to 6/3. Its replacement rationale was ALSO stale (it
    described the silent-ignore #162 had just fixed) and is hand-corrected.
  - #105's "large greenfield 71-action mapper needing its own ADR" describes an importer
    that has since shipped under ADR 0086.

Scheduling barely moved, which is the reassuring result: only TWO tiers changed --
#64 DEMAND-GATE -> P3 (an index over levers that live in #62/#63/#47/#34, so it ships
nothing runnable of its own) and #105 P3 -> DEMAND-GATE. Neither contradicts an
explicit demand-gate/on-trigger ruling in its own body; that was checked for all 51
items carrying a prior tier.

Distribution is RECOMPUTED with the table rather than carried forward, and all four
lines sum to 92. The superseded table keeps its own frozen lines and now says so.

  Tiers: P1 4, P2 19, P3 17, DEMAND-GATE 52
  Quadrants: quick win 22, big bet 5, fill-in 56, money pit 9

The four P1s: #341 (9/3, a handler returning a tuple/set of Sends delivers nothing
silently -- an accept-and-drop CLAUDE.md §12 forbids), #324 (7/2), #325 (6/2), #327 (6/2).

NOT in this commit: 24 items were found to misdescribe their own build state -- prose
asserting a gap that has since shipped, or citing messagefoundry/console/, a package
retired with #103. Those are banner corrections and land separately; the scores here
already price the remainder rather than the original scope.

Two mechanical faults were caught by reading the output rather than trusting the run:
the quadrant regex omitted the hyphen in "fill-in", so 57 of 69 items took the fallback
branch and got a SECOND score inserted beside the first; and the synthesizer's own
distribution lines did not follow from its own table (11 quadrant mismatches, 8
ordering violations, difficulty summing to 95 of 92). The script now refuses to write
when any line carries two score spans or the scored count is not 92.

backlog_status_check.py --min-items 277: OK, 277 items across both files.

* docs(backlog): correct 10 items whose own prose misdescribed build state

The 2026-08-03 re-score flagged 24 open items as misdescribing what the code does.
Re-verified each against the tree as it stands -- after the archive move and after
PR #162, both of which post-date the findings -- and 10 survived. The other 14 did
not, and are recorded here rather than silently dropped:

  #84 #95 #99 #105 #114 #124 #125 #127 #133 #137 #167 #169 #214 #228

Most of those already carry an amendment that covers the stale sentence (#95, #99,
#105, #114, #124, #125, #127, #133, #228), and stacking a second ruling saying the
same thing is noise. The rest did not survive verification: the finding was itself
wrong or overstated, and a wrong correction in a ledger is worse than a stale one.

CORRECTIONS ARE ADDED AS DATED AMENDMENTS, NOT PROSE REWRITES. This file's convention
is to leave the original claim standing and rule against it, so the record shows what
was believed and what replaced it. Silently editing the stale sentence would destroy
the evidence that makes the correction checkable.

Applied to #62 #64 #131 #166 #179 #182 #237 #321 #329 #336. Representative:

  - #329 "Five MEFOR_ALLOW_INSECURE_TLS cells": the census is FOUR. #323 landed and
    routed transports/direct.py through the clamp; it now holds no call to the raw
    predicate at all (:63, :197, :215).
  - #321 "no test asserts the detectors can see a site code": false --
    tests/test_scan_forbidden.py has per-class hit tests for at least the site code
    (:126), a customer name (:83), a case-sensitive code (:91) and a routable IP
    (:107). The detector-coverage half of its Proposed 2 is already in the tree.
  - #62 plans a dual-read over "existing mfenc:v1 rows", but cell-bound mfenc:v2 is
    the default writer (settings.py:383 -> base.py:1841; crypto.py:36), and v2 folds
    (table, column, pk) into the GCM tag -- so a body landing under a different column
    must be RE-ENCRYPTED, not merely re-encoded. That tightens the catch.
  - #64's ordered plan still reads live ("Nothing builds before it"), but the
    measure-first phase completed 2026-07-12 (ADR 0051) and its step-2 lever is
    refused outright (ADR 0055 withdrawn; ADR 0107 "Do not build F2 or F3").

The refuters removed two overclaims before they landed: #62's draft asserted a live
store holds both mfenc markers (a fresh store under the shipped default holds only
v2 -- the defensible claim is that a MIGRATION must expect both), and #64's asserted
the multi-DB log split still remains, which could not be verified against ADR 0098 and
would have been a fresh false claim.

No item closes here: in every case the correction narrows the remainder rather than
discharging it, and the 2026-08-03 scores already price the remainder.

backlog_status_check.py --min-items 277: OK, 277 items, one status banner each.

* docs(backlog): file BACKLOG #1000 — prove each required merge context can fail

Escalated by the coordinator on the ground that it outlives the PR that fixed it.
Deliberately NOT filed as "fix the two-dot diff": that instance already landed in
39b62bf, and filing shipped work is the rot the hygiene gate exists to prevent.

The item is the CLASS. `.github/required-contexts.txt` names 13 contexts that block
merge, and not one of them is proven able to go red. The deliverable is a negative
control per context -- a fixture carrying the exact violation that context exists to
catch -- plus a CI job that fails when a required context has none, so the coverage
cannot silently decay as contexts are added.

Scoped narrower than "test the gates" on purpose: it does not re-test what each gate
checks, since the gates' own suites do that. It asserts one property per context --
this gate is capable of failing.

The argument is that the class has now fired at least four times here, each found by
hand and none by CI:

  #334  semgrep, required and blocking, scans a two-directory allow-list
  #327  six .gitignore rules are the sole control over maintainer-internal docs, and
        nothing asserts they still match anything
  #321  the forbidden-content gate exited 0 on a real site code and partner product
  #325  the same gate's home-path detector misses 1 of 4 spellings of a Windows path

Each is correctly filed as its own defect. None of them establishes the property that
would have caught all four before they shipped, and that property is a different
artifact from any of the individual fixes.

Value 7 / Difficulty 3, quick win, P1 -- not demand-gated; the trigger fired four
times. Ranked table and all four distribution lines recomputed to 93 open items; a
mechanical check confirms 93 banners and 93 rows agree on every triple.

Number allocated atomically via scripts/coord/alloc.ps1 (#1000 -- the first in the
post-partition public sequence, clamped to >= PUBLIC_BACKLOG_FLOOR), never grepped.

backlog_status_check.py --min-items 277: OK, 278 items across both files. The floor is
a floor, so growth passes it; it is there to catch shrinkage.
wshallwshall added a commit that referenced this pull request Aug 4, 2026
… not rows

Supersedes claude/plan-rule1-amendment, which I am abandoning. That branch did two
things at once and only one was mine to do.

WHAT I GOT WRONG FIRST. It reversed RULE 1's "the ranked table and census are OWNER-ONLY"
on the strength of an inter-session agreement that exists in no committed document. The
coordinator put that authority in question and is right to: an unwritten agreement is not
a mandate to rewrite a rule in a DISPATCH document, and a commit behind it would have
given a contested rule the appearance of settlement. RULE 1 stands verbatim as on
origin/main; changing it is the owner's call.

WHAT I GOT WRONG SECOND, caught by the session that had originally given me the rule and
then measured it. I wrote "re-derive from rows whose BANNER IS OPEN". That is still a
ROW-based rule and it is insufficient. The correct unit is an open HEADING:

  An open item is a `## N. ` heading in docs/BACKLOG.md -- NOT inside a fenced code
  block -- whose leading blockquote carries at least one of 🔢 🚧 and none of ✅ ⛔ 🪦.

Alphabet from source, not memory: backlog_status_check.py:79-80.

ROWS FAIL IN BOTH DIRECTIONS, which is why the banner-on-rows form is not enough:
  closed item, ROW RETAINED with ✅   (pending in #177 for #335)  -> over-counts by 1
  item filed with NO ROW at all       (the shape #105 uses)       -> under-counts by 1

AND WITH BOTH PRESENT THE TOTAL IS RIGHT WHILE BOTH SETS ARE WRONG. I did not take this
on report -- simulated across the four reachable states: truth 92, raw row count 92
(matching BY CANCELLATION), rows-with-open-banner 91. A total-only assertion passes it.
Only a two-directional heading<->row comparison catches it, so a census that merely adds
up is not evidence the table and the items agree.

EDGE CASE, and it fires on exactly the work in flight: the status gate has NO fenced-code
handling -- verified, zero fence logic in backlog_status_check.py. A ```markdown block
containing a `## 1234.` banner template inside an item body raises the parsed count, and
pasting a template into an item body is the trigger. ledger_check.py DOES see fenced
text, so an unallocated fenced number trips pre-commit -- but a legitimately allocated one
slips through both gates.

Published main is currently internally consistent: 92 open headings, 92 rows, strict
bijection, contiguous ranks, census matching a fresh recompute. Nothing to repair today;
both defects are pending in open PRs.

The reason the heading and its banner are authoritative is already in the ledger at
docs/BACKLOG.md:165 -- the banner is the live record and wins over the table. So a census
must be REPRODUCIBLE FROM THE RECORD, not TRUE ABOUT THE WORLD: #327's banner is stale
while its work is merged, and correcting the count against code would make every recompute
an argument about who inspected better.
wshallwshall added a commit that referenced this pull request Aug 4, 2026
…aid 93

Caught in review. I moved three of the four lines, not four: "Every one of the **92
open items** is re-scored here" (:144) still read 92 while Tiers and "sum to N" read 93.
Line 1 disagreed with lines 2-4 on the branch as pushed, which is precisely what the
"all four lines sum to N" invariant exists to catch -- and my recompute script only
rewrote the three lines it generated, never the prose sentence.

Verified this tree with the repo's OWN parser rather than my regex:
parse_items -> 93 items, 93 open, 0 closed. My whole-range banner scan agreed exactly.

That agreement is luck, not method, and worth recording as such. parse_items ends an
item's banner block at the first line that is neither blank nor a blockquote, so a `✅`
sitting AFTER an item's prose is batch-filing narrative, not that item's status. My scan
read the whole heading range and would have swallowed one. No item in this file
currently has that shape, so the two methods agree here and would diverge on a file that
does -- a reviewer's hand-rolled checker got three different wrong answers today,
including calling #3, #105 and #141 closed. Use parse_items.

NOT PREDICTING THE POST-#177 NUMBER. Once #177 lands (it closes #335), this tree becomes
93 items / 92 open / 93 rows, so the census wants 92. I am deliberately NOT writing 92
now: predicting it is carrying a delta in my head, which is the thing
re-derive-never-delta forbids. All four lines are set to 93, which is derived and correct
for THIS tree, and the number gets re-derived from the merged tree afterwards.

Worth recording because it is the rule catching its own author: the two defects PARTIALLY
CANCEL. Post-merge the line I missed (92) would have become accidentally correct while
the 93s I did update became wrong -- and a total-only assertion sails straight through
that. It is the same cancellation failure the RULE 1 amendment on this very branch warns
about, arriving in the PR that carries the warning.
wshallwshall added a commit that referenced this pull request Aug 4, 2026
…ockers, amend §D RULE 1 (#178)

* docs: file BACKLOG #1003 (lab validation), amend §D RULE 1 by owner ruling

Three changes, batched into one PR because the queue is rate-limited by PR count, not
by diff size.

1. BACKLOG #1003 — validate the lab and discharge the four hardware-gated residuals.
   A multi-VM server is ~2 weeks out. Four open items are blocked on that one missing
   thing: #99 (the live domain-lab gMSA/SSO smoke — its ONLY remaining residual), #98
   (Kerberos EPA, same DC + AD CS gate), #320 (the decisive windows-2025 sweep, blocked
   on an unregistered runner) and #351 (execute the failover patch against a real SQL
   Server, and measure ADR 0159's _acquire cost).

   TRIGGER IS "LAB AVAILABLE FOR VALIDATION", NOT "LAB VALIDATED" -- owner's correction
   to my wording, and it was circular: proving the lab does what these items need is this
   item's own first deliverable, so gating on validation means the trigger can never fire.

   Not a roadmap umbrella. Its deliverables are runs with recorded outcomes, which is the
   distinction that scored #64 a 1/10 for shipping nothing runnable.

   #351's measurement is called out as the one that is easy to lose: its patch makes the
   test deterministic, which removes the only thing currently raising the latency
   question. The item itself says a lease-election test is the wrong instrument for
   discovering latency -- so the measurement belongs on the rig, and landing the patch
   without it drops the question rather than answering it.

2. #99, #98, #320 and #351 each get a dated note that the hardware blocker HAS AN EXPIRY.
   #99 currently says its residual is "rig/provisioning the project does not own"; #320
   says its runner is unregistered. Both are true today and scheduled to become false.
   Left alone those sentences keep telling every planning pass the work is unreachable --
   the same stale-premise rot the 2026-07-28 reconcile found on five items and the
   2026-08-03 re-score found on twenty-four.

3. §D RULE 1 amended BY OWNER RULING: the ranked table and census are not owner-only,
   they are recomputed by whoever writes the ledger last. Recorded as an owner decision
   rather than a correction, because that is exactly the standing an earlier branch of
   mine lacked -- it reversed the same clause on an inter-session agreement in no
   committed document, and was rightly refused. The argument did not change; the
   authority did.

   Recorded with the condition that makes it safe: it depends on the unit being
   open-bannered HEADINGS and on the re-derivation being one operation with the
   stale-banner sweep. Without both, sessions recomputing is worse than the stale rule.
   Owner-only was never safer on correctness -- it leaves the census stale whenever the
   owner is away, and an owner re-deriving from stale banners launders the same
   false-open.

CENSUS RE-DERIVED under the corrected rule, and the two-directional heading<->row
comparison run rather than a total-only check: 93 open headings, 93 rows, zero headings
without a row, zero rows without an open heading. All four lines sum to 93.

  Tiers: P1 5, P2 19, P3 17, DEMAND-GATE 52
  Quadrants: quick win 24, big bet 5, fill-in 55, money pit 9

The bijection check is the point, not the total: an over-count and an under-count cancel,
so a total that adds up is not evidence the table and the items agree.

Number allocated atomically via alloc.ps1 (#1003; #1002 was already taken). Never grepped.

* docs(backlog): the fourth census line said 92 while the other three said 93

Caught in review. I moved three of the four lines, not four: "Every one of the **92
open items** is re-scored here" (:144) still read 92 while Tiers and "sum to N" read 93.
Line 1 disagreed with lines 2-4 on the branch as pushed, which is precisely what the
"all four lines sum to N" invariant exists to catch -- and my recompute script only
rewrote the three lines it generated, never the prose sentence.

Verified this tree with the repo's OWN parser rather than my regex:
parse_items -> 93 items, 93 open, 0 closed. My whole-range banner scan agreed exactly.

That agreement is luck, not method, and worth recording as such. parse_items ends an
item's banner block at the first line that is neither blank nor a blockquote, so a `✅`
sitting AFTER an item's prose is batch-filing narrative, not that item's status. My scan
read the whole heading range and would have swallowed one. No item in this file
currently has that shape, so the two methods agree here and would diverge on a file that
does -- a reviewer's hand-rolled checker got three different wrong answers today,
including calling #3, #105 and #141 closed. Use parse_items.

NOT PREDICTING THE POST-#177 NUMBER. Once #177 lands (it closes #335), this tree becomes
93 items / 92 open / 93 rows, so the census wants 92. I am deliberately NOT writing 92
now: predicting it is carrying a delta in my head, which is the thing
re-derive-never-delta forbids. All four lines are set to 93, which is derived and correct
for THIS tree, and the number gets re-derived from the merged tree afterwards.

Worth recording because it is the rule catching its own author: the two defects PARTIALLY
CANCEL. Post-merge the line I missed (92) would have become accidentally correct while
the 93s I did update became wrong -- and a total-only assertion sails straight through
that. It is the same cancellation failure the RULE 1 amendment on this very branch warns
about, arriving in the PR that carries the warning.
@wshallwshall
wshallwshall disabled auto-merge August 4, 2026 17:27
@wshallwshall

Copy link
Copy Markdown
Collaborator Author

Superseded by #183, which lands this item's substance correctly.

Two defects made this branch unmergeable as authored, both structural rather than sloppy:

  1. The number was never allocated. wave 3: IDE Steps view, ECH retirement, finite retry -- with ledger banners (5 items) #319 did not come from alloc.ps1, so the ledger gate rejects it. backlog: archive #335, file #1002 for #105's renumber, re-derive the census #183 files the same item as #1002, which is allocated.
  2. No ranked-table row. This branch filed the item as a heading with an open banner and no row, which breaks the heading-to-row bijection in the opposite direction from the fix(test): pin the RecursionError contract without a real stack overflow (#1222) -- unreds main #335 defect backlog: archive #335, file #1002 for #105's renumber, re-derive the census #183 also fixes. The two cancel in a total, which is why a total-only census check would have passed over both.

Also: the docs/AOAG-DEPLOYMENT.md change could not be taken as a file. Main's copy has moved 29 insertions / 23 deletions since this branch was cut, so merging the file would have reverted main's newer content. #183 applies the note as a block with the anchor slug renumbered and verified to resolve.

Nothing is dropped. The item body is carried verbatim apart from the renumber, the AOAG note is carried with its two references retargeted, and the dating this PR's title promised is intact.

wshallwshall added a commit that referenced this pull request Aug 4, 2026
…census (#183)

ONE operation, deliberately. The pieces cannot be split: removing a closed-but-rowed
item and adding a new rowed item both move the rank column and both change every
census line, so two passes would each publish a wrong count in between.

WHAT MOVED

Archived #335 out. Its work landed in #177; it carried a closed banner while KEEPING
its ranked-table row, which is what broke the heading-to-row bijection. Moved verbatim
to BACKLOG-CLOSED.md (60 lines, unedited) and its row dropped.

Filed #1002 - the AG-rig validation that PR #105 filed as #319. #319 was never
allocated through alloc.ps1 and #105 gave it NO ranked-table row, so merging #105 as-is
would have duplicated the item under a second number AND left a rowless heading. #1002
is allocated to this worktree. PR #105 is superseded and will be closed.

Added the AOAG-DEPLOYMENT.md note #105 carried, renumbered to #1002, applied as a block
rather than cherry-picked: main's copy of that file has moved 29 insertions / 23
deletions since #105 branched, so taking the file would have reverted main's newer
content. The anchor slug was verified to resolve against the new heading, character for
character, rather than assumed.

Re-derived all four census lines plus the sum-to assertion. Note line 144 - "Every one
of the **93 open items**" - was SEVEN out, not one: it is the line the recompute script
does not emit, so it drifts on every filing and was last corrected in #178. Fixing the
line again without fixing the generator guarantees a third occurrence; that is recorded
in the plan rather than papered over here.

MEASURED AFTER, with parse_items imported from backlog_status_check.py and never a
hand-rolled scan (CLAUDE.md section 11 - I wrote three hand-rolled checkers in one day
and got three different wrong answers, the third falsely reporting three OPEN items as
closed):

  101 items - 101 OPEN - 0 closed-in-file - 101 live rows
  open heading with no row : NONE
  row whose item is not open: NONE
  ranks contiguous 1..101  : True
  all four census lines sum to 101

The bijection now holds in BOTH directions. A matching total alone would not have shown
that: a closed-but-rowed item and a filed-without-a-row item cancel, and every
total-only assertion passes while both sets are wrong.

ONE NEAR-MISS WORTH RECORDING. My first attempt renumbered 235 ranks. The live table
has 101; the file also holds a superseded 134-row 2026-07-10 table, and an unscoped
renumber walked straight into it and continued the sequence. Reverted and redone with
the renumber bounded to the live section, plus an assertion that the historical rows are
byte-identical afterwards. The gate would not have caught it - it reads item banners,
not ranks.

VERIFIED, including the guards a docs-only PR does not run. The pytest legs are gated on
`code == 'true'`, and `.md` is in the noncode allowlist, so for this PR they are skipped
pre-merge and fire only on the push to main. Run locally: test_cutover_slug_rot.py (the
two-way prose ratchet, sitting exactly at its ceiling), test_backlog_status_check.py and
test_feature_map_claims.py - 30 passed. Ledger gate OK at 288 items across both files.
Leak gate exit 0 with real detectors armed.

At least three items still carry banners that understate what shipped. Stated as "at
least" on purpose: a sweep is what establishes the real count, and an enumeration here
would be the completeness claim CLAUDE.md section 11 warns against.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant