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

Closed

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 (NUMERIC ∪ BOOLEAN ∪ CALENDAR_DATE ∪ INSTANT ∪ CLOCK_TIME ∪ STRUCTURED_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:engine 在 objectql/src/engine.ts 的 INVALID_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

  1. os-warren commented on Sep 5, 2026

    @os-warren
    Collaborator

    Maintainer ruling recorded — C-deny, now: a text operator ($contains / $notContains / $startsWith / $endsWith / $icontains / $like / $ilike) over a field whose DECLARED type can never store a string — NUMERIC_VALUE_TYPES ∪ BOOLEAN_VALUE_TYPES ∪ CALENDAR_DATE_TYPES ∪ INSTANT_TYPES ∪ CLOCK_TIME_TYPES ∪ STRUCTURED_JSON_TYPES, all existing sets in field-value.zod.ts — is refused at the engine's field-aware door with INVALID_FILTER 400 naming the field and its declared type. No new vocabulary is minted. String-valued classes (STRING_VALUE_TYPES, autonumber, option codes, reference ids) pass. formula is judged only when its declared return type is readable at the seam. #14079's option-A row stays beneath the door for every evaluator no door fronts.

    Director seat, summon #14, session session_01LsEjuNMPitCHwEfYftZ1um (GitHub os-warren), 2026-09-05. Provenance: maintainer, live PM chat, decision batch #43 (item 5, presented with the director's recommendation C-deny now — stated as differing from the filing seat's recommendation D-first — and 「同意」 adopts C-deny), verbatim reply 「同意」. Premise: the card body (spec seat, 02:03Z) — the vocabulary the door needs already exists as declared value classes; the door precedent is the comparand door-refusal rows and the engine's INVALID_FIELD door; the only real producer (global search) already refuses at its own door; batch #39 ruled #14079 = A and deferred C to this card.

    Ruled: C-deny. Not taken: C-allow (would break substring filters over select codes, lookup ids and tags that work today), D (leaves the "silent wrong answer" the conformance file's own header names).

    Why (① ≥50%): a narrowing of the accept set that reuses declared value classes and the door shape already running on two neighbouring seams — no new public surface; ③ the deciding facet on a cell whose only real occurrence is a mistake: [] with no signal becomes a 400 that names the field's declared type; ② zero pull, so the door must cost no vocabulary — it does not; ④ one door, no configuration.

    Execution, two lanes, contract-first: (1) domain:spec — door-refusal rows for the refused classes in filter-comparand-type-conformance.ts or a same-shaped sibling table, referencing the existing sets (⛔ no new set), plus the formula-deferred note (Clause-②: yes, @objectstack/spec minor + BREAKING banner: a text operator over a numeric/boolean/temporal/JSON field is now refused at the engine door instead of answering [] or a dialect accident). (2) domain:engine — the door in packages/objectql/src/engine.ts beside INVALID_FIELD, judged against the object's real field map, before any driver dispatch; pins per refused class and per passing class, and the direct-driver path still answering #14079's A row (@objectstack/objectql minor). Sequencing: after PR(s) for #14079 land. Zone 2 (report, not block): whether objectui's filter builder emits such conditions today (the seat cannot measure host apps; say so).

    State transition, same stroke: needs-user-decision → pm:queue. domain:spec unchanged; priority is triage's to set (director's reading: p3 — no incident, a loud-vs-silent question). Ledger: director seat post #12708, batch #43. Related: #14079 · filter-comparand-type-conformance.ts · search-fields.ts.


    Generated by Claude Code

  2. claude commented on Sep 5, 2026

    @claude
    Contributor

    Claim: PM loop round R2 — domain:spec seat, session_01M59rPZZFzqhfMUPFqqZTkf, 2026-09-05T07:13Z. Branch claude/issue-15661-text-operator-declared-type-door-rows, mode:subagent, tier claude-fable-5-1 (= CONTRACT_REVIEW_TIER). Clause-②: yes — a new export on the published @objectstack/spec/data face and a declared accept-set narrowing (the ruling's own Clause-②: yes).

    Clause-②: yes

    Scope = execution lane (1) of the maintainer ruling C-deny (5548768100, batch #43, 「同意」), the spec half only: the door-refusal rows for the refused classes — NUMERIC_VALUE_TYPES ∪ BOOLEAN_VALUE_TYPES ∪ CALENDAR_DATE_TYPES ∪ INSTANT_TYPES ∪ CLOCK_TIME_TYPES ∪ STRUCTURED_JSON_TYPES, all existing sets in field-value.zod.ts (⛔ no new set minted) — plus the passing classes and the formula-deferred note, in filter-comparand-type-conformance.ts or a same-shaped sibling table, with the @objectstack/spec minor + BREAKING-banner changeset the ruling names. Sequencing satisfied: #14079's PR #15686 landed at 05:02Z (a646120dc). Lane (2), the engine door itself, is NOT in this dispatch: it is filed as its own engine-lane card (linked below, Blocked-by: #15661, contract-first) — this seat does not dispatch into packages/objectql.

    Seat readings (origin/main b25a5fc32, 2026-09-05T07:11Z): the comparand table's door-refusal shape (filter-comparand-type-conformance.ts:26, :113-120, 12 rows) asserts parseFilterAST(filter) throws — a syntax-level door; the ruled door is FIELD-aware (declared type), so the rows most likely live in a same-shaped sibling keyed on the declared type class, consumable the way packages/objectql/src/engine-comparand-type-door.test.ts consumes the comparand table. #14079's landed NON_TEXT_STORED_VALUE_TYPES (numeric + boolean) is the SQL faces' type-gate set and stays beneath the door untouched.

    File face: one new (or extended) conformance table + test under packages/spec/src/data/, regenerated packages/spec/api-surface/data.json / export-origins/data.json / docs references (new export; ⛔ never hand-edited), one changeset. ⛔ Not touched: field-value.zod.ts (the sets are referenced, not changed), filter-text-conformance.ts, every driver, packages/objectql/**, packages/services/**, migrations/registry.ts (held). Hot-file check against the lane's in-flight dispatches (#14972 system/i18n-resolver.ts, #15430 automation/builtin-node-config.zod.ts + shared/expression.zod.ts) and the landing PRs (#15707, #15716): no intersection; the generated data/ shard is shared and lands serially (regen merge before the PR if another PR regenerates it first).

    Dedup handed to the dev (MCP search_issues 2026-09-05T07:12Z, 10 results: #15661 the in-set control; #15684 (services, LIKE case-fold), #13744 / #7534 / #6337 / #6518 / #6444 / #5240 closed and different): no engine-door card exists — filed by this seat as the lane-(2) card in this stroke.


    Generated by Claude Code

  3. claude commented on Sep 5, 2026

    @claude
    Contributor

    Claim (os-dev): branch claude/issue-15661-text-operator-declared-type-door-rows, PM session session_01M59rPZZFzqhfMUPFqqZTkf (mode:subagent, domain:spec seat, round R2, 2026-09-05).

    Scope: lane (1) of the maintainer ruling C-deny (5548768100, decision batch #43) — the spec contract rows only: a sibling module packages/spec/src/data/filter-text-operator-declared-type.ts (sets by reference, the pure verdict, a census class table, a fixture object and the derived case table) plus the @objectstack/spec minor changeset with the BREAKING banner. Lane (2), the engine door, is #15773 and is not touched here. Assignee left as the PM set it.


    Generated by Claude Code

  4. claude commented on Sep 5, 2026

    @claude
    Contributor

    os-dev-report

    {
      "issue": 15661,
      "status": "done",
      "branch": "claude/issue-15661-text-operator-declared-type-door-rows",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/15804",
      "premise_still_valid": true,
      "summary": "Lane (1) of the C-deny ruling (5548768100) only — the spec contract. New module packages/spec/src/data/filter-text-operator-declared-type.ts (+ test, + barrel line) exported on @objectstack/spec/data: TEXT_FILTER_OPERATORS (the seven, pinned equal to StringOperatorSchema's keys), TEXT_OPERATOR_DOOR_REFUSED_TYPES = union of the six EXISTING sets by reference (no new set; pinned by set equality against the imports), TEXT_OPERATOR_DOOR_PASSING_TYPES (STRING ∪ autonumber ∪ single+multi option ∪ reference ∪ file classes), the pure verdict textOperatorDoorVerdict (door-refusal | passes | deferred), FORMULA_RETURN_TYPE_AS_FIELD_TYPE, the class table TEXT_OPERATOR_DOOR_TYPE_CLASSES pinned as a census of FieldType (49 members, each in exactly one row, both directions), a FieldSchema/ObjectSchema-legal fixture object, and the derived case table TEXT_OPERATOR_DOOR_CASES (420 cases: 154 door-refusal with code INVALID_FILTER + status 400 + mustMention [key, declared type, return type, operator]; 210 passes; 56 deferred). Changeset @objectstack/spec minor with BREAKING banner, ADR-0087 not-required (no-migration-prescription), gate green; api-surface/data.json and export-origins/data.json regenerated by the tooling (+20/+20, zero removals); migrations/registry.ts untouched. PREMISE verified on origin/main by symbol: all 13 sets present in field-value.zod.ts (H1); formula returnType = z.enum(['number','text','boolean','date']).optional() at field.zod.ts (H3) — every value is itself a FieldType member, so a formula is judged AS the field type its return type names (text passes; number/boolean/date refused; absent or undeclared spelling ⇒ deferred); summary verified a NUMERIC_VALUE_TYPES member (refused for that reason); the comparand table's door-refusal rows assert parseFilterAST throws — a syntax door with no field map — so H2 resolved as (b), a sibling module keyed on the declared type; the five driver comparand suites, engine-comparand-type-door and service-analytics comparand-shape are untouched and read green; NON_TEXT_STORED_VALUE_TYPES and FILTER_TEXT_CASES untouched, relationship stated in the docblock (H4, pinned: refused set = NON_TEXT ∪ temporal ∪ JSON); StringOperatorSchema has exactly the seven keys (H5). Two derived rows, recorded as derived on their class rows and in the PR body: FILE_REFERENCE_TYPES → passes (the ruling's criterion is 'DECLARED type can never store a string'; a file field stores an id/url string today and a sys_file id string after ADR-0104 D3, so it is outside the refused criterion — not ruled by name); and 49 dotted-key cases (f_json.leaf etc.) → deferred so the engine door cannot re-close filter-dotted-head's deliberate structured-JSON carve-out (#8371) by reading the head's declared type. The file is deliberately NOT a *-conformance.ts case-set and is not registered in scripts/check-driver-conformance.mjs CASE_SETS (the census enrols drivers; drivers sit beneath this door and must keep answering FILTER_TEXT_CASES' row) — check:driver-conformance does not read it; its one consumer is #15773 (the engine door), referenced verb-less in the PR body. Unexpected: none on the card (assignee was os-project-manager as dispatched; my claim comment is 5550572330). Zone 2 (report, not block): whether objectui's filter builder emits a text operator over a non-textual declared field is NOT measurable from this repo — not measured. Regen-merge not owed: no commit on main since my merge-base touched the generated shards (re-checked at f501453b9 before opening the PR). needs:contract-review hung via the additive REST endpoint and confirmed by compare-read-back (['needs:contract-review']). Branch merged origin/main (up to 6a3cc134c) before verification; spec rebuilt after the merge.",
      "tests": "All at head 8742baf9f (= PR head, confirmed via REST after push); exit codes captured before any pipe; heavy runs lock-serialised (os-verify-lock VERDICT lines quoted). BUILD: pnpm --filter @objectstack/spec build → 'check-dts-emitted: @objectstack/spec - 34/34'; consumer closure turbo run build --filter=PKG^... ×7 --concurrency=2 → 'Tasks: 15 successful, 15 total'. SPEC: vitest run src/data/filter-text-operator-declared-type.test.ts → 'Test Files 1 passed (1) / Tests 23 passed (23)'; whole package vitest run --maxWorkers=2 → 'Test Files 474 passed (474) / Tests 12734 passed (12734)'; pnpm --filter @objectstack/spec typecheck (tsc --noEmit && check:scripts-typecheck && check:test-typecheck) → exit 0, 'check:test-typecheck: OK — @objectstack/spec's test layer compiles under packages/spec/tsconfig.test.json', and tsc -p tsconfig.test.json --listFiles counts the new test file (1 hit) so the build config's test exclusion hides nothing. CONSUMERS (prefix direction ...@objectstack/spec = downstream, closure built first), one lock hold, all exit=0: driver-memory memory-comparand-type-conformance 20/20; driver-sql sql-driver-comparand-type-conformance 43 passed 2 skipped (unprovisioned live-dialect cells); driver-sqlite-wasm 19/19; driver-turso 19/19; driver-mongodb 19/19; objectql engine-comparand-type-door 12/12; service-analytics __tests__/comparand-door-single-source 39/39. Built entries export the new symbols from packages/objectql: CJS require('@objectstack/spec/data') and ESM import both answer textOperatorDoorVerdict: function, 420 cases, refused 19 / passing 29. GATES: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack with NO paths, re-derived after the merge (the pre-merge derivation warned STALE TREE) → 74 commands, 72 green, 2 NOT MEASURED — check:dual-build-cjs-loads and check:type-check-debt both exit 3 printing 'PREREQUISITE NOT MET … This is NOT a pass: nothing was measured' (54 packages have no dist/ in this worktree; neither takes a scope flag) — declared narrowing: the spec-scoped CJS/ESM load above stands in for the first, the second re-measures ledgered packages my diff does not touch; both are CI-owned repo-level runs. Derivation added beyond the lead, all green: check:generated, check:nul-bytes, check:type-check-coverage, check:driver-memory-census, check:engine-double-contract, check:objectui-changeset, check:objectui-pin-citations, check:pm-half-states, check:query-options-erasure, check:refd-timer-probe, check:watch-hint-literal, check:dispatcher-error-vocabulary, check-changeset-no-major, check-empty-changeset, check-closing-keyword-parity, check-comment-mask-corpus, release-rehearsal-clone --self-test; check-adr-0087-registration → '1 declared-breaking changeset(s), each carrying an ADR-0087 disposition … [BREAKING+bang] not-required (no-migration-prescription)'. check:generated --fix regenerated exactly api-surface/ and export-origins/ ('2 of the 2 stale'), authorable-surface.base.json untouched. ABLATION (committed first; trap-guarded restore on EXIT INT TERM; absolute paths): TEXT_OPERATOR_DOOR_REFUSED_TYPES emptied (six spreads → ABLATION-15661 marker); landed on disk proven by anchored counts (spread line 1→0, marker 0→1; blob d9af49037…→db284cca7…), not by the editor's exit; no build leg (the spec tests import the module by relative source path; dist/ untouched during the ablation). Expected direction: red confined to the new test file, census pin staying green (class table untouched). Observed on the whole package: 'Test Files 1 failed | 472 passed | 1 skipped (474)', 'Tests 6 failed | 12726 passed | 1 skipped' — the failing file is the new test, the six reds are the set-equality pin, the disjoint/union-is-FieldType pin, the wider-than-NON_TEXT pin, both H3 verdict pins and the three-verdict count pin; the 1 skipped is an environment-conditional test elsewhere (clean run has no skip). Restore: git checkout HEAD -- FILE → hash-object equals HEAD blob d9af49037e487e4302783700b2f4317fd93379d7 on both sides, git diff HEAD 0 lines, porcelain 0, marker count 0. ADR-0087 census: git grep for a text operator applied to a field key over examples/** and content/docs/** → 26 occurrences over name (13), title (5), email (4), domain (1: the docs' $endsWith '.com' example) — all textual by declaration, zero over a non-textual declared field; positive control: the same grep finds the 5 name/email $contains occurrences. Cleanup: node_modules removed and worktree removed without force after the report was posted.",
      "mcp_calls": "0 — every GitHub read went through git, the public issue-page payload (hasNextPage false, both comments captured), or the repo-scoped REST channel (probe GET issues/15661 → 200); every write (claim comment 5550572330, PR 15804, label, report comment) went through REST with read-back",
      "open_questions": [
        {
          "question": "FILE_REFERENCE_TYPES (image · file · avatar · video · audio) are not named by the ruling; the card's own table said 'not decided here'. The landed row derives 'passes' from the ruling's stated criterion — a DECLARED type that can never store a string is refused — since a file field stores an opaque id/url string (or a legacy inline object) today and a sys_file id string after ADR-0104 D3. Confirm at contract review that a derivation from the criterion is the intended reading rather than a by-name ruling.",
          "options": [
            "A — passes (as landed): the door refuses only the six classes; a text operator over a file field reaches the evaluators beneath the door, which answer #14079's row when the stored value is an object and match the string when it is a URL/id.",
            "B — deferred: the door records no verdict for file classes until ADR-0104 D3 fixes one stored form; behaviourally identical to A today (the filter proceeds unchanged), but the row would say 'not judged' instead of 'passes'.",
            "C — door-refusal: refuse by name; breaks substring filters over a legacy URL-valued file field that work today, the shape the ruling rejected C-allow for."
          ],
          "recommendation": "A. 实际业务需求: zero measured pull either way (the census found no text operator over a file field in examples/** or content/docs/**), so the row must cost nothing — A adds no mechanism. 项目长远合理性: A is the ruling's criterion applied literally and keeps the door a pure deny-list over declared classes; B invents a fourth reason for 'deferred' that D3 would have to retire; C reverses a stated non-decision. 防 AI 犯错: A and B behave identically at the seam, so neither hides a mistake the other would surface; C would refuse a working query. 创业阶段不扩散: A minimal; re-judge only if D3 lands a non-string stored form (recorded on the class row)."
        }
      ],
      "out_of_scope_findings": []
    }
    

    Generated by Claude Code

  5. claude commented on Sep 5, 2026

    @claude
    Contributor

    Review — ACCEPT · Clause-② contract review: PASS (in-seat, tier claude-fable-5-1; domain:spec seat, session_01M59rPZZFzqhfMUPFqqZTkf, 2026-09-05T08:43Z)

    PR #15804 at head 8742baf9f (6 files, +895/−0; NOT governed — predicate 0 of 6 at 2026-09-05T08:42Z), report 5550647890, dev claim 5550572330. Readings taken against GitHub and the branch, not the report.

    Checklist

    Clause-② contract judgment (derived judgments itemised):

    1. TEXT_OPERATOR_DOOR_REFUSED_TYPES is the union of the six ruled sets by reference (spreads of the field-value.zod.ts exports; pinned by set equality against the imports) — no vocabulary minted; a member added to a class later is refused without a change here. ✓ matches the ruling.
    2. TEXT_OPERATOR_DOOR_PASSING_TYPES = STRING_VALUE_TYPES ∪ autonumber ∪ SINGLE_OPTION_TYPES ∪ MULTI_OPTION_TYPES (tags protected, as the ruling's C-allow refusal requires) ∪ REFERENCE_VALUE_TYPES ∪ FILE_REFERENCE_TYPES. The file classes are the one row the ruling does not name — decided here: A, passes, as a derivation of the ruling's own criterion ("a DECLARED type that can never store a string"): a file field stores an opaque id / url string today and a sys_file id string after ADR-0104 D3, so it is outside the refused criterion; B (deferred) would invent a second reason for "not judged" that D3 would later have to retire, and C (refuse by name) is the shape the ruling rejected for breaking working filters; zero measured pull either way (the census), so the row must cost nothing — A adds no mechanism. The class row records the derivation and its re-judge trigger. This is an application of the recorded ruling, not a new ruling.
    3. formula: judged as the FieldType its declared returnType names (text passes; number / boolean / date refused through the same sets; absent ⇒ deferred) — pinned that the map equals FieldSchema.returnType's enum and every value is a FieldType member. ✓ the ruling's "judged only when readable at the seam".
    4. Dotted keys (address.city) ⇒ deferred, so the engine door cannot re-close filter-dotted-head's structured-JSON carve-out ([finding] The FILTER axis has no DOTTED-path verdict — where: { project_id.name: 'x' } rides its head segment past both doors, where SORT refuses the same spelling (#4256) #8371) by reading the head's declared type. ✓ correct boundary.
    5. The census pin: every FieldType member (49) appears in exactly one class row, refused ∪ passing ∪ {formula} = FieldType. ✓ no silent absence.
    6. Public surface: eight new exports on @objectstack/spec/data, shards regenerated; deliberately NOT a driver case-set (not enrolled in check-driver-conformance.mjs) because drivers sit beneath the door and keep answering FILTER_TEXT_CASES' row — the correct reading of "driver-memory's reference matcher answers $notContains NO for every valued NON-STRING row — the live mingo path answers YES #14079's option-A row stays beneath the door". ✓
    7. Semver: @objectstack/spec minor with the BREAKING banner and the ADR-0087 not-required (no-migration-prescription) disposition, exactly as the ruling's execution paragraph names; the changeset says the door lands in @objectstack/objectql under its own card. ✓
    8. NON_TEXT_STORED_VALUE_TYPES (numeric + boolean) stays narrower than the door's set on purpose, pinned (H4). ✓

    No fork between the dev's judgments and the seat's; the one flag is decided above. Implemented-by: claude/issue-15661-text-operator-declared-type-door-rows · Reviewed-by: session_01M59rPZZFzqhfMUPFqqZTkf. PASS — both carriers cleared in this stroke per the 2026-08-31 ruling (the card carrier hung and cleared with the PR's so the pair check reads the fixed spelling); landing follows the ordinary path.

    Landing: flip ready + auto-merge (squash) when every check on 8742baf9f concludes green; on MERGED this seat probes TEXT_OPERATOR_DOOR_REFUSED_TYPES on origin/main against the pre-merge tip, strips pm:dispatched, clears the assignee, posts the landing note — and #15773 (the engine door, Blocked-by: #15661) returns to the queue by the unlock scan for the engine lane, its dispatch consuming TEXT_OPERATOR_DOOR_CASES as the module's docblock describes.


    Generated by Claude Code

  6. 3 remaining items

  7. claude commented on Sep 5, 2026

    @claude
    Contributor

    Landed — PR #15804 merged through the merge queue at 2026-09-05T10:08:37Z, merge commit 6f1ce7d26 (squash of head 8742baf9f; enqueued 09:12:06Z, 56 min in the queue behind a five-PR batch; the seat's flip and landing timers fired into a session blocked by the account usage wall 09:4xZ → 11:40Z, so this stroke is executed at 11:45Z). Probe on origin/main at 2026-09-05T11:44Z (tip 59953d5a3): git grep -n TEXT_OPERATOR_DOOR_REFUSED_TYPES origin/main -- packages/spec/src/data/filter-text-operator-declared-type.ts → 3 hits (docblock :35, export :176, textOperatorDoorVerdict :248); control on the pre-merge tip f141e156b: the file is absent (0). Card state: pm:dispatched stripped and assignee cleared in one write (read back). Lane inventory diff: only #15661 closes; #15773 (engine lane, pm:blocked, Blocked-by: #15661) is returned to its queue by the unlock scan — not this seat's to dispatch. Contract review record: verdict 5550664975, Clause-② PASS, carriers cleared, pair check ✓, provenance 5550805329.


    Generated by Claude Code

  8. claude commented on Sep 5, 2026

    @claude
    Contributor

    Release: session_01M59rPZZFzqhfMUPFqqZTkf (domain:spec seat) — released 2026-09-05T11:46Z, assignee cleared in that stroke; reason: landed — PR #15804 merged 10:08:37Z (6f1ce7d26), card closed by the PR; destination: closed, landing note 5551558437. Posted at 12:27Z under the Release: rule that landed at 11:29Z (#15846), after the release itself.


    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

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions