Skip to content

feat(objectql): find() guarantees its array — an afterFind that replaces the container is refused - #16232

Merged
os-zhuang merged 5 commits into
mainfrom
claude/issue-15823-find-afterfind-array-guard
Sep 6, 2026
Merged

feat(objectql): find() guarantees its array — an afterFind that replaces the container is refused#16232
os-zhuang merged 5 commits into
mainfrom
claude/issue-15823-find-afterfind-array-guard

Conversation

@claude

@claude claude Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Fixes #15823

Implements the director ruling of 2026-09-06 (comment 5557017787), direction 1: find() guarantees the array, and an afterFind that replaces the container is refused loudly.

(Angle brackets do not survive this surface's sanitizer, so declared types are spelled with square brackets below: Promise[any[]] is the real Promise of any[].)

The defect, reproduced before anything was changed

packages/objectql/src/engine.ts declares async find(object, query?, options?): Promise[any[]] and, on the hook path, ended with return hookContext.result — nothing between the afterFind dispatch and that return re-checked the value. A handler assigning ctx.result = { records: [ ... ] } therefore made a read declared to resolve to an array resolve to an envelope: no throw, no diagnostic, no log.

Reproduced live on unmodified origin/main (bdc02182b) by running packages/plugins/plugin-auth/src/find-envelope-limb-removal.test.ts -t 'the control': 1 passed, its assertion being expect(Array.isArray(value)).toBe(false) across fourteen real read call sites driven through their real production entry points, over a real ObjectQL on a real SqlDriver.

What changed

1. The guard, at the seam the ruling names. In find(), immediately after await this.triggerHooks('afterFind', hookContext) and before maskSecretFields / stripSearchCompanionFromRead — both of which already assume the array and both of which read hookContext.result. Not at the return.

2. A named ADR-0112 error. FindHookResultNotArrayError (packages/objectql/src/find-hook-result-shape.ts), code FIND_HOOK_RESULT_NOT_ARRAY, status 500. One code, one wording; the message names the hook event and the object, and developerMessage carries the remedy. Registered in ERROR_CODE_LEDGER under @objectstack/objectql.

3. Shaping stays legal. Array.isArray is the whole predicate, deliberately: mutating rows in place, deleting keys, filtering rows out and assigning a different array are all untouched. Anything keyed on identity (comparing against opCtx.result, freezing, cloning) would refuse legitimate reshaping, which is what ADR-0077 line 71 means by "shape reads".

Two judgement calls, declared

  • undefined / null are refused too. The ruling names only the envelope case. A handler that assigns neither is not replacing the container with an envelope, but it breaks Promise[any[]] exactly as much, and admitting it leaves a second hole in the same slot that no caller can distinguish from the first. A read that should answer nothing assigns []; a handler that wants to refuse a read throws — the supported spelling every other hook guard already uses. Pinned either way, and argued in the module docblock.
  • 500, not 400 or 403. The request was well-formed and authorized; a hook this deployment installed broke a server-side contract, and there is nothing the caller can change or retry. The cost is stated rather than discovered later: a declared 5xx has its prose withheld at the HTTP doors (declaresServerFault), so an HTTP caller reads Internal server error plus this code. That is the right split — the registered code is the machine-readable half and crosses intact, while the message is addressed to the hook's author, who meets it in-process and in the server log.

Which published surfaces widened — measured, not assumed

The dispatch brief asked whether the code's home is a second published surface in packages/types/src/response-envelope.ts. It is not. That file imports ErrorCode as a type from @objectstack/spec/api and declares no union of its own; packages/types is untouched by this diff (git diff bdc02182b..HEAD -- packages/types is empty) and 0 of its published declarations carry the new member (live control: ErrorCode appears 9 times in types/dist/index.d.ts, so the grep is real).

The two surfaces that do widen, read off built declarations under each package's files[]:

package published declaration occurrences
@objectstack/objectql dist/index.d.ts, dist/index.d.mts 7 each
@objectstack/spec dist/api/index.d.ts 106
@objectstack/spec dist/export.zod-w7_kGqGb.d.ts (shared chunk) 13

Worth recording because it is a live instance of a known false-negative shape: packages/spec/dist/index.d.ts — the root barrel — carries 0 occurrences (live control on the same file: 23 export lines). Reading the root barrel alone would have answered "nothing widened" with confidence; the widening lives on the ./api subpath and in a shared chunk neither barrel names by symbol.

Both are additive — no export removed, no code renamed. content/docs/references/api/{contract,error-code-ledger}.mdx regenerated by gen:docs; the union's own count line moves +295 more to +296 more, which is the widening stated by the generator.

Pins — the ruling's three cases, plus the one it left open

packages/objectql/src/engine-find-hook-result-shape.test.ts, 10 cases:

  • (a) an afterFind assigning ctx.result = { records: [...] } is refused — asserting the ADR-0112 envelope (code and status, never a bare toThrow()), the class, the event, the object and the observed shape;
  • (a') the refusal fires before either array-assuming consumer walks the replaced container — driven with a poisoned length/0 getter pair and asserting the getters were read 0 times, rather than asserting about source order;
  • (b) mutate-in-place, reassign-a-different-array, and reassign-[] all still answer an array ([] being exactly what a truthiness-shaped guard would have refused by accident);
  • (c) the no-hook path unchanged;
  • (d) undefined, null and a string all refused with the same code, each naming its observed shape;
  • membership of the generated ErrorCode union, with a control proving the union rejects an unregistered spelling.

The one consequence in the tree, handled without skipping anything

Exactly one suite in packages/** registers an afterFind that assigns a non-array: the #15597 discrimination control in packages/plugins/plugin-auth/src/find-envelope-limb-removal.test.ts. Measured with the guard in: 1 failed, 24 passed — its expect(Array.isArray(value)).toBe(false) is precisely what the ruling retires.

Nothing was skipped, disabled or quarantined. That case is rewritten to assert what is now true, keeping both of its jobs: it drives the same handler across all fourteen real reads and asserts every one refuses with FIND_HOOK_RESULT_NOT_ARRAY (a stronger statement of the same fact), and a second case exercises expectBareArray's discrimination directly, since no engine can hand it an envelope any more. That suite is now 26 passed. #15597's own conclusion is untouched here and is not reopened — its argument was that the fourteen limbs were right to remove even given an open seam.

The two other real afterFind registrations both shape rows in place and keep the array (packages/rest/src/export-integration.test.ts, the FLS-delete and partial-masking hooks); plugin-audit's read-audit.ts never touches ctx.result. None of them moves. The remaining ctx.result = sites in the tree belong to hand-built fake engines that construct their own context and never reach this seam.

Ablation — the pins were seen to fail

Guard deleted with a whole-line anchor (ANCHOR_HITS=1), mutation proved on disk before measuring (git hash-object 630e3c67 to 4af01f1b; removed-text count 0, injected-marker count 1), then @objectstack/objectql rebuilt and ablation-dist-preflight --absent confirming the marker gone from all 8 built files — which matters because plugin-auth resolves @objectstack/objectql through dist/, unaliased.

  • engine-find-hook-result-shape.test.ts: 5 failed / 5 passed — the five refusal cases go red; the shaping, no-hook and ledger cases correctly do not depend on the guard.
  • find-envelope-limb-removal.test.ts: 1 failed / 25 passed — the rewritten control goes red.

(Ablation figures are from the pre-merge tree; the merge changed no line of the guard, and both suites were re-run green on the merge head.) Restore leg proved in the same shell: whole-tree git status --porcelain 0 lines, blob back to the HEAD blob 630e3c67, git diff HEAD empty; then rebuilt and ablation-dist-preflight (present form) confirming the marker back in 4 built files and the tree clean. The mutating script carried an EXIT INT TERM trap restoring through an absolute REPO_ROOT path.

Verification

Derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack from the real change set (no hand-written path list): 106 families, re-derived three times — twice before the merge and once after — and byte-identical every time.

main moved onto three of this branch's files while this work was in flight (engine.ts, objectql/src/index.ts and the census page), so origin/main was merged in through scripts/pm/os-regen-merge.sh — the repo's own sequence, which commits the merge before regenerating so an os-regen-driven artifact is re-derived from the merged tree rather than text-merged (that driver merges at exit 0 while silently keeping one side). The pre-commit hook held the merge for exactly one stale artifact, content/docs/permissions/system-context.mdx, regenerated with pnpm gen:system-context-census and discharged in the same commit. GitHub now reports mergeable: true.

Everything below was re-run on the merge head 5c8f04199, not inherited from the pre-merge run:

  • 106 / 106 green at 5c8f04199, working tree clean, exit codes captured by redirect-then-read (never through a pipe).
  • pnpm lint (repo-wide eslint . --no-inline-config): exit 0, before and after the merge. Not narrowed.
  • Full workspace build turbo run build --filter=./packages/* --filter=./packages/*/*: 71 successful, 71 total, re-run after the merge.
  • pnpm --filter @objectstack/spec check:generated on the merged tree: 15 / 15 artifacts current; and this branch's own ledger row, engine guard, module, index export, changeset and both generated doc entries verified present by quoted-exact git grep after the merge — the step-4 assertion the merge script prints.
  • Both pin suites re-run on the merge head: 10 passed and 26 passed.
  • The guard is still at the ruled seam after the merge: dispatch :9566, refusal :9581, maskSecretFields :9591.
  • pnpm --filter @objectstack/objectql typecheck: green, and both new files proven inside the tsconfig.test.json program by --listFiles (1 hit each; control on a non-existent path 0).
  • Two gates went red on the way and were repaired, both this branch's own: check:system-context-census (the guard's 21 lines shifted fourteen line-number doc anchors — measured green at the merge base first, so the rot is mine, then the script's own --fix; line numbers only) and check:test-source-alias (the new pin's await import moved to a module-top import so the first load is paid during collection, not inside a clocked test body).
  • One NOT MEASURED was chased to a real reading rather than banked: check:type-check-debt first exited 3 (V8 OOM under a 4 GB wrapper heap — the gate's own text says exit 3 is not a finding). Re-run at 10 GB: exit 0, 12 ledger entries re-measured, none above its recorded number.

Clause-② and release

Clause-②: yes, ruled — the accepted behaviour of the published hook contract narrows, and @objectstack/spec's ErrorCode accept-set widens. needs:contract-review is hung by me, the PR creator, on both carriers — issue #15823 and this PR — additively, each with a comparative read-back proving union(read, target) exactly and nothing stripped.

Recorded because it is a live instance of a known incident: after the merge push the label was gone from both carriers — the auto-labeler's whole-set write had landed documentation / size/l / tests / tooling over it. It was re-hung additively (the labeler's four kept), re-read after a delay, and both carriers hold it now. A contract-review clear is bound to a head, so the re-hang was owed by the push regardless. node scripts/pm/check-clause2-carriers.mjs --pair 16232 returns exit 0 on the current head — the checker proven current against origin/main by blob hash first, and the exit code captured before any pipe.

⛔ 免复核不放行. This PR is left draft and is not released, queued, armed or approved by this seat.

Changeset: @objectstack/objectql minor and @objectstack/spec minor, stating the refusal and its code.

Scope fence

findOne (:9692), update (:10858) and delete (:13075) also end on return hookContext.result, and all three declare Promise[any] — no enforceable declaration to violate. The ruling files that as its own finding for the engine seat and it is not ridden in here — filed instead as #16231, unassigned, labelled finding + domain:engine. The #15094 normalizer-limb sweep is likewise not touched — the ruling's item 3 is a consequence recorded for later.

Re-derived readings from the dispatch brief, published as measured on bdc02182b: return hookContext.result still has four sites, not three — find :9425 (Promise[any[]]), findOne :9671 (Promise[any]), update :10837 (Promise[any]), delete :13054 (Promise[any]) — so find remains the only one with a concrete declared shape. No fifth site has appeared. The brief's anchors held exactly: triggerHooks('afterFind', ...) at :9552, maskSecretFields at :9557, stripSearchCompanionFromRead at :9563.

Authored by Claude Code in session session_01ARYe3yQTQCUFm5qPYNgKaJ. (Attribution is kept in prose here: a body edit does not preserve the session-URL footer, and the platform appends its own block.)


Generated by Claude Code

…ainer

`ObjectQL.find` declares `Promise<any[]>` but ended its hook path with
`return hookContext.result`, with nothing between the `afterFind` dispatch
and that return re-checking the value. A handler assigning
`ctx.result = { records: [ ... ] }` therefore made a `find()` declared to
resolve to an array resolve to an envelope, silently.

Ruled 2026-09-06 (direction 1): `find()` guarantees the array, and a hook
that replaces the container is refused loudly. The check sits immediately
after `triggerHooks('afterFind', ...)` and BEFORE `maskSecretFields` /
`stripSearchCompanionFromRead`, both of which already assume the array.

Shaping stays legal: mutating rows in place, dropping keys, filtering rows
out and assigning a different ARRAY are all untouched. `Array.isArray` is
the whole predicate.

Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
Co-authored-by: Claude <noreply@anthropic.com>
…e envelope

#15823 closes the seam this control drove: an `afterFind` assigning a
non-array no longer produces an envelope at the fourteen real reads, it
produces `FIND_HOOK_RESULT_NOT_ARRAY`. The control keeps both of its jobs
— the mechanism is driven on every block and the refusal asserted, and
`expectBareArray`'s discrimination is now checked directly, since no engine
can hand it an envelope any more.

#15597's own conclusion is untouched: the fourteen limbs were removed on
the argument that they were right to remove even given an open seam.

Also fixes the refusal message's article (`a object` -> `an object`) and
pins it.

Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
Co-authored-by: Claude <noreply@anthropic.com>
…odule top

Two gate repairs the guard's diff owes:

- `check:system-context-census` reads DOC anchors keyed by LINE NUMBER, and
  the 21 lines the guard adds (plus the import) shifted fourteen of them in
  `content/docs/permissions/system-context.mdx`. Repaired with the script's
  own `--fix`; line numbers only, nothing semantic. Measured green at the
  merge base first, so the rot is this branch's.
- `check:test-source-alias` refuses a first module load paid inside a clocked
  test body: the new pin's `await import('@objectstack/spec/api')` moves to a
  module-top import, so the transform is paid during collection.

Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
Co-authored-by: Claude <noreply@anthropic.com>
`main` moved onto three of this branch's files (engine.ts, objectql/index.ts
and the system-context census page). Landed through
`scripts/pm/os-regen-merge.sh` so the os-regen-driven artifact is regenerated
from the MERGED tree rather than text-merged: the driver merges those paths at
exit 0 while silently keeping one side, and only a regeneration exposes it.

