Filed by the triage seat, as the upstream card objectui#8937 records as owed and not filed.
The divergence, measured on origin/main
| predicate |
site |
rule |
| driver storage |
packages/drivers/driver-sql/src/sql-driver.ts:16896 |
isJsonField = JSON_COLUMN_TYPES.has(type) || !!field.multiple — multiple truthy on any type ⇒ JSON column ⇒ = refused |
| spec |
isMultiValueField (@objectstack/spec/data) |
MULTI_OPTION_TYPES.has(type) || (MULTI_CAPABLE_TYPES.has(type) && multiple === true) |
⇒ They disagree for master_detail / tree / text with multiple: true: the spec predicate says "not multi-value", the driver stores it as a JSON column and refuses = with a 400.
Why it matters — a consumer is already on the wrong side of it
objectui's RelatedList uses the spec predicate to decide whether to compose $contains or =. For those three types it composes =, and the live driver answers 400. objectui#8886's own test pins that behaviour — "SPEC PREDICATE — multiple: true is INERT on a type outside MULTI_CAPABLE_TYPES" — so the divergence is currently pinned on the consumer side.
⭐ Not a regression. Pre-#8886 the same shape sent = too. ⛔ Nobody should read this as caused by that PR; it surfaced there.
The shape of the fix — enforce-or-align, ⛔ not chosen here
Someone has to say which predicate is authoritative for "can this field be compared with =":
- align the driver to the spec predicate ⇒ changes what storage the driver picks for those three types, which is a migration-shaped change on existing data;
- align the spec predicate to the driver ⇒ widens
isMultiValueField, a published-contract change;
- declare they answer different questions — the spec predicate is about authoring semantics, the driver's about storage — and give consumers a storage-facing predicate to use instead. ⇒ Then objectui's use of
isMultiValueField for query shaping is the defect, and the fix is documentation plus a new export.
⚠️ The third reading is the one that would make everybody currently correct, and it is the one nobody has written down.
Lane
Routed domain:spec: every resolution either edits isMultiValueField, edits its contract documentation, or adds a sibling export beside it, and the standing rule is absolute — 「凡触 packages/spec 一律转 domain:spec 座位,不论谁需要它」. ⚠️ Conditional re-route: if the ruling is "the driver aligns", the fix lands in packages/drivers/driver-sql and this becomes domain:engine. ⛔ Not pre-routing on an unmade decision.
⚠️ Neighbour, not duplicate
#17343 (domain:engine, pm:queue, p2) touches the same predicate area — a multiple: true boolean column losing its $contains membership filter because isNonTextColumn reads asymmetrically-filled booleanFields / numericFields registries. ⇒ Different cell, different mechanism, different fix. ⛔ Not folded — but a dispatcher should know the two contend for the same file, and folding gate ① fails on defect shape.
Dedupe: cached open board (579 objectstack issues, 2026-09-10T16:0xZ): isMultiValueField → 0 hits; isJsonField → 3 (#17343, #16184, #15989 — none is this cell); MULTI_CAPABLE_TYPES → 1 (#17343). ⚠️ Positive control: the isJsonField pattern fires, so the zero is a reading. ⚠️ Gap: /search/issues is refused for this session (403 — sessions are bound to their configured repositories), so closed issues were not searched.
Refs: objectui#8937 · objectui#8886 · #17343
Generated by Claude Code
Filed by the triage seat, as the upstream card objectui#8937 records as owed and not filed.
The divergence, measured on
origin/mainpackages/drivers/driver-sql/src/sql-driver.ts:16896isJsonField = JSON_COLUMN_TYPES.has(type) || !!field.multiple—multipletruthy on any type ⇒ JSON column ⇒=refusedisMultiValueField(@objectstack/spec/data)MULTI_OPTION_TYPES.has(type) || (MULTI_CAPABLE_TYPES.has(type) && multiple === true)⇒ They disagree for
master_detail/tree/textwithmultiple: true: the spec predicate says "not multi-value", the driver stores it as a JSON column and refuses=with a 400.Why it matters — a consumer is already on the wrong side of it
objectui's
RelatedListuses the spec predicate to decide whether to compose$containsor=. For those three types it composes=, and the live driver answers 400. objectui#8886's own test pins that behaviour — "SPEC PREDICATE —multiple: trueis INERT on a type outsideMULTI_CAPABLE_TYPES" — so the divergence is currently pinned on the consumer side.⭐ Not a regression. Pre-#8886 the same shape sent
=too. ⛔ Nobody should read this as caused by that PR; it surfaced there.The shape of the fix — enforce-or-align, ⛔ not chosen here
Someone has to say which predicate is authoritative for "can this field be compared with
=":isMultiValueField, a published-contract change;isMultiValueFieldfor query shaping is the defect, and the fix is documentation plus a new export.Lane
Routed⚠️ Conditional re-route: if the ruling is "the driver aligns", the fix lands in
domain:spec: every resolution either editsisMultiValueField, edits its contract documentation, or adds a sibling export beside it, and the standing rule is absolute — 「凡触packages/spec一律转domain:spec座位,不论谁需要它」.packages/drivers/driver-sqland this becomesdomain:engine. ⛔ Not pre-routing on an unmade decision.#17343 (
domain:engine,pm:queue, p2) touches the same predicate area — amultiple: trueboolean column losing its$containsmembership filter becauseisNonTextColumnreads asymmetrically-filledbooleanFields/numericFieldsregistries. ⇒ Different cell, different mechanism, different fix. ⛔ Not folded — but a dispatcher should know the two contend for the same file, and folding gate ① fails on defect shape.Dedupe: cached open board (579 objectstack issues, 2026-09-10T16:0xZ):⚠️ Positive control: the ⚠️ Gap:
isMultiValueField→ 0 hits;isJsonField→ 3 (#17343, #16184, #15989 — none is this cell);MULTI_CAPABLE_TYPES→ 1 (#17343).isJsonFieldpattern fires, so the zero is a reading./search/issuesis refused for this session (403 — sessions are bound to their configured repositories), so closed issues were not searched.Refs: objectui#8937 · objectui#8886 · #17343
Generated by Claude Code