Repository navigation
Description
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/expandare 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
Activity
hotlong commentedon Aug 12, 2026
Triage: routed
domain:cli(the by-id read handler inpackages/rest, same file family as sibling card #8001), held asfinding— the filer's own assessment stands: since #7606 the drop is a located, self-reporting refusal, no caller is silently mis-served, and "not obviously worth paying for" is an honest grade. Grading happens at the findings round; the three options on the card are the menu. Type intent if promoted: Task.One ordering note recorded now: if promoted, option 1 (fold the table on the by-id route) widens that route's accepted spellings — that is surface expansion on a public route, so the promotion grading should run the Feature-vs-Bug boundary test rather than assuming bug-class.
Generated by Claude Code
hotlong commentedon Aug 12, 2026
Finding-grading round: escalated to
needs-user-decision(domain:cli).Premise re-verified on
origin/main:DATA_RECORD_READ_PARAMS = ['select','expand'](rest-server.ts:1782) and the in-code comment at :1779-1780 explicitly defers this question to this card. Widening a public route's accepted spellings is surface expansion — maintainer floor.Question: is
RPC_QUERY_ALIAS_SLOTS(the canonical↔alias table inpackages/spec/src/data/data-engine.zod.ts) universal across data routes, or the list route's ingress only? Options: (1) fold the alias table on the record-read route (canonicalfieldsstarts working); (2) keep the narrow set but refuse unknown spellings loudly instead of dropping them silently; (3) keep + document as-is.Four-prong analysis:
?fields=gets no error and wrong data. Option 2 (loud refusal) is the most error-resistant if the surface is to stay narrow; option 1 removes the inconsistency entirely.Recommendation: option 2 now (loud refusal, no surface growth), with option 1 only if the ruling declares the alias table universal — in which case it should be applied to all data routes in one card, not this route alone. Related: #8001 (sibling, closed this round), #7606 (parent).
Generated by Claude Code
hotlong commentedon Aug 12, 2026
Maintainer ruling — 2026-08-12 (live PM chat,
session_01GxKQfv3k8b6a2d2QrZU411; maintainer sam, verbatim: 「接受你的全部建议。」 accepting the decision-box analysis in full). Moved out of the decision box →pm:queue.Ruling: option 2 — keep the narrow accepted set, but refuse unknown spellings loudly instead of dropping them silently.
GET /data/:object/:idcontinues to acceptselect/expand, and a caller who sendsfields(or any other alias-table spelling) gets a named refusal rather than silently-ignored parameters. No public surface growth.⛔ Option 1 (fold the alias table onto this route) is not taken here: if the alias table is ever declared universal across data routes, that is one card applying it to all data routes, not a per-route widening — and it would need its own ruling. The in-code deferral comment at
rest-server.ts:1779-1780should be updated to record this ruling rather than left pointing at an open question.Rationale on the record: today's shape is the worst of the three — the canonical spelling silently no-ops on this route while an alias works, which is precisely the trap an AI author walks into with no error to learn from. Sibling #8001 was closed this round (each route correct by its own standing rule); this ruling closes the silent-drop hole without creating the cross-route surface expansion that #8001 rejected.
Generated by Claude Code
os-zhuang commentedon Aug 13, 2026
Held this round, not shelved — and the card has shrunk since the ruling. Recording both now (
domain:cliseat #6024, sessionsession_01P7vaLs7bhBPi9m3JyzkhDj, round 1). Stayspm:queue, unassigned.Why held: the maintainer's option-2 work lands in
packages/rest/src/rest-server.ts, and #7912 (nav servability pruning,filterAppForUser) was dispatched into that same file this round. Different regions, same file ⇒ hard serial, never the same batch. This card is next in that file's queue.What re-verifying on
origin/mainturned up — the behavioural half is already there. Line numbers in the ruling have drifted, so anchor on the symbol, not the number:DATA_RECORD_READ_PARAMS = ['select', 'expand']is now at:1515(the ruling says:1779-1780).:6834already callsrefuseUnknownQueryParams(req, res, DATA_RECORD_READ_PARAMS), and that gate responds rather than throws — deliberately, because thecatchbelow rewrites every 400 into a 404, so a refusal routed through it would reach the caller as "no such record".So
?fields=is already refused by name today rather than silently dropped. The ruling's substance is satisfied in code. What remains is the one thing the ruling explicitly asked for and nothing has done:The docblock above
DATA_RECORD_READ_PARAMS(~:1503-1514) still reads as an open alias-coverage question tracked by this card — "an alias-coverage question for the spec table and this handler to settle together". It has been settled: option 2, narrow set kept, ⛔ option 1 (foldingRPC_QUERY_ALIAS_SLOTSonto this route) explicitly not taken, and if the alias table is ever declared universal that is one card covering all data routes with its own ruling.⇒ This is now a comment-only card (S, mechanical,
mode:subagent,model: sonnet) unless the dev measures otherwise. That reframing is itself the trap this lane keeps hitting: a comment pointing at a decided question is a stale-premise generator, and the next reader of that file would have re-litigated a settled ruling. Whoever takes it should verify the refusal really covers the alias spellings before concluding the code half is done — ⛔ do not inherit that from this comment — and if only the prose needs changing, shipping only the prose is the correct delivery, not a thin result.Generated by Claude Code
os-zhuang commentedon Aug 13, 2026
串行解除,认领并派工(
domain:cli座位 #6024,sessionsession_01P7vaLs7bhBPi9m3JyzkhDj,round 2)。claude/issue-8039-alias-ruling-commentmode:subagent(S,按上一轮的重定框)阻塞原因已消失
上一轮我把这张卡按住,理由是 #7912 占着
packages/rest/src/rest-server.ts。该文件本轮已被两次改动并落地 —— 最近一次是 PR #8487(#8373,导出时区),它在 route 里读context.timezone并往下传,+25/-4。该文件的热点队列现在由本卡持有。:1515(DATA_RECORD_READ_PARAMS)和:6834(by-id handler 调refuseUnknownQueryParams)是 PR #8487 之前的测量,现在同样不可信。按符号定位,不要按行号。 这张卡的行号已经错过两代了(裁决里的:1779-1780→ 上一轮的:1515→ 现在未知),这本身就是一条值得记住的车道教训。⛔ 不要继承上一轮结论
上一轮我记的是「行为半边已经在代码里,只剩注释」。那是上一轮的测量,不是本轮的前提。 派工 brief 里已明确要求 dev 自己复验
refuseUnknownQueryParams(req, res, DATA_RECORD_READ_PARAMS)是否真的覆盖了 alias 拼写(fields/populate),再决定代码半边是否已完成 —— 不许从本评论继承。如果复验下来只有散文需要改,那么只交付散文就是正确的交付,不是敷衍的结果。维护者裁决里点名要的就是这件事:
一条指向已决问题的注释是陈旧前提的发生器 —— 下一个读那段代码的人会把一个已经裁掉的问题重新打一遍。
裁决内容(要写进注释的东西)
维护者 2026-08-12 裁定 option 2:保持窄接受集(
select/expand),对未知拼写响亮拒绝而非静默丢弃。⛔ option 1(把RPC_QUERY_ALIAS_SLOTS折叠到本 route)明确不采纳 —— 若将来宣布别名表跨 data routes 通用,那是一张覆盖所有 data routes 的卡,要有它自己的裁决,不是在这里做单 route 加宽。下一位
#8264 排在本卡之后持有
rest-server.ts,已在那边记录。Generated by Claude Code
os-zhuang commentedon Aug 13, 2026
os-dev-report
{ "issue": 8039, "status": "done", "branch": "claude/issue-8039-alias-ruling-comment", "pr": "https://github.com/objectstack-ai/objectstack/pull/8510", "premise_still_valid": true, "summary": "Prose-only delivery, and it is the correct one. Measured the code half myself rather than inheriting the previous round's claim: ran the existing pinned suite rest-server-closed-query-params.test.ts against origin/main before touching anything, driving GET /data/:object/:id's real handler with {fields:'title'} and {populate:'owner'}. Both are refused with a located 400 VALIDATION_ERROR naming select/expand, and getData is never called -- refuseUnknownQueryParams(req, res, DATA_RECORD_READ_PARAMS) already covers both alias-table spellings exactly as maintainer option 2 requires; option 1 (folding RPC_QUERY_ALIAS_SLOTS onto this one route) is correctly not implemented. So the only remaining work was the ruling comment itself: rewrote the docblock above DATA_RECORD_READ_PARAMS (rest-server.ts:1539, located by symbol -- every prior line number on this card was stale) to record the 2026-08-12 ruling instead of pointing at an open question -- narrow set kept deliberately, fields/populate refused by name, option 1 explicitly rejected and why, and what would have to change (a ruling declaring the alias table universal across ALL data routes, one card, never a per-route widening here). Also corrected an inaccuracy in the old comment's own prose, which described fields/populate as 'silently' returning the full record -- that described the pre-#7606 state; today they are refused, not dropped.", "tests": "Build closure: pnpm --filter '@objectstack/rest^...' build -- green. Typecheck: cd packages/rest && npx tsc --noEmit -- clean, no output. Targeted suite run BEFORE the edit (baseline measurement) and again AFTER (regression check on a comment-only diff): npx vitest run rest-server-closed-query-params.test.ts --maxWorkers=2 -- 'Test Files 1 passed (1)', 'Tests 27 passed (27)' both times, identical, including the two alias-refusal cases ('?fields= is refused -- the CANONICAL spelling this route never implemented' and '?populate= -- the expand slot's unimplemented alias -- is refused the same way'). Whole-package sanity pass pre-edit: pnpm --filter '@objectstack/rest' test -- 'Test Files 113 passed (113)', 'Tests 1870 passed (1870)'. Local gates (derived via node scripts/pm/dispatch-gates.mjs against the actual changed paths, one extra family surfaced beyond what the dispatch prompt implied -- check:authz-resolver, matched on the whole-file glob): check:authz-resolver, check:route-envelope, check:meta-type-normalized, check:filter-alias-parity, check:changeset-gate-self-tests, check:objectui-changeset, check:cross-package-test-inputs, check-empty-changeset.mjs, check-changeset-no-major.mjs, check-adr-0087-registration.mjs, check-nul-bytes.mjs -- all green. check:objectui-pin-fresh reports the objectui pin as STALE, but this is pre-existing on origin/main, unrelated to this diff (the matcher fired only because this PR adds a file under .changeset/, and the gate is release.yml-scoped / advisory on ordinary code PRs by its own message).", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
os-zhuang commentedon Aug 13, 2026
已落地并结单 —— PR #8510 merged。
domain:cli座位 (#6024) 评分。落地探针取 PR 新增的
.changeset/data-record-read-alias-ruling-comment.md,在refs/heads/main上存在;同批.changeset/列举里的大量既有条目作正对照。评级:ACCEPT
这张卡最容易出的错是「继承上一轮的测量」,dev 没有犯。
派工时我明确警告过:上一轮我记录的「行为半边已经在代码里」是上一轮的测量,不是本轮的前提,不许继承。dev 照做,而且不是重读一遍源码就算数:
refuseUnknownQueryParams(packages/rest/src/query-allowlist.ts)从DATA_RECORD_READ_PARAMS构造Set(allowed),在 handler 解构{ select, expand }之前就拒掉集合外的任何 query key。?fields=title和?populate=owner各跑一次,断言status === 400、body.error.code === 'VALIDATION_ERROR'、定位消息点名select/expand,以及 —— 最关键的一条 ——getData从未被调用。「仍然 200」正是静默加宽的样子,所以这条断言不是锦上添花。结论:代码半边确实已满足 option 2,于是只交付散文就是正确的交付。dev 也明确说明「本卡不适用 ablation」,并用改前对
origin/main的直接测量替代 predict/run 配对 —— 而不是硬造一个反向验证仪式。行号第三次漂移,按符号定位
dev 记录了当前位置:
DATA_RECORD_READ_PARAMS在rest-server.ts:1539,by-id handler 的refuseUnknownQueryParams调用在:7007。裁决里的:1779-1780、上一轮的:1515/:6834都已作废。交付内容
注释现在记录裁决而不是指向一个已决问题:窄集合有意保留、
fields/populate由refuseUnknownQueryParams点名拒绝而非静默丢弃、option 1 明确否决及其理由、以及什么条件下才会改变(一张宣布别名表跨所有 data routes 通用的裁决,作为一张卡落地,绝不是在这里悄悄单 route 加宽)。维护者裁决里点名要的那件事至此完成。一条指向已决问题的注释是陈旧前提的发生器 —— 下一个读那段代码的人本来会把一个已经裁掉的问题重新打一遍。
顺带
dev 报告
check:objectui-pin-fresh报 objectui pin 陈旧,并测定这是origin/main上既有的、与本 diff 无关(本 PR 不碰.objectui-sha或 console 文件,匹配器只是因为新增了.changeset/下的文件才触发),且该 gate 在普通代码 PR 上是 advisory。判断正确,没有把一个既有状态算进自己账上,也没有顺手去"修"。串行队列
packages/rest/src/rest-server.ts释放,#8264 接手,本轮即派。Generated by Claude Code
feat(rest): closed query-parameter sets become ingress policy, first …
docs(rest): record the objectstack-ai#8039 ruling on DATA_RECORD_READ…