Skip to content

feat(cutover): develop directly on the public repo — relocate the leak gate, retire the publish machinery - #3

Merged
wshallwshall merged 3 commits into
mainfrom
cutover-phase1
Jul 27, 2026
Merged

feat(cutover): develop directly on the public repo — relocate the leak gate, retire the publish machinery#3
wshallwshall merged 3 commits into
mainfrom
cutover-phase1

Conversation

@wshallwshall

Copy link
Copy Markdown
Collaborator

Switches MessageFoundry from the private-repo + published-mirror model to developing directly here.

The publish step was quietly enforcing several invariants. Retiring it means each one needs an explicit control, and this PR adds them.

The gate was vacuous on this repo

forbidden-content is a required check. Before this PR it invoked a scanner that had been deny-listed off the mirror, logged scan_forbidden.py absent (OSS mirror) — skipping, and exited 0. A required check that cannot fail.

It now runs the relocated scanner fail-closed over the whole tree, and hard-errors when the secret is absent on a non-fork run rather than degrading silently.

Presence is not sufficiency

Requiring a token source only proves one arrived. A partially-mangled secret previously loaded as few as 1 of 21 detectors and passed a gate calling itself fail-closed. So requiring tokens now also requires:

  • every floor section (names/estate/site_prefixes) to be non-empty;
  • a per-section floor. A bare total is a SUM, so growth in a cheap section masks collapse in an expensive one — names 7→1 alongside estate 13→19 still totals 21;
  • entries that parse but can never fire to be rejected: an invisible/zero-width codepoint inside a token (it survives str.strip(), unlike NBSP — exactly what a paste through a rendering surface produces), the (?!) never-match sentinel, non-ASCII digit prefixes, and duplicates.

Detection gaps closed

  • Identifier form. _ is a word character, so a \b-anchored pattern cannot see its token inside OB_TOKEN_ORU. A second pass with _ neutralised closes this for every name pattern. Restricted to single-token patterns — applied to multi-word patterns it matched 9 ordinary snake_case identifiers on the real tree, every one a false positive.
  • Two detectors restored that were lost in the relocation (_WORKTREE_SLUG, _HOME_PATH), verified byte-identical to the originals.

Reason-only on every path, including failures

Parse warnings no longer echo the offending entry, and a REASON matching its own pattern is replaced with a generic label. Both would have published a customer token into a world-readable Actions log — on the success path, in the second case.

Also

  • .pre-commit-config.yaml: the hook is added, not editedpublish.ps1 stripped it from every published snapshot, so it has never existed here. It carries --require-tokens, because pre-commit can pass args but cannot set env for one hook; without it a fresh clone commits with zero customer detectors and reports "Passed".
  • Allowlist breadth validation: one degenerate entry (., .*) vetoed every line before any detector ran, disabling the gate while the counts line still reported full tables.
  • security.yml gains a main-only push trigger — a fork PR is scanned structural-only by design, so nothing fully-loaded ever re-scanned fork-contributed content after merge.
  • Two docs genericized: they wrote the real site prefix to describe the detector's shape. That is what takes a whole-tree scan from 3 hits to 0, and why no legacy path-exemption was ported — fixing the source beats exempting it.
  • The gate's own tests no longer trip it; literal probes are assembled at runtime, matching the convention the suite already used for IPs.

Verified

whole-tree leak scan exit 0, all 21 detectors, fail-closed
full suite 8,891 passed / 0 failed
console suite 338 passed; 1 failure reproduces on clean main, unrelated, tracked separately
ruff format clean
ruff check / mypy 13 / 21 — identical to a baseline measured on main
mutation testing 21 targeted reverts of the new guards, 0 undetected

…st used to protect

The cutover retires scripts/publish/publish-denylist.txt. On the private repo CLAUDE.md, .claude/,
docs/security/ and the other private-only paths were TRACKED, and publish.ps1 stripped them when
producing the mirror. Development now happens directly on the public repo, so nothing strips them
any more -- a gitignore rule is the only remaining control, and the cutover procedure runs
`git add -A`.

Verified before this commit: CLAUDE.md (33KB of internal engineering guidance) and
.claude/settings.json were both untracked-and-committable in the fresh clone.

Not ignored on purpose: tests/test_scan_forbidden.py, tests/test_anon_core.py and
.github/dependabot.yml. They were deny-listed as well, but this change set publishes
synthetic/public versions of them; ignoring them would silently drop intended content.
…k gate, retire the publish machinery

Switches MessageFoundry from the private-repo + published-mirror model to developing directly here.
The publish step was the thing enforcing several invariants; each one now needs an explicit control.

SCANNER RELOCATED AND HARDENED (scripts/publish/ -> scripts/security/)
The token list is externalized: the committed scanner carries only STRUCTURAL detectors plus a
synthetic .example, and the real customer/vendor tokens arrive from the MEFOR_FORBIDDEN_TOKENS
secret in CI and a git-ignored local file at commit time.

Requiring a token SOURCE is not sufficient, so this also adds:
  * every floor section (names/estate/site_prefixes) must be non-empty -- a partially-mangled secret
    previously loaded as few as 1 of 21 detectors and passed a gate calling itself fail-closed;
  * a PER-SECTION floor (MEFOR_MIN_DETECTORS=names=7,estate=13,site_prefixes=1). A bare total is a
    SUM, so growth in a cheap section masks collapse in an expensive one;
  * rejection of entries that PARSE but can never FIRE -- an invisible/zero-width codepoint inside a
    token (which survives str.strip(), unlike NBSP, and is what a paste through a rendering surface
    produces), the (?!) never-match sentinel, non-ASCII digit prefixes, and duplicates;
  * an identifier pass: `_` is a word character, so a \b-anchored pattern cannot see its token inside
    OB_TOKEN_ORU. Restricted to SINGLE-TOKEN patterns -- applying it to multi-word patterns matched 9
    ordinary snake_case identifiers on the real tree, every one a false positive;
  * reason-only reporting on every path, including failures. Parse warnings no longer echo the
    offending entry, and a REASON that matches its own pattern is replaced with a generic label --
    both would have published a customer token into a world-readable Actions log;
  * allowlist breadth validation: one degenerate entry ('.', '.*') vetoed every line BEFORE any
    detector ran, disabling the whole gate while the counts line still reported full tables;
  * --require-tokens[=N], because pre-commit can pass args to a hook but cannot set env for one.

