Repository navigation
fix(spec)!: an aggregation entry refuses an undeclared key by name on every door, REST matching MCP (#28609) - #28706
Conversation
… every door AggregationNodeSchema becomes a strictObject (the unknown-key campaign's message, hints where/filters/conditions -> filter and as -> alias); the retired distinct keeps its tombstone sentence. REST POST /data/:object/query answered 200 with the key stripped; it now answers 400 VALIDATION_FAILED naming the key, before any read. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5m158G1nvmxNYr7Jwq
…node whole The tool's own closed copy of the entry folds into AggregationNodeSchema, which now refuses undeclared keys at the producer: one refusal, the REST door's words, and the retired distinct answers its tombstone on both doors. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5m158G1nvmxNYr7Jwq
…d aggregation-entry key Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5m158G1nvmxNYr7Jwq
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5m158G1nvmxNYr7Jwq
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5m158G1nvmxNYr7Jwq
…facing history sentence Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5m158G1nvmxNYr7Jwq
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5m158G1nvmxNYr7Jwq
📓 Docs Drift CheckThis PR changes 2 package(s): 6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 173 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 7fd2bc83834e75842d3bff055a0f190005e6f924 && git checkout 7fd2bc83834e75842d3bff055a0f190005e6f924
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin da6f0ce35da60bf81e79890b227815a989f9b165 09f089de54fbb0fd6adaacc09b021d3ad47509c9 && git checkout -B drift-repro da6f0ce35da60bf81e79890b227815a989f9b165 && git merge --no-ff 09f089de54fbb0fd6adaacc09b021d3ad47509c9
node scripts/docs-audit/affected-docs.mjs --json da6f0ce35da60bf81e79890b227815a989f9b165
|
|
domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo Contract reviewServed-tier: Face: schema ( ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
No must-fix found. Implemented-by: VERDICT: PASS Generated by Claude Code |
|
domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo 席位核对ACCEPT · The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.
Generated by Claude Code |
Requested by Leehom · project thread
Fixes #28609
Clause-②: no (narrowing)
Metadata-protocol: none
Skills-docs: none — the published skills (
skills/objectstack-query,skills/objectstack-api) and the docs teach only the declared entry members (function,field,alias,filter) and never an extra entry key, so nothing they teach changes; the refusal itself names the key and the spelling to send.维护者速读
aggregations[]的每一项)现在在 spec 源头就关闭:写了 spec 没声明的键(比如拼错的bogusKey,或把条目自己的filter写成where)会被点名拒绝。REST、运行时分发器和 MCP 的aggregate_records读同一个 schema,拒绝文字一字不差。filter, so a conditional count answers the unconditional total (REST honours it) #28550)让 MCP 先拒绝了这种键,而 REST 仍然悄悄丢掉它照常返回 200,同一个条目在两个入口得到两种结论。VALIDATION_FAILED;只含声明键的查询不变。回滚即还原query.zod.ts一处和 MCP 一处。packages/spec/src/data/query.zod.ts(AggregationNodeSchema变成strictObject)、packages/mcp/src/mcp-http-tools.ts(删掉 MCP 自己那份条目副本,直接用 spec 节点)、packages/rest/src/aggregation-entry-undeclared-key-door.test.ts。pnpm --filter @objectstack/rest exec vitest run src/aggregation-entry-undeclared-key-door.test.ts,预期 5 个用例全绿(3 个拒绝、2 个对照)。What changes
AggregationNodeSchemais closed at the producer (packages/spec/src/data/query.zod.ts). It is now astrictObjectwith the unknown-key campaign's message (Unrecognized key(s) on this aggregation entry: KEY.), near-miss hintswhere/filters/conditions→filterandas→alias, and a history sentence. The retireddistinctkeeps itsretiredKey()tombstone and answers that sentence, never the generic refusal (strictObject keeps tombstones out of suggestions). Every door that parses the node inherits it:POST /api/v1/data/:object/queryonRestServerand on the runtime dispatcher (both parseFindDataRequestSchema), andEngineAggregateOptionsSchema.aggregations.packages/mcp/src/mcp-http-tools.ts).aggregate_recordstook each entry as its ownstrictToolInputcopy of the node's four members (MCP: aggregate_records silently drops an aggregation entry'sfilter, so a conditional count answers the unconditional total (REST honours it) #28550), a second closed key list. It now takesAggregationNodeSchemawhole, so the refusal is the spec node's own sentence, the same one REST prints, anddistinctanswers its tombstone on both doors (MCP used to give it the generic unknown-key refusal).filterstill reaches the bridge unchanged.tools/listnow listsdistinctas a schema nothing satisfies (not: {}) with its[REMOVED]text, and the member descriptions are the spec's.data/strip count 64 → 63 (regenerated), and the hand prose rows forquery.zod.tsre-read and updated to say the node is strict..changeset/28609-aggregation-entry-strict.md:@objectstack/specand@objectstack/mcppatch,fix(spec)!:, BREAKING banner, both declaration lines, ADR-0087not-required (no-migration-prescription)(a query aggregation entry is a request surface, never stored metadata). It carries one sentence superseding the pending MCP: aggregate_records silently drops an aggregation entry'sfilter, so a conditional count answers the unconditional total (REST honours it) #28550 changeset's "the REST query door is untouched".Tests
packages/rest/src/aggregation-entry-undeclared-key-door.test.ts(new): the real door (RestServer→findData→ObjectQL.aggregate→ sqliteSqlDriver).bogusKey, entrywhere, entryas: 400VALIDATION_FAILED,fields[]atquery.aggregations.1carrying exactlyAggregationNodeSchema.safeParse(entry)'s message, driver never read. Controls:distinctkeeps its tombstone; declared-only entries (count, sum, filtered count) answer the same grouped numbers.packages/runtime/src/domains/data-write-request-gate.test.ts: one row inQUERY_REFUSED, so both doors (RestServer and the dispatcher catch-all) refuse it and the cross-door parity block asserts they answer alike.packages/rest/src/request-schema-gate.conformance.test.ts: one more violating body on the/queryroute.packages/mcp/src/mcp-http-tools.aggregation-entry.test.ts: refusals now contain the spec node's message;distinctgives the tombstone sentence and no "Unrecognized key";tools/listpin updated (distinctpresent,not: {}).packages/spec/src/data/query.test.ts: new block for the closed node (slip, four hints, tombstone not suggested, closure throughQuerySchemaandEngineAggregateOptionsSchema, declared-only control); two stale comments that said the node is non-strict corrected.Acceptance notes
0b6a52e8through the REST door: all three entry slips answered 200 with the grouped numbers.none, not the order's expectednarrowed. AGENTS.md defines the line as the change to what a metadata author can write.check:authorable-surfacestayed green (the node's key list is unchanged; strictness is not on that surface), the spec's own docblock calls QueryAST a request surface never stored in metadata, and the claim recordednoneas expected. So the changeset ispatchwith the BREAKING banner, the shape AGENTS.md prescribes for a non-protocol breaking change.service-analyticsbuilds its aggregations in themethoddialect forIDataEngineand never imports or parsesAggregationNodeSchema, so nothing there moves. No open PR touchedquery.zod.tsor the MCP data tools at branch time or before this PR.dropped-refinements.baseline.jsondid not move (the order's census hypothesis): strictness is not a refinement;data/AggregationNodestill records onlyfilter.lazy.funcpins in driver tests that call drivers directly,fnin a plugin-security AST test, the analyticsmethoddialect), none of which goes through this schema.Verification
All readings at head
09f089deunless named (the merge oforigin/mainplus the ledger regeneration; nothing of this card moved after the suites below).Red first, on base 0b6a52e
pnpm --filter @objectstack/rest exec vitest run src/aggregation-entry-undeclared-key-door.test.tswith the spec unchanged: 3 failed / 2 passed.bogusKey, entrywhereand entryaseach answered 200 (expected undefined to be 400, the handler wrote the grouped rows with no status); thedistincttombstone and the declared-only control passed. With the fix: 5/5.Ablation (one-shot, not a kept test)
node scripts/ablation-replace.mjsturned the node back into a plainz.object(shape)(anchor hit 1/1),pnpm --filter @objectstack/spec build,ablation-dist-preflight spec ablationShapefound the marker in 2 built files. Reds: REST door 3 + REST gate table 1, runtime two-door rows 2, MCP entry file 3, spec block 4 (13 in all); every control stayed green. Restore leg: blob equal to HEAD,git diff HEADempty, rebuilt,--absentpreflight clean over 588 built files.Package suites and dogfood
@objectstack/spec: 869 files, 24570 passed (before the last spec-test commit; that file then 96/96 on its own).@objectstack/rest: 364 files, 6915 passed, 332 skipped.@objectstack/runtime(after building@objectstack/runtime...): 504 files, 8189 passed, 19 skipped.@objectstack/mcp: 48 files, 612 passed.(narrowing)arm),pnpm --filter @objectstack/dogfood testunder the lock after building@objectstack/dogfood^...: VERDICT command-exit 0, 369 passed / 3 skipped files, 2981 passed / 28 skipped tests.Derived gates
node scripts/pm/dispatch-gates.mjs --ranon the final head: 85 derived families accounted for, all with exit codes, 0 NOT-MEASURED, every exit 0. Two needed a second run:check:doc-authoringfirst red on an issue id in the node's customer-facinghistorysentence (removed, then green), andcheck-plugin-teardown-shape --self-testexit 3 until its fixture commit was fetched (then green). Also green on the final head:check:strictness-ledger,check:authorable-surface,check:api-surface(public API surface unchanged),check:docs,check:type-check-debt,check:dual-build-cjs-loads,check-adr-0087-registration --base origin/main. Repo-wide lint and thetest:repoproject are CI's.Human Minimum
packages/specholds no runtime logic.pnpm lintpasses (CI).main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_01FEkQ5m158G1nvmxNYr7Jwq
Generated by Claude Code