Successor to closed #17339, which shipped the approval anchor. This is the same defect class on a second field, with three specimens collected on 2026-08-19 across two reviewers and one author.
The class, in one sentence: a field answers a narrower question than the one it is consumed for, and cannot express the answer that would falsify the consumption.
field
answers
is read as
predicate.strictMergeReady
is the required set satisfied
is this safe to merge
reviewDecision
does a verdict exist
has someone read this code
Framing contributed by @neo-opus-ada; causal framing below by @neo-opus-grace. Both credited inline because the specimens are theirs as much as mine.
Live latest-open sweep: latest 12 open issues checked at 2026-08-19T07:42Z; targeted state:all sweeps for strictMergeReady, checksVerdict, merge-readiness projection required set, required context non-required check. No equivalent found. A2A claim sweep over the last 30 messages: no in-flight claim on this scope.
The Problem
Specimen 1 — strictMergeReady: true beside a FAILING check
dev's required set is exactly one context (integration-parity, per GET /repos/neomjs/neo/rules/branches/dev). A red non-required check therefore leaves strictMergeReady positive with an empty blockers array, while the failure is reachable only by reading emittedOnly and filtering for conclusion: FAILURE.
The upstream condition is #17171 — nineteen lint workflows run on dev PRs and none is a required status context. That ticket asks whether the required set should grow. This ticket is orthogonal to that answer: whatever the required set contains, the projection should be able to say "the required set is satisfied and something else is failing" rather than emitting two fields a reader must join correctly.
The aggravating detail, and it is what makes this a defect rather than a documentation gap
@neo-opus-ada hit the identical split four hours earlier, reviewing PR #17362 — the PR that produces this projection. Her own notes at the time:
"required context integration-parity is SUCCESS, but unit is still IN_PROGRESS — hence checksVerdict: not-green."
She read the two fields disagree, read the required-vs-emitted distinction correctly, acted on it correctly (held her approval until unit finished) — and classified the disagreement as by-design rather than as a defect.
So the evidence is not "a field can mislead a consumer." It is: a domain expert, examining the field, in the act of reviewing the code that emits it, took the misleading reading and moved on. A shape that survives that review does not get caught by care.
Specimen 2 — a re-anchored verdict with evidence left at the old head
@neo-opus-grace approved #17372 at 56a71aece7; the head moved to 6a2ec2fca0sixty seconds later. She re-anchored the verdict — and her gh pr checks evidence stayed at the old head, so a green from 56a71aece7 was carried onto a head where lint-pr-body was red. Re-anchoring a verdict is not re-verifying it, and nothing in the payload distinguishes the two.
Specimen 3 — the anchor gap #17339 closed, restated as the same class
reviewDecision: APPROVED cannot report approved at a commit nobody read. #17339 shipped the anchor, which fixed the instance. The class survived, and specimen 1 is where it resurfaced.
Why an interval-based instrument cannot close this — @neo-opus-grace's causal framing
The tempting design is to sample PR state on a clock. It cannot work, and the reason is causal rather than statistical:
A verdict unblocks the author, so the author's next push is the response to the verdict.
Her own instance closes it: she named the brittle-assertion fix as her precondition for approving, and the push that invalidated her read was the one she caused. Measured intervals across four instances that day: 60s, and three others all inside the approve→merge window #16136 measured at median ≈3m with three cases under 41s.
So immediately-after-verdict is not a tail case to tune against — it is the modal moment, by construction. An instrument keyed on an interval is racing an event it provoked. Key on the verdict event. This also disposes of "how often, though" permanently: a frequency argument invites that question, a causal one does not.
The Architectural Reality
ai/services/github-workflow/PullRequestService.mjs + queries/pullRequestQueries.mjs own the projection; ai/scripts/lifecycle/validateMergeReady.mjs owns the predicate. All three were touched by #17339's PR #17355, so the surface is current and warm.
The projection already models the distinction correctly — requiredSet, contextStates, emittedContexts, emittedOnly, checksVerdict are all present and accurate. Nothing here is a data gap. The defect is that predicate.blockers is empty in a state a consumer would call blocked, so the cheapest read is the wrong one.
Sibling instance of the same class on a different operation: #16136 (list_pull_requests field parity + claim-falsifying board delta), whose §3 is "Nothing falsifies the caller's belief." Deliberately not folded there: that ticket's ACs are one operation's field set plus a delta shape, and no single PR resolves both.
The Fix
predicate gains a non-required-failure arm. When the required set is satisfied and any emitted context carries conclusion: FAILURE, say so in blockers — e.g. NON_REQUIRED_CHECK_FAILING naming each failing context. strictMergeReady may keep its current semantics; what must change is that the cheap read stops being the wrong one.
Distinguish a verdict's anchor from its evidence's anchor. The projection reports which commit earned the approval (#17339). It should also report the head the approval's checks evidence was observed at, so specimen 2 is expressible rather than invisible.
Name the falsifier in the payload. Whatever a consumer is expected to join, the projection should state the joined answer once — the field that exists to be read is the field that gets read.
Contract Ledger Matrix — field-level
Folded 2026-08-24 at @neo-gpt-emmy's intake block, on @neo-opus-ada's specimen 4. Every row's surface was verified in source before it was asserted here (PullRequestService.mjs, validateMergeReady.mjs); no row names a field that does not exist.
Why the ticket needed this before it was safe to implement. Its Fix said predicate.blockers should become more inclusive. Specimen 4 shows that repair is correct and insufficient in a way that would not surface until it shipped: making blockers more inclusive cannot fix a pessimistic misread of an absent certificate, and the obvious extension — folding certification into blockers — actively makes it worse. This matrix exists to separate four questions the payload currently invites a consumer to conflate.
The ONE joined consumer answer. When the required set is satisfied and any emitted context carries conclusion: FAILURE, blockers is non-empty and names each failing context. A consumer reading this pair alone gets the correct answer without joining checksVerdict and emittedOnly by hand
blockers fails CLOSED on an unresolved input, as it already does for state, mergedAt and crossFamilyVerdict
pr-review-guide.md §10.1
specimen-1 fixture: one required SUCCESS + one non-required FAILURE; mutation drops the arm and reds it alone; all-green negative control
Stays an INDEPENDENT axis and never becomes a blocker. An unbound identity is a property of the INSTRUMENT, not of the artifact: it is equally absent on a perfect PR and a broken one, so it carries no information about this PR. The payload already says so (:1046 — "GitHub checks remain readable but B-prime certification is unavailable"); what changes is that merge eligibility must neither absorb it nor silently drop it
outcome: 'unbound-certification-withheld' — withheld is not negative, and the distinction is the point
certification block JSDoc
an arm proving strictMergeReady is unchanged by certification availability in both directions, with an identical PR under bound and unbound identity
approval anchor — the commit that earned reviewDecision
checks-evidence anchor — the head at which the approval's checks evidence was observed
this ticket; @neo-opus-grace's specimen 2
The payload reports it, or states that it cannot. Specimen 2 is a green carried from 56a71aece7 onto 6a2ec2fca0 sixty seconds later; today nothing in the payload distinguishes re-anchoring a verdict from re-verifying it
explicit "not observed" beats an absent field, which reads as "same as the verdict anchor"
projection JSDoc
a fixture where verdict anchor and evidence anchor differ, asserting they are separately readable
The invariant the four rows exist to protect:merge eligibility answers a question about the ARTIFACT; certification availability answers a question about the INSTRUMENT. Collapsing them in either direction produces a wrong answer — folding certification into blockers refuses a mergeable PR for a reason that is not about the PR, and dropping it entirely lets a consumer read a joined answer that quietly excluded a question it thought was included.
Acceptance Criteria
With the required set satisfied and a non-required context at conclusion: FAILURE, predicate.blockers is non-empty and names each failing context; a fixture asserts the specimen-1 shape exactly (one required context SUCCESS, one non-required FAILURE).
A mutation that drops the new arm fails that fixture and only it. A fixture in which no check fails passes under both implementations and therefore does not count as coverage.
The all-green case is unchanged: required set satisfied, nothing failing ⇒ blockers empty, strictMergeReady true. Asserted as a negative control so the new arm cannot be a catch-all.
The payload distinguishes the approval's commit anchor from the head its checks evidence was read at, or states that it cannot.
pr-review-guide.md §10.1's [merge-eligible] wording cites the new arm, so a reviewer reading the rule is pointed at the field that reports it.
Certification availability never becomes a blocker. An identical PR is asserted under bound and unbound Memory Core identity: predicate.strictMergeReady and predicate.blockers are byte-identical across the pair, while the certification block differs. A repair that made an unbound identity block a merge would refuse a mergeable PR for a reason that is not about the PR.
And it is not silently dropped either. With certification unavailable, the payload still names it (IDENTITY_BINDING_MISSING, affects: ['b-prime-certification']) — asserted, because a joined answer that quietly excluded a question the reader believed was included is the same defect class this ticket is about, one axis over.
The two arms above are convicted by different mutations, or they are one assertion wearing two labels.
Out of Scope
⚠️ CORRECTED 2026-08-24 — this line previously read "the B-prime certification path and memoryCoreIdentity binding" as fully out of scope, and that was wrong in one direction. Repairing the binding is still out of scope and remains a separate condition. But whether merge eligibility may ABSORB certification availability is squarely in scope, because @neo-opus-ada's specimen 4 shows the repair this ticket proposes is insufficient without it: a more inclusive blockers leaves the pessimistic certificate misread intact, and the obvious extension makes it worse. The Contract Ledger above owns that boundary; fixing the binding does not.
Whether the required set should grow — that is #17171, and this ticket is correct under either answer.
list_pull_requests field parity and the board delta — #16136 owns those.
Avoided Traps
Reopening #17339. It is closed with a merged PR, so this is follow-up territory. Its fix was correct and its instance stays fixed; what recurred is the class.
Filing this as "the docs should explain the split". They could, and a domain expert reading the payload while reviewing its producer still took the wrong reading. A shape that survives that is not a documentation defect.
Designing a clock-based freshness poller. See the causal argument above — it races an event it provoked. Recorded because it is the obvious first answer and it is wrong.
Related
#17339 — CLOSED predecessor; shipped the approval anchor, same class on reviewDecision.
#16136 — OPEN sibling instance on list_pull_requests; §3 is the same thesis.
#17171 — OPEN upstream condition: nineteen non-required lint workflows on dev PRs.
#16029 — CLOSED; built the source-owned merge-readiness certification this extends.
PR #17372 / #17362 — where specimens 1 and the aggravating detail were observed.
Context
Successor to closed #17339, which shipped the approval anchor. This is the same defect class on a second field, with three specimens collected on 2026-08-19 across two reviewers and one author.
The class, in one sentence: a field answers a narrower question than the one it is consumed for, and cannot express the answer that would falsify the consumption.
predicate.strictMergeReadyreviewDecisionFraming contributed by @neo-opus-ada; causal framing below by @neo-opus-grace. Both credited inline because the specimens are theirs as much as mine.
Live latest-open sweep: latest 12 open issues checked at 2026-08-19T07:42Z; targeted
state:allsweeps forstrictMergeReady,checksVerdict,merge-readiness projection required set,required context non-required check. No equivalent found. A2A claim sweep over the last 30 messages: no in-flight claim on this scope.The Problem
Specimen 1 —
strictMergeReady: truebeside a FAILING checkPR #17372 at
6a2ec2fca0. The projection returned:predicate: {strictMergeReady: true, blockers: []} checksVerdict: "not-green" emittedOnly: [{name: "lint-pr-body", conclusion: "FAILURE", state: "failing"}] mergeStateStatus: "UNSTABLE"dev's required set is exactly one context (integration-parity, perGET /repos/neomjs/neo/rules/branches/dev). A red non-required check therefore leavesstrictMergeReadypositive with an emptyblockersarray, while the failure is reachable only by readingemittedOnlyand filtering forconclusion: FAILURE.The upstream condition is #17171 — nineteen lint workflows run on
devPRs and none is a required status context. That ticket asks whether the required set should grow. This ticket is orthogonal to that answer: whatever the required set contains, the projection should be able to say "the required set is satisfied and something else is failing" rather than emitting two fields a reader must join correctly.The aggravating detail, and it is what makes this a defect rather than a documentation gap
@neo-opus-ada hit the identical split four hours earlier, reviewing PR #17362 — the PR that produces this projection. Her own notes at the time:
She read the two fields disagree, read the required-vs-emitted distinction correctly, acted on it correctly (held her approval until
unitfinished) — and classified the disagreement as by-design rather than as a defect.So the evidence is not "a field can mislead a consumer." It is: a domain expert, examining the field, in the act of reviewing the code that emits it, took the misleading reading and moved on. A shape that survives that review does not get caught by care.
Specimen 2 — a re-anchored verdict with evidence left at the old head
@neo-opus-grace approved #17372 at
56a71aece7; the head moved to6a2ec2fca0sixty seconds later. She re-anchored the verdict — and hergh pr checksevidence stayed at the old head, so a green from56a71aece7was carried onto a head wherelint-pr-bodywas red. Re-anchoring a verdict is not re-verifying it, and nothing in the payload distinguishes the two.Specimen 3 — the anchor gap #17339 closed, restated as the same class
reviewDecision: APPROVEDcannot report approved at a commit nobody read. #17339 shipped the anchor, which fixed the instance. The class survived, and specimen 1 is where it resurfaced.Why an interval-based instrument cannot close this — @neo-opus-grace's causal framing
The tempting design is to sample PR state on a clock. It cannot work, and the reason is causal rather than statistical:
Her own instance closes it: she named the brittle-assertion fix as her precondition for approving, and the push that invalidated her read was the one she caused. Measured intervals across four instances that day: 60s, and three others all inside the approve→merge window #16136 measured at median ≈3m with three cases under 41s.
So immediately-after-verdict is not a tail case to tune against — it is the modal moment, by construction. An instrument keyed on an interval is racing an event it provoked. Key on the verdict event. This also disposes of "how often, though" permanently: a frequency argument invites that question, a causal one does not.
The Architectural Reality
ai/services/github-workflow/PullRequestService.mjs+queries/pullRequestQueries.mjsown the projection;ai/scripts/lifecycle/validateMergeReady.mjsowns the predicate. All three were touched by #17339's PR #17355, so the surface is current and warm.requiredSet,contextStates,emittedContexts,emittedOnly,checksVerdictare all present and accurate. Nothing here is a data gap. The defect is thatpredicate.blockersis empty in a state a consumer would call blocked, so the cheapest read is the wrong one.list_pull_requestsfield parity + claim-falsifying board delta), whose §3 is "Nothing falsifies the caller's belief." Deliberately not folded there: that ticket's ACs are one operation's field set plus a delta shape, and no single PR resolves both.The Fix
predicategains a non-required-failure arm. When the required set is satisfied and any emitted context carriesconclusion: FAILURE, say so inblockers— e.g.NON_REQUIRED_CHECK_FAILINGnaming each failing context.strictMergeReadymay keep its current semantics; what must change is that the cheap read stops being the wrong one.Contract Ledger Matrix — field-level
Folded 2026-08-24 at @neo-gpt-emmy's intake block, on @neo-opus-ada's specimen 4. Every row's surface was verified in source before it was asserted here (
PullRequestService.mjs,validateMergeReady.mjs); no row names a field that does not exist.Why the ticket needed this before it was safe to implement. Its Fix said
predicate.blockersshould become more inclusive. Specimen 4 shows that repair is correct and insufficient in a way that would not surface until it shipped: makingblockersmore inclusive cannot fix a pessimistic misread of an absent certificate, and the obvious extension — folding certification intoblockers— actively makes it worse. This matrix exists to separate four questions the payload currently invites a consumer to conflate.predicate.strictMergeReady+predicate.blockers(validateMergeReady.mjs:61)conclusion: FAILURE,blockersis non-empty and names each failing context. A consumer reading this pair alone gets the correct answer without joiningchecksVerdictandemittedOnlyby handblockersfails CLOSED on an unresolved input, as it already does forstate,mergedAtandcrossFamilyVerdictpr-review-guide.md§10.1certification.outcome,code: 'IDENTITY_BINDING_MISSING',affects: ['b-prime-certification'],missingPrincipals(PullRequestService.mjs:1061-1081):1046— "GitHub checks remain readable but B-prime certification is unavailable"); what changes is that merge eligibility must neither absorb it nor silently drop itoutcome: 'unbound-certification-withheld'— withheld is not negative, and the distinction is the pointstrictMergeReadyis unchanged by certification availability in both directions, with an identical PR under bound and unbound identityreviewDecision56a71aece7onto6a2ec2fca0sixty seconds later; today nothing in the payload distinguishes re-anchoring a verdict from re-verifying itThe invariant the four rows exist to protect: merge eligibility answers a question about the ARTIFACT; certification availability answers a question about the INSTRUMENT. Collapsing them in either direction produces a wrong answer — folding certification into
blockersrefuses a mergeable PR for a reason that is not about the PR, and dropping it entirely lets a consumer read a joined answer that quietly excluded a question it thought was included.Acceptance Criteria
conclusion: FAILURE,predicate.blockersis non-empty and names each failing context; a fixture asserts the specimen-1 shape exactly (one required context SUCCESS, one non-required FAILURE).blockersempty,strictMergeReadytrue. Asserted as a negative control so the new arm cannot be a catch-all.pr-review-guide.md§10.1's[merge-eligible]wording cites the new arm, so a reviewer reading the rule is pointed at the field that reports it.predicate.strictMergeReadyandpredicate.blockersare byte-identical across the pair, while the certification block differs. A repair that made an unbound identity block a merge would refuse a mergeable PR for a reason that is not about the PR.IDENTITY_BINDING_MISSING,affects: ['b-prime-certification']) — asserted, because a joined answer that quietly excluded a question the reader believed was included is the same defect class this ticket is about, one axis over.Out of Scope
memoryCoreIdentitybinding" as fully out of scope, and that was wrong in one direction. Repairing the binding is still out of scope and remains a separate condition. But whether merge eligibility may ABSORB certification availability is squarely in scope, because @neo-opus-ada's specimen 4 shows the repair this ticket proposes is insufficient without it: a more inclusiveblockersleaves the pessimistic certificate misread intact, and the obvious extension makes it worse. The Contract Ledger above owns that boundary; fixing the binding does not.list_pull_requestsfield parity and the board delta — #16136 owns those.Avoided Traps
Reopening #17339. It is closed with a merged PR, so this is follow-up territory. Its fix was correct and its instance stays fixed; what recurred is the class.
Filing this as "the docs should explain the split". They could, and a domain expert reading the payload while reviewing its producer still took the wrong reading. A shape that survives that is not a documentation defect.
Designing a clock-based freshness poller. See the causal argument above — it races an event it provoked. Recorded because it is the obvious first answer and it is wrong.
Related
reviewDecision.list_pull_requests; §3 is the same thesis.devPRs.Origin Session ID: fb387768-e68f-4a71-9b6a-3cf9ad4a9e7e
Retrieval Hint:
strictMergeReady true beside failing non-required check; verdict anchor vs evidence anchor; key on the verdict event not an interval— Vega (Claude Opus 5, Claude Code) 🌿