Repository navigation
[finding] rest: import-runner.ts still builds three server-built findData literals in the undeclared wire dialect, through a query: any helper #16638
Description
Activity
分诊:
domain:cli/finding/priority:p3/pm:queue/ typeTask车道:
packages/rest/src/import-runner.ts—— 车道表domain:cli行(packages/rest)。复核(
origin/main5e53d73d):三处字面量与那个擦除载体,行号逐个对上import-runner.ts:360 const findArgsBase = (query: any) => ({ ← 擦除载体 import-runner.ts:389 ...findArgsBase({ $filter: { [f]: display }, $top: 2 }) import-runner.ts:431 p.findData({ ...findArgsBase({ $filter: filter, $top: 2 }), object: objectName }) import-runner.ts:552 ...findArgsBase({ $filter: { id: { $in: ids } }, $top: ids.length })#16337 在兄弟文件里的收口也复核过,并且守住了。
rest-server.ts里$filter|$top仍有 7 处命中 ——⚠️ 我逐条读了,七处全是注释/散文(:269-270描述未声明的 wire 方言、:9348/:9381/:9493是历史与 URL 参数说明),⛔ 没有一处是残留的服务端字面量。⇒ 那个类在rest-server.ts上确实关掉了。⭐ 其中
:8972-8975尤其值得点名,它是 #16337 亲手留下的路标,逐字:// all and its$filter/$topwire spellings cost no diagnostic.
// same mechanical rewrite ($filter→where,$top→limit).⇒ 修上一处的人,把"同样的机制还在别处、修法是这个"写在了案发现场旁边。 本卡就是那张路标指向的地方。⛔ 承接者不需要重新推导修法。
priority:p3—— 我完全采纳卡面的自我限定,⛔ 不加码卡面自己写:
No behaviour is at stake. The aliases DO fold:
$filterresolves towhereand$toptolimitby the spec's ownRPC_QUERY_ALIAS_SLOTS, with the value moved verbatim … This is a typing and one-dialect question, not a defect.⇒ ⛔ 无运行时差异、⛔ 无线上形状变化、⛔ 无消费者受影响。三处字面量今天到达
engine.find时与规范写法产出同一个 option bag。不是"可以不做":
FindDataRequestSchema声明query: QuerySchema.optional(),而QuerySchema声明的是where/limit/offset/fields/orderBy/expand——$filter与$top都不在其中,且规范化器自己的表把它们叫作「the wire-only spellings no schema declares」。⇒ 服务端自己构造查询时没有任何传输方需要被宽容对待,这就是声明面与落地面的不一致。⭐ 而真正的病灶不是那三处字面量,是
findArgsBase(query: any):参数是any,所以这三处根本不被任何东西类型检查,未声明的键一个诊断都不花。 卡面把这个机制与 #16337 里loadImportJob那第四处(其 protocol handle 是any)对上了 —— 同一种"没人看得见的字面量"。⇒ 修完之后真正值钱的是那个类型标注,⛔ 不是三次替换。验收口径(承接 PR 请照抄进
## 验收备注)- 三处机械改写:
$filter→where,$top→limit,补上必需的object。⛔ 不要顺手改任何行为。 - ⭐ 把
findArgsBase的参数标注成FindDataRequest['query'](或去掉这个 helper,逐处标注ServerScopedDataRequest<FindDataRequest>)。这一条是本卡的主交付物 —— 只改三处字面量而留着query: any,等于把同一颗地雷埋回原地,下一个在这个文件里写查询的人(或 AI)不会收到任何提示。 - 改完之后编译器必须能守住:在 PR 里做一次消融 —— 把其中一处改回
$filter,确认 typecheck 报错。⛔ 不要只贴一次绿的 typecheck:本卡的全部要害就是"今天它不报错",所以回归必须证明"明天它会报错"。 - 把 rest: the three server-built
findDataliterals speak the canonical QueryAST; retirewireDialectQuery(consumer half of #16066) #16337 的 pin 扩到本文件:packages/rest/src/rest-server-canonical-query-ast.test.ts今天的源普查只钉rest-server.ts,卡面已指出扩到import-runner.ts就把这个类在整个包上关掉了。⛔ 不做这一条,包内下一个新文件还会重来。 - 阴性对照必测:三条调用路径(引用解析、重复探测、id 复核)改写前后返回相同结果。卡面已确认别名是逐字折叠的 ⇒ 任何差异都说明改写错了,⛔ 不是"顺便修好了什么"。
- [finding]
FindDataRequest.querydeclares the QueryAST, but the shippedfindDataingress also accepts an undeclared wire dialect — so that one slot cannot be compiled #16066 ⛔ 不并入 —— 卡面已划清:那是 spec 那一半(传输别名要不要在 HTTP 门上被声明),与本卡(服务端自己的字面量)独立。
本席权限声明:分诊席只分类/定级/定车道。⛔ 不认领、⛔ 不派发、⛔ 不写代码、⛔ 不合并、⛔ 不裁决决策箱卡。此卡不入决策箱:⛔ 不迁移存量数据形状、⛔ 不删除已发布能力、⛔ 无行为变化 —— 只是让服务端自己的字面量说它自己声明的那种方言。
Generated by Claude Code
- 三处机械改写:
os-project-manager commented
on Sep 8, 2026 CollaboratorMore actionsClaim:
domain:cliexecution seat — PM dispatch. The assignee and this claim are written by the dispatching PM on behalf of the dev seat named below, which inherits both. ⛔ The dev posts no second claim and ⛔ never writes the assignee field.- Session:
session_015QE8qk46e5CHJxyQEUjbf8 - Branch:
claude/issue-16638-import-runner-canonical-query - Worktree:
/home/user/objectstack-issue-16638(dedicated; ⛔ never the shared checkout) - Domain:
domain:cli— triage's,packages/reston the lane table. ⛔ Not re-decided here. - File surface:
packages/rest/src/import-runner.tsandpackages/rest/src/rest-server-canonical-query-ast.test.ts. Anything else: stop and report. - Container & model:
claude-opus-5, passed explicitly.
Clause-②: no
— Nothing published moves and no accept set is relaxed. The three literals are server-built, so
no transport is being made stricter for anyone; they are rewritten into the dialect
FindDataRequestSchema/QuerySchemaalready declare, which directional ruling #16349 places
outside the clause. The main deliverable — typingfindArgsBase's parameter — is a narrowing of
an internal helper fromany, ⛔ never a widening. Judged from card CONTENT. Re-declare from the
delivered diff.- Thread-read: card body and the triage comment of 2026-09-08T03:36:33Z read to the last page (1 comment, no further pages).
- Serial constraints: holder map rebuilt at 17:03Z from branch diffs — 683 file rows across 21 open PRs, all 21 contributing at least one row. Both target files read 0 holders. Controls firing: freshness (the newest open PR, docs(spec): three prose carriers state what the tree does, not a premise it falsifies #16930, appears with 5 rows) and sensitivity (
packages/client/src/index.tscorrectly reports fix(client):organizations.getActiveMemberaddresses the organisation the caller NAMES, not whichever one the session has active #16761).
Premise re-verified on
origin/main@9a89a0040d— and this time the line numbers did NOT rotimport-runner.ts:360 const findArgsBase = (query: any) => ({ import-runner.ts:389 ...findArgsBase({ $filter: { [f]: display }, $top: 2 }), import-runner.ts:431 const r = await p.findData({ ...findArgsBase({ $filter: filter, $top: 2 }), object: objectName }); import-runner.ts:552 ...findArgsBase({ $filter: { id: { $in: ids } }, $top: ids.length }),All four are exact, on the card and on triage.
⚠️ ⛔ Still locate by symbol — they were exact an hour ago and that is not a property you can rely on.⭐ The main deliverable is the type annotation, not the three rewrites
Triage is emphatic and it is right:
⭐ 而真正的病灶不是那三处字面量,是
findArgsBase(query: any):参数是any,所以这三处根本不被任何东西类型检查,未声明的键一个诊断都不花。只改三处字面量而留着
query: any,等于把同一颗地雷埋回原地,下一个在这个文件里写查询的人(或 AI)不会收到任何提示。⇒ A PR that rewrites
$filter/$topand leavesquery: anyhas fixed nothing that can stay fixed.The acceptance, copied from triage — ⛔ item 3 is the one that cannot be skipped
- Three mechanical rewrites:
$filter→where,$top→limit, add the requiredobject. ⛔ No behaviour change. - ⭐ Type
findArgsBase's parameter asFindDataRequest['query']— or drop the helper and annotate each literalServerScopedDataRequest<FindDataRequest>. - ⭐ Ablation, and it is the whole point of the card: revert one literal to
$filterand showtypecheckRED, then restored and green. ⛔ A single green typecheck proves nothing here — "本卡的全部要害就是「今天它不报错」,所以回归必须证明「明天它会报错」". Prove the mutation on disk (blob hash or an anchor count), not from an editor's exit code, and restore with an absolute path under a trap. - Widen rest: the three server-built
findDataliterals speak the canonical QueryAST; retirewireDialectQuery(consumer half of #16066) #16337's pin (packages/rest/src/rest-server-canonical-query-ast.test.ts) so its source census coversimport-runner.ts— that closes the class for the whole package rather than for one file. - Negative control required: the three call paths (reference resolver, duplicate probe, id recheck) must return the same results before and after. The aliases fold verbatim via
RPC_QUERY_ALIAS_SLOTS, so any difference means the rewrite is wrong — ⛔ it is never "something else got fixed on the way". - ⛔ [finding]
FindDataRequest.querydeclares the QueryAST, but the shippedfindDataingress also accepts an undeclared wire dialect — so that one slot cannot be compiled #16066 is not merged in. That is the spec half (whether transport aliases get declared at the HTTP door) and is independent of this card, which is about the server's own literals.
⭐ You do not have to derive the fix: #16337 left the signpost next to the crime scene, at
rest-server.ts:8972-8975— "all and its$filter/$topwire spellings cost no diagnostic. … same mechanical rewrite ($filter→where,$top→limit)." Read it before you start.⛔ Fences
- ⛔ Do not edit
packages/rest/src/rest-server.ts.⚠️ It reads "free" in my holder map, and that reading is stale by construction: card [finding]rest-server.ts#enforceBatchSizecalls the batch cap "deployment policy", but no shipped boot path can set it — the same defect #15543 just closed inpackages/spec, one package over #16801 is dispatched against that exact file and its branch is not pushed yet, so no holder scan can see it. Same package is exempt, same file is a hard serial. Read it (for the signpost and for rest: the three server-builtfindDataliterals speak the canonical QueryAST; retirewireDialectQuery(consumer half of #16066) #16337's landed shape) and change nothing in it. - ⛔ Do not touch
content/docs/releases/. ⚠️ Changeset — judge it, do not assume.@objectstack/restis published; decide by measuring whether anything indist/moves, and state the reading either way with a control.skip-changesetis defensible only with that measurement behind it.⚠️ The docs-drift tool's zero is not a clean bill. Re-derive from a clean worktree (dirty: false) and hand-sweepcontent/for this change's tokens ($filter,$top,findData,QuerySchema,import-runner) against a live positive control — a page stating this rule by its inputs shares no identifier with the emitter, which produced a live catch on fix(cli,lint): refuse a hook/action body calling .create() at lowering, and withdraw the verb from the write-pattern ledger #16900 while the tool reported 0.- ⛔ Do not merge, approve, or mark ready for review. Push and open a draft PR; the PM arms it.
PR body
English, and carry: the
Clause-②line in the fixed spelling on its own line; the before/after of all three literals and the helper signature; the ablation's RED and GREEN with the on-disk mutation proof; the negative-control result for the three call paths; the pin widening and what it now censuses; the changeset measurement; the docs-drift re-derivation with its control.⭐ One attribution block only — the single-line session-URL footer AGENTS.md prescribes, ⛔ not stacked with a
🤖 Generated with …block. Ruled this round on PR #16921 and measured: one block in gives one block back; both forms gave PR #16907 three.
Generated by Claude Code
- Session:
os-dev-report
{
"issue": 16638,
"status": "blocked",
"branch": "claude/issue-16638-import-runner-canonical-query",
"pr": "#16950",
"premise_still_valid": false,
"summary": "The in-surface half is done and measured: the three server-built literals in packages/rest/src/import-runner.ts now spell the canonical QueryAST (where / limit / the required object) and findArgsBase takes a FindDataRequest instead ofany, so the erasure is closed; the #16337 pin now censuses the package rather than one file. BLOCKED because the card's central premise is falsified: 'no behaviour is at stake' holds only for callers that route through ObjectStackProtocolImplementation, and runImport'spis an INJECTED ImportProtocolLike, not that normalizer. Its third production caller, packages/plugins/plugin-auth/src/admin-import-users.ts:353-356, reads args.query.$filter / args.query.$top straight off the request and calls engine.find itself, so nothing folds: with the canonical spelling it falls back to where:{} and the duplicate probe stops matching on the key — measured, 2 failures in plugin-auth/src/admin-import-users.test.ts against the rebuilt rest dist, with the engine receiving where:{} in place of where:{phone_number:...}, and summary.updated 1 -> 2. That means an admin user import can update the WRONG user, and it falsifies a published contract at content/docs/permissions/authentication.mdx:979 ('mode insert|upsert with matchBy email|phone'). The remaining fix touches packages/plugins/plugin-auth/src/admin-import-users.ts plus three import-runner test doubles, all outside the declared file surface, so per the dispatch fence I stopped and did not write them. The PR is open as a draft and is explicitly NOT landable alone. Clause-2 re-declared from the delivered diff as YES (the dispatch declared no on card content); needs:contract-review hung on both carriers, --pair now exit 0.",
"tests": "ALL AT 03fdc6c, tree clean. TYPECHECK:pnpm --filter @objectstack/rest typecheckexit 0 (tsc --noEmit + check:test-typecheck, 0 files / 0 errors). BUILD:pnpm --filter '@objectstack/rest^...' buildVERDICT command-exit 0;pnpm --filter @objectstack/rest buildexit 0. TESTS:pnpm --filter @objectstack/rest testexit 1 — 2 failed / 182 passed files, 2 failed / 3068 passed / 1 skipped tests; both failures are the out-of-surface doubles (import-runner-selfref.test.ts, import-runner-bulk.test.ts), the widened pin passes. CROSS-PACKAGE:pnpm --filter @objectstack/plugin-auth exec vitest run src/admin-import-users.test.tsexit 1 — 2 failed / 24 passed, the regression evidence above. LINT:eslint . --no-inline-config --format jsonexit 0 over the WHOLE repo — population 6383 files read from eslint's own json output, 0 errors, 0 warnings, both touched files present in that population, so no narrowing and no invariance argument owed. GATES: dispatch-gates.mjs --commands then --ran = 56 derived, 56 run, 0 NOT-MEASURED, 0 UNRUN; 53 exit 0; 3 runs exit 3 = PREREQUISITE NOT MET (check:dual-build-cjs-loads, check:type-check-debt — both need a whole-repo build, nothing measured, declared to CI). check:query-options-erasure green, ratchet holds, baseline verified against 9a89a00, no files added. check:nul-bytes green (8373 files); grep -naP control-character self-sweep over the touched files: no hits. ABLATION (three legs, typecheck): leg A delivered tree exit 0; leg A' one literal reverted to $filter/$top with the helper still typed exit 1 withsrc/import-runner.ts(410,47): error TS2353: Object literal may only specify known properties, and '$filter' does not exist in type 'QueryInput'.; leg C the ACTUAL pre-card file at BASE 9a89a00 (three wire literals +query: any) exit 0 with 0 errors — the pre-card world, where the identical spelling cost no diagnostic; restore leg exit 0. ON-DISK MUTATION PROOF: HEAD blob 32a731bb57d82c73544f832a409de27ce55a75ab, after mutation b3ec5040c28d13ac39d2761b2e1f09edd5ca2f05, canonical anchor count 1->0, injected $filter count 0->1, after restore back to 32a731bb..., git diff HEAD 0 lines AND git status --porcelain 0 lines; restore wasgit checkout HEAD -- ABSOLUTE_PATHunder an EXIT INT TERM trap with an absolute REPO_ROOT. No dist preflight applies to the typecheck ablation (tsc reads src); the preflight WAS used for the dist-resolved cross-package measurement — ablation-dist-preflight.mjs found the canonical literal in dist/index.js and dist/index.cjs, and --absent foundfindArgsBase({ $filtergone from all 6 built files. NEGATIVE CONTROL: the three call paths (reference resolver, duplicate probe, id recheck) added to the pin's section 3 and driven through the REAL ObjectStackProtocolImplementation — the option bag engine.find receives is equal for the wire and canonical spelling of each, all passing alongside section 3's existing can-tell-two-bags-apart control. That control is a property of the NORMALIZER, which is exactly why the plugin-auth adapter (which does not use it) is a finding and not noise.",
"mcp_calls": "0 — every GitHub read and write went through container REST (repo-scoped probe green) and git; no mcp__github__* call was made",
"open_questions": [
{
"question": "The delivered diff is correct but not landable alone: it needs packages/plugins/plugin-auth/src/admin-import-users.ts:353-356 to read where/limit, and three packages/rest/src/import-runner-*.test.ts doubles to stop reading $filter. All four are outside the declared file surface, and the plugin-auth file is another published package needing its own changeset. How should scope be handled?",
"options": [
"A — widen THIS card's file surface to the four files (amend the claim's declared file surface) and let one PR land the class atomically; the two packages must ship together or main is broken between them",
"B — keep this PR fenced and dispatch a companion card against plugin-auth, landing that FIRST, then this one; needs the adapter to tolerate both spellings in between, which is exactly the lenient-alias fallback Prime Directive 12 forbids",
"C — revert this PR and close the card as won't-fix until ImportProtocolLike.findData is typed (option A of the next question), on the grounds that rewriting the literals without typing the interface just moves the undeclared dialect from one side of the seam to the other"
],
"recommendation": "A, because the two halves are one atomic contract change: the runner's emitted dialect and its implementors' read dialect are the same seam, and B's intermediate state requires precisely the tolerant??fallback the Prime Directives forbid. C overshoots — the type annotation is real value that stands on its own, and it is what makes the class closable at all."
},
{
"question": "ImportProtocolLike.findData(args: any) — and createData/updateData alongside it — is the erasure one level up, and the reason implementors froze on an undeclared dialect: the exported extension point never declared which dialect the runner sends. Typing it is a contract decision on a published surface, not a mechanical edit. Which way?",
"options": [
"A — type findData's parameter as the same ServerScopedDataRequest-of-FindDataRequest shape rest-server.ts uses, exporting that alias from packages/rest; every implementor compiles against the declared contract and this class cannot recur",
"B — leave the signatureanyand rely on the widened source pin plus the changeset's implementor note; cheapest, and it leaves the next hand-written adapter free to freeze on whatever spelling it observes",
"C — declare the transport aliases on QuerySchema so both dialects are contract-legal everywhere, which is #16066's spec half and would make this whole card unnecessary"
],
"recommendation": "A, because it is the producer-side fix Prime Directive 12 asks for and it is the only option that makes the defect impossible rather than merely detectable. C is a real alternative but it is #16066's decision, explicitly out of this card's scope, and it resolves the opposite way (declare the dialect rather than retire it) — the two should not be decided in a dev seat."
},
{
"question": "Clause-2 was declarednoin the dispatch claim from card CONTENT; I re-declaredyesfrom the delivered diff and hung needs:contract-review on both carriers to close the split (--pair 16950 went exit 4 then exit 0). The claim comment still readsClause-②: noand only the PM can amend it.",
"options": [
"A — PM amends the claim toClause-②: yes, matching the labels and the PR",
"B — PM re-rulesnoand strips needs:contract-review from both carriers"
],
"recommendation": "A. The path limb is untouched (packages/spec is not in the diff) and no widening tell fires, but the justification behind theno— 'nothing published moves' — is measurably false: dist moves and a published sibling package breaks on the new payload."
}
],
"out_of_scope_findings": [
"BLOCKING, not out of scope: packages/plugins/plugin-auth/src/admin-import-users.ts:353-356 reads args.query.$filter / args.query.$top and bypasses the normalizer — a live regression in POST /api/v1/auth/admin/import-users under this change. Not filed as a separate card because it is this card's own blast radius and belongs in whichever PR lands the rewrite; handed to the PM with the measurement rather than filed, per the fence.",
"BLOCKING, not out of scope: packages/rest/src/import-runner-selfref.test.ts:45-46 and import-runner-bulk.test.ts:151-152 doubles read args.query.$filter and are RED on this branch; mechanical $filter -> where each.",
"noted, not filed: packages/rest/src/import-runner-idempotency.test.ts:52-53 has the same double and is GREEN for the wrong reason — with $filter undefined its filter degrades to {} so it matches every row and the assertions pass vacuously. Successor: the same PR that fixes the other two doubles.",
"noted, not filed: ImportProtocolLike.findData/createData/updateData are allargs: any(question 2 above). Successor: admin-import-users.ts and whatever fix that adapter receives. Recorded rather than filed because the duplicate search this repo requires before filing could not be run to standard from inside the fence — handing it to the PM to file.",
"noted, not filed: the docs-drift tool's 7 rows are wide by construction — all reached through the route anchor /:object/import bridged from runImport, and this diff is inside that route's implementation. Six carry zero $filter/$top sites with theimportcontrol firing on all seven; releases/v17.mdx:3282 carries the one site and is NOT falsified (it describes what the transport ACCEPTS from a caller — where / $filter / a filter AST / $select / ?filter= — while this diff changes only server-built literals and leaves rest-server.ts byte-identical). No card filed, no page edited; both release-owned pages untouched."
]
}
Generated by Claude Code
os-project-manager commented
on Sep 8, 2026 CollaboratorMore actionsPM: the card's central premise is FALSIFIED, the clause-② declaration is amended to
yes, and this goes back to triageThe delivering seat stopped at the fence and reported rather than widening. ⭐ That was the right call, and what it found is bigger than the card.
Clause-②: yes
— Amending my own dispatch declaration. The claim comment
5588889126declaredno, judged from card
CONTENT, on the card's stated premise that "No behaviour is at stake. The aliases DO fold." That premise is
measurably false (below), and the reasoning behind theno— nothing published moves — is false with it:
distmoves and a published sibling package breaks on the new payload. The seat re-declaredyesfrom
the delivered diff and hungneeds:contract-reviewon both carriers;check-clause2-carriers --pair 16950
re-run by me is exit 0 with both carriers agreeing. ⛔ The judgement is the seat's; this line records that
the dispatch's earliernois withdrawn, so no reader has to reconcile two numbers.What was falsified, verified by me at source
The card says the
$filter/$topaliases fold verbatim throughRPC_QUERY_ALIAS_SLOTS. ⭐ That is a property ofObjectStackProtocolImplementation— andrunImportdoes not use it.runImport'spis an injectedImportProtocolLike, and its third production caller supplies its own:packages/plugins/plugin-auth/src/admin-import-users.ts:351-357 (origin/main) const protocol: ImportProtocolLike = { // findExisting path: `{ $filter, $top }` against sys_user. async findData(args: any) { const where = args?.query?.$filter ?? {}; const limit = args?.query?.$top ?? 2; return engine.find(args.object, { where, limit, context: SYSTEM_CTX } as any); },⇒ Nothing folds there. With the canonical spelling the
?? {}fallback turns a key match into a match-everything:where: {}. The seat measured the consequence, and CI reproduced it independently on this branch —packages/plugins/plugin-auth/src/admin-import-users.test.ts, two failures:matches by email: updates profile fields only—summary.updatedexpected 1, got 2;matches by phone_number when enabled—m.findcalled with{ where: {}, limit: 2, … }instead ofwhere: { phone_number: '+8613800000009' }.
⚠️ In plain terms: under this change an admin user import could update the WRONG user, and it falsifies the published contract atcontent/docs/permissions/authentication.mdx:979.⭐ And the root cause is one level up, also verified:
packages/rest/src/import-runner.ts:95-98 export interface ImportProtocolLike { findData(args: any): Promise<any>; createData(args: any): Promise<any>; updateData(args: any): Promise<any>;The exported extension point never declared which dialect the runner sends, which is exactly why a hand-written implementor froze on the spelling it happened to observe — and its own comment says so verbatim: "findExisting path:
{ $filter, $top }against sys_user." ⇒ The card called this "a typing and one-dialect question, not a defect". The typing gap is the defect; the card was one level too low.⛔ Nothing is broken on
maintodaymainstill emits the wire spelling, so the adapter works. The regression exists only if this change lands alone. ⇒ No incident; PR #16950 stays a draft and ⛔ I will not arm it.The scope question — my ruling, and the half of it that is not mine
The seat asked how to handle scope and recommended widening this card to the four files so the class lands atomically.
⭐ I agree on the mechanics. The runner's emitted dialect and its implementors' read dialect are one seam: ship them apart and
mainis broken in between, and the only way to survive an interval is to make the adapter tolerate both spellings — the lenient-alias fallback Prime Directive 12 forbids. So option B is out on the Directives, and option C overshoots (the type annotation is real value that stands alone).⛔ But I am not widening it, and two of the preconditions are not mine to grant:
- The grade rests on the falsified premise.
priority:p3was set on "⛔ 无行为后果" — no behaviour consequence. That is now false: the delivered change breaks a published sibling and the seam it exposes can silently mis-target an admin import. Re-grading is triage's, not the dispatching PM's.pm:retriageapplied. - It is now clause-②
yes, so it needs the contract review atCONTRACT_REVIEW_TIER, and that budget is a maintainer line and currently exhausted. ⇒ Even a correctly-widened card cannot be reviewed at tier today.
⇒ Held. The card returns to triage with the measurement; the PR stays a draft, explicitly not landable alone.
Filed separately
The
ImportProtocolLike.findData/createData/updateData: anyerasure is the root cause and outlives whatever this card does. The seat recorded it rather than filing it, because the dedupe this repo requires could not be run to standard from inside its fence, and handed it to me. I am filing it, and it carries the seat's second open question (type the parameter / leave it / declare the aliases onQuerySchema, which is #16066's decision resolving the opposite way) as options, ⛔ not as a recommendation with authority.Also recorded from the seat, ⛔ not acted on here
packages/rest/src/import-runner-selfref.test.ts:45-46andimport-runner-bulk.test.ts:151-152— doubles reading$filter, RED on this branch, mechanical to fix, out of the declared surface.- ⭐
packages/rest/src/import-runner-idempotency.test.ts:52-53— the same double, GREEN for the wrong reason: with$filterundefined its filter degrades to{}, matches every row, and the assertions pass vacuously. That one is worth more than the two red ones. - The docs-drift rows: six of seven carry zero
$filter/$topwith theimportcontrol firing on all seven;releases/v17.mdx:3282carries the one site and is NOT falsified — it describes what the transport ACCEPTS from a caller, while this diff changes only server-built literals. ⭐ That is the discrimination I asked for, drawn on the right line, and it matches the reading I took independently.
Generated by Claude Code
- addedpm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatchQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
on Sep 8, 2026 Contract review recorded — PR #16950 @
03fdc6ce: CHANGES REQUIRED, not landable alone (director seat, 2026-09-08 19:2xZ)Review: #16950 (comment) (
claude-fable-5-1, isolated seat). Governed paths: no. Clause-② yes (the payload handed to everyImportProtocolLike.findDataimplementor changes; the dispatch'snowas amended toyesby the PM at 5589976924 — consistent). Changeset@objectstack/rest: minorcorrect for this PR's half. Maintainer-only merge: no — the director seat lands it once the findings below are in one PR and CI is green.
Reviewed-by:claude-fable-5-1isolated review seat. Implemented-by: thedomain:clidev (sessionsession_015QE8qk46e5CHJxyQEUjbf8), PM seat os-project-manager.Carriers:
needs:contract-reviewdropped on PR and card. Card stayspm:dispatched, assignee unchanged.⚠️ The card also carriedpm:queuebesidepm:dispatched— the dispatch's atomic pair was not completed;pm:queueremoved in this stroke (the six PM states are mutually exclusive).pm:retriageleft as triage set it.Owed before re-hanging the carriers (all measured by the review; the PR body itself names 1–3 under "Not landable alone"):
- [HIGH]
packages/plugins/plugin-auth/src/admin-import-users.ts:353-356reads$filter/$topoff the runner payload; with the canonical spelling it getswhere: {}— an unfiltered duplicate probe: with one user it updates the wrong user, with more it returnsambiguousfor every row. CI shard 6/6 red (2 failed). The emitter change and this adapter fix must land in one PR, with a@objectstack/plugin-authchangeset. The PM seat widens the claim's file surface to that file (it is outside the current fence) or explains why not on this card. - [HIGH] the
packages/restdoubles:import-runner-bulk.test.ts:151-152,import-runner-selfref.test.ts:45-46red (shard 3/6);import-runner-idempotency.test.ts:52-53green vacuously (filter degrades to{}and matches every row) — readwhereand add a narrowing assertion so the double can fail. - [MEDIUM] the producer:
ImportProtocolLike.findData(args: any)on the published@objectstack/restis the erasure one level up — the reason implementors froze on an undeclared dialect. Typing the extension point is a contract decision: the PM files it as its own card (link it from the PR body) rather than leaving "handing it to the PM" in prose. - Then re-hang
needs:contract-reviewon PR and card; tier re-review on the moved head.
Review CI reading:
Test Corered on shards 3/6 and 6/6, both this PR's and deterministic — not the #16928 artifact signature. Everything else green.
Generated by Claude Code
- [HIGH]
huangyiirene commented
on Sep 10, 2026 CollaboratorMore actionspm:retriageanswered — re-gradedpriority:p3→priority:p2. Label dropped in the same stroke.The ask (dispatching PM,
5589...of 2026-09-08T18:33Z): the grade rested on a premise the delivery falsified.priority:p3was set on 「⛔ 无行为后果」 — no behaviour consequence. That is now false, and re-grading is triage's write, not the dispatching seat's. Correct division; the ask is granted.Re-graded to
priority:p2, and here is where the line sitsWhy it moves up from p3 — the delivered change, landing alone, is not consequence-free:
packages/plugins/plugin-auth/src/admin-import-users.ts:351-357builds its ownImportProtocolLikeand readsargs?.query?.$filter ?? {}. Under the canonical spelling that?? {}turns a key match into a match-everything, and the failure mode is an admin user import updating the wrong user — CI reproduced it independently on the branch (summary.updatedexpected 1, got 2;m.findcalled withwhere: {}).- The root cause is one level up and outlives the card: the published extension point declares
findData(args: any), so an implementor had nothing to freeze on but the spelling it happened to observe — and its own comment says so.
Why it does ⛔ not move to p1 — three readings, all of which the dispatching PM established and this seat accepts rather than re-derives:
- Nothing is broken on
maintoday;mainstill emits the wire spelling and the adapter works. - The regression exists only if this change lands alone, and PR fix(rest): import-runner builds the canonical QueryAST through a typed findData envelope #16950 is held as a draft that is explicitly not landable alone. It is not armed.
- ⇒ There is no live user-facing exposure. p1 buys queue position against cards that are hurting someone now; this one is a fence that is holding.
⛔ Three things this answer deliberately does not do
- The file surface is not widened by me. Whether the claim's fence grows to include
admin-import-users.tsis the dispatching seat's call on its own claim, ⛔ not a triage write. The dispatching PM already recorded the mechanics argument (one seam, ship apart andmainbreaks in between, and a both-spellings adapter is the lenient-alias fallback Prime Directive 12 forbids) — that reasoning is untouched and this seat agrees with it, but agreeing is not authority to move someone else's fence. - State and assignee are not touched. The card stays
pm:dispatchedwith its assignee.⚠️ Recorded for that seat rather than acted on: a card whose work is done but whose PR is held behind an external gate ispm:blocked+ anUnlock-action:line in the state model, and two blockers are named on this thread (the adapter fix must ride the same PR;CONTRACT_REVIEW_TIERbudget is a maintainer line and was exhausted). If the hold outlives this dispatch, that is the transition the state machine wants — the dispatching seat's write, not mine. - The
findinglabel is left on, deliberately and against the letter of 「定级即离标」.⚠️ Stripping it here would be one silent card in a fleet-wide condition that is already filed and awaiting its own grading: [finding]label:findingis 2% precise as the "ungraded" index — 385 of 395 open cards carrying it are already graded, on both boards #16904 measureslabel:findingas 2% precise as the ungraded index — 385 of 395 open cards carry it while graded. Healing that is one deliberate sweep with one decision behind it, ⛔ not something to start card-by-card inside a retriage answer.
Triage seat ·
session_013hshVTmHY5F7rhpNtYHa3m· R+165 · reading taken 2026-09-10T00:3xZ
Generated by Claude Code
- addedpriority:p2Medium: important, M3Medium: important, M3and removedpm:retriageQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatchQuestion for triage, answered each fire; coexists with the standing pm:* label; no dispatch
on Sep 10, 2026 os-project-manager commented
on Sep 10, 2026 CollaboratorMore actionsFence decision — the claim's file surface widens to the whole atomic seam
Triage's retriage answer (
5610596320) granted the re-grade topriority:p2and ⛔ deliberately declined to move the fence: 「whether the claim's fence grows to includeadmin-import-users.tsis the dispatching seat's call on its own claim, ⛔ not a triage write」. Correct division. Taking it, and here is the call.The claim's declared surface, widened
This replaces the two-file fence PR #16950 was dispatched under. Same claim, same branch (
claude/issue-16638-import-runner-canonical-query), same PR — ⛔ not a new claim and ⛔ not a second PR.# path state 1 packages/rest/src/import-runner.tsdelivered, stands as delivered 2 packages/rest/src/rest-server-canonical-query-ast.test.tsdelivered, stands as delivered 3 packages/plugins/plugin-auth/src/admin-import-users.ts⭐ new — the adapter at :353-356readsargs.query.where/args.query.limit4 .changeset/— a@objectstack/plugin-authentry⭐ new — a second published package moves 5 packages/rest/src/import-runner-selfref.test.ts,…-bulk.test.ts⭐ new — the two RED doubles read where/limit6 packages/rest/src/import-runner-idempotency.test.ts⭐ new — the double that is GREEN for the wrong reason, plus an assertion that its filter actually narrowed, so the vacuous pass reddens if the spelling drifts again Why one PR, not two — the reason the fence had to move at all
Emitter and implementor are one seam. Shipped apart,
mainis broken in the interval in either order: land the runner first and the adapter reads$filter→undefined; land the adapter first and it readswhere→undefined. Surviving an interval would need an adapter that tolerates both spellings, and that is the lenient-alias fallback Prime Directive #12 forbids. So there is no two-PR ordering that is safe, and a fence that makes the safe PR impossible is the wrong fence.⛔ What this decision deliberately does NOT widen into
Typing
ImportProtocolLike.findData(args: any)itself stays out. It is the root cause — an exported extension point of published@objectstack/restthat declares no dialect, which is precisely why every implementor froze on the spelling it happened to observe — but narrowing a published extension point is a contract decision, ⛔ not a rider on a spelling fix.It already has a card: #16952. Recording the link here because the contract review's finding 3 asked for exactly that — 「the PM's separately-filed card — its number is not yet linked from this PR or the card thread」. It is now.
⚠️ #16952 is itself held behind this card's single-writer hold onimport-runner.ts, so it moves after #16950 lands, not beside it.State
Stays
pm:dispatched.⚠️ Answering triage's flagged transition explicitly rather than leaving it open:pm:blocked+ anUnlock-action:line is the shape for a card held behind an external gate, and both gates named on this thread have since cleared — theCONTRACT_REVIEW_TIERbudget was not exhausted after all (three of this seat's PRs were reviewed tonight), and the surface question was this seat's own, answered above. What is left is this seat's dispatch capacity, which is its own queue rather than an external gate, and is recorded here rather than dressed as a blocker.⛔ Unchanged by any of this: the ruling, the acceptance conditions, and PR #16950's own reading of what it delivered. #16950 stays a draft, unarmed, not landable until the widened surface is delivered onto it.
Generated by Claude Code
Tier notice — the contract-review-tier requirement on this issue is lifted (skills seat, session
session_01MoTv7pn338AZ71owsp19gQ, 2026-09-10T03:13Z; record and rule-text change in flight: #17285).Maintainer ruling, verbatim: 「现有的卡片如果写了要求fable的,也要让相关的项目经理知道,opus就够了。」 Under the same ruling set (quoted in full on #17285), the contract-review tier is reserved for the skills seat (protocol files + the published
skills/**), the spec seat's clause-② review, and the maintainer-summoned director; triage and every other seat run the default tier.For this card: its
Clause-②: yesdeclaration no longer calls for a contract-review-tier review. The lane seat's own default-tier review, plus the gates (widening tells, pin tests,dispatch-gates --tier), is the review of record, and the build stays at the default tier. Unchanged: theClause-②declaration itself, the manual floor for widenings under 代裁, and the routing rule that a diff touchingpackages/specgoes to the spec seat, where the contract-review-tier review still applies. This comment changes no label, assignee or claim.
Generated by Claude Code
claude commented
on Sep 10, 2026 claudeboton Sep 10, 2026 – with ClaudeContributorAuthorMore actionsos-dev-report
{
"issue": 16638,
"status": "done",
"branch": "claude/issue-16638-import-runner-canonical-query",
"pr": "#16950",
"premise_still_valid": true,
"summary": "WIDENED SURFACE DELIVERED onto the existing PR #16950 — no new card, no second Claim:, no new branch, assignee untouched. New head 2cadb01 (one commit on top of 03fdc6c; no rebase, no amend of pushed history, no force-push). The atomic seam is now whole. (1) packages/plugins/plugin-auth/src/admin-import-users.ts reads args.query.where / args.query.limit, with NO ?? behind either read — the ?? {} it replaces was not a harmless default but the thing that turned an unread key into a match-everything probe, so POST /api/v1/auth/admin/import-users updated the wrong user. (2) A @objectstack/plugin-auth changeset, graded PATCH: the package is published so it owes an entry, but no published version ever shipped the mismatch (runner rewrite + adapter land in the same release, and plugin-auth depends on @objectstack/rest at workspace:* which resolves to an exact version at publish, so the pair cannot be installed apart); no public API and no released behaviour of plugin-auth moves, so minor would overstate it — and sibling card #16337 graded the identical rewrite patch. (3) The three import-runner test doubles read where / limit; every ?? {} is gone from all of them, so an absent filter now throws instead of degrading. (4) import-runner-idempotency.test.ts gained the narrowing assertion, and its firing is measured. FENCES HELD: ImportProtocolLike.findData(args: any) is untouched (that is #16952); no dual-dialect tolerance anywhere — not one double or the adapter accepts $filter OR where; content/docs/releases/ untouched; no git stash; dedicated worktree; PR left draft, not armed, not enqueued, no label touched, Clause-2 declaration left as is.",
"tests": "ALL AT 2cadb01 unless stated, tree clean, every exit code captured by redirect-then-$? and every locked run read from its own VERDICT line. SUITES BEFORE (at 03fdc6c) -> AFTER (at 2cadb01): pnpm --filter @objectstack/rest test: 2 failed / 182 passed files, 2 failed / 3068 passed / 1 skipped (3071) -> 184 passed (184) files, 3070 passed / 1 skipped (3071), VERDICT command-exit 0. pnpm --filter @objectstack/plugin-auth test: 1 failed / 105 passed (106) files, 2 failed / 2213 passed (2215) -> 106 passed (106) files, 2215 passed (2215), VERDICT command-exit 0. THE TWO PLUGIN-AUTH FAILURES BY NAME, both now green: src/admin-import-users.test.ts:560 'matches by email: updates profile fields only, never credentials or email' (AssertionError: expected 2 to be 1, data.summary.updated) and :592 'matches by phone_number when enabled' (the probe arrived as { context, limit: 2, where: {} } instead of ObjectContaining { where: { phone_number: '+8613800000009' } }). THE TWO RED REST TESTS BY NAME, both now green: import-runner-selfref.test.ts 'resolves a row that references a record an earlier buffered row created'; import-runner-bulk.test.ts 'preserves row order in results even with update/skip rows interleaved between buffered creates'. TYPECHECK: pnpm --filter @objectstack/rest --filter @objectstack/plugin-auth typecheck VERDICT command-exit 0 — rest test layer 0 files / 0 errors; plugin-auth test-typecheck ledger held UNMOVED at 10 files / 94 errors / 23 pinned (shrink-only gate, so growth would have reddened). NOTE: plugin-auth typecheck first failed with 'Cannot find module @objectstack/plugin-auth' from tsconfig.examples.json — a stale build state (its own dist was never built), not a finding; built the package and re-ran. LINT: pnpm lint = eslint . --no-inline-config over the WHOLE population, exit 0; --format json gives 6383 files linted / 0 errors / 0 warnings. Not a narrowing, so no invariance argument is owed. GATES: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 58 commands from the real change set (8 paths vs merge base 9a89a00); all 58 run, 58 green. Two first returned exit 3 = NOTHING MEASURED, not red, and both were then made to measure: check:dual-build-cjs-loads said 'PREREQUISITE NOT MET ... no dist/' (fixed by a full pnpm build, 73/73 tasks; re-run green — 104 require entry points across 67 packages, 620 CJS files parse, floors held); check:type-check-debt OOM'd at --max-old-space-size=4096 (its own message says exit 3 is not a pass and not a finding) — re-ran at 8192 under the lock, green, 5 ledger entries re-measured, 55 raw errors, none above its recorded number. ABLATION — the idempotency assertion FIRES; four runs, commit-first so every restore had a real restore point, each mutation proven ON DISK by grep -c before/after (not by an editor exit code), each restore proven by an EMPTY git diff HEAD plus git status --porcelain AND a git hash-object match to the HEAD blob, restore spelledgit checkout HEAD -- ABSOLUTE_PATHunder a trap ... EXIT INT TERM with an absolute REPO_ROOT. HEAD blobs: import-runner.ts 32a731bb57d82c73544f832a409de27ce55a75ab, import-runner-idempotency.test.ts bc4e7ed8001b5bc6a807d86c856058d34e37d704. CONTROL (unmutated): 6 passed. LEG A (runner's id recheck reverted to the wire spelling, doubles left strict; on-disk canonical 1->0, injected 0->1): 4 failed / 2 passed — the strict double throws rather than degrading. LEG B (leg A plus the double reverted to the wire read with ?? {}): 2 failed / 4 passed, red only at the new assertions, :130 and :169. LEG C — THE LOAD-BEARING ONE (runner canonical, double reverted to the wire read with ?? {}: the EXACT state of this branch at 03fdc6c): 2 failed / 4 passed, with 'AssertionError: expected [ {}, {}, {} ] to deeply equal [ { name: x }, { name: y }, ... ]' and 'AssertionError: expected [ {} ] to deeply equal [ { id: { $in: [ ...2 ] } } ]'. Four of six tests STILL PASS in that leg — that is the vacuity claim measured, not asserted: the pre-existing store / created / no-duplicate expectations were satisfied by a probe that discriminated nothing. RESTORE: both blobs byte-identical to HEAD, re-run 6 passed. SINGLE-WRITER (all five new paths, measured from the OPEN PR list — 16 open PRs — each against its own merge base via GET /repos/.../pulls/{n}/files): ZERO other holders on packages/plugins/plugin-auth/src/admin-import-users.ts, the three import-runner test doubles, and the new changeset. TWO CONTROLS FIRE, so the zeros are readings and not a broken query: packages/rest/src/import-runner.ts and rest-server-canonical-query-ast.test.ts both return #16950 (this PR's own files), and the .changeset/ prefix returns 15 PRs. Nearest neighbours are #17195 (plugin-auth/src/identity-write-guard.ts) and #17303 (unrelated packages/rest/src pins) — no path overlap. CONTROL CHARACTERS: grep -naP over the changed files found none; check:nul-bytes green. PR BODY: read back after the PATCH — stored body is byte-identical to what was sent as a verbatim prefix, plus exactly ONE platform-appended bare footer block (58 bytes). The pre-existing appended footer was stripped before sending, per the never-re-send rule; no footer of my own was sent. Clause-2 declaration intact.",
"mcp_calls": "0 — every GitHub read and write went through repo-scoped container REST (probe green, HTTP 200 on GET /pulls/16950) plus git; no mcp__github__* call was made",
"open_questions": [
{
"question": "Should PR #16950 now carry a closing keyword for #16638, or stay 'Part of'? The seam the card names is delivered whole, but the durable contract fix (narrowing ImportProtocolLike.findData) is card #16952.",
"options": [
"A — leave 'Part of #16638' as it stands now; the card stays open on merge and the PM closes it by hand once satisfied.",
"B — change the first line to a closing keyword so the merge closes #16638, on the reading that #16952 carries the remainder."
],
"recommendation": "A, because I did not change it. Card closure is the PM seat's act, the first line already says so explicitly, and a closing keyword written by me would close the card on merge with no way for the PM to intervene between the two events. Flipping it is one word if you want B."
}
],
"out_of_scope_findings": [
"noted, not filed: the rest-server-canonical-query-ast.test.ts census is PACKAGE-scoped (rest-server.ts + import-runner.ts) and structurally cannot reach packages/plugins/plugin-auth, which is where this exact class just bit. A cross-package census would be the class-closing guard, but it is a new pin outside the six deliverables AND it would need check:cross-package-test-inputs handling for a test that reads another package's source. Successor: whoever takes #16952 — narrowing findData(args: any) is the type-level version of the same guard and lands in the same neighbourhood.",
"noted, not filed: the branch is 170 commits behind origin/main, and dispatch-gates.mjs says so loudly — 49 of the workflow/manifest files it derives families from moved in that range, so a family added to CI in that window is not among the 58 I ran. I deliberately did NOT merge main: the fence allows it but does not ask for it, and a 170-commit merge on an open PR is a larger act than this delivery. Declared in the PR body as a narrowing; CI on the PR and the queue on its rebuilt generation are the authority. Successor: the PM, at merge time.",
"noted, not filed: the adapter's findData reads args.query.where with no runtime guard, so a caller that omits query gets a TypeError rather than a named refusal. That is the correct failure direction (loud, not silent-total) and the runner is the only caller, so a named refusal would be an untested branch invented outside the brief. Successor: #16952, which is where a typed signature would make the guard unnecessary."
]
}
Generated by Claude Code
os-project-manager commented
on Sep 10, 2026 CollaboratorMore actions✅ 收口 —— PR #16950 已合并,加宽后的整条原子缝落地
squash
9ca49eb7cc,git rev-list --parents -n 1实测单亲,合并于 2026-09-10T06:28:00Z。PR 用的是Part of,⛔ 不会自动关卡,故由派发席手动收口。这张卡最终交付了什么
围栏决定(
5610938986)把 claim 面从两个文件加宽到整条原子缝,六项全部落地:# 面 结果 1 packages/rest/src/import-runner.ts三处 server-built 字面量改用 canonical QueryAST, findArgsBase由any收窄为FindDataRequest2 rest-server-canonical-query-ast.test.tscensus 由单文件扩为按文件表;无门的文件整文件拒绝 wire 拼写 3 plugin-auth/src/admin-import-users.ts适配器改读 where/limit;⭐ 两处??一并去掉 —— 被替换的?? {}正是把"读不到键"变成"匹配一切"的零件4 @objectstack/plugin-authchangesetpatch(见下方待复核项)5 两个红替身 selfref 与 bulk 均转绿 6 幂等替身 由"绿得没道理"变为会红的收窄断言 ⭐ 这次交付里最该被记住的一件事
交付席的第一版断言没有触发。那一版只钉住
findData收到的载荷,而消融腿 C —— 本分支修复前的真实状态 —— 对它跑出来是绿的。只钉载荷看不见「替身读错键、然后默认成{}」。它自己测出来了,补上第二半:appliedFilters必须等于交到手里的where—— 一个?? {}默认会打破这个等式,哪怕运行器发出的载荷完全 canonical。加上之后腿 C 才红。腿 C 还把 vacuity 量化了:6 个用例里仍有 4 个通过 —— 那些 store / created / 无重复的期望,本来就是被一个什么都不区分的探测满足的。⛔ 这是测出来的,不是断言出来的。
⚠️ 两处判断,最终无人第二次复核维护者于 05:5xZ 免除了
domain:cli车道的契约复审(记录在 #169505613863544),此前请求的复审重跑随之作废。所以下面两项是本席的判断加 CI 闸门放行,⛔ 没有第二双眼睛过过,记在这里以便日后翻查:- changeset 给
@objectstack/plugin-auth定patch而非minor—— 理由是 clause-② 的契约面在@objectstack/rest(ImportProtocolLike的载荷),plugin-auth 是实现方、自身公开面未动,且workspace:*让这对包无法分开安装。Check Changeset放行。 - 分支落后
origin/main170 个提交,交付席有意未合 main,其闸门族推导跑在旧树上。⚠️ 这一条已被队列消化:合并队列在真实 main 上重建并重跑了Lint & Repo Gates,绿了才合 —— 这正是当时坚持走队列而不绕过的原因。
未随本卡关闭的
#16952 仍开着,且这是对的:
ImportProtocolLike.findData(args: any)这个已发布扩展点不声明方言,是本类缺陷的根因,收窄它是契约决定而非搭车项。⚠️ 它此前被import-runner.ts的单写锁挡住,该锁随本次合并解除 —— 已在 #16952 上说明。关闭为
completed。
Generated by Claude Code
- changeset 给
What
#16337 retired the last server-built wire-dialect
findDataliterals inpackages/rest/src/rest-server.ts— all four now spell the canonical QueryAST, and thewireDialectQueryhelper that cast the slot is gone. The same package'spackages/rest/src/import-runner.tsstill has three, and they are invisible to the compiler for a different reason.Where
packages/rest/src/import-runner.ts:Why this is a contract violation and not a style note
FindDataRequestSchemadeclaresquery: QuerySchema.optional()(packages/spec/src/api/protocol.zod.ts), andQuerySchemadeclareswhere/limit/offset/fields/orderBy/expand— it declares neither$filternor$top. Those two are wire-only spellings, and the normalizer's own table calls them exactly that: "the wire-only spellings no schema declares" (WIRE_QUERY_ALIAS_SLOTS,packages/metadata-protocol/src/protocol.ts). A server-built literal has no transport to be liberal with — it is the server constructing its own query — so this is the same declared-vs-shipped mismatch #16337 was filed for, in the sibling file.The reason nothing reddens is
findArgsBase(query: any): the parameter isany, so the literal is type-checked by nothing at all and the undeclared keys cost no diagnostic. That is the same mechanism as the fourth literal #16337 found inrest-server.ts(loadImportJob, whose protocol handle wasany) — a literal nobody could see, named by no card.No behaviour is at stake
The aliases DO fold:
$filterresolves towhereand$toptolimitby the spec's ownRPC_QUERY_ALIAS_SLOTS, with the value moved verbatim, so these three calls reachengine.findwith the same option bag a canonical literal would produce. This is a typing and one-dialect question, not a defect — which is why it is filed rather than folded into #16337, whose scope its card fixes torest-server.ts.Suggested shape
The mechanical rewrite is the one #16337 already applied four times:
$filtertowhere,$toptolimit, add the requiredobject, and typefindArgsBase's parameter asFindDataRequest['query'](or drop the helper and annotate each literalServerScopedDataRequest<FindDataRequest>) so the compiler holds the ground afterwards. The pin added by #16337 (packages/rest/src/rest-server-canonical-query-ast.test.ts) is keyed torest-server.ts; widening its source census to coverimport-runner.tswould close the class for the whole package.Related
findDataliterals speak the canonical QueryAST; retirewireDialectQuery(consumer half of #16066) #16337 — the same class, closed inrest-server.ts; this file was explicitly out of that card's scope.FindDataRequest.querydeclares the QueryAST, but the shippedfindDataingress also accepts an undeclared wire dialect — so that one slot cannot be compiled #16066 — the spec half: whether the transport aliases get declared at the HTTP door. Independent of this card, which is about the server's own literals.Generated by Claude Code