The one artifact the pre-commit hook held the merge for —
`content/docs/permissions/system-context.mdx` — is regenerated here with
`pnpm gen:system-context-census` (15 anchors re-derived against the merged
engine.ts; line numbers only).

Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the size/l label Sep 6, 2026
@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/objectql, @objectstack/spec, touching 11 documentable anchor(s).

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

  • content/docs/api/client-sdk.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))
  • content/docs/api/data-flow.mdx (via afterFind (literal, a string literal in find; a string literal on a changed line))
  • content/docs/api/error-catalog.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))
  • content/docs/api/error-handling-server.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object), ctx.result (literal, a string literal in buildMessage))
  • content/docs/automation/hook-bodies.mdx (via ctx.result (literal, a string literal in buildMessage))
  • content/docs/automation/hooks.mdx (via afterFind (literal, a string literal in find; a string literal on a changed line), ctx.result (literal, a string literal in buildMessage))
  • content/docs/deployment/troubleshooting.mdx (via developerMessage (symbol, a field of class FindHookResultNotArrayError))
  • content/docs/getting-started/quick-reference.mdx (via hook.zod.ts (literal, a string literal in constructor))
  • content/docs/kernel/contracts/data-engine.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))
  • content/docs/kernel/events.mdx (via afterFind (literal, a string literal in find; a string literal on a changed line), ctx.result (literal, a string literal in buildMessage))
  • content/docs/kernel/runtime-services/audit-service.mdx (via afterFind (literal, a string literal in find; a string literal on a changed line))
  • content/docs/permissions/record-view-auditing.mdx (via afterFind (literal, a string literal in find; a string literal on a changed line))
  • content/docs/protocol/objectql/schema.mdx (via afterFind (literal, a string literal in find; a string literal on a changed line))
  • content/docs/protocol/objectql/types.mdx (via developerMessage (symbol, a field of class FindHookResultNotArrayError))

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

  • content/docs/releases/v16.mdx (via afterFind (literal, a string literal in find; a string literal on a changed line))
  • content/docs/releases/v17.mdx (via ERROR_CODE_LEDGER (symbol, a top-level const object))

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
  • 4 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 131 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 0c3eaf578ec9e2786318f9cf868be06e14c4b6a0packageMentionDocs.

Which tree this was computed on

This run read content/docs from a0c6ffad64fe306a29e4c3b2149cdd81e599a9de — the merge of head 5c8f04199e90162fe3a44539a259bf3e872dd881 into base 0c3eaf578ec9e2786318f9cf868be06e14c4b6a0, 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 a0c6ffad64fe306a29e4c3b2149cdd81e599a9de && git checkout a0c6ffad64fe306a29e4c3b2149cdd81e599a9de
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0c3eaf578ec9e2786318f9cf868be06e14c4b6a0 5c8f04199e90162fe3a44539a259bf3e872dd881 && git checkout -B drift-repro 0c3eaf578ec9e2786318f9cf868be06e14c4b6a0 && git merge --no-ff 5c8f04199e90162fe3a44539a259bf3e872dd881

node scripts/docs-audit/affected-docs.mjs --json 0c3eaf578ec9e2786318f9cf868be06e14c4b6a0

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

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Sep 6, 2026

Copy link
Copy Markdown
Contributor

Contract review (clause ②) — PASS on content · landable on green CI — PR #16232 at head 5c8f0419 (Fixes #15823 · priority:p2)

Reviewed by the director seat at tier (claude-fable-5-1, session session_01TezFG8ZMrNH6n5VTNpPpdH), 2026-09-06 07:04Z, on the domain:engine seat's hand-off; this PR implements the director ruling of 2026-09-06 (5557017787, direction 1), so the review checks conformance to that ruling rather than re-opening it.

Clause ② answer: yes, on both limbs, exactly as declared.

  • Limb 1: @objectstack/objectql exports FindHookResultNotArrayError, FIND_HOOK_RESULT_NOT_ARRAY_CODE, FIND_HOOK_RESULT_NOT_ARRAY_STATUS, describeFindHookResult (root barrel, 7 occurrences each in dist/index.d.ts / .d.mts); @objectstack/spec's ERROR_CODE_LEDGER gains FIND_HOOK_RESULT_NOT_ARRAY under @objectstack/objectql, so the generated ErrorCode union and ApiErrorSchema.code widen by one member — on the ./api subpath and a shared chunk, not the root barrel, which the round measured rather than assumed (the known root-barrel false negative). Both additive.
  • Limb 2: the published hook contract narrows — an afterFind that leaves ctx.result a non-array is refused with a registered 500 — which is the ruling's direction 1 made enforceable. Array.isArray is the whole predicate, so every legal shaping (mutate in place, filter, reassign a different array, []) is pinned as still accepted.

Content against the ruling: guard placed immediately after the afterFind dispatch and before maskSecretFields / stripSearchCompanionFromRead, the two consumers that already assume the array — pinned with a poisoned-getter case asserting 0 reads, not with source order. The two declared judgement calls are consistent with direction 1 and are pinned either way: undefined / null refused (a read that answers nothing assigns []; a hook that refuses throws), and 500 because the request was valid and a server-side extension broke a server-side contract — with the cost stated that declaresServerFault withholds the prose at the HTTP doors while the registered code crosses intact. The message does not begin with a SQL verb (the sanitizeRowError constraint its neighbours record). The one in-tree consequence — the #15597 discrimination control that asserted the envelope leaked — is rewritten to assert the refusal across the same fourteen real reads, nothing skipped or quarantined. findOne / update / delete (all Promise<any>) are fenced out to #16231, as the ruling's item filed.

