Skip to content

fix(engine): probe a multiple:true reference field with a spelling its storage answers - #9437

Merged
os-zhuang merged 5 commits into
mainfrom
claude/issue-9362-cascade-probe-multiple-lookup
Aug 18, 2026
Merged

fix(engine): probe a multiple:true reference field with a spelling its storage answers#9437
os-zhuang merged 5 commits into
mainfrom
claude/issue-9362-cascade-probe-multiple-lookup

Conversation

@os-zhuang

@os-zhuangos-zhuang commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Fixes#9362

Verified at eee5f89b2b — every command and number below was run at that head.

Two changes ship together: the probe repair (the card) and, per the maintainer's option-B ruling, a temporary refusal on the set_null limb the repair makes reachable.


1. The probe repair (#9362)

The defect

Any object pointed at by any registered multiple: truelookup / master_detail field had its data-plane delete refused outright:

POST /api/v1/data/showcase_account {"name":"anything","status":"active"} -> 201
DELETE /api/v1/data/showcase_account/{id} -> 400 INVALID_FILTER

On the stock showcase that is showcase_account, because showcase_field_zoo.f_lookups is Field.lookup('showcase_account', { multiple: true }). It is schema-driven, not data-driven: the dependents probe runs once per DECLARED relation, so emptying the referring table changes nothing.

cascadeDeleteRelations built a bare-equality filter for every reference field aimed at the object being deleted, including the multi-value ones. Such a field stores an array, which every SQL backend here puts in a JSON TEXT column, so bare equality compares the whole serialization (["a","b"]) against one id and can never hold. driver-sql refuses that spelling (INVALID_FILTER / 400, #7398) rather than compiling a silently wrong answer.

What is NOT touched

The fix

A multi-value field is asked with $contains — the membership spelling the refusal itself prescribes, and the one every driver here answers (driver-sql, plus driver-sqlite-wasm / driver-turso which extend it, lower it to LIKE '%v%' over the serialization; driver-mongodb and driver-memory to a $regex that matches per element). The single-valued probe is byte-identical to what it was.

$contains is a SUBSTRING test, so the pushdown answers a superset — with ids acc_1 and acc_10, a row holding acc_10 matches a probe for acc_1. The rows are therefore narrowed exactly afterwards, element-wise, the same reading dangling-reference-audit.ts already applies to a stored reference. An id needing JSON escaping is asked for in both stored spellings, so the guard cannot fail OPEN on it either.

The regression window is MEASURED, not inferred

The card carried its causation flagged as a source inference ("未做 bisect,因果为源码强推断非证明"). It was cheap, so it was run. packages/objectql/src/engine.ts was checked out at a751f7d4f7^ — immediately before #8895 landed — rebuilt, and the real-driver suite run against it:

legpre-#8895 (a751f7d4f7^)with this PR
delete with an EMPTY referring table200, row gone200, row gone
a live dependent through the array200, row gone (expected undefined to be 'DELETE_RESTRICTED')409 DELETE_RESTRICTED

So #8895 did not create the fault: it converted a silent fail-open on multi-value relationships into a hard 400. That second row is also the first direct measurement of the fail-open #8895 existed to close, on this relationship kind.


2. The holding position (maintainer-ruled option B)

Repairing the probe is what makes the set_null limb run for a multi-value relationship for the first time in this codebase — before #8895 the probe swallowed its own failure and skipped the relation, after #8895 it raised INVALID_FILTER and aborted. That limb writes null over the WHOLE array. Measured on the real stack: a row holding ["acc_a","acc_b"] re-reads as null once acc_a is deleted, dropping the live reference to acc_b.

So while a multiple: true field would take the set_null limb, the delete is now refused (DELETE_RESTRICTED / 409) instead of executed. Explicitly a holding position, not a semantic: the right answer is "remove just the deleted member", but the residual shape when the array empties ([] or null) is observable and unpinned. That question is tracked in objectstack#9438 and stays open — nothing here closes it.

Shape. It is the required-FK escalation directly above it, applied to an adjacent case: one if, the same behavior reassignment, reading the same behavior === 'set_null'. It reverts by deleting that if and the multiValueHold limb of the sentence.

Coverage of the explicit spelling — a correction to what I proposed. I had proposed escalating only the defaultedset_null. Reading the code, that was wrong: fdef.deleteBehavior || 'set_null' collapses an absent declaration and an explicitly authored set_null into one value, so the required-FK escalation beside it already covers both — its comment's "only the defaulted" contrasts against cascade/restrict, not against an explicit set_null. Escalating both is therefore what mirroring the existing shape means; escalating defaulted-only would have required adding a distinction the pinned pattern does not make, and would have left the explicit spelling running the very write this holds back.

No new wire code — and this is a judgment I did not take alone.packages/spec/src/system/operation-message.ts already states the rule for this exact envelope:

The two delete_restricted* variants are one wire code with two sentences ... Splitting the SENTENCE, never the code, is the same rule the field catalog states: DELETE_RESTRICTED stays one member of the ADR-0112 vocabulary that clients match on.

A second reason on this refusal is precisely the case that rule was written for, and the existing delete_restricted / delete_restricted_required pair is the same discriminator without a new code. A code minted here would also have to be retired under ADR-0087 when the tracking issue lands — a tombstone and a migration entry, which is the opposite of reverting in one line. The distinction therefore rides developerMessage, the developer-audience half #7307 established, which names the refusal TEMPORARY and cites objectstack#9438 literally so removal is one grep; the business sentence is unchanged because the user's action is unchanged. If a machine-readable discriminator is wanted anyway, say so — a structured field is one line, and a wire code needs its own maintainer decision rather than riding this PR.

What the hold deliberately leaves alone

Asserted in both suites, because over-firing here would re-break the deletes part 1 just repaired:

dispositionbehaviour
multi-value, defaulted set_null409, array intact
multi-value, explicit set_null409, array intact
multi-value, explicit cascade200, dependents deleted
multi-value, explicit restrict409 with its OWN sentence (no 9438, no TEMPORARY)
single-valued set_null200, foreign key cleared as before
any multi-value relation with no dependent rows200, row gone — the card's P0 repro

Verification

Reverse verification of the probe repair (each leg rebuilt; packages/runtime resolves objectql from dist, so the marker count in dist/index.mjs was checked before every run): reverted -> unit suite 7 failed | 1 passed; reverted + rebuilt (marker 0) -> real-driver suite 3 failed (3), every failure expected 'INVALID_FILTER' to be …; restored + rebuilt (marker 3) -> green.

Reverse verification of the escalation, on its own — ablated with the probe repair left intact (markers: escalation 0, probe 3): the three "fires" pins go RED plus the real-driver hold case (3 failed | 12 passed and 1 failed | 5 passed).

The four over-fire controls stay green under that ablation by construction — they assert the guard does not fire, and removing it cannot make them fail. So they were ablated the other way too, by widening the condition to ||: all four go RED (4 failed | 11 passed and 2 failed | 4 passed), including the pre-existing multi-value cascade pin. Neither direction is vacuous.

Full suites at eee5f89b2b: @objectstack/objectql3826 passed (3826), @objectstack/runtime2473 passed (2473), @objectstack/rest2022 passed (2022) (run because it is the other package declaring multi-value lookups), typecheck clean.

Gates re-derived (node scripts/pm/dispatch-gates.mjs) and run at eee5f89b2b: check:changeset-gate-self-tests, check:cross-package-test-inputs, check:durability-log-level, check:objectui-changeset, check:stack-collection-maps, check:query-options-erasure, check:engine-double-contract, check:where-matcher, check:type-check-coverage, check:type-check-debt (--re-measure, full workspace closure built first: 33 entries, none above its recorded number), check-adr-0087-registration, check-changeset-no-major, check-empty-changeset, check-engine-split-ratio, check-nul-bytes, check-error-code-casing, check-affected-docs — all green, none skipped.

A first draft of the driver double evaluated the #7398 refusal per ROW rather than on the FILTER, so with an empty table it refused nothing and the card's own reproduction passed with the fix reverted. Caught on the reverse-verification lap and fixed in fc538c5831.

Related

#9390 is a duplicate report of the same defect from a different QA run and is not addressed by any keyword here; it remains open for triage to dedup.


Generated by Claude Code

os-steveand others added 3 commits August 18, 2026 00:28
…s storage answers (#9362)
`cascadeDeleteRelations` built a bare-equality dependents filter for every
`lookup` / `master_detail` field aimed at the object being deleted, including
the ones declaring `multiple: true`. Such a field stores an array, which every
SQL backend here puts in a JSON TEXT column, so bare equality compares the whole
serialization against one id; `driver-sql` refuses that spelling with
`INVALID_FILTER` / 400 (#7398). Result: any object pointed at by any registered
`multiple: true` lookup could not be deleted at all — schema-driven, so an empty
referring table did not help. On the stock showcase that is `showcase_account`.
The driver's refusal and #8895's discriminate-or-propagate `catch` are both
correct and both untouched. The fix is at the probe's construction site: a
multi-value field is asked with `$contains`, the membership spelling the refusal
prescribes and the one every driver here answers. `$contains` is a substring
test, so the pushdown answers a superset and the rows are narrowed exactly
afterwards — element-wise, the same reading the dangling-reference audit applies
to a stored reference. An id needing JSON escaping is asked for in both stored
spellings so the guard cannot fail open on it. No filter or predicate surface is
widened, and the single-valued probe is unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTKPDRoynY8i3HmdSFUxFj
…rows
`driver-sql` raises the #7398 JSON-column refusal while COMPILING the
predicate, so an empty table refuses exactly as a full one does — which is what
makes the card's fault schema-driven. The double evaluated it per row, so with
no rows to scan it refused nothing: measured on the reverse-verification lap,
the card's own reproduction (a delete refused with an EMPTY referring table)
passed with the fix reverted. A double looser than the driver it stands in for
turns a green suite into no suite.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTKPDRoynY8i3HmdSFUxFj
…sure surface
`SqlDriver.count`'s query argument is optional and typed, so the four
`{} as any` / `{ where: … } as any` casts bought nothing and pushed
check:query-options-erasure's test-surface ceiling 240 -> 244. Raising that
number is a reviewed edit, not a remedy.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTKPDRoynY8i3HmdSFUxFj
@github-actions

github-actionsBot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/objectql, touching 6 documentable anchor(s).

6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/data-api.mdx(via set_null (literal))
  • content/docs/data-modeling/field-types.mdx(via set_null (literal))
  • content/docs/data-modeling/fields.mdx(via set_null (literal))
  • content/docs/data-modeling/validation-rules.mdx(via set_null (literal))
  • content/docs/deployment/troubleshooting.mdx(via set_null (literal))
  • content/docs/protocol/objectql/types.mdx(via set_null (literal))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v15.mdx(via set_null (literal))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: ObjectQL (symbol, 59 pages)

Coarse fallback — 14 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json origin/mainpackageMentionDocs.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

PM review — accepted on the merits. One sequencing call goes to the maintainer, and you were right to draft it.

This is the strongest dev report I've reviewed today. Crediting the specific things, because several of them are the difference between a fix and a worse bug.

1. You found the hazard on the other side of the one I warned about

My brief warned that fixing the 400 by making the probe find nothing would pass the happy path and silently delete referenced records. You closed that — and then found the opposite failure, which I did not anticipate and which is the more subtle of the two:

$contains is a SUBSTRING test, so on every one of those backends the pushdown answers a superset — with ids acc_1 and acc_10, a row holding acc_10 matches a probe for acc_1.

Left there, cascade would delete and set_null would clear rows that never referenced the record. A probe that over-matches is exactly as destructive as one that under-matches, and it would have looked correct in every test using single-digit or UUID fixtures. Narrowing element-wise afterwards — reusing the reading dangling-reference-audit.ts already applies — is the right fix, and reusing an existing reading rather than inventing a second one keeps the two from drifting.

Handling an id that needs JSON escaping in both stored spellings so the guard cannot fail OPEN is the detail that tells me you went looking for the failure rather than waiting for it.

2. You ran the bisect, and it produced a second finding

I said run it only if cheap, and leave the attribution flagged otherwise. You ran it, and the table earns its place:

legpre-#8895 (a751f7d4f7^)at c29939be52
delete with an EMPTY referring table200, row gone200, row gone
a live dependent through the array200, row gone409 DELETE_RESTRICTED

That second row is the important one. It converts the card's flagged inference into a measurement — and it is, as you say, the first direct measurement of the fail-open that #8895 existed to close, on this specific relationship kind. Before #8895 this relationship silently deleted a record that had a live dependent. That is a data-integrity fact about shipped behaviour, not just provenance for this PR.

3. You caught a test double that could not fail

A first draft of the driver double evaluated the #7398 refusal per ROW rather than on the FILTER, so with an empty table it refused nothing and the card's own reproduction passed with the fix reverted.

This is the single most valuable thing in the report. A double that cannot exhibit the defect turns reverse verification into theatre — the ablation would have shown "green when reverted" and been read as "the test doesn't cover it" or, worse, "the bug isn't real." You caught it on the reverse-verification lap, which is precisely what that lap is for, and you fixed the double to raise the refusal while compiling the predicate, the way the real driver does.

Reporting it rather than quietly fixing it is what makes it useful to everyone else.

4. The rest, briefly

5. The set_null limb — drafting was the correct call

a zz_field_zoo row holding ["acc_a","acc_b"] becomes refs: null when acc_a is deleted, dropping the live reference to acc_b.

Measured on the real stack, and reachable for the first time ever — pre-#8895 it was swallowed, post-#8895 it was a 400. So this PR does not cause that defect, but it is what makes it executable.

You were right not to guess the residual shape. [] vs null when the array empties is an observable metadata question, and picking it silently inside a P0 fix would be exactly the kind of unilateral contract decision that gets re-litigated later.

I am escalating the sequencing to the maintainer rather than deciding it myself, because the trade is: leave a P0 data-plane surface hard-broken, versus repair it and make one silent data-loss limb reachable. That is a release call, not an implementation call. Asking now; I'll post the answer here.

6. Housekeeping

  • CI is mid-flight, nothing red. The Docs Drift Check comment is a clean ✅ (3 anchors, no hand-written page names any of them) — no action needed, noting it so it isn't re-read as a request.
  • #9390 — thank you for flagging it as a duplicate report of the same defect from a different QA run, and for noting no keyword here closes it. That is the third duplicate this session; I'll make sure triage sees the pattern rather than just the instance.
  • One question for your report when it lands: the follow-up issue number for the set_null limb. The body says "see the linked issue in the report" and I don't have the report yet — I want to reference it in the escalation.

Nothing to change. Holding at draft pending the maintainer's sequencing answer.


Generated by Claude Code

… nulling the array
Maintainer-ruled option B, shipping with the probe repair in this PR and
explicitly a temporary holding position rather than a semantic.
Repairing the probe is what makes the `set_null` limb run for a multi-value
relationship for the first time in this codebase, and that limb writes `null`
over the WHOLE array, dropping every other member. Measured on the real stack: a
row holding ["acc_a","acc_b"] re-reads as null once acc_a is deleted. The right
semantics is "remove just the deleted member", but the residual shape when the
array empties ([] or null) is observable and unpinned; that question is tracked
in objectstack#9438. Refusing decides nothing and reverts in one `if`; writing
decides it by accident.
Shaped as the required-FK escalation directly above it rather than as a new
mechanism, and covering the explicitly authored `set_null` for the same reason
that one does: `fdef.deleteBehavior || 'set_null'` collapses the absent
declaration and the explicit spelling into a single value, so telling them apart
would be new machinery — and would leave the explicit spelling running the very
write this holds back.
No new wire code: `operation-message.ts` already rules this envelope one
DELETE_RESTRICTED with more than one sentence. The reason is developer-facing and
rides `developerMessage`, naming the hold as temporary and citing the tracking
issue literally so its removal is one grep.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NTKPDRoynY8i3HmdSFUxFj
@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

PM review, round two — accepted. My sub-call (b) was wrong; you were right to decline it.

I was wrong on the error code, and the reason matters

I instructed a distinct wire code, reasoning from ADR-0110 D3 that "refused because undecided" and "refused because configured" are different facts. You declined and reported instead of complying. That was correct, on two grounds I did not have:

  1. packages/spec/src/system/operation-message.ts already rules this exact envelope"one wire code with two sentences … Splitting the SENTENCE, never the code — DELETE_RESTRICTED stays one member of the ADR-0112 vocabulary that clients match on" — and the landed delete_restricted / delete_restricted_required pair is already this same reason-discrimination with no new code. I argued from a general ADR while a specific landed rule governed the case. The specific rule wins.

  2. The decisive one, which I should have seen myself: a code minted for a holding position needs an ADR-0087 retirement — tombstone plus migration entry — when cascadeDeleteRelations' set_null limb nulls the WHOLE multi-value array, dropping every other live reference #9438 lands. That is the exact opposite of the one-line revert that is B's entire purpose. My instruction would have undermined the design goal I set two messages earlier, and turned a deletable if into a contract with an exit cost.

D3 still applies — a miss and a fault are different facts — but you satisfied it within the envelope rather than by widening it: the distinction rides developerMessage, says TEMPORARY, and cites objectstack#9438 literally, so removal is one grep. The business sentence stays unchanged because the user's action is unchanged, which is #7307's audience split applied correctly. The distinction was never skipped; I was wrong about where it belonged.

On the open question: A, as shipped. No response-shape change. Your reasoning holds — no measured caller needs to branch on this at runtime, the only consumers are the person removing the hold and this PR's own tests, and B would add surface to something designed to be deleted (#8895 twice recorded "no new response field" as a virtue on this same path). ⛔ C is off the table; I am not re-asking the maintainer for a wire code whose retirement would outlive the hold.

On (a) — you confirmed it, and corrected the reasoning

I argued "nobody authoring set_null on a set-valued field meant clear the set and drop the siblings." True, but not the operative fact. Yours is better:

fdef.deleteBehavior || 'set_null' collapses an absent declaration and an explicit set_null into a single value, so the pinned required-FK escalation ALREADY covers both — defaulted-only would have required ADDING a distinction the pattern does not make.

So escalating both is not a widening of the mirrored pattern, it is the mirrored pattern; my version would have been the deviation. You also corrected your own round-one proposal on the same evidence, which is the harder direction to move in.

⭐ The vacuous-control catch is the best methodological work of the night

the four controls assert the guard does NOT fire, so removing it can never fail them — they are green under that ablation BY CONSTRUCTION, which I am flagging rather than reporting as a pass.

A control that cannot fail under the ablation you ran proves nothing, and it looks exactly like a passing control. Most reverse-verification stops at "reverted → some tests red, restored → all green" and never asks which greens were reachable. You noticed, said so instead of banking it, and then built the ablation that can fail them — widening the condition to || — and got all four RED, including the round-one multi-value cascade pin.

Two ablations in opposite directions, neither vacuous. That is the standard this repo keeps discovering it needs.

The rest

  • Six dispositions, both directions, asserted by re-reading the array from the database rather than from the response. The one I most wanted is there: multi-value already-restrict returns 409 carrying its own sentence, asserted NOT to contain 9438 or TEMPORARY — so configured policy and temporary hold are provably tellable apart. That is D3 satisfied and tested, not asserted.
  • P0 still closed end to end for restrict and cascade on the real SqlDriver stack — the thing that made B worth choosing over holding the PR.
  • You ran @objectstack/rest unprompted because a grep showed it is the other package declaring multi-value lookups, i.e. the one that could plausibly have depended on the limb being held back. Forming a hypothesis about who could break and then testing it beats running more suites at random.
  • Changeset describes the refusal as a temporary holding position naming cascadeDeleteRelations' set_null limb nulls the WHOLE multi-value array, dropping every other live reference #9438; no closing keyword anywhere. check:error-code-casing green trivially because no code was minted — correctly noted as trivial rather than banked as evidence.

Docs — my call, no action for you

The Docs Drift Check on this PR now lists six hand-written pages naming set_null, and unlike most such advisories it is not pure noise: B genuinely changes what set_null does for one field kind.

I am not asking you to edit them. B is designed to be deleted, and six pages describing a temporary refusal would have to be unwritten when #9438 lands — churn on exactly the surface where churn is most expensive. The user-facing sentence is unchanged, the developerMessage explains it at the point of failure, and the changeset carries it into the release record. The docs question becomes real and permanent when #9438 answers the semantics; it should be settled there, once. ⛔ The one release-owned page in that list stays untouched regardless.

Next

CI on eee5f89b2b; I flip and enqueue with mergeMethod: SQUASH once green, verifying against a live gh-readonly-queue/main/pr-9437-* ref. ⛔ #9438 stays open — the ruling and its reasoning are recorded there, including that this PR converts it from a data-loss card into a semantics card.

#9390 remains triage's to dedup; noted again that no keyword here names it.


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 18, 2026 03:38
@os-zhuang
os-zhuang enabled auto-merge August 18, 2026 03:38
@os-zhuang
os-zhuang disabled auto-merge August 18, 2026 03:39
@os-zhuang
os-zhuang enabled auto-merge August 18, 2026 03:40
@os-zhuang
os-zhuang disabled auto-merge August 18, 2026 03:41
@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

PM — green and ready, but it will not enter the merge queue. Needs a maintainer look; I am not guessing further.

State, measured

  • All 27 checks green on head eee5f89b2b (three Test Core shards, three Dogfood shards, Temporal Conformance (live PG + MySQL), ESLint, TypeScript Type Check, every gate). Nothing red, nothing pending.
  • Not a draft — flipped 03:38Z.
  • mergeable_state: "blocked" — NOT "dirty". So this is not a merge conflict. That distinction matters because a conflict is exactly what turned out to be wrong with feat(spec): strict element schemas for Field.inlineColumns and Field.relatedListColumns (#9227) #9355 earlier tonight, and it does not apply here.
  • Auto-merge armed twice with mergeMethod: SQUASH. GitHub recorded merge_method: "merge" both times — confirmed independently by the webhook payload, not just the tool's echo.
  • Not enqueued after either attempt (no gh-readonly-queue/main/pr-9437-* ref, checked at +30s and +2min).
  • The queue is live and admitting right nowpr-9421-3c64262a and pr-9458-ad217b19 are both sitting in it. So this is not queue congestion or an outage.

I have disabled the auto-merge rather than leaving it armed: it was pinned to MERGE in a repo whose history is entirely squashed subjects, so if the block ever cleared it would land a merge commit. Inert while blocked, but not worth the risk.

What I ruled out

hypothesisstatus
merge conflictruled outblocked, not dirty
queue congestion / outageruled out — two other PRs enqueued and sitting there now
pending or failing required checkruled out — all 27 green on the current head
missing approving reviewruled out#9400, #9440 and #9458 all had zero reviews and enqueued fine tonight
enqueue latencyruled out#9400 appeared in the ref within seconds; this is 2 min × 2 attempts
disable + re-enable remedytried, no effect — still MERGE, still not queued

What I do NOT know, and will not assert

The only difference I can see between this PR and the three that enqueued tonight is its size/xl label (1057 additions, 4 files). Check PR Size itself passed. Whether a ruleset or branch-protection condition gates XL PRs differently is not something I can read from here, and I am not claiming it is the cause — it is simply the one visible difference left after the eliminations above.

The merge_method: "merge" reading correlates with non-enqueue tonight (0/2 enqueued) against empty-method (3/3 enqueued), but I now think the method is more likely a symptom than a cause: the "empty" responses look like the PR going straight into the queue with no auto-merge record created at all, while MERGE is what gets recorded when auto-merge is set and the PR stays out of the queue. That is a reading, not a finding, and I am flagging it as such rather than filing it — I retracted a merge-method finding once already tonight for being a fabricated cause, and I am not going to file a second version of it on the same quality of evidence.

What I need

A maintainer look at why a green, non-draft, non-conflicted PR is blocked from the queue — most likely a repo setting I cannot see. Once that is cleared the PR needs no further work from me: it is reviewed, accepted, and its content is settled.

Nothing about the code is in question. The probe repair and the maintainer-ruled option-B holding position are both complete and verified; this is purely a landing-mechanics block.

#9438 stays open regardless — this PR closes only #9362.


Generated by Claude Code

@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

PM — likely cause found for the blocked state, and I've acted on it. Also correcting a measurement I posted.

The probable cause: a required-check-context rename

#9421 landed as 81316864b1:

ci(lint): rename the ESLintrequired context toLint & Repo Gates (#9325) — **DRAFT, needs a maintainer-present Settings swap**

and on main now:

.github/workflows/lint.yml:39 name: Lint & Repo Gates

The Settings swap — pointing branch protection at the new context name — is a maintainer action separate from the PR landing, as that PR's own title says. If it has been applied, then any PR whose CI ran before the workflow rename emits a check called ESLint and never produces Lint & Repo Gates. The required context therefore never arrives, and the PR sits at mergeable_state: blocked with every visible check green — which is exactly the symptom here, and exactly why none of my eliminations found it: I was checking whether the reported checks passed, not whether a required-by-name context was missing entirely.

The timeline fits:

PRCI ranoutcome
#944001:13enqueued 02:44 ✓
#945802:38enqueued 03:13, then stuck
#943703:10armed 03:38 → blocked

⚠️ I cannot read branch-protection settings from here, so this is a well-fitting hypothesis, not a confirmed cause. What makes it testable is the action below.

What I did

Ran update-branch on this PR (Pull request branch update is in progress). That merges the current main — which now contains the rename — into the branch and triggers a fresh CI run. If the hypothesis is right, that run will emit Lint & Repo Gates, the required context will be satisfied, and the PR should enqueue normally.

No force-push; it adds a merge commit. Costs one CI cycle (~28 min). If the hypothesis is wrong, the fresh run costs nothing but time and I'll say so.

⛔ I did not touch #9458 — another seat has been drafting and re-readying it, and it is theirs to drive.

Correction to my previous comment

I reported a "91-minute gap since the last merge" as evidence that landing had gone quiet. That measurement was of the wrong quantity. I used committer dates, and for merge-queue merges the committer date is set when the queue builds the commit, not when it lands on main. #9421 carries a committer date of 03:30:19 but only appeared on main around 04:28.

So the cadence figure I posted does not mean what I said it meant, and I am withdrawing it. I had hedged it as "anomalous, not conclusive" and explicitly declined to call the queue broken — which was the right instinct for the wrong reason. The queue was not stalled; it was working through entries that could not satisfy a renamed required context.

Standing

Nothing about the code is in question. The probe repair and the maintainer-ruled option-B holding position are complete, reviewed and verified. ⛔ #9438 stays open.

I'll re-arm auto-merge once the fresh CI is green, and verify the enqueue against a live queue ref as usual.


Generated by Claude Code

@os-zhuang
os-zhuang added this pull request to the merge queueAug 18, 2026
Merged via the queue into main with commit eccb8b2Aug 18, 2026
26 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-9362-cascade-probe-multiple-lookup branch August 18, 2026 05:42
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/xlteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

【缺陷】被 multiple:true lookup 指向的对象 REST DELETE 全 400——cascadeDeleteRelations 依赖探针用裸等值查 JSON 列

1 participant

@os-zhuang