CI + COMMIT-TIME WIRING
  * security.yml: the forbidden-content job was VACUOUS on this repo (it invoked a scanner that was
    deny-listed off the mirror, logged "skipping", and exited 0 behind a required check). It now runs
    the relocated scanner fail-closed over the whole tree, and hard-errors when the secret is absent
    on a non-fork run rather than silently degrading.
  * security.yml gains a main-only push trigger: a fork PR is scanned structural-only by design, so
    without this nothing fully-loaded ever re-scanned fork-contributed content after merge.
  * .pre-commit-config.yaml: the forbidden-content hook is ADDED, not edited -- publish.ps1 stripped
    it from every published snapshot, so it has never existed here. It carries --require-tokens, so a
    checkout without the token file refuses to commit instead of passing green with zero detectors.

PRIVATE-PATH PROTECTION (see the preceding commit)
CLAUDE.md and .claude/ were TRACKED privately and kept out of the mirror by the deny-list. With the
deny-list retired, gitignore is the only remaining control.

ALSO
  * ADR 0030 4h: the anonymizer's site-code prefix is externalized, so it is no longer a literal in
    tracked source. test_security_static's unresolvable-regex pin is updated accordingly -- with the
    reason, not silently.
  * Two docs genericized: they wrote the real site prefix to describe the detector's shape. This is
    what takes a whole-tree scan from 3 hits to 0, and is why no legacy path-exemption was ported.
  * The gate's own tests no longer trip it: literal probe strings are assembled at runtime, matching
    the convention the suite already used for IPs. Allowlisting the test files would have blinded the
    gate to the exact classes they exercise.
  * Retires release-sync-check.yml and backlog-hygiene.yml (scripts/publish/ and test_publish_gate.py
    were already absent here).

VERIFIED
  * whole-tree leak scan: exit 0 with all 21 detectors loaded, fail-closed
  * full suite 8,891 passed / 0 failed; console suite 338 passed with one failure that reproduces on
    a clean checkout of main and is unrelated (tracked separately)
  * ruff format clean; ruff check 13 and mypy 21 -- both identical to a baseline measured on main
  * 21 targeted mutations of the new guards, 0 undetected
…ip the parity tests

CI caught what local runs could not: test_leak_tables_are_sourced_from_the_guard_when_present failed
on all three test legs while passing locally.

ROOT CAUSE, and it is caused by this change set's own central decision. The test skipped when the
guard was ABSENT, else asserted the token tables were populated. That premise -- guard present implies
tokens populated -- held only while the guard lived under the deny-listed scripts/publish/ AND carried
its tokens as literals. This cutover breaks both halves: it relocates the guard to scripts/security/
(so it IS present on the public repo) and externalizes the tokens (so presence proves nothing). CI
took the non-skip path with no token source and found empty tables. It passed locally only because a
developer working tree has the git-ignored token file.

Now gated on the TOKEN SOURCE -- the condition that actually determines whether there is anything to
source. Verified in all three states: passes with tokens, skips without, and still FAILS when the
bridge in tee/anon/leak.py is blanked. Not traded for a vacuous skip.

TWO PARITY TESTS WERE SILENTLY SKIPPING
_load_scan_forbidden() still pointed at scripts/publish/scan_forbidden.py, which no longer exists, so
pytest.skip fired at module level. Those assertions are the only thing keeping tee/anon/leak.py's
token tables identical to the guard's -- without them the tee copy can drift unnoticed. Repointed;
they now run (10 passed, 0 skipped) and go red under mutation. Same failure shape as the vacuous
forbidden-content job: a check that looks green because it never executed.