Changesets: @objectstack/objectql: minor, @objectstack/spec: minor — correct for two additive surface moves; no ADR-0087 marker owed. Generated docs (contract.mdx +295 → +296, error-code-ledger.mdx, system-context.mdx anchors via the census --fix) are tool output. Governed-merge audit on the 10 paths: 0 hits. --pair 16232: the card's claim (5557072352) is in the fixed spelling.

CI at 5c8f0419: 13 success · 2 skipped · 16 still running. needs:contract-review comes off PR + card #15823 now. On green CI the next director pass re-runs --pair 16232 and flips ready-for-review + auto-merge (squash); a moved head is re-hung and re-read.


Generated by Claude Code

Copy link
Copy Markdown
Contributor

Director seat, 07:50Z — needs:contract-review was re-applied to this PR at 07:24Z (no comment accompanied it); the head is unchanged at 5c8f0419, so the verdict above (5557630549) still covers exactly what would land. CI on that head is now complete: 37 success · 7 skipped · 0 failing. Re-clearing the label against the same verdict and landing per that verdict: --pair 16232 ⇒ ready-for-review + auto-merge (squash). If the re-hang carried a finding, post it here and the landing is withdrawn.


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review September 6, 2026 07:53
@os-zhuang
os-zhuang enabled auto-merge September 6, 2026 07:53
@os-zhuang
os-zhuang added this pull request to the merge queue Sep 6, 2026
Merged via the queue into main with commit 7778115 Sep 6, 2026
49 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-15823-find-afterfind-array-guard branch September 6, 2026 08:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants