Skip to content

Commit d91c558

Browse files
os-zhuangclaude
andauthored
docs(objectql,metadata-protocol): pin why the two __search doors answer differently (#7876) (#8079)
`$searchFields=__search` is a 400; `select=__search` is a 200 without the key. The maintainer ruled on 2026-08-12 that the divergence is intended (direction C) and asked for the reasoning to be written at both doors so the question is not reopened. Two comments, zero behaviour change: - `ObjectQL.stripSearchCompanionFromRead` — why this door is silent, with the option-B refusal explicitly declined and the condition that would reopen it. - `assertSearchFieldsAreSearchable` — the actual `$searchFields` refusal, which sits in the same file as the projection gate it diverges from. Claude-Session: https://claude.ai/code/session_014C8pAprWdmtecFsEprZax4 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8c767f5 commit d91c558

2 files changed

Lines changed: 54 additions & 0 deletions

File tree

packages/metadata-protocol/src/protocol.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6187,6 +6187,33 @@ export class ObjectStackProtocolImplementation implements
61876187
* `owner_id.name` — plausible from the select/sort axes — would be
61886188
* silently dropped there, and this gate letting it through would
61896189
* reintroduce the fallback it exists to close.
6190+
*
6191+
* ## The PROJECTION axis answers the same name differently — on purpose
6192+
*
6193+
* A name this gate refuses can still be spelled in `select` and come back
6194+
* 200 with the key simply absent: {@link assertProjectionFieldsExist} gates
6195+
* on whether a field is KNOWN, not on whether it is RETURNABLE, and the
6196+
* engine's read path then drops what the caller may not see
6197+
* (`omitInternalFields` for `internal: true` columns,
6198+
* `stripSearchCompanionFromRead` for the hidden `__search` companion).
6199+
* Measured on `__search`: `searchFields=__search` is a 400 here, while
6200+
* `select=__search` is a 200 whose body lacks it.
6201+
*
6202+
* That is not a gap someone forgot to close — it was asked as its own
6203+
* question and ruled intended on 2026-08-12 (#7876, direction C). The two
6204+
* axes are different KINDS of surface. `searchFields` is AUTHORING input:
6205+
* it tells the server how to RUN the query, so a value the server will not
6206+
* honour changes WHICH ROWS come back — the fail-open this gate exists for.
6207+
* `select` is a READ PROJECTION: it names what the caller would like back,
6208+
* the row set is untouched either way, and a column the caller may not see
6209+
* is simply not in the body.
6210+
*
6211+
* ⛔ Do not close the asymmetry by teaching the projection gate to refuse
6212+
* unreturnable columns. That was the alternative on #7876 and it was
6213+
* declined: it converts requests that answer 200 today into failures, for
6214+
* symmetry, on spellings with no measured callers. A real caller burned by
6215+
* a silent drop reopens the question on THAT measurement; the asymmetry
6216+
* alone does not.
61906217
*/
61916218
private assertSearchFieldsAreSearchable(object: string, requested: unknown, param: string): void {
61926219
// Shape first, BEFORE the field-map tiering below — same order as the

packages/objectql/src/engine.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4909,6 +4909,33 @@ export class ObjectQL implements IObjectQLEngine {
49094909
* not one. `isSystem` is server-derived (never client input), the same
49104910
* trust the read-only strips on the write path already place in it.
49114911
*
4912+
* ## Why this door is SILENT where the `$searchFields` door returns a 400
4913+
*
4914+
* Asked on #7876 and ruled there on 2026-08-12 (direction C): the divergence
4915+
* is intended, and it is not reopenable on symmetry alone. The two doors are
4916+
* two KINDS of surface.
4917+
*
4918+
* - `$searchFields` is AUTHORING input — it tells the server how to RUN the
4919+
* query. A value the server will not honour has to be said out loud, or
4920+
* the caller gets a WIDER answer than the one they narrowed to, in a
4921+
* response with nothing to distinguish it from a satisfied one. That is
4922+
* why `assertSearchFieldsAreSearchable` refuses the name with a 400
4923+
* (#4254) — the refusal is protecting the ROW SET.
4924+
* - `select` is a READ PROJECTION — it names what the caller would like
4925+
* back. Dropping a column the caller may not see leaves the answer
4926+
* correct: the rows are still the rows that were asked for, one key
4927+
* lighter. {@link omitInternalFields} directly above answers
4928+
* `?select=id,key` exactly this way, for exactly this reason (#7728), so
4929+
* silence here is the platform's existing read-path rule, not an
4930+
* exception to it.
4931+
*
4932+
* ⛔ Do not add a refusal here to make the two doors agree. That was option B
4933+
* on #7876, weighed and declined: it turns a request that answers 200
4934+
* today into a failure, for tidiness, on a spelling no non-system caller in
4935+
* this tree uses — the companion's only deliberate reader is the backfill
4936+
* carved out above. If a REAL caller is ever burned by the silent drop, that
4937+
* measurement reopens it; the asymmetry by itself does not.
4938+
*
49124939
* ⚠️ `requestedFields` must be the CALLER's `fields`, captured before
49134940
* `planFormulaProjection` — that pass rewrites the projection to every stored
49144941
* column when a formula is in play, companion included.

0 commit comments

Comments
 (0)