fix(hooks): filter: is a live alias of where, not a silent drop — measured per method - #1527
Conversation
Three sources carried two incompatible answers about what `ctx.api` does with a `filter:` predicate key, and the wrong one is agent-facing instruction whose two error directions have opposite safety consequences: "silently dropped" implies an unscoped read, "aliased" implies a correct one. Measured per method against the pinned @objectstack packages (17.2.0), on a real ScopedContext over a real ObjectQL engine: `filter` is aliased to `where` on find, findOne and count, and on the update/delete options bag. Nothing is dropped — the engine throws on any option it does not recognise, so `filters` (plural) and every misspelling fail loudly, and findOne with no predicate at all throws rather than returning an arbitrary row. The measurement lands as executing assertions with negative controls (an unrecognised key must throw; no-match probes separate "applied" from "dropped"; an apparatus control proves the engine discriminates at all), and the prose in test/hook-query-predicate.test.ts, src/objects/_hook-api.ts and AGENTS.md is corrected to match. The `where`-only convention and its repo-wide guard are unchanged. Only the reason changes: one idiom, because a query carrying both keys with different values throws at runtime — not silent data loss. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019hUuCQStzXGMFSX4dzww5t
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
|
Maintainer ruling, 2026-09-03, given in chat to the director seat (session_01WXyGTWPbbreqXow7Z2pZCk) on decision batch 1, item 4 (this PR, option A: merge whole, AGENTS.md hunk included). Verbatim: 「以上都同意,你帮我合并pr」. Executing on that instruction: marking ready and merging directly (squash) from the maintainer's own account. No approving review is submitted by any seat; the human merge is the review record for the governed path Generated by Claude Code |
|
Path correction: the direct merge was refused by a repository rule ("Changes must be made through the merge queue"), so on the same maintainer instruction the director seat armed auto-merge (squash) from the maintainer's account instead. That is the maintainer's "merge when ready" click, executed on their word; the human decision stays the review record. No approving review is submitted by any seat. Generated by Claude Code |
Fixes #1229
Three sources, two incompatible answers, and the wrong one is agent-facing instruction. This measures the pinned engine per method, then corrects all three so the tree stops carrying two answers.
AGENTS.mdis a governed surface (2026-08-18 cross-repo ruling) ⇒ human merge. See "Landing path" below.The measurement
Measured on the pinned
@objectstackpackages (17.2.0), on the object the kernel actually injects asctx.api— a realScopedContextover a real ObjectQL engine on the in-memory driver, not the test harness. Three distinguishable rows, every probe targeting the third, so "predicate applied", "degrades to the first row" and "matches everything" cannot be confused with one another.filter:behaviourfindwhere— predicate applied (n=1, the row asked for)findOnewhere— predicate applied (the row asked for, not row 1)countwhere— predicate applied (1, not 3)The three methods agree.
updateanddeletefold the same key on their options bag, so the write paths agree too.Never dropped, and never silently anything: the engine rejects any option it does not recognise, so
filters(plural) and every misspelling throw, andfindOnewith no predicate at all throws rather than returning an arbitrary row. On this version a bad predicate key cannot produce an unscoped read.Two further answers nothing in the tree stated:
Conflicting options … 'where', 'filter' are spellings of the same parameter (canonical 'where')). There is no precedence: the engine refuses to choose.where: {}counts as a different value, not as "no opinion" — so an empty base predicate plus afilter:override is a runtime throw, not a merge.Negative controls
A green that also appears when the apparatus is broken proves nothing, so three controls are asserted alongside, not assumed:
wibble:) must throw onfind,findOne,countanddelete— that is what a key this engine does not know does. Without it, "filterreturned the right row" is not evidence of aliasing. If it ever passes silently, the engine has lost its unknown-option guard and every alias green above stops meaning anything.not.toBe(first.id),toBe(0),toEqual([])).Red leg
The assertions were verified to fail, not merely asserted to pass. Rewriting three expectations to the stale reading (findOne degrades to the first row · count counts the whole object · an unknown key is ignored) turns 5 of 71 red; the mutation was confirmed on disk by marker count and blob hash before the run, and the restore confirmed byte-identical to the
HEADblob afterwards. Details in the report on #1229.What the prose said, and why it mattered
src/objects/_hook-api.tsandAGENTS.md§2 both saidfilter"fails silently" —findOnereturning the object's first row,countcounting the whole object. The sharpest form was inside one file:test/hook-query-predicate.test.tsboth asserted the alias works and told authors, in its guard's failure message, that "the kernel drops it and reads the wrong record".That is not a doc nit, because the two errors point in opposite directions. "Silently dropped" means a hook querying by
filtermatches every row — an unscoped read. "Aliased" means it matches correctly. The stale belief is the one that errs unsafely, and it makes an author mis-judge the blast radius of everyfiltercall site.The history is kept and dated rather than deleted: the repo really did pay for a silent drop once — seventeen hook calls whose predicate vanished — on a kernel that is not the one pinned here. The test file's header now says explicitly which paragraphs are history and which are measurements of the currently pinned engine.
What did NOT change
where-only convention stays.no hook-side code may query by 'filter', scanning every.tsundersrc/objects/. Only its failure message changes, because the message asserted the stale reason.HookQuerystill omitsfilter, so the mistake stays a compile error.filter. It is now stricter than the kernel, which is the safe direction; the block is renamed and documented so that is legible as deliberate rather than as drift.The corrected reason, in quotable form:
Landing path
AGENTS.mdis a governed surface ⇒ draft, human merge, no auto-merge.The
AGENTS.mdhunk is cleanly separable — it is one self-contained paragraph in §2, touching no other file, and the code/test/changeset half stands on its own without it. It is kept together here because the card's whole point is that the tree stopped carrying two answers, and landing the code half alone re-creates exactly that state — three sources, two answers — for as long as the doc PR sits. Splitting is the reviewer's call; the report on #1229 says so and defers.Gates
pnpm verifygreen on this commit (7e811f8c): validate · typecheck · lint · lint:i18n-gate · hygiene · hygiene:tokens · build · test. 158 files / 3325 passed, 1 skipped, 0 failed. The token ratchet is comment-stripped, so the expanded docblock costs 0 against the ceilings (business semantics ~83,156 / ~85,000).🤖 Generated with Claude Code
https://claude.ai/code/session_019hUuCQStzXGMFSX4dzww5t
Generated by Claude Code