Found while implementing #7606 (measuring that route's closed query-parameter set). Filed unassigned per Prime Directive #10; not fixed there, because widening a route's accepted spellings is surface expansion, not the recognition policy #7606 landed.
The condition
packages/spec/src/data/data-engine.zod.ts declares the alias table (RPC_QUERY_ALIAS_SLOTS) — "the ONE place the alias → canonical mapping is declared":
| slot |
canonical |
alias |
| fields |
fields |
select |
| expand |
expand |
populate |
GET /data/:object/:id reads exactly one line off the query string:
const { select, expand } = req.query || {};
So it honours the alias of the fields slot (select) and the canonical of the expand slot (expand), and folds nothing. ?fields=title and ?populate=owner never reach getData.
The asymmetry is per-slot and reads as arbitrary from outside: the same caller writing ?fields= and ?expand= gets one of the two honoured.
Why it mattered before #7606, and what changed
Until #7606 the drop was silent: ?fields=title returned the FULL record with a 200, indistinguishable from a caller who asked for everything — the exact silent-widening shape that card exists for.
Since #7606 the route declares a closed set (DATA_RECORD_READ_PARAMS = ['select', 'expand']) and both spellings are now refused with a located 400 naming select / expand as what the route accepts. So the failure is no longer silent, and this is no longer a correctness hole — it is an inconsistency with the declared alias table.
The two spellings were deliberately left OUTSIDE the closed set rather than added to it: putting them in the allowlist without implementing the fold would advertise a capability the handler does not have (declared ≠ enforced, Prime Directive #10), which is worse than refusing.
The sibling route already folds
GET /data/:object (list) passes its whole query to the metadata-protocol normalizer, which folds by the same table, extended with the wire-only spellings (filters, $filter, $expand). So ?fields= works on the list route and is refused on the by-id route.
The options
- Fold the table on the by-id route — read the slots through
foldQueryAliasSlots (or the normalizer) and add the folded spellings to the closed set. One rule, declared in one place, both routes agreeing. Costs an alias-conflict answer (?fields=a&select=b) that the route does not have today.
- Leave it. The refusal is located and self-reporting, and
select / expand are the documented spellings for this route. The table then describes the list route's ingress, not the whole data surface.
- Retire the divergence from the other end — decide the by-id route's two names ARE the contract and note in the table that it is not universal.
Not obviously worth paying for; recording it so the next person to touch either the alias table or this handler is choosing rather than discovering.
Dedup
Searched open issues for the alias-table identifier, for select/fields/populate spelling wording, and for by-id read-route query handling. No open card covers it; #7606 is the parent this came out of, #8001 is the sibling recording a different cross-route divergence on the same file.
Generated by Claude Code
Found while implementing #7606 (measuring that route's closed query-parameter set). Filed unassigned per Prime Directive #10; not fixed there, because widening a route's accepted spellings is surface expansion, not the recognition policy #7606 landed.
The condition
packages/spec/src/data/data-engine.zod.tsdeclares the alias table (RPC_QUERY_ALIAS_SLOTS) — "the ONE place the alias → canonical mapping is declared":fieldsselectexpandpopulateGET /data/:object/:idreads exactly one line off the query string:So it honours the alias of the fields slot (
select) and the canonical of the expand slot (expand), and folds nothing.?fields=titleand?populate=ownernever reachgetData.The asymmetry is per-slot and reads as arbitrary from outside: the same caller writing
?fields=and?expand=gets one of the two honoured.Why it mattered before #7606, and what changed
Until #7606 the drop was silent:
?fields=titlereturned the FULL record with a200, indistinguishable from a caller who asked for everything — the exact silent-widening shape that card exists for.Since #7606 the route declares a closed set (
DATA_RECORD_READ_PARAMS = ['select', 'expand']) and both spellings are now refused with a located400namingselect/expandas what the route accepts. So the failure is no longer silent, and this is no longer a correctness hole — it is an inconsistency with the declared alias table.The two spellings were deliberately left OUTSIDE the closed set rather than added to it: putting them in the allowlist without implementing the fold would advertise a capability the handler does not have (declared ≠ enforced, Prime Directive #10), which is worse than refusing.
The sibling route already folds
GET /data/:object(list) passes its whole query to themetadata-protocolnormalizer, which folds by the same table, extended with the wire-only spellings (filters,$filter,$expand). So?fields=works on the list route and is refused on the by-id route.The options
foldQueryAliasSlots(or the normalizer) and add the folded spellings to the closed set. One rule, declared in one place, both routes agreeing. Costs an alias-conflict answer (?fields=a&select=b) that the route does not have today.select/expandare the documented spellings for this route. The table then describes the list route's ingress, not the whole data surface.Not obviously worth paying for; recording it so the next person to touch either the alias table or this handler is choosing rather than discovering.
Dedup
Searched open issues for the alias-table identifier, for
select/fields/populatespelling wording, and for by-id read-route query handling. No open card covers it; #7606 is the parent this came out of, #8001 is the sibling recording a different cross-route divergence on the same file.Generated by Claude Code