Repository navigation
fix(spec)!: analytics query doors refuse an empty cube name with 400 VALIDATION_FAILED, not 500 - #27691
Conversation
…VALIDATION_FAILED, not 500 AnalyticsQueryRequestSchema.cube takes .min(1) with one prescriptive sentence (ANALYTICS_CUBE_NAME_REQUIRED); the two plain Error throws in service-analytics (queryIn, generateSql) become the same typed refusal for in-process callers. Semantic migration entry registered; tests at the schema, the door and the service; changeset for both packages. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
… reference for the new export Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
…ch harness can measure Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
…both sides, and an absent non-string cube reads as absent The analytics door hands the body to fieldsFromZodIssues so an absent cube maps to the ADR-0114 required code, matching the engine's in-process guard; the guard classifies only a string as too short. Review findings folded in. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
…ckage typecheck reads it Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
…ntry from the schema member and the one D3 mapper No hand-rolled code split: the value runs through AnalyticsQueryRequestSchema.shape.cube and fieldsFromZodIssues with the value beside it, so the entry is the door's for the same value (required / min_length / invalid_type). The changeset names the runtime line and the full required flip for every absent key of the analytics body. Delta-review findings folded in. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
…e cube slot, and the pins the changeset promises The member carries a type-level error beside .min(1), so an absent or null cube reads the same sentence at the door and in-process; the helper memoises the member and treats an accepted value as a programming error; the door test pins the required flip for measures and a bodiless request; runtime moves at minor. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
…unction Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
…e prose with the member's shape Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
📓 Docs Drift CheckThis PR changes 3 package(s): ⛔ 3 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 172 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 3016867153ed3b8026fb0eed8f82d3618267e0a2 && git checkout 3016867153ed3b8026fb0eed8f82d3618267e0a2
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin a75ddba178a9196c87e60025409160a40322fac1 a0b621038aad223ccd675c51b20242c18c196040 && git checkout -B drift-repro a75ddba178a9196c87e60025409160a40322fac1 && git merge --no-ff a0b621038aad223ccd675c51b20242c18c196040
node scripts/docs-audit/affected-docs.mjs --json a75ddba178a9196c87e60025409160a40322fac1
|
…aves, not as a filed card Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017ihL2fi9zfJhUTubyJYb6V
|
triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK Contract-review-claim: session_01L6bhgV4fMGJHSdSdx3u9mK · head a0b6210 · 2026-10-29T21:18Z Generated by Claude Code |
|
domain:spec#4 · marchtian · session_017ihL2fi9zfJhUTubyJYb6V 席位核对 · ACCEPT (lane part) · card #27505 · 2026-10-29T21:21Z
Contract-review-handoff: triage — CONTRACT_REVIEW_TIER unavailable (HTTP 429 "You've reached your Fable limit", 2026-10-29T17:05Z) · head a0b6210 |
|
triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK Contract reviewServed-tier: Inputs read: card #27505 (body and all six comments: triage 6495245828, retriage 6497972144, retriage answer 6498803806 — the ruling this PR implements, claim 6501118904, claim amendment 6506611960, os-dev-report 6508587731); PR #27691 body, file list (12 files, +460/-8) and the net diff against ① Derived judgments
② Semver levelChangeset
③ Boundary flags
④ Design judgmentpackages/spec/src/api/analytics.zod.ts — against ruling 6498803806 ( packages/spec/src/migrations/entries/semantic/18.analytics-query-empty-cube-name-refused.ts — against ADR-0087 D3 and its first consumer, the registry builder ( Narrowing floor, the remaining non-prose paths, judged against the published spec on Findings: must-fix — none. Should-fix — one, in ②: respell the Clause-② carriers Implemented-by: VERDICT: PASS Generated by Claude Code |
Requested by 田騏 · project thread
Fixes #27505
Clause-②: no (narrowing)
Metadata-protocol: narrowed
Fable review: owed — the account's Fable quota was exhausted at 2026-10-29 ~17:05Z; the seat runs it when the quota returns.
维护者速读
cube改为必填且非空(.min(1),一句带修法的拒收文案);POST /analytics/query、/analytics/sql收到空 cube 名时在入口直接答 400VALIDATION_FAILED,不再进引擎;service-analytics 里query/generateSql的两处裸Error改为同一个类型化拒收(400VALIDATION_FAILED),进程内调用方拿到与 HTTP 入口相同的答案。INTERNAL_ERROR,监控和调用方都当成服务端故障,会告警、会重试;同样的错误把键省掉却答 400。cube: ""的调用方。附带一处可见变化:analytics 入口对缺失必填键的fields[].code从invalid_type改为 ADR-0114 的required(状态码与error.code不变)。回滚 = revert 本 PR。packages/spec/src/api/analytics.zod.ts的cube成员;②packages/services/service-analytics/src/cube-name-refusal.ts;③packages/runtime/src/domains/analytics.ts中把 body 传给fieldsFromZodIssues的那一行。pnpm --filter @objectstack/runtime exec vitest run src/analytics-empty-cube-refusal.test.ts,预期Tests 10 passed (10):两个入口对空 cube 都答 400VALIDATION_FAILED,且 service 未被调用。## Contract review记录)。What changes
@objectstack/spec—AnalyticsQueryRequestSchema.cubebecomesz.string({ error: ANALYTICS_CUBE_NAME_REQUIRED }).min(1, ANALYTICS_CUBE_NAME_REQUIRED)(JSON SchemaminLength: 1).ANALYTICS_CUBE_NAME_REQUIREDis a new export: one prescriptive sentence ("Name the cube to query: …") for every refusal of the slot (absent, non-string, empty). ADR-0087 disposition: a semantic migration entry,analytics-query-empty-cube-name-refused(no D2 conversion: a cube name is a request value, never stored metadata). Regenerated:api-surface/api.json,export-origins/api.json,content/docs/references/api/analytics.mdx.@objectstack/service-analytics— the two plainError('Cube name is required …')throws (queryIn,generateSql) becomecubeNameRequiredError(cube):validationFailurefrom@objectstack/typeswithstatus: 400, the schema's sentence as message, and afields[]entry built by running the value through the schema's owncubemember and the one ADR-0114 D3 mapper, so the field code is the door's for the same value (required/min_length/invalid_type).@objectstack/runtime— the analytics door now passes the body tofieldsFromZodIssues(issues, body)(as the/datadoors do), so an absent required key readsrequiredinstead of Zod'sinvalid_type. This is the one line outside the claim's declared file surface; see Acceptance notes." ": deliberately unchanged..min(1)does not cover it and no trim rule is added; it is a name no cube carries and keeps its404 CUBE_NOT_FOUNDfrom cube resolution. Pinned on both the schema side and the service guard side.Tests
packages/spec/src/api/analytics.test.ts:cube: ""gives onetoo_smallissue atcubereading the prescription; a missing cube keepsinvalid_typewith the same sentence;"o"and" "still parse; JSON Schema carriesminLength: 1.packages/runtime/src/analytics-empty-cube-refusal.test.ts(new), both doors:cube: ""gives400,error.code: VALIDATION_FAILED,fields: [{ field: 'cube', code: 'min_length' }], and the service is never called; the envelope parses againstApiErrorSchema; missingcube/ missingmeasures/ no body keep their 400 with coderequired; a named cube reaches the service with the body untouched.packages/services/service-analytics/src/__tests__/analytics-service.test.ts: in-processquery()/generateSql()with''or no cube reject with{ code: 'VALIDATION_FAILED', status: 400, name: 'ValidationError' }and the door's field entry;nullreadsinvalid_type; whitespace is not this refusal. The oldtoThrow('Cube name is required')pin is replaced (pin sweep: no other repo pin on either old message outsidedriver-memory, which is out of scope, see below).Package runs at head 427f5e9
pnpm --filter @objectstack/spec typecheckexit 0;… service-analytics typecheckexit 0;… runtime typecheckexit 0.pnpm --filter @objectstack/service-analytics test: Test Files 196 passed (196), Tests 4739 passed, 282 skipped.pnpm --filter @objectstack/spec test: Test Files 752 passed (752), Tests 22349 passed, 1 todo.pnpm --filter @objectstack/runtime test: Test Files 478 passed (478), Tests 7794 passed, 19 skipped.Ablation at head 427f5e9 (one-off, via scripts/ablation-replace.mjs, restored to the HEAD blob)
analytics.test.ts38/38, runtimeanalytics-empty-cube-refusal.test.ts10/10, serviceanalytics-service.test.ts46/46..min(1, ANALYTICS_CUBE_NAME_REQUIRED)deleted from the schema (anchor 1 → 0, blob 73d6676181 → a99fae0ade; spec and runtime read spec source through the alias): spec 2 failed / 36 passed (the empty-cube refusal and theminLengthpin); runtime 4 failed / 6 passed (the empty-cube refusal and the envelope pin, on both doors). Missing-cube pins stay green, as expected: the type-levelerroris untouched. Restored: blob == HEAD,git diff HEADempty.throw cubeNameRequiredError(queryInput.cube);replaced withthrow new Error('Cube name is required');(anchor 2 → 0, blob 34d1d84686 → 2ea1302a68): service 5 failed / 41 passed (every typed-refusal pin; the whitespace pin stays green as expected). Restored: blob == HEAD,git diff HEADempty.Acceptance notes
analytics-service.ts, tests beside, and changesets. The diff also touchespackages/runtime/src/domains/analytics.ts(one line: hand the body to the D3 mapper) and addspackages/runtime/src/analytics-empty-cube-refusal.test.ts. Reason: without the body, the door answers an absentcubewithinvalid_typeand the in-process guard answersrequired, so the same condition would get two codes. As a result an absentmeasuresand a request with no body now readrequiredtoo. Status anderror.codedo not change. The seat decides whether that line stays.@objectstack/runtime(minor) besides the two packages the order names, for the field-code change above.MemoryAnalyticsService(@objectstack/driver-memory) keeps its own plainError('Cube name is required')for the same input; noted in the changeset, not changed here.Verification
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 110 commands at 427f5e9. All were run, with each exit code recorded before any pipe, 109 exited 0 on the sweep.pnpm check:type-check-debtexited 3 there (PREREQUISITE NOT MET: it did not get the verify lock while this branch's dogfood run held it) and exited 0 when re-run under the lock (0 ledger entries re-measured … none above its recorded number).--ranreconciliation: 110 derived, 110 run, 0 NOT-MEASURED (a derived zero: every entry carries its exit code).(narrowing)arm), head 427f5e9:pnpm --filter @objectstack/dogfood testgives Test Files 351 passed, 3 skipped (354); Tests 2825 passed, 20 skipped (2845); exit 0.Generated by Claude Code
🤖 Generated with Claude Code