Skip to content

objectql having has no $icontains comparand-shape gate — an empty comparand matches EVERY row (2 of 5 FILTER_TEXT_CASES rejection rows unenrollable) #7158

Closed

Description

@os-zhuang

Found while closing #7047 (the ADR-0112 envelope on having's refusals), by driving the having face against FILTER_TEXT_CASES for the first time. Out of #7047's fence — that card is about the ENVELOPE on refusals that already happen; this is a refusal that does not happen at all.

Measured (2026-08-10, origin/main @ 3e8e669, by execution)

FILTER_TEXT_CASES declares five expectRejection rows. Driven against matchesHaving / applyHaving over FILTER_TEXT_ROWS:

row having
$regex is REFUSED refused (enveloped by #7047)
$regex + $options refused as one mistake refused (enveloped by #7047)
a dangling $options is REFUSED refused (enveloped by #7047)
an empty $icontains comparand is REFUSED NOT refused — evaluated, matched ALL NINE rows
a non-string $icontains comparand is REFUSED NOT refused — evaluated, matched none
matchesHaving({ name: 'ACME Corp' }, { name: { $icontains: '' } })  // => true
applyHaving(FILTER_TEXT_ROWS, { name: { $icontains: '' } })         // => all 9 rows
matchesHaving({ name: 'ACME Corp' }, { name: { $icontains: 42 } })  // => false

Consequence

The empty-comparand row is the sharp one. Every string contains the empty substring, so the predicate constrains NOTHING — a filter that compiles to nothing does not narrow a query, it widens it (#3948). The author wrote a constraint and got the unfiltered aggregate back, with no error.

The non-string row is the quieter half: 42 is answered false — "no rows" — for a comparand the declared type (StringOperatorSchema: $icontains: z.string()) does not permit, which is the silent-wrong-answer shape #4706 retired $regex over.

What the other faces do

The driver faces get this from a comparand gate having has never had:

  • driver-memory — filter-refusal.ts icontainsComparandError(), both rejections in one constructor, INVALID_FILTER / 400.
  • driver-sql — the twin icontainsComparandError, word for word.

having is the sixth JS evaluation face of the same vocabulary (#6520 gave it the shared ASCII fold) and the only one with no comparand gate.

Why this is not #7047

Closing this means REFUSING filters this face evaluates today — a behaviour change beyond an envelope card, with its own changeset and its own client-visible effect. #7047 kept the same fence #6993 kept before it.

Current state of the coverage

packages/objectql/src/having-filter-text-conformance.test.ts (added by #7047) enrolls the 13 rows cases and the 3 retired-operator rejection rows, and pins these two exclusions as measurements — asserting that the face does not refuse them, and that the empty comparand matches all nine rows. So the gap is red-on-change rather than a silent hole: adding the gate turns those pins red and forces the two rows into the enrolment.

Update that file's UNENROLLED_REJECTION_CASES block when closing this.

Activity

  1. os-zhuang commented on Aug 10, 2026

    @os-zhuang
    ContributorAuthor

    Triage: pm:queue (domain:engine-core stays — packages/objectql/src/having-filter.ts).

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 10, 2026
  3. self-assigned this
    on Aug 10, 2026
  4. os-help commented on Aug 10, 2026

    @os-help
    Collaborator

    Claim: PM loop round 3 (spec-surface seat, cross-lane under maintainer directive)
    Session: session_016R9de1FqP7NvwKvqXi92Gh
    Branch: claude/issue-7158-having-icontains-gate
    Worktree: objectstack-issue-7158
    Domain: domain:engine-core
    File surface: packages/objectql/src/having-filter.ts, packages/objectql/src/having-filter-text-conformance.test.ts (the UNENROLLED_REJECTION_CASES block), .changeset/ (stop on breach; explain in the report)
    Container & model: M card (behavior change — new refusals), mode:subagent, model: opus
    Serial constraints cleared: #7047's PR #7161 merged ~01:0xZ touching this exact file family (having's refusal envelope) — cleared as merged, carried as same-day churn into the dispatch; the fix builds on the post-#7161 tree. My in-flight #7073 is packages/formula — different package, disjoint. No in-flight claim on packages/objectql.

    Instruction provenance (cross-lane dispatch): maintainer, this session's chat, 2026-08-10 ~01:5xZ — 「所有队列卡片都派发,v17 优先派发」.


    Generated by Claude Code

  5. os-help commented on Aug 10, 2026

    @os-help
    Collaborator

    ACCEPT — PR #7236 (review of record, session session_016R9de1FqP7NvwKvqXi92Gh).

    What shipped: having gains the twin icontainsComparandError gate — both rejections in one constructor, message verbatim from the driver twins after the position clause, riding #7161's invalidFilterError helper so this face keeps ONE envelope for one mistake (the #7035 class explicitly avoided, as dispatched). 3 files, +225/−98, changeset patch stating the client-visible effect plainly.

    Verified against GitHub: draft targeting main, first line Fixes #7158; CI converged at ce0b476 — 25 checks, zero failures; ESLint success (03:30:14Z) and TypeScript Type Check success (03:37:22Z) personally confirmed.

    Review credits:

    1. Both directions as counts: 13/13 evaluation rows verbatim green (accepted surface does not shrink), 5/5 rejection rows enrolled (was 3/5), suite delta exactly +2 — no test lost in the restructure. Rejection assertions are code + status + mustMention through one shared helper, never bare toThrow().
    2. The measurement pins were flipped, not deleted — the same four expressions that pinned the exclusion now assert the refusal. Reverse verification: 5 red, exclusively on the comparand rows, evaluation/retired-operator/guard all green — load-bearing for exactly what it claims.
    3. Two placement judgments argued, not just made: the gate sits above the no-value early exit (a comparand's shape is a property of the filter, not the row — one filter must not get two verdicts decided by data), and the dead typeof limb was hoisted with the column-value guard deliberately kept (row judgment vs filter judgment).
    4. Honest process disclosure: git checkout origin/main -- <file> during reverse verification discarded uncommitted source edits; re-applied and committed before any further measurement, all reported numbers from the restored tree. Lesson recorded — commit before reverse verification. Worth folding into the os-dev definition alongside the stash rule (same trap class, different tool).

    Next: ready → merge queue.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

pm:dispatchedDispatched to a dev agent by /pm-dispatch

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions