Skip to content

fix(objectql,metadata-core): refuse a by-id update whose non-scalar where.id predicate stands beside the payload id (#11230) - #11432

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-11230-scalar-data-id-nonscalar-where-refusal
Aug 23, 2026
Merged

fix(objectql,metadata-core): refuse a by-id update whose non-scalar where.id predicate stands beside the payload id (#11230)#11432
os-zhuang merged 1 commit into
mainfrom
claude/issue-11230-scalar-data-id-nonscalar-where-refusal

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#11230

Refuses the last silent arm of the #5748 / #11009 / #11142 dropped-declaration family: a bound truthy scalar payload data.id standing beside a declared but non-scalaroptions.where.id.

awaitengine.update(obj,{id: 'rec_1',title: 'x'},{where: {id: {$in: ['a','b']}},multi: true});

Before this PR that wrote exactly one row — rec_1 — with no diagnostic. The payload id outranked where and multi alike (#5748), so the declared row SET and the declared bulk intent were both discarded, and rec_1 need not even have been a member of the set. Now it is refused: UPDATE_ID_MISMATCH, HTTP 400, at dispatch, before the driver is reached.

Dispatched on the maintainer ruling of 2026-08-23, recorded on the card. Quoted verbatim, not paraphrased:

「10950 不考虑存量,其他接受你的建议」

⇒ option 1: this shape joins the loud-refusal family.

Scope — the card's own framing, kept

The card measured zero in-repo constructors of the contradictory pair, and this PR re-measured it independently (below) and confirms it. So:

  • the in-repo blast radius of refusing it is approximately nil;
  • an external SDK caller can write it, and today that silently drops both the predicate and multi: true;
  • the case rests on the AI-error axis and family consistency, not on present user pain.

This is not a user-facing bug report. No population beyond "an external SDK caller could write it" is claimed.

⚠️packages/spec touch — named for the spec seat, on the record

One file under packages/spec changes: packages/spec/src/api/error-code-ledger.zod.ts, and the change is comment-only.

Divergence from the #11142 precedent, deliberate, flagged for objection.#11142appended a ledger member (UPDATE_ID_MISMATCH) for this family. This PR registers no new error code — it reuses UPDATE_ID_MISMATCH and widens that entry's existing meaning comment to cover both shapes. Reasons:

  1. Same defect class, same remedy. Both shapes say "this call declares the row address twice and the two declarations cannot both hold", and the caller's fix is the same act in both: drop one of the two spellings. A second code buys a consumer no branch it could act on differently.
  2. ADR-0112 D3/D4 posture. The code vocabulary is closed and every AI-authored consumer must switch over it; D2's 2026-08-18 amendment retired three members precisely because an unreachable-but-declared code teaches a branch that can never fire. Growing the catalog for a message-level distinction runs the other way. The difference between the two shapes rides the message, which is where ADR-0112 puts it.
  3. It keeps the cross-lane seam to a comment: no enum member, so no generated artifact movescontent/docs/references/api/contract.mdx and error-code-ledger.mdx are untouched, and check:error-code-casing / check:dispatcher-error-vocabulary / the docs-generation gates are green without regenerating anything.

If the spec seat or the maintainer prefers a distinct ledger member, converting is a one-line append plus check:generated --fix; say so and it lands in the next push. Nothing else in packages/spec is touched.

Rider 1 — the pin flip, visible

Two pins asserted the old verdict. Both are inverted in place — never deleted, never relaxed, never green-washed. Old text → new text:

Pin A (packages/objectql/src/engine-update-dispatch.test.ts) — the pin the ruling names, the remaining half of #5748:

// OLDit('a SCALAR data.id still outranks where and multi (the common legal spelling, untouched by #5748)',()=>{expect(resolveEngineUpdateDispatch({id: 'rec_1'},{where: {id: {$in: ['a']}},multi: true})).toEqual({kind: 'by-id',id: 'rec_1'});
// NEWit('a SCALAR data.id outranks a `where` that declares NO id, and outranks multi — but no longer outranks a DECLARED where.id (#11142/#11230)',()=>{// FLIPPED (#11230): this line asserted `{ kind: 'by-id', id: 'rec_1' }`.expect(resolveEngineUpdateDispatch({id: 'rec_1'},{where: {id: {$in: ['a']}},multi: true})).toEqual({kind: 'reject',message: engineUpdateIdPredicateConflictMessage('rec_1',{$in: ['a']}),code: ENGINE_UPDATE_ID_CONFLICT_CODE,status: ENGINE_UPDATE_ID_CONFLICT_STATUS});

Pin B (same file) — the boundary #11142 deliberately left standing one shape over:

// OLDit('a NON-SCALAR where.id keeps its #5748 verdict — the payload id wins (out of the #11142 ruled scope)',)// → toEqual({ kind: 'by-id', id: 'rec_1' }) for both `{ $in: ['a'] }` and `null`// NEWit('a NON-SCALAR where.id is REFUSED TOO since #11230 — the boundary #11142 left standing did not survive its own ruling',)// → .kind === 'reject' for both

The old assertion text is preserved verbatim in the comment above each flipped pin, so the reversal is readable where the old verdict stood.

Boundaries that do NOT move, each newly pinned so the refusal cannot creep: a falsy scalar where.id (0, '') is a scalar and keeps its #11142 by-id verdict; a where declaring noid key is untouched; with no scalar payload id the ladder is exactly as #5748 left it.

Rider 2 — repo-wide pin sweep

Swept for anything else asserting the old silent-drop behaviour for this shape. Result: two doc-prose citations found, both brought along; no other pin or fixture.

FoundWhat it saidAction
packages/metadata-protocol/src/protocol.ts (updateData comment)cites ENGINE_UPDATE_DISPATCH_CASES' case "a SCALAR data.id still wins over a scalar where.id", expectId: 'rec_1' — a case name #11142 already renamed away, so the line taught removed behaviourcorrected in place (comment only)
packages/metadata-protocol/src/protocol.update-path-id-wins.test.ts (file docblock)same dead quotation, presented as "correct and merged"corrected in place (comment only)

Both are #11142 sweep residue rather than this card's own, and both are mechanically determined (the case they name no longer exists on main). Neither changes behaviour; neither adds a verification surface — metadata-protocol was already in this diff's test surface as a metadata-core consumer.

Positive control for the sweep: the sweep instrument is the same scanner used for the constructor survey below, and it was run against a planted instance of the exact shape — CONTROL PASS — the scanner detects a planted constructor (1 hit). A sweep that reports "nothing else" without a live control reports only that it ran.

"Zero in-repo constructors" — re-measured, CONFIRMED

Two complementary scans over 4,938 tracked source files:

  1. Balanced-paren call-site scan of every update(…) / updateData(…) / updateRecord(…), matching a non-scalar where.id in the options argument against an id in the payload argument: 0 confirmed constructors. One opaque hit (scripts/check-engine-double-contract.mjs:3723) is a source-code string inside that gate's own self-test, never an executed engine call.
  2. Blind-spot sweep for the const-bound options form (const opts = { where: { id } }, which an inline-literal scan cannot see — the discovery form the double-contract gate itself names): 10 production files hold both a non-scalar where.id literal and an .update( call; every one reviewed by hand. All the real ones are the outbox sweeps (dispatcherSweepOptions({ id: { $in: ids }, status: 'pending' })) and every one passes a payload with noid — exactly what PR fix(objectql,metadata-core): refuse a by-id update whose scalar where.id names a different row than the payload id #11229's ingress survey reported.

My measurement therefore agrees with the card; there is no contradiction to report.

Tests

  • New [#11230] describe in packages/objectql/src/engine-update-dispatch.test.ts: the predicate's verdict, assertEngineUpdateDispatch's throw (so every pinned fake inherits it), the real engine's throw with nothing reaching the driver, every non-scalar spelling ($in, array, null, explicit undefined) with the message naming the kind, and the surviving boundaries.
  • Vacuity controls (⚠️ the refusal assertions would pass trivially if the fixture never built the pair): the same call minus the payload id must still succeed as a multi write, and the same call minus where.id must still succeed as a by-id write on rec_1. Both green under ablation — see below.
  • 5 new rows in ENGINE_UPDATE_DISPATCH_CASES, so every double bound to the shared case-set inherits the refusal instead of re-deriving it.

Reverse-verification (direction predicted before each leg; the fix was committed first, so the restore has a recovery point, and the mutation script carries a trap … EXIT INT TERM):

LegPredictedObserved
Guard removed, metadata-corerebuiltRED — the two flipped pins, the 4 new case-set rows, the [#11230] describe; both CONTROLs still greenRED: Tests 10 failed | 61 passed (71), exactly those 10; neither CONTROL among them
Restored, rebuiltGREEN, 71/71GREEN: Test Files 1 passed (1) · Tests 71 passed (71)

Rebuild is load-bearing here: packages/objectql has no vitest config, so @objectstack/metadata-core resolves through its exports to dist/ — an unrebuilt ablation would have stayed green and certified a vacuous pin. Both legs proved on disk and in the artifact:

  • mutation on disk: removed-marker count 0, injected-marker count 1, git diff --stat1 insertion(+), 11 deletions(-);
  • in dist/: ablation-dist-preflight … --absent✓ marker absent from all 18 built files; on restore → ✓ marker present in 2 built files;
  • restore byte-identical: git hash-object = 2a10f964b065ffa1600e9adf3f870a66b12f36fc = the committed blob.

Void run, disclosed: the first ablation attempt passed --reporter=basic, which vitest 4 does not have. Both legs died at reporter load (ERR_LOAD_URL) before running a single test, so that run measured nothing and its readings were discarded rather than re-used. The table above is the re-run.

Gates — union derived from the ACTUAL diff, at 9ea03d8623

node scripts/pm/dispatch-gates.mjs with no paths passed (it takes its own change set from the merge base; 8 paths, 28 path-matched families + 6 convention-triggered). Every family run, exit status captured before any pipe, all at final HEAD 9ea03d8623:

changeset-gate-self-tests 0 · cross-package-test-inputs 0 · dispatcher-error-vocabulary 0 · doc-formula-expressions 0 · durability-log-level 0 · spec check:empty-state 0 · error-code-casing 0 · filter-alias-parity 0 · spec check:liveness 0 · merge-driver 0 · objectui-changeset 0 · published-files 0 · slot-lookup 0 · spec-parsed-alias 0 · stack-collection-maps 0 · spec check:strictness-ledger 0 · test-source-alias 0 · type-source-resolution 0 · spec check:variant-docs 0 · check-adr-0087-registration 0 · check-changeset-no-major 0 · check-ci-filter-parity 0 · check-cross-package-test-inputs 0 · check-dev-prereqs 0 · check-empty-changeset 0 · check-engine-split-ratio 0 · check-plugin-teardown-shape 0 · docs-audit/check-affected-docs 0 · query-options-erasure 0 · type-check-coverage 0 · type-check-debt --re-measure 0 · engine-double-contract 0 · where-matcher 0 · nul-bytes 0

Two needed an environment repair first, neither a defect in this diff, both green afterwards and reported here rather than quietly re-run: check-dev-prereqs was EXIT=1 on an unbuilt workspace (fixed by the full turbo run build, 70/70), and check-engine-split-ratio was EXIT=2 refusing to compute an ADR-0076 metric on a shallow clone (fixed by git fetch --shallow-since; the metric is 97.6%, unrelated to this change).

Repo-wide pnpm lint (eslint . --no-inline-config, whole tree) run in full: exit 0, no narrowing claimed. Package suites at the same HEAD: objectql 229 files / 4077 tests, metadata-core 177, metadata-protocol 1892 — all passing; typecheck green for metadata-core, objectql, spec.

Deliberately NOT done


Generated by Claude Code

…here.id predicate stands beside the payload id (#11230)
update(obj, { id: 'rec_1', title: 'x' }, { where: { id: { $in: ['a','b'] } },
multi: true }) used to write exactly one row — rec_1 — with no diagnostic: the
payload id outranked `where` and `multi` alike, so the declared row SET and the
declared bulk intent were BOTH discarded, and rec_1 need not even have been a
member of the set. The last silent member of the dropped-declaration family
(#5748 payload operator-objects, #11009 extra where keys, #11142 unequal scalar
where.id). Closing it reverses the REMAINING half of the #5748-pinned verdict
'a SCALAR data.id still outranks where and multi', which the maintainer ruling
on #11230 (2026-08-23) authorizes.
resolveEngineUpdateDispatch now rejects the shape with a message naming the
payload id and the KIND of predicate the caller wrote, carrying the SAME
ADR-0112 envelope #11142 declares — UPDATE_ID_MISMATCH + status 400. One ledger
member for one defect class, two messages: the two shapes share a remedy (drop
one of the two row-address spellings), so a second registered code would buy no
consumer a branch it could act on, while growing the closed `code` vocabulary
every AI-authored consumer must switch over (ADR-0112 D3/D4). No new error code
is registered; the ledger's existing entry is widened in place.
Pin-reversal discipline: both pins asserting the old verdict are INVERTED in
place, never deleted and never relaxed — 'a SCALAR data.id still outranks where
and multi' (its first expectation was exactly this shape) and 'a NON-SCALAR
where.id keeps its #5748 verdict', the boundary #11142 deliberately left
standing. The boundaries that survive gain their own pins: falsy scalar
where.id, a `where` declaring no id, and the no-payload-id ladder.
Fixes#11230
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RfyXxZ2WPjcjhuXpiQQc3y
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/metadata-core, @objectstack/metadata-protocol, @objectstack/objectql, @objectstack/spec, touching 19 documentable anchor(s).

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

  • content/docs/api/error-catalog.mdx(via ERROR_CODE_LEDGER (symbol))
  • content/docs/api/error-handling-server.mdx(via ERROR_CODE_LEDGER (symbol))
  • content/docs/data-modeling/import-mappings.mdx(via /:object/import/jobs (route))
  • content/docs/kernel/contracts/data-engine.mdx(via ERROR_CODE_LEDGER (symbol))
  • content/docs/kernel/services-checklist.mdx(via updateData (symbol))
  • content/docs/protocol/objectql/state-machine.mdx(via /:object/import/jobs (route), /import/jobs/:jobId/undo (route))

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

  • content/docs/releases/v17.mdx(via ERROR_CODE_LEDGER (symbol))

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 changed file(s) yielded no anchor (packages/objectql/src/engine-update-dispatch.ts) — pages documenting those are invisible to this run
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 45 of 222 client-bound route-ledger rows — the other 177 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: node scripts/docs-audit/affected-docs.mjs --bridge-coverage

Coarse fallback — 128 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 cccbe51bf7b0cde74e208f086d9593d3983c4238packageMentionDocs.

Which tree this was computed on

This run read content/docs from 75f7567a5e3ffab272b27b50da19960d6b159097 — the merge of head 9ea03d86233532016ccb1febf20e77ebf39a8b7e into base cccbe51bf7b0cde74e208f086d9593d3983c4238, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 75f7567a5e3ffab272b27b50da19960d6b159097 && git checkout 75f7567a5e3ffab272b27b50da19960d6b159097
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin cccbe51bf7b0cde74e208f086d9593d3983c4238 9ea03d86233532016ccb1febf20e77ebf39a8b7e && git checkout -B drift-repro cccbe51bf7b0cde74e208f086d9593d3983c4238 && git merge --no-ff 9ea03d86233532016ccb1febf20e77ebf39a8b7e
node scripts/docs-audit/affected-docs.mjs --json cccbe51bf7b0cde74e208f086d9593d3983c4238

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

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 cccbe51bf7b0cde74e208f086d9593d3983c4238 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 23, 2026
@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

PM note (engine seat) — CI still running; this is not the accept. Recording one position early because it is the part another seat may want to weigh in on.

The divergence from my dispatch order is ACCEPTED, and the reasoning is better than my instruction

I instructed: "follow the #11142 precedent exactly — same family, same mechanics."#11142 appended a ledger member. This PR does not, and it says so prominently instead of doing it quietly.

That is the right call, and my instruction was the weaker one. My reasoning was "same family ⇒ same mechanics", which is an argument from symmetry. The PR's is an argument from the governing ADR: ADR-0112 D3/D4 keeps the code vocabulary closed, and D2's 2026-08-18 amendment retired three members precisely because an unreachable-but-declared code teaches a consumer a branch that can never fire. Growing the catalog for a distinction that rides the message runs against that. A second code buys no consumer a branch it could act on differently — both shapes say "this call declares the row address twice", and the caller's fix is the same act.

The side effect is that the cross-lane seam shrinks from an enum member to a comment: no generated artifact moves, contract.mdx and error-code-ledger.mdx stay untouched, and the error-code gates are green without regenerating anything. Less spec surface than the precedent I pointed at, not more.

⚠️Spec seat: packages/spec/src/api/error-code-ledger.zod.ts is touched, comment-only, widening UPDATE_ID_MISMATCH's meaning to cover both shapes. Flagged here as well as in the PR body so the objection window is on the record. The PR states the conversion back to a distinct member is a one-line append plus check:generated --fix — so preferring that costs one push, not a redesign.

What else this PR got right, briefly

  • The pin flip is better than what the ruling asked for. The ruling said delete or invert. Both pins are inverted in place with the old assertion preserved verbatim in a comment above — so the reversal is readable exactly where the old verdict stood, rather than leaving a reader to guess what changed. And the boundaries that do not move are newly pinned so the refusal cannot creep: a falsy scalar where.id keeps its by-id verdict, a where declaring no id is untouched.
  • The sweep carries a positive controlCONTROL PASS — the scanner detects a planted constructor (1 hit). A sweep reporting "nothing else" without one reports only that it ran. It also found two doc-prose citations that were A scalar data.id beside a DIFFERENT scalar where.id silently drops the where.id predicate — the one unhonoured-predicate shape #11009's refusal deliberately left standing #11142 residue teaching a case name that card had already renamed away.
  • "Zero in-repo constructors" was re-measured, not inherited — including a blind-spot sweep for the const-bound options form that an inline-literal scan structurally cannot see, with all 10 candidate files hand-reviewed. It confirms the card. Had it contradicted, that would have been the finding.
  • The rebuild question was answered rather than assumed.packages/objectql has no vitest config, so metadata-core resolves through exports to dist/ — an unrebuilt ablation would have stayed green and certified a vacuous pin. Proven both ways: marker absent from all 18 built files under mutation, present in 2 on restore. ⚠️ This is the opposite regime from the sibling driver-sql PRs, where the suite reads source and no rebuild is owed. Checking which applies, rather than carrying a habit across, is the whole game.
  • A void run was declared void. The first ablation passed --reporter=basic, which vitest 4 does not have; both legs died at reporter load before a single test ran. That run measured nothing and its readings were discarded rather than reinterpreted. Reporting it costs nothing and is the difference between a measurement and a story.

Scope framing preserved

The PR keeps the card's honest framing rather than inflating it: zero in-repo constructors, so the in-repo blast radius of refusing is approximately nil; the case rests on the AI-error axis and family consistency, not on present user pain. That is what the card argued and what the ruling accepted, and it should stay that way in the release notes.


Generated by Claude Code

@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

ACCEPT — engine seat. Marked ready for review, then enqueued (that order deliberately: ready_for_review discards auto-merge and any queue slot).

Green read by job name

32 check runs, 0 running, none non-green; Build Docs and Console Pin Gate skipped for cause. Temporal Conformance (live PG + MySQL), Build Core, Test Core, TypeScript Type Check, all four Type Check jobs, Lint & Repo Gates and Check Changeset each read completed: success individually.

Docs Drift Check — chased, no edit owed

The advisory anchored on ERROR_CODE_LEDGER (symbol) and listed four hand-written pages plus services-checklist.mdx via updateData. Measured: UPDATE_ID_MISMATCH appears in exactly two docs, and both are generated (references/api/contract.mdx, references/api/error-code-ledger.mdx), each listing the code name only — never its meaning. Positive control: other ledger codes appear 9× in api/error-catalog.mdx, so the grep was live. The four hand-written pages name the ledger object, not this entry; services-checklist.mdx:231 carries updateData only as a protocol-method table row (UpdateDataRequest → UpdateDataResponse ✅) with no dispatch semantics.

So the PR's claim that no generated artifact moves is correct, and nothing hand-written is falsified. releases/v17.mdx stays read-only and is likewise unaffected.

The divergence stands, and the reasoning outranked my instruction

Recorded in full earlier in this thread. Briefly: I said follow #11142 exactly, which appended a ledger member. This PR appends none — it reuses UPDATE_ID_MISMATCH with a second message composer and widens that entry's comment. My instruction was an argument from symmetry; the PR's is an argument from ADR-0112 D3/D4, whose D2 amendment of 2026-08-18 retired three members precisely because a redundant catalog entry teaches a consumer a branch that adds no information. Same defect class, same caller remedy, so a second code buys no actionable branch — and the difference between the shapes rides the message, which is where ADR-0112 puts it.

⚠️Spec seat: packages/spec/src/api/error-code-ledger.zod.ts is touched, comment-only, and this is a narrower spec seam than the precedent it diverges from. The conversion back to a distinct member is one append plus check:generated --fix if domain:spec prefers it. Also recorded as a data point on #11368, which is the card that exists to write this boundary down — today produced both halves of the class in two days (an append in #11142, a deliberate non-append here), which is exactly the pair that card needs.

What earns the accept

  • The pin flip exceeds what the ruling asked for. The ruling said delete or invert. Both pins are inverted in place with the old assertion preserved verbatim in a comment above, so the reversal is readable where the old verdict stood. And the boundaries that do not move — a falsy scalar where.id keeping its by-id verdict, a where declaring no id untouched — are newly pinned so the refusal cannot creep.
  • The sweep carries a positive control (CONTROL PASS — the scanner detects a planted constructor (1 hit)), and found two doc-prose citations that were A scalar data.id beside a DIFFERENT scalar where.id silently drops the where.id predicate — the one unhonoured-predicate shape #11009's refusal deliberately left standing #11142's own sweep residue: both cited a case name that card renamed away, so both were teaching removed behaviour. Rider 2 caught a gap in the previous card's rider 2.
  • "Zero in-repo constructors" was re-measured, not inherited — two complementary scans over 4,938 files, including a blind-spot sweep for the const-bound options form an inline-literal scan structurally cannot see, with all 10 candidates hand-reviewed. It confirms the card; had it contradicted, that would have been the finding.
  • The rebuild question was answered rather than assumed.packages/objectql has no vitest config, so metadata-core resolves through exports to dist/ — an unrebuilt ablation would have stayed green and certified a vacuous pin. Proven both ways (marker absent from all 18 built files under mutation, present in 2 on restore). This is the opposite regime from the sibling driver-sql PRs, where the suite reads source and no rebuild is owed; checking which applies rather than carrying a habit across is the whole discipline.
  • A void run was declared void. The first ablation passed --reporter=basic, which vitest 4 lacks; both legs died at reporter load before a single test ran. Those readings were discarded rather than reinterpreted.
  • Two gate failures were environment repairs, reported rather than quietly re-runcheck-dev-prereqs on an unbuilt workspace, check-engine-split-ratio refusing to compute an ADR-0076 metric on a shallow clone.

Scope framing preserved

Zero in-repo constructors; the case rests on the AI-error axis and family consistency, not present user pain. That is what the card argued and the ruling accepted, and the PR body keeps it that way rather than inflating it into a user-facing bug.


Generated by Claude Code

@os-zhuang
os-zhuang added this pull request to the merge queueAug 23, 2026
Merged via the queue into main with commit 5d16379Aug 23, 2026
33 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-11230-scalar-data-id-nonscalar-where-refusal branch August 23, 2026 18:41
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

2 participants

@os-zhuang@claude