Skip to content

[Decision] refuse a text operator ($contains family) over a field whose DECLARED type is not textual — INVALID_FILTER 400 at the engine's field-aware door (option C of #14079); the textual-type vocabulary is the question #15661

Description

@os-project-manager

Related: #14079

Filed by the domain:spec execution seat (session_01M59rPZZFzqhfMUPFqqZTkf, 2026-09-05T02:03Z) on the director seat's recorded maintainer ruling on #14079 (5548481659, decision batch #39, reply 「同意」), quoted verbatim:

C is deferred, not rejected. A door that refuses a text operator over a field whose DECLARED type is not textual (INVALID_FILTER 400 at the engine's field-aware seam, the comparand-door shape) is the loud answer ③ argues for, and A does not foreclose it. What it needs first is a vocabulary ruling — which of select / lookup / formula / json / code / password (and the rest) count as textual beyond SEARCHABLE_TEXTUAL_TYPES — and that is not a rider on a p2 defect card. Spec seat: file the C card (domain:spec, needs-user-decision, with the dev's option-C text and the type list to adjudicate, Related: #14079), and link it here.

Labels are as that instruction names them. ⛔ Not dispatchable: the decision is the maintainer's. Sequencing: #14079 (ruled A, in flight) lands first and is not blocked by this card; A's contract row stays beneath any door (direct driver calls never pass the engine seam).

Premise, with re-check commands (positive controls named)

  • origin/main b4b37e59 (2026-09-05T01:51Z). The only real producer of a text operator over a non-textual field already refuses at its own door: global search's $searchFields allow-list is SEARCHABLE_TEXTUAL_TYPES ∪ SEARCHABLE_ENUM_TYPES — re-check git grep -n "SEARCHABLE_TEXTUAL_TYPES" origin/main -- packages/spec/src/data/search-fields.ts (control: the same file must also hit SEARCH_AUTO_EXCLUDED_FIELDS).
  • The spec already declares semantic value classes over FieldType — re-check git grep -n "VALUE_TYPES: ReadonlySet" origin/main -- packages/spec/src/data/field-value.zod.ts (control: the same file hits MULTI_CAPABLE_TYPES). Measured members at b4b37e59:
    set members
    STRING_VALUE_TYPES text · textarea · email · url · phone · password · secret · markdown · html · richtext · code · color · signature · qrcode
    NUMERIC_VALUE_TYPES number · currency · percent · rating · slider · progress · summary
    BOOLEAN_VALUE_TYPES boolean · toggle
    CALENDAR_DATE_TYPES / INSTANT_TYPES / CLOCK_TIME_TYPES date / datetime / time
    SINGLE_OPTION_TYPES / MULTI_OPTION_TYPES select · radio / multiselect · checkboxes · tags
    REFERENCE_VALUE_TYPES lookup · master_detail · user · tree (record-id string in stored form)
    FILE_REFERENCE_TYPES image · file · avatar · video · audio (opaque id/url string OR inline object today; ADR-0104 D3 pending)
    STRUCTURED_JSON_TYPES json · composite · repeater · record · location · address · vector
    COMPUTED_VALUE_TYPES formula · summary · autonumber
    The vocabulary the door needs may already exist: "declared type is not textual" can be spelled from these sets without minting a new one. SEARCHABLE_TEXTUAL_TYPES (text · email · phone · url · autonumber · textarea · markdown) answers a narrower question — what global search auto-scans — and is the wrong set to borrow for a filter door.
  • The measured faces (dev report 5542868802 on c550bafc2, seat review 5542938268): JS faces + Mongo type-gate, the SQLite family coerces, live Postgres 17.10 refuses with SQLSTATE 42883 (a 500). Ruling A turns all of that into one declared row; what A leaves is that a mistaken query answers [] silently.
  • The door precedent: packages/spec/src/data/filter-comparand-type-conformance.ts carries verdict: 'door-refusal' rows with code: 'INVALID_FILTER' (re-check git grep -n "door-refusal" origin/main -- packages/spec/src/data/filter-comparand-type-conformance.ts), and packages/objectql/src/engine.ts already judges an unknown field against the object's real field map (INVALID_FIELD, 400 — re-check git grep -n "INVALID_FIELD" origin/main -- packages/objectql/src/engine.ts).

The dev's option-C text (verbatim from 5542868802)

C — REFUSE AT THE DOOR: a text operator over a field whose DECLARED type is not textual is refused before any driver runs — INVALID_FILTER / 400 in the ADR-0112 envelope, at the engine's field-aware seam (packages/objectql/src/engine.ts, beside the INVALID_FIELD unknown-field door that already judges against the object's real field map), exactly as the comparand-type door refuses $icontains: 42 and as $searchFields already refuses a non-textual field. Postgres's measured 500 becomes a declared 400; the evaluators no door fronts (formula's write-side check, having, RLS) keep option A beneath it as defense in depth — the same door-plus-total-evaluator shape FILTER_COMPARAND_TYPE_CASES uses. Contract shape: a door-refusal-verdict row (the comparand table's discriminant), not a FilterTextRowsCase — plus A's evaluated row for the faces behind no door. Cost that must be priced: WHICH field types count as textual is a vocabulary decision (SEARCHABLE_TEXTUAL_TYPES = text/email/phone/url/autonumber/textarea/markdown exists; select/lookup/formula/json/code/password must each be judged), and the engine seam must see the object schema on every read path (the REST and SDK paths do; a direct driver call does not, which is why A stays underneath).

The type list to adjudicate

class (existing set) stored value text operator over it proposed disposition
STRING_VALUE_TYPES string meaningful textual — passes the door
autonumber (computed) string meaningful textual — passes
select / radio one option CODE (string) substring over a code: legal today, rarely intended passes (codes are strings); ⚠️ password / secret are strings too — filtering a secret by substring is a security posture question, out of this card
multiselect / checkboxes / tags array of strings element-wise semantics are their own question (JSON columns) not decided here — stays as today
lookup / master_detail / user / tree record-id string legal, a smell passes (string)
formula depends on the formula's declared return type unknown until the return type is read ⚠️ open — the door can only judge it if the declared return type is available at the seam
NUMERIC · BOOLEAN · date · datetime · time never a string by declaration can never match ⇒ the card's {n: 5} cell refused at the door under any C
STRUCTURED_JSON_TYPES object meaningless refused under C-deny
FILE_REFERENCE_TYPES string OR object today ambiguous until ADR-0104 D3 not decided here

Options × real cost

option what real cost
C-deny — refuse only the classes whose stored value is never a string by declaration (NUMERICBOOLEANCALENDAR_DATEINSTANTCLOCK_TIMESTRUCTURED_JSON, all existing sets) one door-refusal row family in spec, one door in objectql/src/engine.ts beside INVALID_FIELD; zero new vocabulary two-lane execution (spec row + engine door); a mistaken query becomes a 400 naming the field's declared type; formula stays undecided until its return type is readable at the seam
C-allow — refuse everything outside STRING_VALUE_TYPES ∪ {autonumber} strictest breaks substring filters over select codes, lookup ids, tags that WORK today — needs a measured census of real emitters (objectui filter builder, apps) before it could be adopted; not measurable from this repo alone
D — no door; A alone nothing beyond #14079 a mistaken $contains over a number answers [] with no signal (the "silent wrong answer" the conformance file header names); zero mechanism added

四维分析(从业务角度)

一句话问题:客户的报表筛选器对「金额」列写了「包含 5」—— 平台是该悄悄返回空表,还是当场告诉他「金额不是文本,这个条件永远匹配不到」?

  • 实际业务需求 — 零拉动(实测):全仓没有生产者对数值字段写文本算子;唯一真实生产者(全局搜索)在 $searchFields 门口已拒绝非文本字段。本卡决定的只是「写错的条件」答什么。⛔ 未测:objectui 的筛选器构造器与客户应用是否会发出这种条件 —— 那是本仓看不见的读数。
  • 项目长远合理性(权重 ≥50%) — 按本义读(缩小特例、不增契约):C-deny 是收窄接受集(把「永远匹配不到」的条件从静默变成具名拒绝),复用 field-value.zod.ts 已声明的值域类,不新增公开词表;它与 comparand 门、$searchFields 门同构 —— 平台已在两个相邻缝上跑这个形状。代价是多一道门(机制),不是多一套词表。D 零机制,但把「声明 ≠ 执行」留在一个已知格子上。
  • 防 AI 写代码犯错 — 最硬指向 C:AI 生成的筛选条件类型写错时,A 之下得到 [] 零信号;C-deny 之下得到 400 + 字段声明类型,错误在写的那一刻被点名。
  • 创业阶段不扩散 — C-allow 会打破今天能用的 select / lookup / tags 子串筛选,⛔ 不可荐;C-deny 无新词表、无新配置面,但仍是新增机制;D 最紧。零拉动 ⇒ 默认不扩散。

推荐

D 先行(A 独立成立,#14079 落地即完整),C-deny 作为记录在案的形状,出现真实拉动即重开 —— 依据分歧推荐序:②零拉动 ⇒ 荐不扩散;①长远看 C-deny 是对的形状(收窄、复用已声明类、同构于既有门),因此回退项就是 C-deny 而不是 C-allow;③是唯一逆向的一轴,它压不过①②④合计。重开触发写在关单评论:某个真实生产者(objectui 筛选器、客户应用、或一次支持工单)对非文本声明字段发出文本算子。

若维护者裁 C-deny:两车道执行 —— domain:spec 在 comparand 表或同形姊妹表声明 door-refusal 行(引用既有集合,⛔ 不新铸集合);domain:engineobjectql/src/engine.tsINVALID_FIELD 门旁实现;A 的行留在门下;formula 单列为「返回类型可读时再判」。

置信缺口:objectui / 客户应用是否发出此类条件未测;formula 字段的声明返回类型在引擎缝上是否可读未测;MySQL、turso remote、真 mongod 三面(A 的)仍只有源码读数。

os-decision-facets

  • ① 长远合理性(≥50%):C-deny 收窄接受集、复用 field-value.zod.ts 既有值域类、与 comparand 门/$searchFields 门同构 ⇒ 若建门则 C-deny;但 A 已使各面同答一行,余下缺口只是「写错的条件静默为空」。
  • ② 实际业务拉动: —— 无生产者,唯一真实生产者已在门口拒绝 ⇒ 指向 D(不扩散、记录在案)。
  • ③ 防 AI 犯错:指向 C-deny —— 400 点名字段声明类型 vs [] 零信号。
  • ④ 创业阶段不扩散:C-allow ⛔(打破现有可用筛选);C-deny 无新词表但新增机制;D 最紧。
  • 推荐:D 先行,C-deny 为记录在案的回退形状(重开条件:真实生产者出现);⛔ C-allow 不荐。
  • 置信缺口:objectui/客户应用发出面未测;formula 返回类型在引擎缝可读性未测;MySQL · turso remote · 真 mongod 三面未实测。

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions