Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions packages/metadata-protocol/src/protocol.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6154,6 +6154,33 @@ export class ObjectStackProtocolImplementation implements
* `owner_id.name` — plausible from the select/sort axes — would be
* silently dropped there, and this gate letting it through would
* reintroduce the fallback it exists to close.
*
* ## The PROJECTION axis answers the same name differently — on purpose
*
* A name this gate refuses can still be spelled in `select` and come back
* 200 with the key simply absent: {@link assertProjectionFieldsExist} gates
* on whether a field is KNOWN, not on whether it is RETURNABLE, and the
* engine's read path then drops what the caller may not see
* (`omitInternalFields` for `internal: true` columns,
* `stripSearchCompanionFromRead` for the hidden `__search` companion).
* Measured on `__search`: `searchFields=__search` is a 400 here, while
* `select=__search` is a 200 whose body lacks it.
*
* That is not a gap someone forgot to close — it was asked as its own
* question and ruled intended on 2026-08-12 (#7876, direction C). The two
* axes are different KINDS of surface. `searchFields` is AUTHORING input:
* it tells the server how to RUN the query, so a value the server will not
* honour changes WHICH ROWS come back — the fail-open this gate exists for.
* `select` is a READ PROJECTION: it names what the caller would like back,
* the row set is untouched either way, and a column the caller may not see
* is simply not in the body.
*
* ⛔ Do not close the asymmetry by teaching the projection gate to refuse
* unreturnable columns. That was the alternative on #7876 and it was
* declined: it converts requests that answer 200 today into failures, for
* symmetry, on spellings with no measured callers. A real caller burned by
* a silent drop reopens the question on THAT measurement; the asymmetry
* alone does not.
*/
private assertSearchFieldsAreSearchable(object: string, requested: unknown, param: string): void {
// Shape first, BEFORE the field-map tiering below — same order as the
Expand Down
27 changes: 27 additions & 0 deletions packages/objectql/src/engine.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4853,6 +4853,33 @@ export class ObjectQL implements IObjectQLEngine {
* not one. `isSystem` is server-derived (never client input), the same
* trust the read-only strips on the write path already place in it.
*
* ## Why this door is SILENT where the `$searchFields` door returns a 400
*
* Asked on #7876 and ruled there on 2026-08-12 (direction C): the divergence
* is intended, and it is not reopenable on symmetry alone. The two doors are
* two KINDS of surface.
*
* - `$searchFields` is AUTHORING input — it tells the server how to RUN the
* query. A value the server will not honour has to be said out loud, or
* the caller gets a WIDER answer than the one they narrowed to, in a
* response with nothing to distinguish it from a satisfied one. That is
* why `assertSearchFieldsAreSearchable` refuses the name with a 400
* (#4254) — the refusal is protecting the ROW SET.
* - `select` is a READ PROJECTION — it names what the caller would like
* back. Dropping a column the caller may not see leaves the answer
* correct: the rows are still the rows that were asked for, one key
* lighter. {@link omitInternalFields} directly above answers
* `?select=id,key` exactly this way, for exactly this reason (#7728), so
* silence here is the platform's existing read-path rule, not an
* exception to it.
*
* ⛔ Do not add a refusal here to make the two doors agree. That was option B
* on #7876, weighed and declined: it turns a request that answers 200
* today into a failure, for tidiness, on a spelling no non-system caller in
* this tree uses — the companion's only deliberate reader is the backfill
* carved out above. If a REAL caller is ever burned by the silent drop, that
* measurement reopens it; the asymmetry by itself does not.
*
* ⚠️ `requestedFields` must be the CALLER's `fields`, captured before
* `planFormulaProjection` — that pass rewrites the projection to every stored
* column when a formula is in play, companion included.
Expand Down
Loading