Repository navigation
[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
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_FILTER400 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 ofselect/lookup/formula/json/code/password(and the rest) count as textual beyondSEARCHABLE_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/mainb4b37e59(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$searchFieldsallow-list isSEARCHABLE_TEXTUAL_TYPES ∪ SEARCHABLE_ENUM_TYPES— re-checkgit grep -n "SEARCHABLE_TEXTUAL_TYPES" origin/main -- packages/spec/src/data/search-fields.ts(control: the same file must also hitSEARCH_AUTO_EXCLUDED_FIELDS).- The spec already declares semantic value classes over
FieldType— re-checkgit grep -n "VALUE_TYPES: ReadonlySet" origin/main -- packages/spec/src/data/field-value.zod.ts(control: the same file hitsMULTI_CAPABLE_TYPES). Measured members atb4b37e59:set members STRING_VALUE_TYPEStext · textarea · email · url · phone · password · secret · markdown · html · richtext · code · color · signature · qrcode NUMERIC_VALUE_TYPESnumber · currency · percent · rating · slider · progress · summary BOOLEAN_VALUE_TYPESboolean · toggle CALENDAR_DATE_TYPES/INSTANT_TYPES/CLOCK_TIME_TYPESdate / datetime / time SINGLE_OPTION_TYPES/MULTI_OPTION_TYPESselect · radio / multiselect · checkboxes · tags REFERENCE_VALUE_TYPESlookup · master_detail · user · tree (record-id string in stored form) FILE_REFERENCE_TYPESimage · file · avatar · video · audio (opaque id/url string OR inline object today; ADR-0104 D3 pending) STRUCTURED_JSON_TYPESjson · composite · repeater · record · location · address · vector COMPUTED_VALUE_TYPESformula · 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
5542868802onc550bafc2, seat review5542938268): 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.tscarriesverdict: 'door-refusal'rows withcode: 'INVALID_FILTER'(re-checkgit grep -n "door-refusal" origin/main -- packages/spec/src/data/filter-comparand-type-conformance.ts), andpackages/objectql/src/engine.tsalready judges an unknown field against the object's real field map (INVALID_FIELD, 400 — re-checkgit 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_FIELDunknown-field door that already judges against the object's real field map), exactly as the comparand-type door refuses$icontains: 42and as$searchFieldsalready refuses a non-textual field. Postgres's measured 500 becomes a declared 400; the evaluators no door fronts (formula's write-sidecheck,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: adoor-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 | |
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
os-warren commentedon Sep 5, 2026
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 infield-value.zod.ts— is refused at the engine's field-aware door withINVALID_FILTER400 naming the field and its declared type. No new vocabulary is minted. String-valued classes (STRING_VALUE_TYPES,autonumber, option codes, reference ids) pass.formulais 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(GitHubos-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 comparanddoor-refusalrows and the engine'sINVALID_FIELDdoor; 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
selectcodes, 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-refusalrows for the refused classes infilter-comparand-type-conformance.tsor a same-shaped sibling table, referencing the existing sets (⛔ no new set), plus theformula-deferred note (Clause-②: yes,@objectstack/specminor + 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 inpackages/objectql/src/engine.tsbesideINVALID_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/objectqlminor). 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:specunchanged; 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
claude commentedon Sep 5, 2026
Claim: PM loop round R2 —
domain:specseat,session_01M59rPZZFzqhfMUPFqqZTkf, 2026-09-05T07:13Z. Branchclaude/issue-15661-text-operator-declared-type-door-rows,mode:subagent, tierclaude-fable-5-1(=CONTRACT_REVIEW_TIER). Clause-②: yes — a new export on the published@objectstack/spec/dataface and a declared accept-set narrowing (the ruling's ownClause-②: yes).Clause-②: yes
Scope = execution lane (1) of the maintainer ruling C-deny (
5548768100, batch #43, 「同意」), the spec half only: thedoor-refusalrows for the refused classes —NUMERIC_VALUE_TYPES∪BOOLEAN_VALUE_TYPES∪CALENDAR_DATE_TYPES∪INSTANT_TYPES∪CLOCK_TIME_TYPES∪STRUCTURED_JSON_TYPES, all existing sets infield-value.zod.ts(⛔ no new set minted) — plus the passing classes and theformula-deferred note, infilter-comparand-type-conformance.tsor a same-shaped sibling table, with the@objectstack/specminor + 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 intopackages/objectql.Seat readings (
origin/mainb25a5fc32, 2026-09-05T07:11Z): the comparand table'sdoor-refusalshape (filter-comparand-type-conformance.ts:26,:113-120, 12 rows) assertsparseFilterAST(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 waypackages/objectql/src/engine-comparand-type-door.test.tsconsumes the comparand table. #14079's landedNON_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/, regeneratedpackages/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 (#14972system/i18n-resolver.ts, #15430automation/builtin-node-config.zod.ts+shared/expression.zod.ts) and the landing PRs (#15707, #15716): no intersection; the generateddata/shard is shared and lands serially (regen merge before the PR if another PR regenerates it first).Dedup handed to the dev (MCP
search_issues2026-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
wip(spec): text-operator declared-type door contract — sets, verdict,…
wip(spec): fixture id is a plain text field; changeset with BREAKING …
chore(spec): regenerate api-surface and export-origins for the declar…
claude commentedon Sep 5, 2026
Claim (os-dev): branch
claude/issue-15661-text-operator-declared-type-door-rows, PM sessionsession_01M59rPZZFzqhfMUPFqqZTkf(mode:subagent,domain:specseat, 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 modulepackages/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/specminor 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
claude commentedon Sep 5, 2026
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
claude commentedon Sep 5, 2026
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), report5550647890, dev claim5550572330. Readings taken against GitHub and the branch, not the report.Checklist
main, first lineFixes #15661; every other reference (driver-memory's reference matcher answers$notContainsNO for every valued NON-STRING row — the live mingo path answers YES #14079, objectql: refuse a text operator ($containsfamily) over a field whose DECLARED type can never store a string —INVALID_FILTER400 at the engine's field-aware door besideINVALID_FIELD(lane 2 of the #15661 C-deny ruling) #15773, text operator over a temporal column: SQLite matches the stored ISO text, live Postgres refuses with SQLSTATE 42883, and the JS faces answer by stored representation — no face declares the cell #15683, [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, feat(spec): FILTER_TEXT_CASES declares what a text operator answers over a stored non-string value, and every face answers it (#14079) #15686, feat(spec)!: duration-shaped number keys carry their unit in the key name — no-baseline gate + seven ADR-0087 renames (timeoutMs, ttlSeconds/ttlMs, *TimeoutSeconds) #15626, batch Release version 0.1.x for ObjectStack Protocol packages #43) is verb-less. Scope is exactly lane (1) of the ruling: a new contract modulepackages/spec/src/data/filter-text-operator-declared-type.ts+ its test + the barrel line, the two regenerated shards (api-surface/export-origins, +20 each, zero removals, produced bycheck:generated --fix), one changeset. ⛔ Untouched, as dispatched:field-value.zod.ts,filter-text-conformance.ts,filter.zod.ts, the comparand table, every driver,packages/objectql,packages/services,migrations/registry.ts.origin/main(the 13 sets,FieldSchema.returnType, the comparand door's syntax-level shape ⇒ H2 resolved as the sibling module the seat expected). Tests: the new file 23/23; whole spec 474 files / 12734 on8742baf9f;typecheckincl.check:test-typecheckwith the new test file listed; consumer readings on the built closure — the five driver comparand suites,engine-comparand-type-door(12/12) andservice-analyticscomparand-door-single-source(39/39) untouched and green; the built CJS and ESM entries export the new symbols. Ablation emptied the refused set with on-disk proof (spread line 1→0, marker 0→1, blobd9af49037→db284cca7), exactly six pins in the new file went red, restore proved by blob equality; ADR-0087 census overexamples/**+content/docs/**found 26 text-operator occurrences, all over textual declared fields, with a positive control. Gates 72/74 green; the two NOT MEASURED (check:dual-build-cjs-loads,check:type-check-debt) are repo-level prerequisite refusals, declared to CI.TypeScript Type Checkfailure on the earlier headaf59bd254is a superseded run — its four type-check lanes were CANCELLED by the newer push (8742baf9f, the merge ofmainat 08:40Z), and the aggregator reports a cancelled lane as failure; noerror TSwas produced. CI on8742baf9fis running (17 in progress at 08:42Z, 0 red).Clause-② contract judgment (derived judgments itemised):
TEXT_OPERATOR_DOOR_REFUSED_TYPESis the union of the six ruled sets by reference (spreads of thefield-value.zod.tsexports; 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.TEXT_OPERATOR_DOOR_PASSING_TYPES=STRING_VALUE_TYPES∪autonumber∪SINGLE_OPTION_TYPES∪MULTI_OPTION_TYPES(tagsprotected, 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 asys_fileid 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.formula: judged as theFieldTypeits declaredreturnTypenames (textpasses;number/boolean/daterefused through the same sets; absent ⇒deferred) — pinned that the map equalsFieldSchema.returnType's enum and every value is aFieldTypemember. ✓ the ruling's "judged only when readable at the seam".address.city) ⇒deferred, so the engine door cannot re-closefilter-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.FieldTypemember (49) appears in exactly one class row, refused ∪ passing ∪ {formula} =FieldType. ✓ no silent absence.@objectstack/spec/data, shards regenerated; deliberately NOT a driver case-set (not enrolled incheck-driver-conformance.mjs) because drivers sit beneath the door and keep answeringFILTER_TEXT_CASES' row — the correct reading of "driver-memory's reference matcher answers$notContainsNO for every valued NON-STRING row — the live mingo path answers YES #14079's option-A row stays beneath the door". ✓@objectstack/specminor with the BREAKING banner and the ADR-0087not-required (no-migration-prescription)disposition, exactly as the ruling's execution paragraph names; the changeset says the door lands in@objectstack/objectqlunder its own card. ✓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
8742baf9fconcludes green; on MERGED this seat probesTEXT_OPERATOR_DOOR_REFUSED_TYPESonorigin/mainagainst the pre-merge tip, stripspm: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 consumingTEXT_OPERATOR_DOOR_CASESas the module's docblock describes.Generated by Claude Code
3 remaining items
claude commentedon Sep 5, 2026
Landed — PR #15804 merged through the merge queue at 2026-09-05T10:08:37Z, merge commit
6f1ce7d26(squash of head8742baf9f; 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 onorigin/mainat 2026-09-05T11:44Z (tip59953d5a3):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 tipf141e156b: the file is absent (0). Card state:pm:dispatchedstripped 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: verdict5550664975, Clause-② PASS, carriers cleared, pair check ✓, provenance5550805329.Generated by Claude Code
claude commentedon Sep 5, 2026
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 note5551558437. Posted at 12:27Z under theRelease:rule that landed at 11:29Z (#15846), after the release itself.Generated by Claude Code
feat(spec)!: refuse a text operator over a field whose declared type …
fix(objectql)!: having takes the temporal-comparand door where and th…
fix(driver-sql, driver-turso): four filter-refusal doors read the pro…
fix(objectql,metadata-protocol)!: the per-aggregation filter takes wh…
fix(objectql)!: having takes the temporal-comparand door where and th…
fix(objectql)!: having resolves placeholders through the where resolv…
feat(spec): the number-comparand declared-type door's contract and th…
fix(objectql)!: refuse a non-numeric string compared against a number…
fix(service-analytics)!: the analytics read scope and the native wher…
test(spec): the third data/ file group's test titles state each cited…