Skip to content

[finding] $icontains is absent from analytics TEXT_PATTERN_OPERATORS, so the #5234 comparand fence never covered it on the where door — one operator, two answers inside one package #7693

Closed

Description

@os-help

Measured while implementing #7598. Filing unassigned — recording, not claiming.

The fact

comparand-shape.ts's TEXT_PATTERN_OPERATORS lists four operators:

$contains, $notContains, $startsWith, $endsWith

$icontains is not among them. It is in filter-normalizer.ts's
MONGO_TO_CUBE_OP (added by #6520), and read-scope-sql.ts's $icontains arm
does call assertRenderableText. So the analytics where door is the one face
that applies no comparand-shape gate at all to $icontains.

Measured on origin/main @ 5823d593d:

filter analytics where door read-scope-sql
{name: {$contains: {foo: 1}}} REFUSED (INVALID_FILTER / 400) REFUSED (READ_SCOPE_COMPILE_FAILED / 500)
{name: {$icontains: {foo: 1}}} compiles — leaf {operator: 'icontains', values: [{foo: 1}]} REFUSED

The leaf then reaches NativeSQLStrategy.buildFilterClause, whose
likeShape.icontains is 'contains', so likePattern renders it through
String(value) and the statement runs LIKE '%[object Object]%' — the exact
pattern #5234 exists to prevent, at the one operator that arrived after it.

Why it matters

This is #5234's defect, verbatim, at an operator the fence was never extended
to: a parameterised, syntactically perfect LIKE pattern nobody wrote. It is
also a split inside one package — the sibling door refuses the same
comparand — which is the shape #5234 / #5332 / #5567 each spent a round
removing.

Adjacent, already closed at a different consumer: #7158 (objectql having had
no $icontains comparand-shape gate either). Same class, third face.

Scope note

Left untouched by #7598's PR deliberately: that card is about a { $field }
comparand and fixing this would be a second defect riding it. The cell is pinned
as a RECORDED GAP in
packages/services/service-analytics/src/__tests__/cross-field-reference-refusal.test.ts
so it shows in test output rather than being re-discovered; that pin is what
should flip when this is fixed.

The likely fix is one entry in TEXT_PATTERN_OPERATORS plus the pins, but it
wants its own measurement of what else reads that set before it is called a
one-liner.

Refs


Generated by Claude Code

Activity

  1. added
    domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 11, 2026
  2. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    Contributor

    Findings triage (first grade): PROMOTED — finding → pm:queue (labels switched in the same pass as this comment).

    This is a restore-invariant card, the auto-queue class: the repo already states the invariant (#5234 — text-pattern comparands are fenced before they can render LIKE '%[object Object]%'; #5332/#5567 spent rounds removing exactly this one-package-two-answers split), and this card records the invariant broken at the one operator that arrived after the fence (#6520). No decision is being asked — the fence's own family answers it.

    Premise re-verified this round on origin/main @ a3c0865 (card measured @ 5823d593d): packages/services/service-analytics/src/comparand-shape.ts:193-195 still lists exactly four operators, $icontains absent; the RECORDED GAP pin sits at cross-field-reference-refusal.test.ts:340 and is the pin that flips when fixed. Scope note from the card stands: measure what else reads TEXT_PATTERN_OPERATORS before calling it a one-liner.

    Dedup: #7158 (same gap, objectql having face) is closed; #7598's PR deliberately excluded this; no open card or PR covers the analytics where face.

    Not target:v17: triggers only on a malformed (object) comparand — robustness against a wrong input, not a today-hit on well-formed usage.

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


    Generated by Claude Code

  3. added
    pm:queueReady for the PM dispatch loop
    and removed
    findingRecorded observation — held, not dispatchable until the findings triage round grades it
    on Aug 11, 2026
  4. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 11, 2026
  5. self-assigned this
    on Aug 11, 2026
  6. huangyiirene commented on Aug 11, 2026

    @huangyiirene
    Collaborator

    Claim: PM loop round 2
    Session: session_01JVfWSQN9RDDGwdVSmMc29x
    Branch: claude/issue-7693-icontains-comparand-fence
    Worktree: cloud session's own clone (mode:cloud)
    Domain: domain:services
    File surface: packages/services/service-analytics/src/** — comparand-shape.ts (:193 TEXT_PATTERN_OPERATORS), the RECORDED GAP pin in __tests__/cross-field-reference-refusal.test.ts (:340-ish), plus whatever the required consumer measurement implicates (filter-normalizer.ts :358/:577 are the known readers).
    Container & model: M borderline-S — one-entry fix but the card itself demands a consumer measurement before calling it a one-liner, and it changes accept/reject behavior of the analytics where door (within-package); upgraded per 上抬不下压. mode:cloud, model: opus
    Serial constraints cleared: ⚠️ same-package predecessor already merged: #7694 (79c3145, the #7598 $field refusal) touched the same pin test file — triage re-verified this card's premise post-#7694 on a3c0865 (:193-195 four operators, $icontains absent; pin at :340). Dev must work from origin/main, not the card's line numbers. No in-flight PR touches service-analytics.


    Generated by Claude Code

  7. huangyiirene commented on Aug 11, 2026

    @huangyiirene
    Collaborator
    {
      "issue": 7693,
      "repo": "objectstack-ai/objectstack",
      "branch": "claude/issue-7693-icontains-comparand-fence",
      "head_sha": "b6ea956",
      "base": "origin/main @ b54aaab",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/7843",
      "pr_state": "draft",
      "status": "delivered",
      "premise_still_valid": true,
      "premise_evidence": {
        "four_operator_list": "packages/services/service-analytics/src/comparand-shape.ts:193 listed exactly `$contains, $notContains, $startsWith, $endsWith` on b54aaab — `$icontains` absent, unchanged post-#7694 (79c3145) and post-a3c0865",
        "recorded_gap_pin": "cross-field-reference-refusal.test.ts:340 `[#7598] RECORDED GAP` block present on today's tip; asserted the `where` door RETURNED the leaf",
        "reproduction": "where door: normalizeAnalyticsFilterTree({where:{name:{$icontains:{foo:1}}}}) => {kind:'leaf',member:'name',operator:'icontains',values:[{foo:1}]}, no throw. NativeSQLStrategy.generateSql on the same filter => `... LIKE translate($1,'ABC…','abc…') ESCAPE $2`, params [\"%[object Object]%\",\"\\\\\"]. read-scope-sql => READ_SCOPE_COMPILE_FAILED / 500. `$contains` same shape => INVALID_FILTER / 400 on both doors.",
        "sibling_face": "driver-sql's module-private TEXT_PATTERN_OPERATORS (sql-driver.ts:1201) already lists `$icontains` — analytics was the outlier, not the drivers"
      },
      "consumer_measurement_required_by_card": {
        "method": "quoted-exact-name grep `TEXT_PATTERN_OPERATORS` across the whole repo, not just the package",
        "readers_in_service_analytics": [
          "src/comparand-shape.ts:193 — the definition",
          "src/strategies/filter-normalizer.ts:358 — import",
          "src/strategies/filter-normalizer.ts:577 — the only gate site (assertCompilableComparand), generic over the set"
        ],
        "other_hits_all_non_consumers": [
          "packages/drivers/driver-sql/src/sql-driver.ts:1166,1201,1209,1313 — a SEPARATE module-private set of the same name in a package service-analytics does not depend on; already contains `$icontains`",
          "packages/drivers/driver-sql/src/sql-driver-silent-empty-predicate.test.ts:236 — comment",
          "service-analytics/src/__tests__/cross-field-reference-refusal.test.ts:85,343 — comments"
        ],
        "verdict": "the fix IS one entry — no second reader exists inside the package, so no other door changes behavior"
      },
      "changes": [
        "packages/services/service-analytics/src/comparand-shape.ts — `$icontains` added to TEXT_PATTERN_OPERATORS + a measured doc note (the two-answers table, why the ASCII fold rides on top of the pattern text, the #6520/#7158 lineage)",
        "packages/services/service-analytics/src/__tests__/cross-field-reference-refusal.test.ts — RECORDED GAP pin FLIPPED to a refusal block (4 cases: `{$field}` on both doors, plain `{foo:1}` with an envelope-equality assertion against `$contains`, array comparand, narrowness control); the CROSS_FIELD_REFUSALS `$icontains` exclusion filter removed so the shared #5222 corpus is driven whole; header rewritten with the reverse-verification numbers",
        "packages/services/service-analytics/src/__tests__/comparand-shape-refusal.test.ts — `$icontains` added as the fifth member of the LIKE-family loops on BOTH doors (read-scope arm = no-regression control) + header note",
        ".changeset/icontains-comparand-fence-analytics-where.md — patch, @objectstack/service-analytics"
      ],
      "refusal_assertions": {
        "code": "INVALID_FILTER",
        "status": 400,
        "message_shape": "LIKE-family wording (`StringOperatorSchema`, `[object Object]`), asserted byte-identical to the `$contains` message apart from the operator name",
        "note": "no bare expect(...).toThrow() — every new case asserts code AND status per ADR-0112; refusalOf() also fails loudly with the RETURNED value when the compiler builds a predicate it should not have"
      },
      "reverse_verification": {
        "method": "removed the `$icontains` entry from TEXT_PATTERN_OPERATORS, re-ran the WHOLE package",
        "with_fix": "72 files / 1558 tests pass",
        "without_entry": "5 failed / 1553 passed",
        "failures_were_exactly": [
          "comparand-shape-refusal.test.ts > `$icontains` refuses an object comparand (where door)",
          "cross-field-reference-refusal.test.ts > corpus: the `where` door refuses: $icontains against a field reference is refused",
          "cross-field-reference-refusal.test.ts > [#7693] a `{$field}` comparand is refused on BOTH doors now",
          "cross-field-reference-refusal.test.ts > [#7693] the plain malformed comparand `{foo: 1}` …",
          "cross-field-reference-refusal.test.ts > [#7693] an ARRAY comparand is refused too"
        ],
        "stayed_green_on_purpose": "every read-scope `$icontains` assertion (that door's refusal comes from its own assertRenderableText, not from this entry) and every narrowness control (a well-formed `$icontains` comparand compiles identically in both states) — the proof the entry closes a hole rather than retiring #6520's operator"
      },
      "gates": {
        "build_closure": "pass — pnpm --filter @objectstack/service-analytics... build (incl. driver-sql)",
        "package_suite": "pass — 72 files / 1558 tests",
        "check_docs_audit_scope": "pass",
        "eslint_changed_files": "pass — clean",
        "tsc_noEmit_package": "pre-existing failures only — 10 errors, all in untouched test files (measure-source-field-gate.test.ts, objectql-timedimension-projection.test.ts); zero in the three files this PR touches. Present on origin/main before the change.",
        "changeset": "present — patch",
        "engine_double_gate": "n/a — no new test fake with update/delete verbs",
        "ci": "in_progress — PM owns CI convergence, the ready-flip and landing; not idle-polled here"
      },
      "sibling_face_regression_check": "read-scope-sql unchanged and still refusing — asserted in the flipped block (READ_SCOPE_COMPILE_FAILED / 500), in the comparand-shape-refusal fifth loop member, and by like-metacharacter-escape.test.ts's #6520 cases staying green (a well-formed `$icontains` still folds ASCII on both sides and binds ['%admin%','\\\\'])",
      "scope_discipline": [
        "did NOT touch content/docs/releases/",
        "did NOT touch the issue's assignee or labels",
        "did NOT widen the fence to `$like`/`$ilike` (their comparand ARRIVES as the pattern and compiles through the scalar bind arm — the set's own note; out of scope)",
        "did NOT open the `{$eq: {…}}` object-binding account #5234 deliberately left open"
      ],
      "open_questions": [
        "The `where` door's refusal for a `{$field}` `$icontains` comparand uses the LIKE-family wording rather than #7598's cross-field wording. Decided deliberately (and pinned): `$icontains` is a pattern operator, so the caller-facing diagnosis should be the one its four siblings give — the same choice #7598 made for `$contains`, whose pin explicitly keeps the LIKE wording. No PM decision needed; recorded because it is the one wording choice in the change.",
        "`comparand-shape-refusal.test.ts`'s header states a #5234 reverse-verification count (17 failed / 8 passed) measured before this change; adding a fifth loop member on each door shifts those numbers. Left as-is rather than re-measuring #5234's own disable-all-three-call-sites experiment — that is a different experiment from this card's, and re-running it would be scope creep. Flagging in case the PM wants it refreshed in a follow-up."
      ]
    }

    Generated by Claude Code

  8. huangyiirene commented on Aug 11, 2026

    @huangyiirene
    Collaborator

    ACCEPT — PR #7843 (services seat review, session session_01JVfWSQN9RDDGwdVSmMc29x).

    What shipped: $icontains added to TEXT_PATTERN_OPERATORS — the fifth member its four siblings already had — closing the third and last face of the #5234 class (after #7158's objectql having and read-scope's own gate). The #7598 RECORDED GAP pin is FLIPPED into a four-case refusal block, the #5222 corpus is driven whole (its $icontains filter removed), and the LIKE-family loops on both doors carry the operator (read-scope arm as the no-regression control).

    Review notes:

    Marking ready and arming auto-merge now.


    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

domain:servicesRetired 2026-10-22, alias of domain:server (services surface); in-flight cards keep it until landedpm: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