Skip to content

fix(hooks): filter: is a live alias of where, not a silent drop — measured per method - #1527

Merged
hotlong merged 1 commit into
mainfrom
claude/issue-1229-filter-vs-where-remeasure
Sep 3, 2026
Merged

fix(hooks): filter: is a live alias of where, not a silent drop — measured per method#1527
hotlong merged 1 commit into
mainfrom
claude/issue-1229-filter-vs-where-remeasure

Conversation

@os-sales

@os-sales os-sales commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

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.

⚠️ Deliberately draft, do not enable auto-merge. AGENTS.md is a governed surface (2026-08-18 cross-repo ruling) ⇒ human merge. See "Landing path" below.

The measurement

Measured on the pinned @objectstack packages (17.2.0), on the object the kernel actually injects as ctx.api — a real ScopedContext over 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.

method filter: behaviour
find aliased to where — predicate applied (n=1, the row asked for)
findOne aliased to where — predicate applied (the row asked for, not row 1)
count aliased to where — predicate applied (1, not 3)

The three methods agree. update and delete fold 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, and findOne with 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:

  • both keys present — same value is accepted; different values throw (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 a filter: 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:

  1. Unknown-key control. An unrecognised key (wibble:) must throw on find, findOne, count and delete — that is what a key this engine does not know does. Without it, "filter returned 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.
  2. No-match probes. A dropped predicate and an applied one are indistinguishable when the probe matches — both hand back a right-looking row. They separate only on a predicate matching nothing: applied gives the empty answer, dropped gives the unfiltered one. Both stale alternatives are now asserted against by name (not.toBe(first.id), toBe(0), toEqual([])).
  3. Apparatus control. An unfiltered read returns 3 and a scoped read returns 1, so an engine that ignored predicates entirely could not read as a pass.

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 HEAD blob afterwards. Details in the report on #1229.

What the prose said, and why it mattered

src/objects/_hook-api.ts and AGENTS.md §2 both said filter "fails silently" — findOne returning the object's first row, count counting the whole object. The sharpest form was inside one file: test/hook-query-predicate.test.ts both 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 filter matches 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 every filter call 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

  • The where-only convention stays.
  • The repo-wide guard stays, unchanged in effect — no hook-side code may query by 'filter', scanning every .ts under src/objects/. Only its failure message changes, because the message asserted the stale reason.
  • HookQuery still omits filter, so the mistake stays a compile error.
  • The harness still throws on 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:

Write where for one idiom, not for silent data loss. filter is a live alias of where on find, findOne, count, update and delete; nothing is dropped and unknown keys throw. The reason to keep one spelling is that mixing them breaks: a query assembled in two places that carries both keys with different values throws at runtime, and an empty where: {} counts as a different value.

Landing path

⚠️ AGENTS.md is a governed surface ⇒ draft, human merge, no auto-merge.

The AGENTS.md hunk 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 verify green 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

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
@vercel

vercel Bot commented Sep 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
hotcrm Ignored Ignored Sep 3, 2026 8:29am UTC

Request Review

hotlong commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

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 AGENTS.md. All 9 check runs on the head completed with conclusion success.


Generated by Claude Code

@hotlong
hotlong marked this pull request as ready for review September 3, 2026 15:05
@hotlong
hotlong added this pull request to the merge queue Sep 3, 2026
Merged via the queue into main with commit 80dd40c Sep 3, 2026
10 checks passed

hotlong commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd CI plumbing and the verification pipeline documentation Improvements or additions to documentation metadata Declarative metadata — schema, security posture, UI surfaces

Projects

None yet

Development

Successfully merging this pull request may close these issues.

filter vs where on ctx.api: a live test says v17 aliases it, the hook-api type and AGENTS.md still say it fails silently — three sources, two answers

3 participants