Repository navigation
fix(rest)!: GET /meta/_drafts refuses an unknown type or package filter instead of answering an empty list (#29180) - #29295
Conversation
… of answering an empty list
GET /meta/_drafts?type=TYPO and ?packageId=UNKNOWN answered 200
{"drafts":[]}. The type is now judged by the list door's verdict
(refuseUnknownMetaListType) on both transports, and listDrafts judges
the package by the diagnostics sweep's packageIsInstalled rule.
Claude-Session: https://claude.ai/code/session_01ADHkmRSZi4SoegkVztRKBe
Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 27 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 10 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 42 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 f7371cd62cf4fcba6769fea8dd05d108d701faa8 && git checkout f7371cd62cf4fcba6769fea8dd05d108d701faa8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 39951d2a26590ff93226d3c0cf4ecfdc4136031e 5ac557e5ee88b430a8f9cbb3e47d24427c65ab77 && git checkout -B drift-repro 39951d2a26590ff93226d3c0cf4ecfdc4136031e && git merge --no-ff 5ac557e5ee88b430a8f9cbb3e47d24427c65ab77
node scripts/docs-audit/affected-docs.mjs --json 39951d2a26590ff93226d3c0cf4ecfdc4136031e
|
|
domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY Contract-review-claim: The lane seat runs this PR's review at |
|
domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY Contract reviewServed-tier: Read on the head tree via Merge state. Check-runs on the head. 48 check-runs, all completed: 35 success, 13 skipped, 0 failure, 0 cancelled (final tally read after the eight ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgment
Must-fix: none. Should-fix: one (stale module header in Implemented-by: VERDICT: PASS Stamped 2026-11-02T11:33Z. |
|
domain:server#1 · os-steve · session_01ADHkmQjPiXgNoSaEpqeGvY 席位核对 · ACCEPT ·
|
| item | reading |
|---|---|
| Shape | Draft, base main, Fixes #29180. 9 files: GET /api/v1/meta/_drafts refuses an unknown type with the list door's refuseUnknownMetaListType on both transports (rest/meta-collection-routes.ts, runtime/domains/meta.ts); listDrafts refuses an unknown packageId with the #27514 packageIsInstalled rule (protocol-drafts.ts, protocol-meta-diagnostics.ts export, protocol.ts). Three tests, a changeset (patch ×3, BREAKING banner, ADR-0087 not-required (no-migration-prescription)). Metadata-protocol: none. No packages/spec/**, no new code (INVALID_REQUEST). Not governed. |
| Rule | An unknown type or an uninstalled package answers 400 instead of 200 {"drafts":[]}. A disabled package and a draft bound to an installed package keep 200. The rule fails open where the list door fails open. |
| Accepted deviation | The runtime dispatcher twin was edited so both transports stay equal (the review judges it right: the type verdict lives in @objectstack/rest, and leaving the dispatcher out would half-fix dispatcher-only hosts). |
| Evidence | Base: 200 {drafts:[]}; head: 400. Ablation red then restored. metadata-protocol 301 files, rest 368, runtime 515, ui consumers 41, dogfood VERDICT 0, gates 62/62. CI on this head: 48 runs, 35 green, 13 skipped, none red; check-expected-skips OK; merge-tree against today's main clean (review ①). |
| Should-fix, not filed | The protocol-drafts.ts module header still calls the file "a move … otherwise unedited" with two imports. It now carries a #29180 hunk and a third import. That's internal comment drift with no user-visible cost, so it isn't a card under the filing gate. The next PR that edits this file should correct the header sentence. |
| Open question | PR assignee/reviewer unset: not needed, the seat lands it. |
| Landing | 「按照 pm-dispatch 规则复核过的都授权」 (Leehom, 2026-10-25T11:35Z): ready and auto-merge through the relay. |
Requested by Steve · project thread
Fixes #29180
Clause-②: no (narrowing) — measured: on base
5ac557e5^(104cd1a5)_drafts?type=dashbord|dashboardes|viewes|nosuchtypeand?packageId=com.qa.nosuchanswer200 {"drafts":[]}(the refusal pins below go red there); on head they answer400 INVALID_REQUEST, on both transports. Nothing is widened.Metadata-protocol: none — measured: no spec key, value or type moves;
packages/specis untouched.Skills-docs: none — no
skills/**page or docs page documents the_draftsfilters (the two docs mentions,content/docs/ui/apps.mdxL455/L461, cite only its capability gate).维护者速读
GET /meta/_drafts的type写错(如dashbord)或packageId指向不存在的包时,不再回 200 空列表,而是回 400,文案与列表门、诊断门对同一错误的拒绝一致。packages/metadata-protocol/src/protocol-drafts.ts(包检查)、packages/rest/src/meta-collection-routes.ts(类型检查)、packages/runtime/src/domains/meta.ts(派发器孪生门)。pnpm --filter @objectstack/rest exec vitest run src/meta-drafts-unknown-filter.test.ts,预期 9 个用例全绿。What changes
packages/rest/src/meta-collection-routes.ts,packages/runtime/src/domains/meta.ts): both_draftsdoors askrefuseUnknownMetaListType, the list door's own verdict (static spelling contract, then the live type set, fail-open), beforelistDrafts. No second copy of the type vocabulary: it is the same functionGET /meta/:typeandGET /meta/diagnostics?type=call, so the drafts answer is pinned EQUAL to the list door's answer for the same name.packages/metadata-protocol/src/protocol-drafts.ts):listDraftsasks the diagnostics sweep's rule (packageIsInstalled, now exported fromprotocol-meta-diagnostics.ts; nothing re-exports that module, so the published surface does not move): an empty answer for apackageIdthat no pending draft (of any type) is bound to and that the installed-package listing does not carry is refused400 INVALID_REQUEST, same code and sentence family as the sweep'sis not an installed package. Fails open when the listing cannot be read. Living inlistDrafts, it covers both transports.DraftsHostgainsgetMetaItems(the class hands it over as the other moved bodies do); the method docblock names the refusal.packages/specedit.Tests
New files only; no existing test was edited (no forced test edits).
packages/metadata-protocol/src/protocol-drafts-unknown-package.test.ts(5): refusal (status + code + first clause naming the package, with and withouttype); controls: installed package with nothing pending (no type,dashboard,view) →{drafts: []}; uninstalled package with a bound draft is served and, under another type filter, answers empty; listing unreadable or shapeless → fail-open empty; no package named → listing never asked.packages/rest/src/meta-drafts-unknown-filter.test.ts(9): four misspellings → 400 equal toGET /meta/:type's body for the same name,listDraftsnever called; controlsdashboard/dashboards/view→ 200 empty with the argumentslistDraftsreceived; known package → 200 empty; the package refusal leaves the route a 400INVALID_REQUEST.packages/runtime/src/domains/meta-drafts-unknown-type.test.ts(7): the dispatcher twin, same pins against its own list branch.Acceptance notes
?package=(the card's wording in one place) is not a_draftsparameter; the route readspackageIdand the console sendspackageId. Unknown parameter names on_draftsare not refused today (no closed set declared); out of scope here.ui/packages/app-shell, 20listDraftscall sites): every call that sendstypepairs it withclient.list(type)onGET /meta/:type, which already refuses the same names with the same verdict, so the new check refuses nothing the console's paired read does not already refuse. The literal types sent (view,hook,object,app,permission) all pass the static contract.packageIdcomes from the Studio package switcher, which lists/api/v1/packages(installed packages); most call sites also.catchthe drafts read.Verification
Ablation + premise (base code, head tests)
The five
srcfiles restored to base withgit restore --source=BASE(tree only,grep -c 29180= 0 on each), the three new test files run, thengit checkout HEAD --restore;git diff HEADempty,git status --porcelainempty, every path'sgit hash-objectequal to itsHEADblob.× refuses an unknown package with nothing bound to it—AssertionError: type=undefined: expected undefined to be an instance of Error; 1 failed | 4 passed (5).× ?type=dashbord|dashboardes|viewes|nosuchtype …—AssertionError: expected 200 to be 400; 4 failed | 5 passed (9).expected 200 to be 400; 4 failed | 3 passed (7).Every refusal pin red on base, every control green. Tests import their package's
src; no dist resolution in these paths.Suites, typecheck, lint (head 5ac557e)
pnpm --filter @objectstack/{metadata-protocol,rest,runtime} build+typecheck: VERDICT command-exit 0.@objectstack/metadata-protocoltest: 301 passed | 4 skipped files; 22105 passed | 26 skipped tests.@objectstack/resttest: 368 files passed; 6972 passed | 332 skipped tests.@objectstack/runtimetest: 515 files passed; 8325 passed | 19 skipped tests.cd ui && vitest runpackageSurfaces, ObjectHooksPanel.firstSavePinned-11773, InterfacesPillar.customize-28562, ObjectListViewInspector.chartBinding-27933): 4 files, 41 tests passed.--no-inline-configoverpackages/metadata-protocol packages/rest packages/runtime/src: exit 0.pnpm --filter @objectstack/dogfood test, no shard): VERDICT command-exit 0; Test Files 388 passed | 3 skipped (391); Tests 3208 passed | 28 skipped.Derived gates (head 5ac557e)
node scripts/pm/dispatch-gates.mjs --commandsderived 62 families; all 62 run, each exit 0 (check-plugin-teardown-shape --self-testfirst exited 3 on the shallow clone, green after fetching its pinned fixture;check:dual-build-cjs-loadsfirst exited 3 before the closure build, green after;check:type-check-debtrun under the lock: 0 entries above record).--ranreconciliation: 62 derived, 62 run, 0 NOT-MEASURED, 0 UNRUN (a derived zero).🤖 Generated with Claude Code
https://claude.ai/code/session_01ADHkmRSZi4SoegkVztRKBe
Generated by Claude Code