REFERENCE SWEEP -- 32 references across 19 files, four distinct treatments
  * REPOINT   scan_forbidden.py -> scripts/security/, including relative markdown link depth.
  * FLAG      --published no longer exists (it scanned the deny-list's "publishable subset"); the
              scanner now scans the whole tree via --path .
  * REWORD    publish.ps1, publish-denylist.txt and check_release_sync.py are GONE, with no new path.
              Repointing them would have produced links to files that never existed. The Secure Build
              Scorecard in particular asserted the gate scans "the publishable subset" via a deny-list
              this branch deletes -- a correct path with a false claim is worse than a broken link.
  * LEAVE     past-tense statements ("formerly enforced by", "the retired ...") were already correct.

release.yml is deliberately COMMENT-ONLY. Its slug-guard logic is release-critical and contains a
now-dead no-op sed; both belong in their own change rather than buried in a cutover sweep. The
comments still had to be fixed -- a false comment next to a guard is how the guard later gets
"normalized" into breaking.

Also updates .github/CODEOWNERS from /scripts/publish/ to /scripts/security/.

VERIFIED: ruff format clean; ruff check 13 and mypy 21, both identical to a baseline measured on main;
whole-tree leak scan exit 0 with all 21 detectors; 129 tests across the affected suites.
@wshallwshall
wshallwshall merged commit 59fbc93 into main Jul 27, 2026
31 checks passed
@wshallwshall
wshallwshall deleted the cutover-phase1 branch July 27, 2026 00:38
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 added a commit that referenced this pull request Aug 5, 2026
#193)

* fix(ci): dependabot auto-merge decided by exclusion — invert #3 to an allow-set (BACKLOG #336)

Guardrail #3 was a 16-name Python deny-list with no ecosystem qualifier: anything not named
auto-merged if it was a patch, on every ecosystem. Inverted to HOLD-UNLESS-NAMED. Only `actions/`,
`github/` and `dependabot/` are eligible, and only on github-actions; the uv, pip and npm rows ship
EMPTY, an unrecognised ecosystem token holds, and the fail-safe whole-group denial is preserved.
Measured against the live PR #75, not recalled: its body carries five `Updates` entries — four
on the allow row (actions/checkout and three github/codeql-action/*) and pypa/gh-action-pypi-publish,
which is not — so that batch would HOLD as a whole. Expect auto-merge to fire RARELY; that is the
intent of hold-unless-named, not a regression.

Hardening, not an incident. There is no evidence of exploitation, the attacker must already own an
upstream publisher account, and merging to main is not publication (PyPI release is gated on an
owner tag push). MessageFoundry is a not-deployed beta with zero production instances, so this is
stated as what the shipped code WOULD allow on first deployment, never as a present exposure.

§4 RELEASE AGE, with an honest account of its reach. A new `id: age` step holds a SECURITY-track PR
whose candidate version was published under MIN_RELEASE_AGE_HOURS (24), failing closed on an API
error, an absent or unparseable upload timestamp, an unexpected name or version shape, or an
ecosystem with no publish-date source wired. It is INERT with respect to the merge decision as
shipped: age_ok=true is reachable only for uv/pip, eligible=true only for github-actions, and the
merge `if` requires both — disjoint sets. Confirmed by executing the shipped allow-set body over
exactly the ecosystems the age gate can pass (uv and pip both return eligible=false; only
github_actions returns true). The
header and the BACKLOG banner now say exactly that instead of presenting it as an operating
control; recording an unreachable control as operative is the false-premise class
docs/Secure_Development_Standards.md §3 forbids. The step is gated on
`steps.allowset.outputs.eligible == 'true'`, which changes no merge outcome — the merge `if`
already requires that conjunct — but removes an unauthenticated outbound GET made from a job
holding `contents: write` for a PR that was going to be held anyway.

§5 the header's backstop claim, corrected without over-claiming in the other direction. Dropped:
"the only gate that inspects a dependency's shipped bytes at all is security.yml's trivy step".
security.yml:265 marks trivy `continue-on-error: true` and :271 restricts it to schedule/dispatch,
so it is advisory AND never runs on a Dependabot PR. The replacement is the weaker true form: no
REQUIRED check reads a dependency's shipped bytes. The sentence describes what semgrep cannot SEE
rather than where it looks, so the unmerged plan-semgrep-scope widening (BACKLOG #334) cannot
falsify it.

Provenance made honest. The header cited DEPENDENCY-POSTURE-REVIEW.md as the numbered source for
guardrails it does not carry: #3 is INVERTED from the deny-list that document describes, and #4 is
introduced here. The block is retitled AUTO-MERGE GUARDRAILS, #3 is moved out from under the
security-track framing (it gates every PR), and the test docstring carries the same correction.
Amending the vault copy stays the owner's separate obligation — the repo simply stops asserting it
has already happened.

Restated facts removed. The header repeated three cooldown values that live in
.github/dependabot.yml; only two had a test bound, and that file records github-actions' window as
approximate because it ages off the tag's commit date. The header now points at the source instead
(CLAUDE.md §11 — state a load-bearing fact once and link to it).

.github/dependabot.yml comments corrected. The uv block told a maintainer "routine patches
auto-merge AFTER aging; security patches auto-merge now" — both halves false once uv's allow row
ships empty. The github-actions block's "an advisory fix is unaffected" now says it opens as a PR
unaffected but reaches main by human review, because the advisory gate is pip-keyed and never
confirms an action.

docs/testing/master-test-plan/01-environments-data-and-tooling.md:350 still advertised the deleted
auth/token/crypto deny-list as a live control of this workflow. No test pinned that doc, so it
drifted silently; the row now describes what ships.

TESTS
- The behavioural harness runs the shipped bodies under `bash -e`, which is what Actions applies by
  default on Linux (no `shell:` is declared anywhere). Plain bash keeps going where CI aborts the
  step, and `assert proc.returncode == 0` was exactly the assertion that would mask that class. No
  shipped row changes its decision: ten rows were compared head-to-head under plain bash and under
  `bash -e` and agreed on both exit code and emitted output, and the full 20-row parametrised set
  passes under `-e`. So this is a fidelity fix, not a behaviour change — but `_run_step_body` now
  RETURNS the returncode rather than asserting it, so an abort path can be expressed as an expected
  outcome instead of being indistinguishable from a harness bug.
  test_no_step_overrides_the_default_shell is the tripwire for a future `shell:` silently
  invalidating the premise that `bash -e` is what CI runs.
- _load_dependabot's skip is deleted. It skipped on the premise that .github/dependabot.yml is
  "private-only, deny-listed on the OSS mirror". .gitignore names that file under DELIBERATELY NOT
  LISTED as content meant to ship; `git ls-files --error-unmatch` resolves it and `git check-ignore`
  exits 1. The refactor had routed THREE tests through that skip, including the cooldown test whose
  entire reason for existing is that a missing cooldown was invisible to CI. It now asserts presence.
- The jq skip reason claimed the test "runs on the ubuntu CI leg and skips locally". The
  windows-2022 and windows-2025 images ship jq and Git Bash, and both are REQUIRED contexts in
  .github/required-contexts.txt, so it runs there too. The reason now says so — a maintainer reading
  it after a Windows-only red should not be told to expect ubuntu.

ADR 0034 CONSEQUENCE, recorded because leaving it unrecorded is what that ADR warns against: this
file grew 190 -> 403 lines and the `Why pull_request` anchor moved from line 42 to 76. Under the
ADR's convergence rule, dismissed alert #87 re-fires as a new alert number and needs re-dismissing
after merge.

VERIFICATION
- ruff check .           -> All checks passed!
- ruff format --check .  -> 1042 files already formatted
- mypy messagefoundry    -> 21 errors, ALL PRE-EXISTING and NOT from this change: absent
  [fhir]/[dicom]/[webauthn] extras (fhirpathpy, fhir, pynetdicom, pydicom and webauthn are all
  ABSENT from this venv; 12 import-not-found plus their 7 no-any-return / 2 unused-ignore
  consequences). This change touches no file under messagefoundry/, so mypy's inputs are
  byte-identical to HEAD. CI installs the extras and is the authority. The quartet is NOT green
  locally and this commit does not claim it is.
- pytest -q              -> 10291 passed, 841 skipped, 22 warnings in 1282.01s (0:21:22). The
  pre-change baseline measured on this tree was 10289 passed / 841 skipped, so the delta is exactly
  the two tests added here. Run on a byte-frozen tree (sha256 of all five changed files recorded
  before the run and unchanged after), because two comment edits had landed after an earlier run
  started and six tests read docs/BACKLOG.md.
- Falsification: 18 mutations of the workflow and dependabot.yml, plus deleting dependabot.yml
  outright, each confirmed to turn the matching test RED and then restored byte-identically (sha256
  checked after every case). Among them: the allow-set admitting `pypa/`, losing its trailing slash,
  matching by substring, treating an empty name list as eligible, and failing open on an
  unrecognised ecosystem; the age step failing open on an undatable ecosystem, hard-coding a PASS on
  the version-track exit, re-deriving the security track, and losing its allow-set gate; a step
  declaring `shell:`; the merge `if` dropping either the age_ok or the allow-set conjunct; and each
  cooldown being removed or shortened. The four jq-gated rows were falsified separately (threshold
  deleted, missing-timestamp failing open, curl error failing open, and the discriminating PASS
  removed) against a purpose-built jq stand-in implementing the two shipped filters, because this
  box has no jq at all; execution against real jq happens on CI.

* docs(backlog): flip #336 to SHIPPED and supersede one clause of its 2026-08-03 amendment

Banner lines of item #336 ONLY.

VERIFIED BY NUMBER, not by banner text — a byte-identical banner pasted under the wrong item
survives every well-formedness check. `git diff -U0 -- docs/BACKLOG.md` is two hunks, @@ -3353
+3353 @@ and @@ -3358,0 +3359,2 @@; the enclosing `## ` heading computed backwards from each of
the three changed lines (3353, 3359, 3360) is `## 336. Dependabot auto-merge shields review with a
deny-list` in all three cases. The ranked table and the four census distribution lines are
untouched.

THE CENSUS WAS NOT RECOMPUTED. This commit flips one item's banner and deliberately does not touch
the distribution lines.

The banner records guardrail #4 as a FORWARD guard that is inert with respect to the merge decision
as shipped, rather than as an operating control: age_ok=true is reachable only for uv/pip,
eligible=true only for github-actions, and the merge `if` requires both. Recording an unreachable
control as operative is the false-premise class docs/Secure_Development_Standards.md §3 forbids.

The added blockquote uses the glyph-free `**AMENDED 2026-08-04 — ...**` convention. It supersedes
ONE clause of the dated 2026-08-03 note — "The deny-list itself is untouched, so the rest of the
item stands ... §§1, 3, 4, 5 and 6 are unaffected" — which was accurate when measured and is
deliberately left as written rather than rewritten. The deny-list no longer exists, so §6 is
discharged by deletion rather than annotation, and the 16 names survive only as a PROPERTY under
test (`_DENY_PACKAGES` asserts none of them reaches any allow row).

scripts/docs/backlog_status_check.py exits 0 ("290 backlog items, each declaring exactly one
status") — reported as corroboration only. That gate validates that one banner is present and
self-consistent, never that it belongs to this item, so it cannot see the corruption class this
commit had to avoid. The by-number check above is the evidence; the green gate is not.
wshallwshall added a commit that referenced this pull request Aug 5, 2026
…sambiguate PR citations that already mis-resolve (#209)

* fix(quality): the two advisory gates wrote a status glyph into their own summaries

liveness.py and c901_delta.py each emitted a check mark on their clean-result
line, and c901_delta rendered its complexity table with a non-ASCII arrow.

Both write via sys.stderr.write when no summary file is given, and stderr
defaults to backslashreplace, so a stock Windows cp1252 console silently
mangled the text at exit 0 rather than raising. Verified by running the
pre-edit scripts from HEAD under PYTHONIOENCODING=cp1252: neither crashed,
both corrupted. That is the worse failure -- a crash is loud and self-
reporting; silent corruption at exit 0 is not. CI was never affected, because
the step summary is opened utf-8, so this was a local-run defect only.

Say the word instead (CLAUDE.md section 11). PASS trails each sentence because
that is where the glyph sat -- the minimal edit -- and it leaves the substring
tests/test_c901_delta.py:291 asserts ("No function was introduced over the
threshold") byte-identical. That assertion is a containment check rather than
a prefix check, so it does not by itself forbid a leading token; nothing in
the repo pins the placement either way.

The three arrows become ASCII ->, matching lines 227 and 313 of the same file,
which already wrote the identical relation that way.

Both files now carry zero characters cp1252 cannot encode.

* docs(quality): document /simplify, and take the status glyphs out of the rubric

Code_Quality_Standards.md carried 40 check marks and one red circle as status
markers, which CLAUDE.md section 11 forbids outside the two machine-parsed
backlog files. All 41 are gone. Most sat beside the word they decorated and
were simply deleted, that word carrying the meaning on its own; only two were
genuine rewrites -- the red circle in Appendix A.2 became Failing, and the
Appendix A.3 legend, where the glyph was the subject, became prose.

Adds section 5.1 as the single home for /simplify: a local, human-invoked
review that APPLIES its fixes rather than reporting them, which is why it runs
before the local quartet rather than after -- running it after would mutate a
tree the quartet just certified. It is not one of the five measurement gates,
sits outside the AI companion section 6.5 local gate, and carries no Built
status, because it ships with Claude Code rather than with this project and so
leaves no artifact here to score. CLAUDE.md gains a "Before you verify"
heading so the instruction is not governed by a pass-gate it cannot satisfy:
/simplify applies edits and returns no verdict.

Also corrects a PRE-EXISTING Appendix A.3 error that removing the glyphs
surfaced rather than introduced: the legend glossed its status marker as
advisory under two PR numbers, across a five-item list whose fifth item is
blocking and shipped under a third. That legend is byte-identical in every
commit this file has existed in.

HANDOFF-mutation-coverage.md prescribed typing a check mark back into the
rubric, which would have undone the pass -- so the removal was not durable.
Its instruction is corrected, along with its stale signal numbering and column
name, and it now says to write the word.

* docs(quality): the handoff cited the PR that built the gates, not the one that restatused them

The parenthetical said signals 7 and 8 were "restatused in v0.8 (#1040)". Two
different pull requests, verified by subject:

  7540f260  ci(quality): mutation (#7) + diff-coverage (#8) advisory gates (#1040)
  46714159  docs(rubric): v0.8 -- restatus signals 7 + 8 to Built (#1044)

PR #1040 built the gates; PR #1044 did the v0.8 restatus the sentence is
about. Caught by the session sweeping citation ambiguity in these docs.

Written in the settled "PR #NNN" form rather than a bare number, which is what
that sweep is standardising. Bare and bolded #N stays for backlog items, and
the distinction is load-bearing in code: backlog_status_check.py defines
_CL_EXPLICIT as BACKLOG\s+#(\d+) to pick out item citations, so writing
"BACKLOG #1040" for a pull request would make that regex misread it as an item.

* docs(quality): the rubric cited pull requests as bare "#N", which already resolves to the wrong item

#1020, #1028, #1040 and #1047 are PULL REQUEST numbers (97b79fef, 42756bdb,
7540f260, 3d6c8adf -- subject-anchored search). A bare "#N" in this corpus
reads as a backlog item, so these send a reader to the wrong document.

#1020 is not a future risk. Backlog item 1020 exists on main, filed
2026-08-04, about a first-run bootstrap Administrator with no email address --
while the rubric uses #1020 four times to mean the PyPI sdist private-doc leak
fix. Those four citations already land on the wrong item. 1028 is next; 1040
and 1047 are still unallocated.

"PR #N" is the settled repo form rather than a new one, and the repo encodes
the distinction in code: backlog_status_check.py defines _CL_EXPLICIT to match
the word BACKLOG followed by whitespace and a number, under the comment
"Unambiguous CHANGELOG citations of a *backlog item* (not a PR number)". That
regex is also why the inverse rewrite is unavailable -- prefixing a pull
request number with that word would make the parser read it as an item.

39 markers inserted: 38 in the rubric, 1 in the handoff. The rubric's 40
four-digit citations carry 38 markers because one slash-joined run takes a
single "PRs " across its three tokens.

Deliberately left alone: the 11 rubric-signal citations (#3, #6, #7, #8, #9,
#10, #11). Six of those numbers are also real backlog items, so the ambiguity
is genuine, but resolving it is prose surgery ("signal 7", not "PR #7") and
sits with the owner as a separate decision.

Analysis and the marking script are the work of the session on
claude/sleepy-villani-df328d. Verified here as markers-only: stripping every
marker from both the committed and the working text yields identical files.

* backlog: file 1029 -- the /simplify placement decision had no number to cite

Filed closed: the documentation is the whole deliverable, and it shipped in the
three commits below this one.

REWRITTEN BEFORE FILING. The draft item, written when the change was first
made, described a structure that the remediation then reverted -- it claimed a
sixth row in the section 5 gate table, a Built status, and a CLAUDE.md bullet.
None of those is what shipped: section 5's table is unchanged at five rows,
Built is a claim the document explicitly declines to make, and CLAUDE.md
carries a "Before you verify" heading placed ahead of the verification list
rather than a bullet inside it. Every claim in the filed item was read from the
working tree at 17c5212 rather than recalled.

APPENDED, NOT INSERTED, and the instruction to insert was checked rather than
followed. It was handed over on the grounds that 1028, 1030 and 1031 are being
filed concurrently. No such rule exists and the file does not follow one:

  108 items carry 10 descending adjacent pairs in file order, the tail running
  1019, 1018, 1024, 1026, 1025, 1027;
  backlog_status_check.py enforces no ordering -- its only sort is a citation
  report at line 190;
  this file's own header states only that the numbered items are intentionally
  deferred, and docs/README.md calls them "ranked", which is not numeric order.

So nothing states an ordering rule for the numbered items, and imposing numeric
order would discard whatever the existing arrangement encodes. Appending is
where every recent item sits. The only real interaction with the concurrent
items is a textual end-of-file conflict that resolves by keeping both.

Validated with the canonical parser rather than a hand-rolled scan
(CLAUDE.md section 11): parse_items reports item 1029 with closed=['<check>'],
open=[], is_open=False -- exactly one banner, no OPEN/CLOSED contradiction. The
hygiene gate at its ci.yml invocation reports OK, 304 items, each declaring
exactly one status.

Number allocated via scripts/coord/alloc.ps1, never by grepping for the next
free one.

* docs: correct the CRLF rationale in this branch's earlier merge commit

The merge commit 95cd856 states that docs/BACKLOG.md "is 100 percent CRLF" and
that "a resolver that normalises to LF produces a clean-looking merge that
churns every line". That is FALSE about the stored file, and this commit exists
so the correction travels with the claim -- this repository composes its squash
body from the concatenated commit messages, so both land on main together.

Every committed revision of that file is pure LF. Measured on the blobs rather
than the working tree:

    origin/main   CRLF=0  bareLF=5045
    453c95f      CRLF=0  bareLF=4994
    95cd856      CRLF=0  bareLF=5065
    working tree  CRLF=5065  bareLF=0
    core.autocrlf = true

CRLF exists only as the checkout materialisation. The original measurement read
bytes on disk and reported them as the stored form -- the working tree answered
a question about the object store.

THE RESOLUTION ITSELF WAS CORRECT; only the stated reason was wrong. The
resolver read and wrote with newline="", so it preserved the on-disk form
byte-for-byte and let autocrlf normalise on the way in -- the same outcome a
resolver that ignored line endings entirely would have produced.

THE INSTRUMENT THAT SETTLES THIS IS CHURN, NOT A LINE-ENDING COUNT:

    git diff --numstat 453c95f 95cd856 -- docs/BACKLOG.md
    71      0      docs/BACKLOG.md

71 added, zero removed -- items 1030, 1031 and 1032 plus one seam blank line.
A resolver that had normalised would show thousands of lines on BOTH sides.
That reconciles to the line against the independent resolution of the same
collision on another branch, which came back 70/0 and needed no seam line.

Anyone resolving the next end-of-file collision in this file should run the
numstat check and should not chase line endings that are not there.
wshallwshall added a commit that referenced this pull request Aug 5, 2026
… surfaced

1033. The rubric cites its own eleven signals as bare #N, and six of those
numbers are real backlog items -- #3 is OPEN today, and #6/#7/#8/#10/#11 are
closed items. Ten citations on four lines, re-measured against 780ee1d. Owner
ruled on 2026-08-05 that they get disambiguated. The four-digit PR citations in
the same file were fixed in PR #209; this is the short-number half that was
deliberately left out of that scope.

Two traps are recorded because each has already caught a reader. The #3 at L120
is a markdown ANCHOR FRAGMENT inside a link target, not a citation -- converting
it silently breaks the link, and a prior census listed it as a signal because it
counted tokens without printing context. And L299/L319 use backslash-escaped
forms: a grep attempt during this triage returned ZERO matches on a file that
demonstrably contains them, and the empty result was believed until a
self-tested pattern contradicted it. The item says to prove the pattern fires
before trusting a count from it.

1034. The pre-push shim exits 0 with "THE PUSH GUARD IS OFF for this push" when
python is not on PATH. With enforce_admins OFF, push_guard.py is the only thing
refusing an admin's direct push to main, and since the cutover that push is
publication -- so the one control has a silent off switch that depends on an
environment variable. As of today the shim switches off three guards rather
than one, the two added alongside it being the namespace allowlist and the
tip-tree check.

Filed with the adjacent gaps in the same class rather than separately: a fresh
clone or new worktree has no hook at all until install-git-hooks.ps1 runs, and
--no-verify and MEFOR_ALLOW_DIRECT_PUSH=1 skip everything by design. The item
states plainly that a client-side hook cannot be the sole control and that the
durable answer is server-side, with the shim as defence in depth.

Numbers allocated via scripts/coord/alloc.ps1, never by grepping for the next
free one. Validated with parse_items rather than a hand-rolled scan: 114 items,
zero duplicate numbers, 1033 and 1034 each carrying exactly one open banner.
Hygiene gate OK at 309 across both ledger files.
wshallwshall added a commit that referenced this pull request Aug 5, 2026
…d what it CARRIES (#213)

* fix(hooks): the push guard asked where a push LANDS, and nothing asked what it CARRIES

Two guards, both for paths the existing PROTECTED check waves through.

GUARD A -- namespace allowlist. Refuse any push whose remote ref is outside
refs/heads/ or refs/tags/. That is the shape of git push --mirror, which offers
every ref in the clone including remote-tracking namespaces. A mirror push was
refused before only INCIDENTALLY: it also offers local main as an update of
refs/heads/main, so PROTECTED happened to fire. That is a property of one
branch's state, not a rule, and it evaporates the moment main is up to date.

GUARD B -- content check. Refuse a push whose ref's tip tree carries
docs/security. That directory is gitignored, and an ignore rule governs only
UNTRACKED paths, so it does nothing about a ref whose history already tracks
those files. The path this closes is the likeliest of the set and is not a
mirror at all: branch off a ref of that lineage and push it as an ordinary
branch, which every other check here permits by design.

PROVEN, not assumed. Both guards exercised via crafted pre-push stdin against a
throwaway repo, with the fixture self-checked in both directions first (a
fixture whose add -f lost to the ignore rule would make every assertion pass
vacuously):

  case                                        new  old(HEAD)
  remote-tracking ref, mirror shape             1      0
  ordinary branch, clean tip                    0      0
  ordinary branch, tip carries docs/security    1      0
  tag push, clean tip                           0      0
  delete an unprotected branch                  0      0
  direct push to main                           1      1

The old-guard column is the negative control: it returned 0 for exactly the two
cases these guards add, so this is new coverage rather than restated behaviour.
Refusals were checked to name the right reason, not merely to exit 1.

WHAT THESE ARE NOT, stated in the code because the difference decides what a
green run entitles anyone to conclude. Guard B reads the TIP TREE only -- a
branch that added and then removed the files passes with a dirty history, so it
is not a history check. It matches paths, not content. Every check here is
skipped by --no-verify, by MEFOR_ALLOW_DIRECT_PUSH=1, and by the installed
shim's own fail-open, which prints "THE PUSH GUARD IS OFF for this push" and
exits 0 when python does not resolve. A fresh clone or new worktree has no hook
at all until install-git-hooks.ps1 runs. A client-side hook cannot be the sole
control and the docstring says so.

Also fixes a false docstring in the test file, which asserted that git push
--all sends every ref. It does not -- --all is refs/heads only, while bundle
create --all and rev-list --all mean every ref. That belief is what makes
someone treat --all and --mirror as interchangeable.

109 tests pass; ruff and mypy clean.

* docs(ledger): record the vault-ref cleanup, and retire a warning that was true when written

489 refs carrying docs/security content were deleted from this clone on
2026-08-05 with git update-ref -d, across THREE namespaces: refs/remotes/vault
(20), refs/remotes/vaultall (466), and refs/vault (3). That third sits outside
refs/remotes entirely and held the newest, densest content, so a cleanup scoped
to refs/remotes would have missed it.

THE STANDING WARNING AGAINST THIS IS NOW STALE, NOT WRONG. LEDGER-GATE.md and
alloc.ps1 both named "deleting its refs" as the hazard the allocator ratchet
defends against. Re-measured directly: BACKLOG max is 1032 and sub-floor max
353 both with and without the refs, ADR max 0161 either way, the allocator
emits max+1 and never fills gaps, and the ratchets already persist 1031 / 1000
/ 160. The warning was accurate when written, in the era when the floor did
depend on the ref sweep; the ratchet and the public-boundary split made it
independent since. It is updated rather than deleted, because the principle it
teaches still holds.

THE MULTISESSION PLAN GAVE A COMMAND THAT NO LONGER WORKS, and its description
of the ref was wrong when written. It called vault/main a remote-tracking ref;
git rev-parse --symbolic-full-name resolved it to refs/vault/main, and
refs/remotes/vault/main never existed. Nor was a remote named vault ever
configured -- only origin. The refs were orphaned namespaces from two
direct-URL fetches on 2026-07-28, 45 seconds apart. Sessions should read the
vault ledger from the separate MessageFoundry-vault clone instead.

REVERSIBILITY, since deleting refs is only safe if it is undoable. A manifest
of 489 refname/SHA pairs (464 unique commits -- 25 refs share a tip) is held
outside this repo, durably, inside the vault clone's own .git. The objects
remain addressable here, and every tip is REACHABLE from the vault clone's own
refs, so they are gc-safe there rather than merely undeleted. gc.auto is set to
0 in this clone: it was unset with 7060 loose objects against a default
threshold of 6700, already over, so a routine command could have fired an
auto-gc and converted a reversible ref deletion into permanent loss.

NOTHING WAS EVER PUBLISHED FROM THESE REFS. origin/main, all 30 origin refs and
all 195 local branches carry zero docs/security files at tip and in history,
confirmed three independent ways; the two graphs share no merge base.

* backlog: file 1033 and 1034 -- the two follow-ups the push-guard work surfaced

1033. The rubric cites its own eleven signals as bare #N, and six of those
numbers are real backlog items -- #3 is OPEN today, and #6/#7/#8/#10/#11 are
closed items. Ten citations on four lines, re-measured against 780ee1d. Owner
ruled on 2026-08-05 that they get disambiguated. The four-digit PR citations in
the same file were fixed in PR #209; this is the short-number half that was
deliberately left out of that scope.

Two traps are recorded because each has already caught a reader. The #3 at L120
is a markdown ANCHOR FRAGMENT inside a link target, not a citation -- converting
it silently breaks the link, and a prior census listed it as a signal because it
counted tokens without printing context. And L299/L319 use backslash-escaped
forms: a grep attempt during this triage returned ZERO matches on a file that
demonstrably contains them, and the empty result was believed until a
self-tested pattern contradicted it. The item says to prove the pattern fires
before trusting a count from it.

1034. The pre-push shim exits 0 with "THE PUSH GUARD IS OFF for this push" when
python is not on PATH. With enforce_admins OFF, push_guard.py is the only thing
refusing an admin's direct push to main, and since the cutover that push is
publication -- so the one control has a silent off switch that depends on an
environment variable. As of today the shim switches off three guards rather
than one, the two added alongside it being the namespace allowlist and the
tip-tree check.

Filed with the adjacent gaps in the same class rather than separately: a fresh
clone or new worktree has no hook at all until install-git-hooks.ps1 runs, and
--no-verify and MEFOR_ALLOW_DIRECT_PUSH=1 skip everything by design. The item
states plainly that a client-side hook cannot be the sole control and that the
durable answer is server-side, with the shim as defence in depth.

Numbers allocated via scripts/coord/alloc.ps1, never by grepping for the next
free one. Validated with parse_items rather than a hand-rolled scan: 114 items,
zero duplicate numbers, 1033 and 1034 each carrying exactly one open banner.
Hygiene gate OK at 309 across both ledger files.
wshallwshall added a commit that referenced this pull request Aug 7, 2026
Retiring a backlog item moves it verbatim from docs/BACKLOG.md into
docs/archive/backlog/BACKLOG-CLOSED.md. Every citation that named the live file
keeps pointing at a file the item is no longer in. The link still resolves, so
nothing in CI can see it. #1094 fixed two such markers in CLAUDE.md section 12;
this is the same defect at repo scale.

73 citations across 34 files, href-only. No prose was rewritten. Visible labels
changed ONLY where leaving them would contradict the target -- a label reading
`BACKLOG.md` pointing at the archive -- and then only to `BACKLOG-CLOSED.md`.

THE TEST IS "DOES THE CITED FILE CONTAIN THE ITEM", NOT "IS THE ITEM CLOSED".
Those differ, and keying on closure would corrupt correct citations: #1073 is
closed and still legitimately in the live ledger. Item locations came from
parse_items imported from scripts/docs/backlog_status_check.py, per CLAUDE.md
section 11 -- never a hand-rolled scan of the banner alphabet.

DELIBERATELY NOT TOUCHED, each for a stated reason:

  Both ledger files -- ZERO edits to docs/BACKLOG.md and BACKLOG-CLOSED.md.
    Only two sites named them and both are excluded, so this change costs no
    conflict against the merge trains or the pending #1096 filing. The one real
    site (#322 at BACKLOG.md:2720) is left because the file is the most
    contended in the repo and the item number is visible in plain text a search
    away.

  docs/CONNECTIONS.md:2436 -- the #27 serial/ASTM row. Already fixed on a branch
    inside merge train #274. Sweeping it from origin/main would re-fix stale
    text and collide.

  QUOTATIONS OF THE DEFECT. docs/BACKLOG.md:6319, inside #1094, reads "Two of
    its markers CITED [`docs/BACKLOG.md`](BACKLOG.md) #26 and #27" -- past
    tense, describing rot that is already fixed. Repointing it would corrupt a
    historical record. A regex cannot tell this from a live pointer, which is
    the reason this was not done with sed.

  THE WRONG-NUMBER CLASS, which is a different defect and must not be swept into
    this one. ADR 0068:10 cites #11 and ADR 0113:9 cites #239; both numbers are
    absent from the live ledger, but the ARCHIVE's #11 ("`check` dry-run
    cross-products") and #239 ("Re-measure Steps view estate coverage") are
    unrelated to WebAuthn passkeys and to a Windows tray manager respectively.
    Repointing would convert a vague reference into a confidently wrong one that
    lands the reader on the wrong item. Left, and reported.

  MIXED-LOCATION LINKS, where one link covers items in both files so no single
    target is correct: docs/AI-OFF-MATRIX.md:50 (six items), docs/adr/0001:13
    (#1 archived, #3 live), THROUGHPUT-IMPROVEMENTS.md:215 (#62 live, so its
    link is already correct).

VERIFICATION
  - Plan applied by literal replacement on the named line only, requiring the
    quoted string to occur EXACTLY ONCE there; a mismatch aborts rather than
    fuzzy-matching. Dry run: 73/73 clean, 0 problems, before anything was
    written.
  - All 35 distinct anchor fragments introduced match exactly one real "## N."
    heading in the archive, checked after applying, with a known-bad fragment
    run through the same check to prove it can report a miss. Fragments were
    derived with a slugger that does NOT collapse consecutive spaces -- the
    doubled hyphens are correct, not typos.
  - All 74 archive hrefs in the changed files resolve to the archive from their
    own directory depth; the relative prefix differs by depth and was computed
    per file, not pattern-matched.
  - Coverage confirmed with a DELIBERATELY LOOSER regex than the one that built
    the work list: it finds exactly one wrong-file site outside this change set,
    docs/CONNECTIONS.md:2436, which is the intended exclusion.
  - backlog_status_check.py: OK, 365 items. No mixed line endings introduced.
    All 34 changed files are markdown; diff is 67 insertions / 67 deletions,
    line-for-line.
  - Staged by explicit path from the plan, cross-checked against git's modified
    set, so nothing another session is editing was swept in.

Not included: the broken-href class (13 sites, mostly (docs/BACKLOG.md) written
from inside docs/testing/master-test-plan/), the 12 line anchors past EOF, and
the 31 in-range anchors that drifted onto unrelated text. Those are separate
classes under #1095 and are catchable by a link checker, which this repo still
does not run.
wshallwshall added a commit that referenced this pull request Aug 8, 2026
…G #1101) (#292)

The empty_claims_monotonic SLO carried wall clock in its denominator, so anything
that slowed a run collapsed it with no engine change. Measured on one commit, one
box: four contended replicates spread 0.451 to 2.49 against a 0.75 floor. It fired
five times in one day on PRs whose content cannot reach the engine -- a docs and
link-checker diff, and a mocha dev-dependency bump.

The asserted metric is now empty_claims_per_msg. Both inputs are deltas over the
SAME first-to-last in-hold samples, so dividing them cancels the span exactly and
leaves Δempty_claims / Δread. The wall clock disappears algebraically rather than
by assumption, which is the property that makes it survive contention: slowing the
run scales numerator and denominator identically.

This is also the quantity wall #3 actually means. profile.py documents the mode as
measuring the per-commit herd size, which is a per-message quantity; the
per-second form was a proxy for it that the runner could move.

The per-second numbers are retained in the report as the operator-facing figures.
They are simply no longer what gates a merge, and the JSON now carries
total_per_msg alongside them so a triager can see both.

Returns None, never 0.0, when the window absorbed no messages. The ratio is
genuinely undefined there and a fabricated zero would chain through the
monotonicity comparison as if it were a reading.

ALSO FIXES THE LATENT GROUPING DEFECT, in the same pass because it is the same
function. _monotonic_slo grouped by sweep_mode ALONE and chained prev_val across
the sorted group, so a profile setting claim_modes = ["per_lane", "pooled"] would
have compared pooled against per_lane -- and compare.py states pooled's rate
SHOULD be materially lower, meaning a CORRECT engine would have failed it. No
shipped profile combines them, which is the only reason it never fired. Now
groups by (sweep_mode, claim_mode).

VERIFIED AGAINST THE FAILURE MODE, NOT JUST FOR GREEN. The risk in this change is
replacing a gate that fails at random with one that never fails at all, and a
correction is the easiest place to skip measuring because it feels like it has
already paid its dues. So the invariance property and the
still-detects-a-real-regression property are pinned together, and both were shown
to go RED under mutation before being trusted:

  grouping reverted to sweep_mode only    3 failed
  per-second metric restored              2 failed
  restored                                8 passed

A metric that never fires would have passed a stability test on its own.

Not done, deliberately: gating reload_seconds directly. The item raises it as a
conditional -- "if that cost is worth gating" -- which is a separate judgement.

Verified: ruff format and check pass; 162 connscale tests pass (3 skipped);
backlog hygiene 19 passed; the repo-wide link gate reports 5,359 links across 347
files all resolving. harness/ is not under CI mypy (it checks messagefoundry and
messagefoundry_webconsole only).
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