Repository navigation
fix(metadata-protocol)!: the Data API refuses an empty projection and a count flag that is neither true nor false - #26811
Conversation
… refused on every list door (red) Claude-Session: https://claude.ai/code/session_01FEkQ5kRR5RttkLiWWAkDA5 Co-authored-by: Claude <noreply@anthropic.com>
…hat is neither true nor false The list normalizer read both as the absent default: a blank or comma-only $select became [] and was served as no projection (every column), and a $count other than true/false stayed a string the opt-out never matched, so the count ran. Both are now refused 400 VALIDATION_ERROR naming the parameter and the value, in findData, which every list door shares; getData's select takes the same projection rule. The accepted count values are read from the spec's QueryTransportParamsSchema.count. Claude-Session: https://claude.ai/code/session_01FEkQ5kRR5RttkLiWWAkDA5 Co-authored-by: Claude <noreply@anthropic.com>
…ing projection-refusal status Claude-Session: https://claude.ai/code/session_01FEkQ5kRR5RttkLiWWAkDA5 Co-authored-by: Claude <noreply@anthropic.com>
…d count flag refusals Claude-Session: https://claude.ai/code/session_01FEkQ5kRR5RttkLiWWAkDA5 Co-authored-by: Claude <noreply@anthropic.com>
… engine predicate, and pin it Claude-Session: https://claude.ai/code/session_01FEkQ5kRR5RttkLiWWAkDA5 Co-authored-by: Claude <noreply@anthropic.com>
… pin Claude-Session: https://claude.ai/code/session_01FEkQ5kRR5RttkLiWWAkDA5 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 27 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 11 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 11 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin ad522bc4c40197647b519187de311f4937edbc12 && git checkout ad522bc4c40197647b519187de311f4937edbc12
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c8583f80ac47177421c9c1fb71e5e33e431ad9cf 28a10e6f8b29849c8cf41595bc7ed213f9081972 && git checkout -B drift-repro c8583f80ac47177421c9c1fb71e5e33e431ad9cf && git merge --no-ff 28a10e6f8b29849c8cf41595bc7ed213f9081972
node scripts/docs-audit/affected-docs.mjs --json c8583f80ac47177421c9c1fb71e5e33e431ad9cf
|
Contract reviewServed-tier: Inputs read at 2026-10-27T17:15Z: card #26644 body and its three comments (triage Check-runs on the head at read time: completed success — Auto Label, Check PR Size, Check Changeset, Flag docs affected by code changes, PR-body guards, Governed Surface Queue Guard, Lint · PM dispatch-gates, filter, Test UI (aggregate; its shards skipped), Dogfood Regression Gate (aggregate; its three shards and Dogfood Verify CLI skipped — by design on ① Derived judgmentsAccept-set changes the diff implies, each judged:
② Semver level
③ Boundary flagsDev
Dev Dev
Other flags raised by this review:
Implemented-by: Rendered by the seat's subagent (read-only, at 2026-10-27T17:15Z); adopted by the seat session named above. VERDICT: PASS |
席位核对 · ACCEPT ·
|
| item | reading |
|---|---|
| Shape | Draft, base main, Fixes #26644 under triage 6441434576 (p2). 6 files: protocol.ts (findData and getData), three door tests (protocol, REST, dispatcher), one added entry in scripts/engine-double-contract.pinned.json that the new test's findOne double requires, and a patch changeset with a BREAKING block. Clause-②: no (narrowing) and Metadata-protocol: none appear in both the body and the changeset. |
| Fix | findData refuses a projection that names no field (select=, $select=, fields: []) and a count flag that is not true/false, with 400 VALIDATION_ERROR naming the spelling the caller wrote and the value, before any engine read. The count accept set is read from spec's QueryTransportParamsSchema.shape.count, with no spec edit. getData takes the same projection rule. Absent parameters keep their default. |
| Clause-② | The order said yes (narrowing). The seat accepted the dev's no (narrowing), and the review confirms it: scripts/pm/clause2-line.mjs reserves yes (narrowing) for a diff that widens one surface and narrows another, and this diff widens nothing. |
| Dogfood battery | Re-run at the seat's request, pinned to 28a10e6f8b (this head, nothing pushed), full, no shard: 302 test files passed, 3 skipped; 2507 tests passed, 20 skipped (declared skips only); command exit 0. Evidence at pm-orders/server/evidence/26644/dogfood-28a10e6f8b.txt. The earlier run at 25a8556236 differed from the head by 2 lines in a runtime test. review-checklist.md :50 is met. |
| Evidence | CI green on head 28a10e6f8b (34 success, 11 skipped, 0 failed). Governed check: NOT governed, 694 changed lines. Red first on the base: 21 + 3 + 8 failures across the protocol, REST and dispatcher pins. 70 of 70 derived gates exit 0, reconciled with --ran. |
| Carriers | RestServer's GET-by-id catch answers the refusal 404 (a rewrite that predates this PR; ?select=nope already answers 404): #26816. HttpFindQueryParamsSchema declares count and select wider than the door, in packages/spec: #26817 for the spec seat. data-api.mdx gets no new row: its select table lists answers for named fields, and the refusal is stated in the changeset. |
| Landing | 「按照 pm-dispatch 规则复核过的都授权」 (Leehom, 2026-10-25T11:35Z): ready and auto-merge through the relay, once every check on this head is green or an expected skip. |
Generated by Claude Code
Requested by Leehom · project thread
Fixes #26644
Clause-②: no (narrowing)
Metadata-protocol: none
BREAKING (accept-set narrowing,
patch): the Data API refuses an empty projection and a count flag that is neither true nor false with400 VALIDATION_ERROR, where it answered 200. ADR-0087 disposition in the changeset:not-required (no-migration-prescription).维护者速读
$select(如?$select=、?$select=,)或不是 true/false 的$count(如?$count=maybe),现在直接回 400 并点名参数和取值,不再当作「没传」处理。$select会返回全部字段,乱写的$count会照样统计总数,调用方拿到看似正常的 200,发现不了自己写错了。packages/metadata-protocol/src/protocol.ts的这次改动。packages/metadata-protocol/src/protocol.ts(projectionFieldsValue、countFlagValue两个函数及findData/getData里的调用点)、packages/rest/src/data-empty-projection-count-door.test.ts、packages/runtime/src/domains/data-empty-projection-count.test.ts。pnpm --filter @objectstack/metadata-protocol exec vitest run src/protocol.empty-projection-unknown-count.test.ts,预期 37 个用例全绿。What changes
findData(the list normalizer every list door shares). A projection (fields, and its foldedselect/$selectspellings) that names no field is refused: a blank or comma-only string, or an empty array. It used to become[], whichassertProjectionFieldsExisthas nothing to check and every driver serves as no projection, so every column came back under a 200. Acount/$countthat is not the boolean or the stringtrue/falseis refused; it used to stay unlowered, so theoptions.count === falseopt-out never matched and the count ran. Both refusals are400 VALIDATION_ERRORwithparamset to the spelling the caller wrote and a message quoting the value, thrown before the engine is asked.countFlagValueparses withQueryTransportParamsSchema.shape.count(@objectstack/spec/data): the boolean, the stringstrue/false, andnull(absent). Nopackages/specfile is edited.getData(GET /data/:object/:id) takes the same projection rule. Its comment promised "gated exactly as on the list path"; refusing the empty projection on the list alone would have made that false. A blankselectwas falsy and skipped, a comma-only one became[]; both answered every column. It now asks the same helper on any value present.$count=true,$count=falseand a JSONnullanswer as before.Which doors reach the refusal (measured by reading every
findData(/getData(caller onorigin/main0f3d068cf1, and pinned per door below):$count=maybeRestServerGET /data/:objectfindDataVALIDATION_ERRORVALIDATION_ERRORRestServerPOST /data/:object/queryfindData(afterFindDataRequestSchema)VALIDATION_ERRORVALIDATION_FAILEDat the body schema, unchangedGET /data/:objectfindData(viacallData('query'))VALIDATION_ERRORVALIDATION_ERRORPOST /data/:object/queryfindData(afterFindDataRequestSchema)VALIDATION_ERRORFindDataRequestSchemagate as REST (not separately measured)RestServerGET /data/:object/:idgetDataVALIDATION_ERRORat 404 (see Acceptance notes)GET /data/:object/:idgetData(viacallData('get'))VALIDATION_ERRORThe in-process callers (
data-action-routes.tsjob and export reads,import-runner.ts,connector-pull.ts, plugin-security, plugin-auth's user import) pass no projection or a named one; none sends an empty one or a count flag.Tests
packages/metadata-protocol/src/protocol.empty-projection-unknown-count.test.ts(new, 37 cases): every refused spelling withcode,status,paramand the engine never reached; the preservation set (projection lists,countabsent / true / false / null) green in both directions.packages/rest/src/data-empty-projection-count-door.test.ts(new): both REST list routes and the by-id route over a realSqlDriver(SQLite): status,code, the named spelling, zero reads of the object, and the controls.packages/runtime/src/domains/data-empty-projection-count.test.ts(new): the dispatcher's three doors throughHttpDispatcher.dispatch()over a real store; a refusal leavesdispatch()and both host exits (declaredEnvelopeForThrow,resolveThrownHttpError) are asked and must agree.0f3d068cf1with every closure built from it: protocol 21 failed / 16 passed (all 21 refusals, every preservation green); REST 3 failed / 2 passed; dispatcher 8 failed / 1 passed (every refusal answered 200 with every column, or atotal).scripts/engine-double-contract.pinned.json: the new by-id double'sfindOneopens withassertEngineFindOnePredicate;--writerecorded it.Acceptance notes
crud-routes.ts'sGET /data/:object/:idcatch rewrites every 400 to 404 ("a bad id is a miss"), so every projection refusal on that route, an unknownselectfield included (?select=nopeanswers 404INVALID_FIELD, measured in the door test), already answers 404. The empty projection joins that answer with codeVALIDATION_ERROR; the dispatcher's by-id door answers 400. Changing the rewrite is outside this card (reported).HttpFindQueryParamsSchema.countisz.coerce.boolean()(it reads'false'astrueand admits'maybe'), and itsselectisz.string()(admits a blank). It has no runtime reader in this tree; it ships asjson-schema/api/HttpFindQueryParams.jsonand the reference docs. The door readsQueryTransportParamsSchema.count, which is exact.packages/specis outside this lane (reported).select: [](packages/client/src/index.tsserialises it asselect=) now gets the 400 instead of every column. That is the refusal working as intended; no in-repo caller passes an empty list.$toprefusal this mirrors answersINVALID_REQUEST(a ledger code); these answer the standard-catalogVALIDATION_ERROR, as the dispatch ruled.Verification
All readings at the final head
28a10e6f8bunless stated; exit codes captured before any pipe; heavy runs underscripts/pm/os-verify-lock.sh(itsVERDICTline quoted).Package suites and typecheck
pnpm build(full, at27848a9b, the fix's source identical to the head):VERDICT command-exit 0.pnpm --filter @objectstack/metadata-protocol test: exit 0 · Test Files 270 passed, 3 skipped · Tests 21653 passed, 19 skipped.typecheck: exit 0. The new file re-run at the head after its double change: 37/37.pnpm --filter @objectstack/rest test: exit 0 · Test Files 327 passed · Tests 6503 passed, 326 skipped.typecheck: exit 0.pnpm --filter @objectstack/runtime test: exit 0 · Test Files 460 passed · Tests 7457 passed, 19 skipped.typecheck(withcheck:test-typecheck) at the head: exit 0; the new door file 9/9.eslint --no-inline-config packages/metadata-protocol packages/rest packages/runtime: exit 0.Red first, on the base `0f3d068cf1`
protocol.empty-projection-unknown-count.test.ts: Tests 21 failed, 16 passed — every refusal red (accepted, answered), every preservation green.data-empty-projection-count-door.test.ts: 3 failed, 2 passed — e.g.$select= blank (the card): 200 with all columnslabel,noteand the system columns;select=""by id: 200, whole record.data-empty-projection-count.test.ts(dispatcher): 8 failed, 1 passed — each refused request answered{"status":200,"body":{"success":true,…}}, every column or atotal.Dogfood (the narrowing arm)
pnpm --filter @objectstack/dogfood testat25a8556236:VERDICT command-exit 0· Test Files 302 passed, 3 skipped · Tests 2507 passed, 20 skipped · no failures. The head28a10e6f8bdiffers from it only inpackages/runtime/src/domains/data-empty-projection-count.test.ts(a type annotation in a test outside the dogfood suite).Derived gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, then every family run at28a10e6f8b: 70 derived, 70 run, all exit 0.--ranreconciliation:70 derived famil(ies) accounted for — 70 run, 0 NOT-MEASURED.check:engine-double-contractpinned the new by-id double'sfindOne(now opens withassertEngineFindOnePredicate; ledger recorded by--write).test:repo(vitestrepoprojects) not run locally: no repo-project test or census file is in the diff; CITest Coreruns it.Generated by Claude Code