Skip to content

fix(rest)!: GET /meta/app refuses a bracketed id key (?id[]=, ?id[0]=) instead of answering every app - #29046

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-vh6ns8
Nov 2, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-vh6ns8

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Leehom · project thread
Fixes #28930
Clause-②: no (narrowing)
Metadata-protocol: none
Skills-docs: none — no skill or doc page teaches the app list's ?id= filter (git grep over skills/ and content/docs/ finds none), so nothing there names a spelling this refuses.

维护者速读

  • 改了什么:应用列表 GET /meta/app 收到括号写法的 id(?id[]=NAME、?id[0]=NAME,以及任何以 id[ 开头的键)时,答 400 VALIDATION_ERROR 并点名该键,不再忽略过滤、把调用者能看的整份应用列表当作结果返回。
  • 为什么改:按 qs 风格序列化数组的客户端只想取一个应用,却拿回全部应用,还会把第一个当作它要的那个来渲染;这和 Metadata API: GET /api/v1/meta/app?id= with an empty value ignores the filter and answers the caller's full app list #28548 修好的空 ?id= 是同一类静默放宽。
  • 风险与回滚:只动 packages/rest 列表链里的一步(两条传输共用),patch 带 BREAKING 横幅;仅影响发送括号键的 API 调用方,@objectstack/client 与控制台都不发 id。回滚即还原本 PR。
  • 先看:packages/rest/src/meta-item-read-gate.ts(refuseBracketedAppIdFilter 与第 3 步的调用);packages/qa/dogfood/test/meta-read-doors-live.dogfood.test.ts 末尾新块;.changeset/28930-bracketed-app-id-refused.md。
  • 两分钟验证:pnpm --filter @objectstack/rest exec vitest run src/meta-app-publish-gate.test.ts,预期 31 个全绿,其中三条 BRACKETED 用例断言 400。
  • 需要你做的:无,席位落地。

What changes

?id[]=setup and ?id[0]=setup reach the /meta list chain under the key as spelled: every adapter hands query keys through unparsed (readQuery in plugin-hono-server, readDispatchQuery in @objectstack/hono). Step 3 of createMetaListAnswer, the one-app filter, reads query.id only, so it never saw them, and the caller's whole visible list came back with 200.

  • packages/rest/src/meta-item-read-gate.ts: step 3 now asks refuseBracketedAppIdFilter(query) first on the app list. Any query key that opens id[ is refused with 400 VALIDATION_ERROR and no list. The refusal uses the empty ?id= refusal's sentence frame (Metadata API: GET /api/v1/meta/app?id= with an empty value ignores the filter and answers the caller's full app list #28548). Both now go through one builder, throwListFilterRefusal, and the empty ?id= / ?object= messages stay byte for byte the same:
    • empty (unchanged): Query parameter 'id' names no app; got "". The list was not served: an empty app name is not a filter. Name one app, or leave the parameter out to list every app you can see.
    • bracketed (new): Query parameter 'id[]' names no app; the 'id' filter takes exactly one app name, spelled ?id=NAME. The list was not served: a bracketed 'id' key is not a filter. Name one app, or leave the parameter out to list every app you can see.
  • The refusal sits after the per-caller gate (step 2), where Metadata API: GET /api/v1/meta/app?id= with an empty value ignores the filter and answers the caller's full app list #28548 placed the empty refusal, and it throws to each transport's own catch. So RestServer and the runtime dispatcher answer it identically, through the one seam both list doors share.
  • A bracketed key next to a plain ?id= is refused too, because the request carries two spellings of one filter.

Measured: does the seam serve other /meta/:type list doors? Only the app list reads id. Step 3 is keyed on the folded metaType === 'app', so /meta/apps is covered and no other type reads an id spelling. On every other type a bracketed id key stays unread, the way ?id= is (pinned by a control). The query parse is the adapters' and is not this chain's to change. So the fix is /meta/app only, and the siblings are reported under Acceptance notes.

On the order's words "the same 400 the repeated id gets (#28548's words)". The repeated id answer is repeatedQueryParamMessage, which says supplied N times. That would be false for a key supplied once. So the bracketed key gets #28548's own refusal frame, as the claim's "extends #28548's refusal" reads. Status and code are the same as the repeated id's 400.

Tests

Measured at HEAD 2055ce23 (worktree on claude/project-thread-vh6ns8, base 135054eb).

  • packages/rest/src/meta-app-publish-gate.test.ts: it.each(['id[]', 'id[0]', 'id[name]']) asserts 400 + code: VALIDATION_ERROR + the message's first clause naming the key, with no list in the body. It runs for three caller postures, on the plural apps, and with a bracketed key next to a plain id. A control shows a bracketed id key on the view list is not read.
  • packages/runtime/src/domains/meta-list-projection-parity.test.ts: two PARAM_PROBES (?id[]=crm, ?id[0]=crm) drive both transports over every type and caller. The census of row A grows by 2 probes × 26 type cells (991 tests). The "reference moves" test asserts the RestServer answer is a 400.
  • packages/qa/dogfood/test/meta-read-doors-live.dogfood.test.ts (booted showcase over the Hono stack): ?id[]=setup and ?id[0]=setup as the admin and as a signed-up member (the client_liaison holder) answer 400 VALIDATION_ERROR with Query parameter 'id[]' names no app and no items. Controls: ?id=showcase_app returns one app, ?id=setup returns [], ?id= returns 400, a repeated id returns 400, and no id returns the list, where a member never sees setup.
Red first / ablation: dropping the arm reds only the new cases

The mutation replaced the call if (metaType === 'app') refuseBracketedAppIdFilter(query); with void refuseBracketedAppIdFilter; through scripts/ablation-replace.mjs, which is main's behaviour. The tool reported anchor 1 → 0, blob 9c8674a8 → b9866803. Then pnpm --filter @objectstack/rest build (exit 0), and ablation-dist-preflight.mjs @objectstack/rest "refuseBracketedAppIdFilter(query);" --absent (exit 0, marker absent from all 6 built files).

  • rest unit: Tests 3 failed | 28 passed (31). The three failures are exactly the BRACKETED id[] / id[0] / id[name] cases.
  • runtime parity: Tests 1 failed | 990 passed (991). The one failure is the "reference moves" test. Row A stays green because both transports gave the same unfiltered list.
  • dogfood: Tests 2 failed | 12 passed (14). The failures are ?id[]=setup and ?id[0]=setup, which got status: 200 with items: [showcase_app, …], the card's reported answer.

Restore: ablation-replace --restore reported blob == HEAD 9c8674a8 and an empty git diff HEAD. The rebuild exited 0, and the preflight showed the marker present in 2 built files with a clean tree. A first ablation attempt deleted the line outright. The now-unused function then failed the rest DTS build (exit 1), and that run was discarded.

Package suites and typecheck (HEAD 2055ce2)
  • pnpm --filter @objectstack/rest test: exit 0, Test Files 366 passed (366), Tests 6940 passed | 332 skipped (7272)
  • pnpm --filter @objectstack/rest typecheck: exit 0
  • pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 src/domains/meta-list-projection-parity.test.ts: exit 0, Tests 991 passed (991)
  • pnpm --filter @objectstack/runtime typecheck: exit 0
  • pnpm --filter @objectstack/dogfood exec vitest run test/meta-read-doors-live.dogfood.test.ts: exit 0, Tests 14 passed (14)

Acceptance notes

  • Premise. The card and the order place the filter in packages/rest/src/rest-server.ts. It lives in packages/rest/src/meta-item-read-gate.ts (createMetaListAnswer, step 3), the chain both transports call. The defect reproduced as stated (see the ablation leg).
  • The dogfood stack has no setup app. bootShowcase serves only showcase_app to both callers. The card's setup comes from a full os dev boot. The bracketed probe still uses setup as the order asks, and on main it answered the whole list ([showcase_app]). The narrowing control uses ?id=showcase_app, and ?id=setup → [].
  • Sibling, same class (finding, not fixed here). GET /meta/view?object[]=lead answers every view with 200, where ?object=lead answers one. This was measured through RestServer's GET /meta/:type route handler with a three-view fixture: object[] gave 200 ["all_leads","acct_view","loose_view"] and object= gave 200 ["all_leads"]. The adapters pass the key through the same way, so the view switcher's filter is dropped the same way. Not measured: package[] on the list doors (?package= scopes the read, and an unread key would read as "every package").
  • Overlap. Open PR fix(metadata-protocol)!: a package-less write onto a packaged view, dashboard, report, translation or email template is refused, naming the .custom pair (ADR-0146 D11) #28850 also edits meta-read-doors-live.dogfood.test.ts, in its header comment and the acceptance[2] setup. This PR only appends a describe block at the end of the file, so the hunks do not meet. No other open PR touches meta-item-read-gate.ts, rest-server.ts, the rest unit file or the runtime parity file. Every open PR was checked by diff names; fix(metadata-protocol): the by-id OCC comparison answers only for a row the caller reads #28731 was checked through its file list.

Verification

All local readings are at HEAD 2055ce23. Every family CI runs is cited from this PR's check-runs in the report, and none of them was re-run here.

  • Derived gate set (node scripts/pm/dispatch-gates.mjs --commands, from the merge-base): 64 commands run, all exit 0. --ran reconciliation answers 64 derived famil(ies) accounted for — 64 run, 0 NOT-MEASURED. Two first-pass exit 3s were prerequisites, not reds, and each was re-run green:
    • check-plugin-teardown-shape.mjs --self-test needed its pinned commit on this shallow clone (git fetch --depth=1 origin 621a4876…).
    • check:dual-build-cjs-loads read 7 packages with no dist/. After they were built, it reported 115 published require entry point(s) across 70 package(s) load.
      The record is pm-orders/server/28930-gates-ran.txt in the project files.
  • Full dogfood battery (the (narrowing) arm owes it): pnpm --filter @objectstack/dogfood test under the verify lock, after the @objectstack/dogfood^... closure build. VERDICT command-exit 0 · Test Files 383 passed | 3 skipped (386) · Tests 3166 passed | 28 skipped (3194).
  • Not run locally: test:repo (the vitest repo projects, which belong to CI Test Core) and pnpm lint (a repo-wide scan owned by CI). The one repo-project file this diff edits, packages/runtime/src/domains/meta-list-projection-parity.test.ts (listed in packages/runtime/vitest.repo-tests.json), was run directly (991/991). The rest of that project is left to CI.

Generated by Claude Code

…ing every app

`?id[]=NAME` and `?id[0]=NAME` reach the list chain under the key as
spelled, which the one-app filter never read, so the caller's whole
visible list came back. The chain now refuses any key that opens `id[`
on the app list with the empty `?id=` refusal's frame: 400
VALIDATION_ERROR naming the key, no list.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5o6bowfKzFDhW76JZ3
@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

4 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 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; 101 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 ab085a048241b0bf1fde06696fc6cbf659a6707f → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 4d36e6586f65ae2d11a6f14a506615020eb64bab — the merge of head 2055ce2324552f66a7654791d750177a9859837d into base ab085a048241b0bf1fde06696fc6cbf659a6707f, 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 4d36e6586f65ae2d11a6f14a506615020eb64bab && git checkout 4d36e6586f65ae2d11a6f14a506615020eb64bab
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ab085a048241b0bf1fde06696fc6cbf659a6707f 2055ce2324552f66a7654791d750177a9859837d && git checkout -B drift-repro ab085a048241b0bf1fde06696fc6cbf659a6707f && git merge --no-ff 2055ce2324552f66a7654791d750177a9859837d

node scripts/docs-audit/affected-docs.mjs --json ab085a048241b0bf1fde06696fc6cbf659a6707f

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 2055ce2324552f66a7654791d750177a9859837d
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/qa/dogfood/test/meta-read-doors-live.dogfood.test.ts + packages/rest/src/meta-collection-routes.ts (the REST list door: refuseRepeatedQueryParams over META_QUERY_ARITY_DOORS.list, metaTypeSingular, the hand-off to createMetaListAnswer, the handleRouteError catch), packages/runtime/src/domains/meta.ts (the dispatcher's one-segment list branch: metaQueryArityRefusal('list'), pluralToSingular, answerMetaList and its errorFromThrown catch), packages/adapters/hono/src/index.ts (readDispatchQuery), packages/plugins/plugin-hono-server/src/adapter.ts (readQuery), packages/rest/src/query-multiplicity.ts (repeatedQueryParamMessage), packages/rest/src/error-response.ts (handleRouteError), packages/rest/src/index.ts (the export list), packages/runtime/src/endpoint-executor.ts (the other query.id reader, a different door), .changeset/28930-bracketed-app-id-refused.md (read for ②), PR #28602's changeset and its PASS record 6555574011, PR #28850's file list and its hunks on the dogfood file. Inputs: card #28930 body and its three comments (triage 6569547911, claim 6569718113, os-dev-report 6570620675), PR #29046 body, file list and net diff against the merge-base 135054ebaa7fb4f1282714d6856a0a6a032a81fe, and the 54 check-runs on this head. The merge-base and the branch were read with git show on fetched refs; nothing was built, run, re-run or ablated here.

① Derived judgments

Accept-set and public-surface changes the diff implies, each read against the cited code on the merge-base and on the head:

  1. GET /meta/app and GET /meta/apps with any query key that opens id[ (?id[]=NAME, ?id[0]=NAME, ?id[name]=NAME): 200 with the caller's whole visible list → 400 VALIDATION_ERROR, no list, on both transports. Right. Both adapters build the query with Object.fromEntries over c.req.queries() and parse no brackets (readQuery in plugin-hono-server/src/adapter.ts, readDispatchQuery in adapters/hono/src/index.ts), so the key reaches the door spelled id[]. The list door declares no closed set (META_QUERY_ARITY_DOORS docblock: recognition "never applies" to list), and its arity table names id, not id[], so neither entry gate catches the key; on the merge-base step 3 of createMetaListAnswer read query?.id only, and the key fell through to the unfiltered list, which is the card's reproduction. On the head, step 3 asks refuseBracketedAppIdFilter(query) first, keyed on the folded metaType === 'app' (metaTypeSingular on the REST route, pluralToSingular in the dispatcher), so apps is covered and the plural is pinned. The throw carries status = 400, code = 'VALIDATION_ERROR' on an Error, the exact shape Metadata API: GET /api/v1/meta/app?id= with an empty value ignores the filter and answers the caller's full app list #28548's refuseEmptyListFilter throws; the REST route catches into handleRouteError and the dispatcher's answerMetaList into deps.errorFromThrown(e, 500), the two catches PASS record 6555574011 already traced to the wire. The rest unit drives the real GET /api/v1/meta/:type handler and asserts statusCode 400, body.code, no items and no bare array; the parity row A asserts the same triple on RestServer; the dogfood pins it over HTTP on the booted Hono stack as admin and as a member.
  2. The refusal's sentence is one spelling for every bracketed key, in Metadata API: GET /api/v1/meta/app?id= with an empty value ignores the filter and answers the caller's full app list #28548's frame, and the two existing messages are byte-identical. Right. refuseEmptyListFilter now delegates to throwListFilterRefusal(param, param, names, lists, 'got ""', 'an empty NAMES name'); expanding that builder reproduces the merge-base literal word for word for both id/app and object/object, so Metadata API: GET /api/v1/meta/app?id= with an empty value ignores the filter and answers the caller's full app list #28548's two answers do not move. The bracketed arm calls the same builder with the key as sent, so id[], id[0] and id[name] differ only in the quoted key, which is what "one spelling, same words" means for a refusal that must name the spelling it refuses.
  3. A bracketed key beside a plain ?id= is refused too. Right. The bracket check runs before appIdFilter is read, so { id: 'crm', 'id[]': 'account' } is 400 and never narrows to crm; pinned in the rest unit. A bracketed key supplied twice (?id[]=a&id[]=b) arrives as an array under id[], which the arity table does not name, so the bracket refusal answers it with the same sentence: one answer for every bracketed shape.
  4. The refusal sits after the per-caller gate (step 2) and is caller-invariant. Right. It reads only the query's keys, names only the key and a fixed sentence, and the rest unit runs it for studio.access, manage_users and no permission; the dogfood runs it for the admin and the client_liaison member. A caller who may see nothing learns nothing from it.
  5. Everything else is unchanged. Right, read in the diff: ?id=NAME narrows as before (showcase_app one app, setup on a stack without it an empty list); an absent id is undefined and takes no new branch; the empty ?id= and the repeated id keep their 400s (the arity refusal at the door entry is untouched on both transports); the guard is keyed on app, so a bracketed id key on any other type stays unread, as ?id= is there (the rest control on view, and the census's 2 probes × 26 type cells on both transports); ids[, ID[ and the like do not match startsWith('id['), consistent with ids= and ID= never being read; a non-object query returns early.
  6. Public surface: nothing added. Right. refuseBracketedAppIdFilter and throwListFilterRefusal are module-private; the diff does not touch packages/rest/src/index.ts; no new error code, status, answer member or export. The only published change is the accept set of one door shrinking.
  7. "Does the seam serve other /meta/:type list doors?" The dev's answer, /meta/app only, is right. On the head the sole query.id read on any /meta door is step 3 (git grep over packages/rest/src, packages/runtime/src and both adapters, tests excluded); the other reader, endpoint-executor.ts, is the OpenAPI object_operation record id, a different door with a declared parameter. The arity table confirms it: id is single-valued on list alone. So there is no second door with an id filter to extend to, and "both transports" is the seam's full reach.
  8. The order's "the same 400 the repeated id gets, Metadata API: GET /api/v1/meta/app?id= with an empty value ignores the filter and answers the caller's full app list #28548's words". The dev read it as status and code (both 400 VALIDATION_ERROR) plus Metadata API: GET /api/v1/meta/app?id= with an empty value ignores the filter and answers the caller's full app list #28548's sentence frame, not repeatedQueryParamMessage. Right: that message says "supplied N times" and would be false for a key sent once, and the repeated-id answer predates Metadata API: GET /api/v1/meta/app?id= with an empty value ignores the filter and answers the caller's full app list #28548 (finding: GET /api/v1/meta/app?id=… — the ?id= filter is inert, the same 3 apps come back for any value #7566 / hono adapter: createHonoApp's catch-all flattens a repeated query name to its last value, so every dispatcher arity rule is unreachable on a @objectstack/hono host #23214 arity), so "Metadata API: GET /api/v1/meta/app?id= with an empty value ignores the filter and answers the caller's full app list #28548's refusal" can only mean refuseEmptyListFilter's frame, which is what the diff reuses. The pre-existing two-dialect note (the chain's thrown refusal writes the flat { error, code } on REST, the entry arity gate the nested one) was accepted on 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 and is not moved here.

Claims checked for wrong statements about the surface: the changeset's "Now" message is the literal the builder produces; the PR body's "every adapter hands query keys through unparsed" holds for both Hono adapters read here (the node reference adapter reads searchParams.getAll, also unparsed); the body's "no other type reads an id spelling" holds per item 7. None found wrong.

② Semver level

.changeset/28930-bracketed-app-id-refused.md: '@objectstack/rest': patch; line-initial Clause-②: no (narrowing) and Metadata-protocol: none; a BREAKING banner; exactly one adr-0087: not-required (no-migration-prescription) marker; Before / Now / Unchanged / Who-sees-it; the remedy as FROM ?id[]=NAME (or ?id[0]=NAME) TO ?id=NAME. The PR title carries !, and the PR body carries the same two declaration lines line-initial. AGENTS.md Post-Task Checklist step 3: (narrowing) is the BREAKING arm, and a non-protocol change ships patch with its banner and ADR-0087 disposition (ruling A, #25105). The level matches what the diff publishes: only @objectstack/rest ships the narrowed door; @objectstack/runtime moves a test file its package does not ship; packages/qa/dogfood is private; nothing under packages/spec moves. Metadata-protocol: none is right: a query-key spelling on a REST door changes nothing a metadata author writes. Check Changeset and Lint & Repo Gates (which runs check:changeset-gate-self-tests, the ADR-0087 registration gate included) are green on this head.

The declaration deviation, judged. The order expected Clause-②: yes (narrowing); the dev declared no (narrowing). The dev is right. In this repo's one reader of the line (scripts/pm/record-recognisers.mjs, reviewFace), yes is the widening declaration ("a widening in application or UI code, a translation key, a version declaration"), the fourth face is keyed on the exact spelling Clause-②: no (narrowing) ("a Clause-②: no card that NARROWS a published accept set", ruling record 6203713500), and yes (narrowing) is reserved for "a diff that widens AND narrows". This diff widens nothing (① item 6), so a yes would have been a false widening declaration; no (narrowing) is the same spelling #28602 declared for the same class at the same seam and passed on (record 6555574011). The claim itself wrote "expected ... the dev measures and declares", so the order's line was a prediction, not a ruling. The gate outcome is unchanged either way: both spellings owe this review at CONTRACT_REVIEW_TIER, and check-adr-0087-registration reads the arm, not the value, for breaking-ness. Skills-docs: none holds: git grep on the head finds no page under skills/, content/docs/ or apps/docs/ that teaches meta/app?id.

③ Boundary flags

  • open_questions: [] in the os-dev-report 6570620675. Nothing to answer.
  • Seam location (premise correction). The card and the order placed the filter in packages/rest/src/rest-server.ts; it lives in createMetaListAnswer step 3 of packages/rest/src/meta-item-read-gate.ts, the chain both meta-collection-routes.ts and runtime/src/domains/meta.ts call. Verified at both call sites. The defect reproduced as stated (the card's measurement and the dev's ablation agree on the merge-base's answer). Closed.
  • Evidence: ablation. Not re-run here (read-only). Read for coherence: the mutation replaced the one call site with void refuseBracketedAppIdFilter; so the function stayed declared and the DTS build stayed green, which is a true ablation of the call; the reds reported are exactly the new assertions, three it.each rows in the rest unit, the one "reference moves" it in parity, the two bracketed its in dogfood; and the census row A staying green under ablation is the correct expectation, since both transports then give the same unfiltered list. Coherent with the diff. Accepted.
  • Evidence: the full dogfood battery. (narrowing) owes it (os-dev.md); reported under the verify lock at this head as 383 files passed, 3 skipped, 3166 tests passed, 28 skipped, no failures. The CI twin, Dogfood Regression Gate, is green on this head. Accepted.
  • Evidence: derived gates. The dev's local record says 64 of 64 families run and green, with two prerequisite re-runs declared. The verdicts of record are the head's check-runs: 54 of 54 completed, 37 success and 17 skipped, none failed, including all eight Test Core shards, Type Check (four), Lint (nine), Check Changeset, PR-body guards, Governed Surface Queue Guard, Spec property liveness, Build Core, Test UI, Live E2E UI and Dogfood Regression Gate. The skipped ones are opt-in harnesses, UI-matrix placeholders and the schedule gate. No merge-base comparison is needed, since nothing is red.
  • Overlap with fix(metadata-protocol)!: a package-less write onto a packaged view, dashboard, report, translation or email template is refused, naming the .custom pair (ADR-0146 D11) #28850. Verified on its file list and patch: its two hunks on meta-read-doors-live.dogfood.test.ts are the header comment plus an ensurePair/inPair import near line 23 and the acceptance[2] PUT setup near line 91; this PR appends one describe at the end of the top-level block (from line 164) and uses only helpers already in the file (stack.apiAs, codeOf, messageOf, admin, liaison). Disjoint hunks, no shared symbol; whichever lands second rebases without a conflict. The dev's "no other open PR touches meta-item-read-gate.ts, rest-server.ts, the rest unit or the parity file" is the dev's reading at report time; the seat's pre-enqueue overlap re-read remains its own.
  • Out of scope: GET /meta/view?object[]=lead answers every view (and package[ is unmeasured). Judged against the order's ruling, which binds the id filter ("whether the same ?id= filter (or the same query parse) serves other /meta/:type list doors; if one seam serves them, they get the same answer; if not, /meta/app only and report the others"): the view list's object filter is a different key, read by step 4, so the ruling as written does not pull it into this PR, and the dev did what it says, fixed /meta/app only and reported the sibling. It is a separate card, not a FAIL of this one. Recorded for that card: it is the same defect class in the same chain, the fix is mechanical (refuseBracketedAppIdFilter hard-codes id/app; a (param, names, lists) twin on step 4 serves object[), and package[ is a third site, read at the door entry by metaItemPackageBinding(req.query?.package) before the chain on both transports, where an unread key reads as "every package". The seat may instead fold the object[ arm by claim amendment, as Metadata API: GET /api/v1/meta/app?id= with an empty value ignores the filter and answers the caller's full app list #28548 folded the empty ?object= (6555101824), at the cost of a patch round and a fresh record on the new head; that is the seat's call, not this record's.
  • Observation, not a finding of the narrowing. The dogfood control's last assertion, 'a member never sees setup', is vacuous on the showcase stack, which boots no setup app for either caller (the dev says so under Acceptance notes); the pruning the card observed came from a full os dev boot. The bracketed probes still use setup as the order asked and still red on the merge-base (the whole list came back), so the pin's purpose holds; only that one control sentence promises more than the stack can show. Non-blocking.
  • needs:contract-review is on the PR; this PASS on the current head lets the seat remove it in the same stroke. Author objectstack-fleet[bot], so no author note is owed.

④ Design judgment

The narrowing, read against the published contract: GetMetaItemsRequestSchema declares no id member, so the spec gives a bracketed id no meaning; ?id= is #7566's transport-level declaration ("narrows to the app of that name"), and no text declares an array spelling; @objectstack/client's meta.getItems builds ?package= only and the console sends no id on this read (the producer census PASS record 6555574011 made, unchanged by this diff; git grep on the head finds no id[] builder outside unrelated UI param widgets). So this is the spec's own refusal class made real, the one #27484 and #28548 ruled for a value that names nothing, not an implementation gap narrowed at the consumer. Attempts to break it: a doubled bracketed key (one answer, ① item 3); a percent-encoded id%5B%5D (Hono decodes it to id[] before queries()); ids[ and ID[ (unmatched, consistent with the plain spellings); a dispatcher-only host (same chain, pinned by the census on every type × caller); a non-object query (early return). Nothing breaks.

Must-fix: none. Should-fix: none that blocks; the vacuous control sentence in ③ may be reworded in the next edit of that file.

Implemented-by: claude/project-thread-vh6ns8
Reviewed-by: session_0186vfjHHMsRXC9EUT9hTFMo

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo

席位核对

ACCEPT · domain:server seat 2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo · 2026-11-02T06:47Z · head 2055ce23 · claimed by seat 2 on #28930 (comment 6569718113)

The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.

  1. Shape:
  2. Contract review at CONTRACT_REVIEW_TIER: PASS on this head (6570942095).
  3. Gates: CI on this head shows 37 success, 17 skipped and 0 red, with every run completed. The merge queue re-runs it against current main.
  4. Follow-up: the dev's finding that ?object[]= on the view list answers every view is the same class on a different key. It will be filed as its own card at landing.
  5. Serial: none. fix(metadata-protocol)!: a package-less write onto a packaged view, dashboard, report, translation or email template is refused, naming the .custom pair (ADR-0146 D11) #28850 touches the same dogfood file in disjoint hunks.
  6. Landing: pr_ready + automerge_enable through the relay.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 2, 2026 06:48
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 2, 2026 06:48
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 2, 2026
Merged via the queue into main with commit d659c1d Nov 2, 2026
58 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-vh6ns8 branch November 2, 2026 08:08
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-steve · session_01ADHkmQxwoGb5fSnrFkpxmt

Landing record · PR #29046

domain:server seat 2 · session_01ADHkmQxwoGb5fSnrFkpxmt · 2026-11-02T09:07Z · card #28930

Reading Probe Result
Merged PR #29046 state merged, merge commit d659c1d91d
On main git merge-base --is-ancestor d659c1d91d origin/main yes
Queue refs git ls-remote origin refs/heads/gh-readonly-queue/* matching 29046 0

Generated by Claude Code

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[]=NAME (bracketed array spelling) ignores the filter and answers the caller's full app list

1 participant