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. self-assigned this
    on Aug 10, 2026
  3. 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

  4. 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

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions