Skip to content

fix(rest)!: an empty ?id= on the app list or ?object= on the view list is refused 400 naming the parameter instead of read as absent - #28602

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-cq2t0t
Nov 1, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-cq2t0t

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH

Requested by Leehom · project thread

Fixes #28548

Clause-②: no (narrowing) — GET /meta/app / GET /meta/apps with an empty ?id=, and GET /meta/view / GET /meta/views with an empty ?object= (on RestServer and on the runtime dispatcher, which hand their store answer to the same list chain), are refused with 400 VALIDATION_ERROR naming the parameter; they answered 200 with the caller's whole visible list before. Nothing is widened: no export, answer member, error code or status is added.
Metadata-protocol: none

维护者速读

  • 改了什么:GET /api/v1/meta/app?id=(id 为空)和 GET /api/v1/meta/view?object=(object 为空)现在回 400 VALIDATION_ERROR,消息点名参数,不再返回调用者能看到的全部应用或全部视图。
  • 为什么改:以前空 id 被当成“没写”,调用方本想取一个应用,名字却是空的,结果拿到整张应用列表,看起来像正常结果;视图列表的空 object 是同一个毛病,席位要求在本 PR 一并修。和 data: a blank or comma-only $searchFields is read as absent, so a search the caller narrowed runs over the default search fields instead of being refused #27484(空 $searchFields 被当成没写)同一类,按同样方式拒收。
  • 风险与回滚:只影响发了空 id 或空 object 的 API 调用方;@objectstack/client 和 Console 读这两个列表都不带这两个参数。非空值、不带参数、重复参数的回答都不变。回滚 = revert 本 PR。
  • 先看:packages/rest/src/meta-item-read-gate.ts(createMetaListAnswer 第 3、4 步和 refuseEmptyListFilter)、packages/rest/src/meta-app-publish-gate.test.ts(有意翻转的钉子)。
  • 两分钟验证:pnpm --filter @objectstack/rest exec vitest run src/meta-app-publish-gate.test.ts,预期 27 个用例全过(空 id 三种调用者都回 400,空格 id 仍回空列表,空 object 回 400)。
  • 需要你做的:无,席位落地(进队前欠一次契约复核)。

What changes

  • createMetaListAnswer step 3 (packages/rest/src/meta-item-read-gate.ts): the app filter used to skip an empty id (typeof appIdFilter === 'string' && appIdFilter !== ''), so the request was served as if no filter had been sent. An empty value now goes to the new refuseEmptyListFilter(), which throws 400 VALIDATION_ERROR with the message Query parameter 'id' names no app; got "". …. The throw reaches each transport's own error door (RestServer's handleRouteError, the dispatcher's errorFromThrown), the way refuseUnknownMetaListType already answers on this route, so neither transport hand-builds a body.
  • createMetaListAnswer step 4 (seat-directed fold, same class): ?object= on view was read with a truthiness test (query?.object), so GET /meta/view?object= listed every view, unsorted and including views bound to no object, as if no filter had been sent. An empty value now goes to the same helper: 400 VALIDATION_ERROR, message Query parameter 'object' names no object; got "". …. A named object still switches; ?object= on other types is still ignored.
  • Order kept. Both refusals sit in steps 3 and 4, after the per-caller visibility gate (step 2), which keeps its place. The chain stays the one place a list step lives, so both transports answer the same.
  • Unchanged. A non-empty id narrows as before (one app, or an empty list when it names none, ' ' included). No id lists every app the caller may see. A repeated id is still the arity 400 at the door entry. ?id= on any other type is still ignored.
  • Docblock of createMetaListAnswer steps 3 and 4 updated to say an empty value is refused.
  • Pin flipped on purpose: meta-app-publish-gate.test.ts "an ABSENT id still returns the whole published set" asserted that ?id= (empty) returns the full list, citing an unset select as its producer. No producer sends it (see Acceptance notes), and the card's expected result rules the other way, so that half moves to the new refusal case.
  • Changeset .changeset/28548-empty-app-id-refused.md: patch on @objectstack/rest, BREAKING banner, ADR-0087 not-required (no-migration-prescription). @objectstack/runtime changes only a test, which its files[] does not ship.

Which precedent, and why

The card offers two outcomes: refuse with a 400 naming the parameter, or match nothing as ' ' does. This follows the precedent PR #27670 set for #27484: that change refused a blank $searchFields with 400 VALIDATION_ERROR and a message naming the parameter, rather than reading it as a value that matches nothing. Along the four axes: there is no real caller that sends an empty id (measured below), so the loud answer costs nobody; a refusal is the contract-first answer and the one that stops an AI-written client from mistaking a blank name for a real filter; and it adds no gate or surface, only a refusal on the existing door.

Tests

Red first, then green (head 9c45ac5)
  • id (first commit): red on the base code with the new tests — meta-app-publish-gate.test.ts "an EMPTY id is refused…" (expected 200 to be 400) and meta-list-projection-parity.test.ts row A "the reference moves" on the new ?id= assertion. The row A matrix cells for ?id= (empty) passed on the base, because both transports agreed on the wrong 200.
  • object (second commit): red on 9246901 with the new tests — "an EMPTY view object is refused…" (view: expected 200 to be 400, 1 failed / 26 passed) and row A on the new ?object= assertion (1 failed / 934 passed).
  • Green on 9c45ac5 after a rest rebuild: meta-app-publish-gate.test.ts + meta-view-dashboard-audience-gate.test.ts 46/46; meta-list-projection-parity.test.ts + dispatcher-query-arity.test.ts 968/968.
  • New cases: an empty id as a builder (studio.access), as manage_users and as a caller holding nothing, on /meta/app and /meta/apps; an empty object on /meta/view and /meta/views. Each asserts status 400, code VALIDATION_ERROR, the message's first clause, and no items. The dispatcher's twin is pinned by the new ?id= (empty) and ?object= (empty) probes in @objectstack/runtime's list parity census: every type × caller cell must answer the same status + code + items on both transports, and row A asserts 400 VALIDATION_ERROR with no items for each.
  • Controls: ?id=crm returns that one app, no id returns the visible list, ?id= with a space still returns 200 [], repeated id stays 400, ?id= on view is still ignored; ?object=lead still switches to that object's views and no object still lists them all (asserted inside the new object case).
Ablation (each leg red on both suites, then restored to the HEAD blob)
  • id leg (on 9246901): --anchor " const appIdFilter = metaType === 'app' ? query?.id : undefined;" --replacement " const appIdFilter = metaType === 'app' && query?.id !== '' ? query?.id : ((globalThis as any).ABLATE_28548 = undefined);", which puts back the old skip. Anchor 1 → 0, blob 24ba18a4 → acc770c9; rebuilt; preflight hit dist/index.js + dist/index.cjs. Red: the rest case (caller holding [studio.access]: expected 200 to be 400) and runtime row A. Restored: blob == HEAD 24ba18a4, git diff HEAD empty; rebuilt; --absent clean.
  • object leg (on 9c45ac5): --anchor " if (metaType === 'view' && query?.object === '') refuseEmptyListFilter('object', 'object', 'view');" --replacement " if ((globalThis as any).ABLATE_28548B === 'on' && metaType === 'view' && query?.object === '') refuseEmptyListFilter('object', 'object', 'view');", which lets the empty value fall back to the old truthiness skip. Anchor 1 → 0, blob 35301758 → 15af187f; rebuilt; preflight hit dist/index.js + dist/index.cjs. Red: the rest object case (view: expected 200 to be 400, 1 failed / 26 passed) and runtime row A (1 failed / 934 passed). Restored: blob == HEAD 35301758, git diff HEAD empty; rebuilt; --absent clean over 6 built files, tree clean.
Package suites, typecheck, lint (head 9c45ac5)
  • pnpm --filter @objectstack/rest test :: exit 0 · Test Files 362 passed (362) · Tests 6907 passed, 332 skipped
  • pnpm --filter @objectstack/runtime exec vitest run src/domains/meta-list-projection-parity.test.ts src/domains/dispatcher-query-arity.test.ts :: exit 0 · 968 passed (the parity file is the only runtime file changed; it is in the repo project, which CI's Test Core runs)
  • pnpm --filter @objectstack/rest typecheck :: exit 0
  • pnpm --filter @objectstack/runtime typecheck :: exit 0 (@objectstack/driver-turso built first, a test-only import outside the closure)
  • pnpm exec eslint --no-inline-config packages/rest packages/runtime :: exit 0
Dogfood battery (9c45ac5)

pnpm exec turbo run build --concurrency=2 --filter='@objectstack/dogfood^...' then pnpm --filter @objectstack/dogfood test under os-verify-lock.sh, no shard, no path: VERDICT command-exit 0 · Test Files 364 passed, 3 skipped (367) · Tests 2934 passed, 28 skipped (2962) · no failures. (Also green on 9246901 with the same counts.)

Acceptance notes

  • Producers checked for an empty id or object: @objectstack/client's meta.getItems(type, { packageId }) sends no id; no file under ui/packages, ui/apps, packages/client/src or packages/cli/src builds a meta/app?id= request, and the console reads meta.getItems('view') with no query. The QA ledger rows under docs/qa/platform-checklist that read /meta/app?id= all name an app; none asserts the empty answer.
  • The refusal body on RestServer is the flat { error, code } its route error door writes for a thrown refusal (the same door refuseUnknownMetaListType uses on this route); the repeated-id 400 at the door entry writes the nested { error: { code, message } }. Both carry VALIDATION_ERROR. The dispatcher writes its own envelope with the same error.code. Not changed here.
  • ?id= with a space keeps its answer (200 []): the ruling keeps every non-empty id as it was. data: a blank or comma-only $searchFields is read as absent, so a search the caller narrowed runs over the default search fields instead of being refused #27484 refused a whitespace-only $searchFields, but there the value is split into names and trimmed; an app name is compared whole.
  • The empty ?object= refusal was folded in on the seat's direction (four adjacency conditions: same class, same file, mechanical, same gate family); the claim's file surface already covered identical empty-as-absent filters in this gate.
  • Branch claude/project-thread-cq2t0t is the card thread's harness-pinned branch, named in claim 6554674218; it carries no issue number. Signature: domain:server#3 · os-project-manager · session_01FEkQ5pQoUJCqKk6SJQb6Qx.
  • No open PR touched packages/rest/src/meta-item-read-gate.ts at the claim's read.

Verification

Local subset, head 9c45ac50: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 60 families; every one run with its exit code captured before any pipe, all 60 exit 0 (check:type-check-debt under os-verify-lock.sh). --ran reconcile: "60 derived, 60 run, 0 NOT-MEASURED, 0 UNRUN". Three first-pass readings were not verdicts and were re-run once the parallel dogfood closure build had settled: check:dts-closure (exit 1 while that build was rewriting dists; 239/239 declaration files present on re-run), check:dual-build-cjs-loads (exit 3, missing dists; passed after the remaining seven were built) and check:type-check-debt (exit 99, lock held by the dogfood battery). check-plugin-teardown-shape.mjs --self-test ran after fetching its pinned fixture commit. The same 60 were all exit 0 on 9246901. CI-run families: see this PR's check-runs.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (not touched)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes on the touched packages.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: n/a.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (the maintainer summary block is the dispatch contract's Chinese section).
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

… parameter instead of read as absent

`GET /meta/app?id=` with an empty value skipped the app filter and served the
caller's whole visible list. The shared list chain now refuses it after the
per-caller gate with 400 VALIDATION_ERROR naming `id`, the answer the #27484
fix gives a blank `$searchFields`. A non-empty id, an absent id and the
repeated-id 400 are unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/rest, touching 3 documentable anchor(s).

19 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 882fcdbe79dbd299a66217da5221a113f851d639.

⛔ 5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • the SDK route bridge reached 60 of 214 client-bound route-ledger rows — the other 154 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 154: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 99 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 19 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 882fcdbe79dbd299a66217da5221a113f851d639 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from ab5df89dcd4bb42f0ec36b2ce71bf92ed269f94a — the merge of head 70a3f972988a14f0d6e878f9404820af06e4886e into base 882fcdbe79dbd299a66217da5221a113f851d639, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin ab5df89dcd4bb42f0ec36b2ce71bf92ed269f94a && git checkout ab5df89dcd4bb42f0ec36b2ce71bf92ed269f94a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 882fcdbe79dbd299a66217da5221a113f851d639 70a3f972988a14f0d6e878f9404820af06e4886e && git checkout -B drift-repro 882fcdbe79dbd299a66217da5221a113f851d639 && git merge --no-ff 70a3f972988a14f0d6e878f9404820af06e4886e

node scripts/docs-audit/affected-docs.mjs --json 882fcdbe79dbd299a66217da5221a113f851d639

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 882fcdbe79dbd299a66217da5221a113f851d639 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…g the parameter, as the empty app `id` is

`GET /meta/view?object=` with an empty value failed the truthiness test and
served every view, unsorted and including views bound to no object. The list
chain now refuses it after the per-caller gate with 400 VALIDATION_ERROR naming
`object`, through the same helper the empty app `id` uses.

Co-Authored-By: Claude <noreply@anthropic.com>
…message only, not in a param member

Co-Authored-By: Claude <noreply@anthropic.com>
@objectstack-fleet objectstack-fleet Bot changed the title fix(rest)!: an empty ?id= on the app list is refused 400 naming the parameter instead of read as absent fix(rest)!: an empty ?id= on the app list or ?object= on the view list is refused 400 naming the parameter instead of read as absent Nov 1, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 9c45ac50ac6cb068459016581d68ede456cb49dd
Local-runs: none
Scope: packages/rest/src/meta-item-read-gate.ts, packages/rest/src/meta-app-publish-gate.test.ts, packages/runtime/src/domains/meta-list-projection-parity.test.ts + packages/rest/src/meta-collection-routes.ts (the list door entry: arity refusal, metaItemPackageBinding, the handleRouteError catch), packages/runtime/src/domains/meta.ts (answerMetaList, the dispatcher's list branch and its errorFromThrown catch), packages/runtime/src/http-dispatcher.ts (errorFromThrown), packages/rest/src/error-response.ts (handleRouteError / resolveErrorResponse, the 4xx passthrough body), packages/types/src/data-error-classification.ts (thrownCodeFields), packages/types/src/thrown-http-error.ts, packages/rest/src/rest-server.ts (metaItemPackageBinding), packages/metadata-protocol/src/protocol.ts (projectionFieldsValue, the #27484 / PR #27670 refusal shape), .changeset/27484-blank-search-fields-refused.md, packages/spec/src/api/protocol.zod.ts (GetMetaItemsRequestSchema), packages/spec/src/api/errors.zod.ts (StandardErrorCode), packages/client/src/index.ts (meta.getItems), packages/cli/src/commands/meta/list.ts, ui/packages/data-objectstack/src/index.ts, ui/packages/app-shell/src/console/marketplace/MarketplacePage.tsx, ui/apps/console/src/pages/developer/PublicFormsPage.tsx, docs/adr/0017-object-has-many-view.md, docs/qa/platform-checklist/areas/platform-core.json, scripts/check-dispatcher-error-vocabulary.mjs. Inputs: card #28548 body and all five comments, PR #28602 body, file list and net diff on this head, the 52 check-runs on this head, origin/main at a9c6eac6f.

① Derived judgments

Accept-set changes the diff implies, each judged against the cited code on main:

  1. GET /meta/app and /meta/apps with ?id= empty: 200 with the caller's whole visible list → 400 VALIDATION_ERROR. Right. createMetaListAnswer step 3 on main reads if (typeof appIdFilter === 'string' && appIdFilter !== ''), so the empty value skipped the filter (the card's premise, re-read in the live tree). The head adds if (appIdFilter === '') refuseEmptyListFilter('id', 'app', 'app') with appIdFilter still keyed on metaType === 'app', so only the app list is touched. The plural spelling is folded to the singular before the chain on both transports (metaTypeSingular in the REST route, pluralToSingular in the dispatcher), and the rest test pins apps.
  2. GET /meta/view and /meta/views with ?object= empty: 200 with every view (unsorted, including views bound to no object) → 400 VALIDATION_ERROR. Right. Step 4 on main is a truthiness test (query?.object), so '' fell through to the unfiltered list; the head adds if (metaType === 'view' && query?.object === '') refuseEmptyListFilter('object', 'object', 'view') before it. Folded in under the seat's claim amendment 6555101824; the dev's round-1 out-of-scope finding is closed by it.
  3. Everything else on the two lists is unchanged. Right, read in the diff: a non-empty id or object narrows as before (' ' and nope give 200 [], both pinned); an absent one is undefined and takes neither new branch; a repeated id or object is still refused at the door ENTRY, before the chain, by refuseRepeatedQueryParams / metaQueryArityRefusal over META_QUERY_ARITY_DOORS.list (package, preview, object, include, id), and that is untouched; ?id= on a type other than app and ?object= on a type other than view are still ignored because both new guards key on the folded metaType.
  4. Order: the refusal sits after the per-caller gate (step 2), which keeps its place. Right. Both throws are in steps 3 and 4, after the gate's judged list is formed. The refusal reads nothing from the visible list, its message names only the parameter and the literal "", and it is identical for every caller (the rest test loops studio.access, manage_users and no permission), so a caller who may see nothing learns nothing from it that a caller who may see everything does not.
  5. Both transports answer the same. Right. The REST route hands its store answer to createMetaListAnswer and catches into handleRouteError; the dispatcher's answerMetaList hands its store answer to the same chain and catches into deps.errorFromThrown(e, 500). VALIDATION_ERROR is a StandardErrorCode (errors.zod.ts:55), so thrownCodeFields keeps it as code on the REST flat body and errorFromThrown keeps it on the dispatcher envelope with no declaredCode demotion; the dispatcher vocabulary gate treats a registered code's stamp site as no finding, and Lint & Repo Gates is green on this head. The runtime parity census adds the ?id= (empty) and ?object= (empty) probes, so every type × caller cell now holds status + code + items equal across the two doors, and row A asserts 400 VALIDATION_ERROR with no items on the reference.
  6. Public surface: nothing added. Right. refuseEmptyListFilter is a module-private function (not in packages/rest/src/index.ts's export list); no new error code, status, answer member or export. The throw shape (status, code, param on an Error) is the one projectionFieldsValue already uses for data: a blank or comma-only $searchFields is read as absent, so a search the caller narrowed runs over the default search fields instead of being refused #27484 and assertFilterParamSuppliedOnce for the arity refusal.
  7. Pin flipped on purpose. Right. The finding: GET /api/v1/meta/app?id=… — the ?id= filter is inert, the same 3 apps come back for any value #7566 preservation case asserted that ?id= empty "must not become a new 400", citing an unset select that submits it; no such producer exists (see ④), the card's expected result rules the other way, and the half that asserted an absent id lists everything is kept.

One wrong claim about the surface, in prose only (see ② for the finding): the changeset's "Now" bullet says param is "set to the parameter" on the wire. On main, resolveErrorResponse's 4xx passthrough writes { error, code, issues?, object? } and never reads error.param; resolveThrownHttpError (the shared resolver both thrownCodeFields and errorFromThrown read) carries no param either. So on both transports the parameter is named in the message, as #27484's changeset says of its own refusal, and param does not reach the body. The tests assert the message's first clause and code, never param, so no pin is wrong; the prose is.

② Semver level

.changeset/28548-empty-app-id-refused.md: '@objectstack/rest': patch, Clause-②: no (narrowing) and Metadata-protocol: none as line-initial declarations, a BREAKING banner, exactly one adr-0087: not-required (no-migration-prescription) marker, a Before / Now / Unchanged / Who-sees-it account and the remedy. AGENTS.md §3 rules a non-protocol change, widening or breaking, ships patch and keeps its BREAKING banner and ADR-0087 disposition; (narrowing) is the BREAKING arm. The level matches what the diff publishes: @objectstack/rest ships the narrowed door, @objectstack/runtime moves only a test file its files[] does not ship, and nothing under packages/spec is touched (no artifact regeneration owed). The PR body carries the same two declaration lines, line-initial, and the title's ! matches the banner. Clause-②: line: no (narrowing) on both the PR body and the changeset, consistent with the claim 6554674218's expectation for the #27484-precedent outcome, and with this record's face. Check-runs: Check Changeset completed success on this head; a second Check Changeset run on the same head is still in progress at this read, which is an honest reading, not a failure.

Should-fix (prose, no code change): the changeset's "Now" bullet "param set to the parameter" claims a body member neither door writes (the closing paragraph of ①). Reword to "the message names the parameter" (the #27484 entry's own wording) or drop the clause, so the shipped CHANGELOG does not promise a key a consumer could pin.

③ Boundary flags

  • open_questions: [] in both os-dev-reports (6555083587, 6555454646). Nothing to answer.
  • Round-1 out_of_scope_findings (empty ?object= on the view list): folded by the seat's amendment 6555101824 and implemented in round 2 on this head. Closed.
  • Deviation "PR body not patched by the dev": the PR body as read on this head already describes both the ?id= and the ?object= refusal, the fold and the seat's direction, and the title names both, so the body is current. Closed.
  • Deviation "test:repo not run whole locally": Test Core (all eight shards) is green on this head. Closed.
  • Acceptance note "two 400 dialects on one route" (the chain's thrown refusal answers the flat { error, code }; the arity refusal at the door entry answers the nested { error: { code, message } }): pre-existing, shared with refuseUnknownMetaListType on this route, and the dev did not hand-build a body, which is the rule the unknown-type refusal's docblock states. Accepted as flagged, not escalated.
  • Acceptance note "whitespace id keeps 200 []": consistent with the card's accepted alternative ("matches nothing as the whitespace value does") and with the chain comparing an app name whole; data: a blank or comma-only $searchFields is read as absent, so a search the caller narrowed runs over the default search fields instead of being refused #27484 trims because it splits a comma list. Accepted.
  • No labels on the PR: correct, @objectstack/rest ships, so skip-changeset would be wrong.

④ Design judgment

The narrowing, read against the published spec on main:

Must-fix: none. Should-fix: the one in ②.

Implemented-by: claude/project-thread-cq2t0t
Reviewed-by: session_01FEkQ5kE5BrPUPNMMyv6oCH

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract-review-claim: session_01L9qibw58ehQ2gRHP2h2Puz · head 70a3f972988a14f0d6e878f9404820af06e4886e · 2026-11-01T09:46Z

Delta only: the seat's record 6555574011 is on 9c45ac50; this head differs by one changeset line (the 6555784146 carry). The lane posts the same-head record so the ledger reads it on this head.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-tesla · session_01L9qibw58ehQ2gRHP2h2Puz

Contract review (delta)

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 70a3f972988a14f0d6e878f9404820af06e4886e
Local-runs: none
Scope: the PR-only interdiff 9c45ac50..70a3f972 (one file, .changeset/28548-empty-app-id-refused.md, one line) read against the seat's record 6555574011 on 9c45ac50; the whole diff on this head (4 files, +128/−12 vs its merge-base 310cb96d) re-read once against that record's ①; card #28548 and its six comments; the ACCEPT 6555784146.
Inputs: card + PR body + PR diff + check-runs (read 2026-11-01T09:48Z: all 48 latest-per-name check-runs completed, 33 success + 15 skipped, none failed, none running) — 48 checks

Delta judgment

  • What moved: the changeset's "Now" bullet drops "param set to the parameter" for "(the message names the parameter)" — the seat's one should-fix in 6555574011 ②, applied. Nothing else: no source, test or spec file differs between the two heads (git diff 9c45ac50 70a3f972 --stat: 1 file, 1 line). git merge-tree --write-tree origin/main 70a3f972 clean.
  • The record carries: ①1–①7 of 6555574011 were re-read in the diff on this head and hold — step 3 if (appIdFilter === '') refuseEmptyListFilter('id', 'app', 'app') keyed on the folded metaType === 'app'; step 4 metaType === 'view' && query?.object === '' ahead of the truthiness switch; both after the per-caller gate; the throw shape (status, code, param on an Error) is data: a blank or comma-only $searchFields is read as absent, so a search the caller narrowed runs over the default search fields instead of being refused #27484's; refuseEmptyListFilter is module-private; the finding: GET /api/v1/meta/app?id=… — the ?id= filter is inert, the same 3 apps come back for any value #7566 pin's empty-id half flipped on purpose and its absent-id half kept; the runtime parity census gains the two empty probes and row A asserts 400 VALIDATION_ERROR with no items. The prose claim the should-fix named is now gone, so ①'s one wrong-in-prose line is closed.
  • ② on this head: @objectstack/rest: patch, Clause-②: no (narrowing), Metadata-protocol: none, BREAKING banner, one ADR-0087 not-required (no-migration-prescription) marker, line-initial in the changeset and the PR body; the PR title's ! matches. Consistent with AGENTS.md §3 for a non-protocol narrowing (patch keeps its BREAKING banner), and Check Changeset is success on this head.
  • ③: no label on the PR (right: @objectstack/rest ships). The ACCEPT 6555796278 says "landing sent (ready + auto-merge)": the PR is ready on this read, but auto_merge is not set on it — the seat re-arms it (or the relay's enable step is re-read) before expecting the queue to take it. No open question; the empty ?package= reading is recorded as an observation in 6555574011 ④ and stays outside this card.
  • ④: unchanged from 6555574011 — the spec's GetMetaItemsRequestSchema declares neither id nor object, so no declared capability is removed; no producer in @objectstack/client, the Console, the CLI or the QA ledger sends either parameter empty; the refusal is caller-invariant and carries no list fact.

Findings: none owed on this head.

Implemented-by: claude/project-thread-cq2t0t
Reviewed-by: session_01L9qibw58ehQ2gRHP2h2Puz (delta over session_01FEkQ5kE5BrPUPNMMyv6oCH's 6555574011)

VERDICT: PASS (delta)

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH

席位核对 · ACCEPT · PR #28602 · 2026-11-01T08:55Z

Head-sha: 70a3f972988a14f0d6e878f9404820af06e4886e

维护者速读(席位终稿)

核对六项

  1. 形态:draft → main;Fixes #28548,本 PR 交付卡的期望(空 id 拒收)及席位要求并入的空 object,合并即关卡正确;其余闭合关键词零命中(已扫)。文件 4 个,在认领申报面与修订 6555101824 内。标题已按新范围更新。
  2. 前提实测:红先:空 id 在 base 上 rest 与 runtime 行 A 皆红(200≠400);空 object 在 9246901a 上同样红。消融两条腿各自在两种传输上转红,复原后 blob 回 HEAD。
  3. 门禁:本 head check-runs 52 个,37 success、15 skipped、0 失败,mergeable clean。dev 本地 9c45ac50:60/60 派生门禁 exit 0,dogfood 364 文件 / 2934 测试全绿,两种 typecheck 绿。70a3f972 只改 changeset 一句。
  4. 接受的偏差:翻转了旧钉子「ABSENT id 返回全集」中把空 ?id= 当缺省的那一半,卡的期望结果正是反向裁定,且量过无任何生产者发送空 id,接受。
  5. 范围外去向:空 ?package= 仍读作不限包(复核记为观察,非本卡范围,all 的已声明同族语义),不另立卡。Metadata-protocol: none,无 Skills-docs: 欠账。PR 正文中三处 param 的说法已由席位删去,以与线上一致。
  6. 落地条款:Clause-②: no (narrowing) ⇒ 契约复核达档 PASS 在案:comment 6555574011(CONTRACT_REVIEW_TIER,head 9c45ac50);其后 70a3f972 只动 .changeset/**(prose face),记录随之承接。check-governed-merges --pr 28602:NOT governed。落地授权,维护者原话:「按照 pm-dispatch 规则复核过的都授权」(2026-10-25T11:35Z)。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Metadata API: GET /api/v1/meta/app?id= with an empty value ignores the filter and answers the caller's full app list

1 participant