Skip to content

fix(rest)!: /meta list refuses a bracketed object key on views and a bracketed package key on the list and book tree - #29298

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

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-9szbaq

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread
Fixes #29199
Clause-②: no (narrowing)
Metadata-protocol: none
Skills-docs: none — no skill or doc page teaches a bracketed object or package key (git grep over skills/, content/docs/ and apps/docs/ finds none); the pages that name ?object= and ?package= teach the plain spelling, which is unchanged.

维护者速读

  • 改了什么:视图列表 GET /meta/view 收到括号写法的 object(?object[]=NAME、?object[0]=NAME)时答 400 VALIDATION_ERROR 并点名该键;/meta 列表与 book tree 收到括号写法的 package(?package[]=ID、?package[0]=ID)时同样答 400。以前这些键被忽略,返回全部视图或跨全部包的列表。
  • 为什么改:按 qs 风格序列化数组的客户端只想取一个对象的视图或一个包的内容,却拿回全部,还会当作它要的那一份来用;和 fix(rest)!: GET /meta/app refuses a bracketed id key (?id[]=, ?id[0]=) instead of answering every app #29046 修好的 ?id[]= 是同一类静默放宽。
  • 风险与回滚:只动 packages/rest(列表链一步 + 两个读门入口的同一判定,两条传输共用),patch 带 BREAKING 横幅;只影响发括号键的 API 调用方,@objectstack/client 与控制台都不发。所有写门本来就以未知参数拒收 package[],本 PR 不改写行为。回滚即还原本 PR。
  • 先看:packages/rest/src/meta-item-read-gate.ts(refuseBracketedListFilter、metaBracketedPackageRefusal 与第 4 步的调用);packages/rest/src/meta-collection-routes.ts 列表门入口新增的几行;.changeset/29199-bracketed-object-package-refused.md。
  • 两分钟验证:pnpm --filter @objectstack/rest exec vitest run src/meta-app-publish-gate.test.ts src/meta-query-arity-doors.test.ts,预期全绿,其中 BRACKETED view object[…] 与 BRACKETED package 用例断言 400。
  • 需要你做的:无,席位落地。

What changes

?object[]=lead, ?object[0]=lead, ?package[]=ID and ?package[0]=ID reach the /meta doors under the key as spelled: every adapter hands query keys through unparsed (readQuery in plugin-hono-server, readDispatchQuery in @objectstack/hono). The view switcher reads query.object and the package binding reads query.package, so neither saw them, and the caller got every view, or the list across every package, with 200.

  • packages/rest/src/meta-item-read-gate.ts
    • fix(rest)!: GET /meta/app refuses a bracketed id key (?id[]=, ?id[0]=) instead of answering every app #29046's refuseBracketedAppIdFilter becomes one helper keyed by parameter, refuseBracketedListFilter(query, param, names, lists). Step 3 asks it for id on app and step 4 now asks it for object on view. The id answer is byte-identical; the sentence builder is now listFilterRefusalMessage, and the empty ?id= / ?object= messages are byte-identical too (their existing pins pass unchanged).
    • object (new): Query parameter 'object[]' names no object; the 'object' filter takes exactly one object name, spelled ?object=NAME. The list was not served: a bracketed 'object' key is not a filter. Name one object, or leave the parameter out to list every view you can see.
    • metaBracketedPackageRefusal(door, query) (new, module-level, not added to the package index): on the two arity doors that read package (the list and the book tree) a key that opens package[ is refused as data, rule bracketed-parameter, in the same sentence frame. metaQueryArityRefusal asks it after the multiplicity rule, so the runtime dispatcher's list and tree get it with no runtime source change.
    • list: Query parameter 'package[]' names no package; the 'package' filter takes exactly one package id, spelled ?package=ID. The list was not served: a bracketed 'package' key is not a filter. Name one package, or leave the parameter out to list across every package. The tree says The tree was not served and read the tree across every package.
  • packages/rest/src/meta-collection-routes.ts (list) and packages/rest/src/meta-item-read-routes.ts (book tree): RestServer's two doors ask the same decision right after refuseRepeatedQueryParams, before the binding is read, and write it in the nested envelope their arity gate writes.
  • A bracketed key beside the plain one is refused on all three (two spellings of one filter).

Measured: which doors does each seam serve (order item 2, 3).

Door ?object[]= ?package[]= on main This PR
GET /meta/view, /meta/views (list chain step 4) 200, every view 200, every package 400 / 400
GET /meta/:type, any other type not read (like ?object=) 200, every package unchanged / 400
GET /meta/book/:name/tree n/a 200, every package 400
GET /meta/:type/:name, /layers n/a 400 unknown parameter (closed set) unchanged
PUT / DELETE /meta/:type/:name, POST …/publish (write doors) n/a 400 unknown parameter (closed set), protocol never called unchanged
GET /meta/diagnostics n/a 400 unknown parameter (closed set) unchanged

Step 4 is the only list step that reads object, keyed on the folded view (views is covered). The write doors were measured before any change (a scratch probe driving each RestServer route with ?package[]=; the dispatcher's save and item read ask the same closed sets through metaItemQueryRefusal), so no write behaviour changes here. The order's "at the binding's one seam" could not be taken literally: metaItemPackageBinding(raw) receives the VALUE of query.package, which is undefined for a bracketed key, so it cannot see the key. The seam both transports ask before every arity-door read of package is the arity decision, which is where the refusal sits; rest-server.ts (the binding's home) is untouched.

Producer census (order item 5). @objectstack/client builds ?package= with URLSearchParams.set (both meta.getItems surfaces) and sends no object on the list; the console (ui/**, read only) has no qs library and no bracketed object/package builder; no docs or skill example spells either key bracketed. No real producer sends them.

Tests

  • packages/rest/src/meta-app-publish-gate.test.ts: it.each(['object[]', 'object[0]', 'object[name]']) on view and views for three caller postures asserts 400, code, the full sentence and no list; a bracketed key beside a plain object is 400; controls: ?object=lead switches, no object lists all, and a bracketed object key on app is not read (200).
  • packages/rest/src/meta-query-arity-doors.test.ts: the decision as data for package[], package[0], package[id] on the list and the tree (beside a plain package too); a repeated plain package still answers the multiplicity sentence first; not judged on _drafts, state or packages[; RestServer's list and tree answer the nested 400 and getMetaItems is never called; controls: the plain package still scopes, none reads every package.
  • packages/runtime/src/domains/dispatcher-query-arity.test.ts: the dispatcher's list and tree refuse package[] / package[0] with the same code and sentence before any read, and its list chain refuses object[].
  • packages/runtime/src/domains/meta-list-projection-parity.test.ts: probes ?object[]=lead, ?object[0]=lead, ?package[]=crm on the list census (every type × caller, both transports), ?package[]=crm on the item, tree and layers censuses; the "reference moves" test asserts RestServer's 400 for object[] and package[].
  • packages/qa/dogfood/test/meta-read-doors-live.dogfood.test.ts (booted showcase over Hono), appended describe: /meta/view?object[]= / ?object[0]=showcase_task and /meta/object?package[]= / ?package[0]=com.example.showcase answer 400 VALIDATION_ERROR naming the key, no items, as the admin and as a member; controls: ?object=showcase_task is that object's views and fewer than all, ?package=com.example.showcase is narrower than no package.

No existing test was edited to pass; every hunk in a test file is an addition.

Red first on main (45e9b2f)

The pins were committed before the fix (919e71b) and run against main's source and dist:

  • rest: Tests 13 failed | 58 passed (71) — the three BRACKETED view object[…] cases (expected 200 to be 400) and the ten bracketed-package cases. Every control passed.
  • runtime: Tests 6 failed | 1174 passed (1180) — the five dispatcher cases and "the reference moves". The census rows stayed green, as expected: on main both transports gave the same unfiltered answer.
  • dogfood: the reds are read off the ablation legs below (the same code as main for each arm).
Ablation: each refusal, put back, reds only its own pins

Through scripts/ablation-replace.mjs (wrap mode), pnpm --filter @objectstack/rest build, then ablation-dist-preflight.mjs before reading any run.

  1. object arm. The step-4 call if (metaType === 'view') refuseBracketedListFilter(query, 'object', 'object', 'view'); replaced by a marker statement. Anchor 1 → 0, blob 894bc70c → aae79d48; marker present in 2 built files. rest 3 failed | 32 passed (35) (the three object[…] cases); runtime 2 failed | 1178 passed (1180) (the dispatcher object[] case and the reference test); dogfood 2 failed | 17 passed (19) (/meta/view?object[]= and ?object[0]=).
  2. package arm. metaBracketedPackageRefusal's door check replaced by an unconditional return undefined. Anchor 1 → 0, blob 894bc70c → 2a7a9483; marker present in 2 built files. rest 10 failed | 26 passed (36) (the ten bracketed-package cases); runtime 5 failed | 1175 passed (1180) (the four dispatcher list/tree cases and the reference test); dogfood 2 failed | 17 passed (19) (/meta/object?package[]= and ?package[0]=).

Each restore: blob back to HEAD 894bc70c, git diff HEAD empty, rebuild exit 0, marker absent from all 6 built files, whole tree clean.

Acceptance notes

  • Premise. Holds as the card states it, and wider: the bracketed package key also reads as "every package" on the list (every type) and on the book tree.
  • File surface vs the claim. The claim named meta-item-read-gate.ts and, if package[] red, the binding's home (rest-server.ts). The fix lives in meta-item-read-gate.ts plus three-line door hand-offs in meta-collection-routes.ts and meta-item-read-routes.ts; rest-server.ts is untouched (see "Measured" for why the binding cannot see the key). The runtime parity and dispatcher arity tests gained pins; no runtime source changed.
  • Two dialects, as before. The view object[] refusal is thrown inside the list chain after the per-caller gate, so RestServer writes the flat { error, code } body fix(rest)!: GET /meta/app refuses a bracketed id key (?id[]=, ?id[0]=) instead of answering every app #29046's id[] refusal writes. The package[] refusal is answered at the door entry, where package is read, so RestServer writes the nested { error: { code, message } } its arity gate writes. Status and code are identical in both.
  • Same class, not fixed here (report for the seat). GET /meta/_drafts reads ?packageId= and ?type= with no closed set, so a bracketed packageId[] or type[] very likely reads as every package or every type; ?preview[]=draft on the list reads as no preview (the published list) and ?include[]= as no inclusion. Not measured here; they are outside the card's keys.

Verification

Every local reading below is at HEAD 1ee7e4f5 (the fix 34997b8f plus a merge of origin/main), in a worktree on claude/project-thread-9szbaq. Families CI runs are cited from this PR's check-runs in the report, not re-run here.

Package suites and typecheck (HEAD 1ee7e4f)
  • pnpm --filter @objectstack/rest test: VERDICT command-exit 0 · Test Files 367 passed (367) · Tests 6980 passed | 332 skipped (7312)
  • pnpm --filter @objectstack/rest typecheck: exit 0
  • pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 src/domains/dispatcher-query-arity.test.ts src/domains/meta-list-projection-parity.test.ts: exit 0 · Tests 1180 passed (1180)
  • pnpm --filter @objectstack/runtime typecheck: exit 0; pnpm --filter @objectstack/dogfood typecheck: exit 0
  • lint: root eslint with --no-inline-config over packages/rest, packages/runtime, packages/qa/dogfood: exit 0, no findings
Full dogfood battery (the narrowing owes it)

pnpm --filter @objectstack/dogfood test under the verify lock, after the @objectstack/dogfood^... closure build on this head: VERDICT command-exit 0 · Test Files 389 passed | 3 skipped (392) · Tests 3220 passed | 28 skipped (3248).

Derived gate set

node scripts/pm/dispatch-gates.mjs --commands (from the merge-base, at 1ee7e4f5): 64 commands, all exit 0. --ran reconciliation: 64 derived famil(ies) accounted for — 64 run, 0 NOT-MEASURED. Three first-pass exit 3s were prerequisites, not reds, each re-run green: check-plugin-teardown-shape.mjs --self-test (its pinned fixture commit fetched on this shallow clone), check:dual-build-cjs-loads (7 packages outside the dogfood closure had no dist/; built, then 115 published require entry point(s) across 70 package(s) load) and check:type-check-debt (its closure build queued behind the dogfood battery on the shared lock; re-run under the lock, 0 raw tsc error(s)).

  • Not run locally: test:repo (the vitest repo projects, CI Test Core). The one repo-project file this diff edits, meta-list-projection-parity.test.ts, was run directly (above).
  • origin/main has moved 22 commits since the merge; none touches a file this PR edits (git diff --name-only over the range), so the queue's re-run against current main is the remaining check.

Generated by Claude Code

@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Nov 2, 2026
@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

20 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 b177920076f7fd27ac6d0b6717d56f0db04b64f4.

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

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • 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 b177920076f7fd27ac6d0b6717d56f0db04b64f4 → packageMentionDocs.

Which tree this was computed on

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

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

⚠️ 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 b177920076f7fd27ac6d0b6717d56f0db04b64f4 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-steve · session_01ADHkmQxwoGb5fSnrFkpxmt

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 1ee7e4f58fcb24aeeb60f3290f423ee6d545e8e0
Local-runs: none
Scope: packages/rest/src/meta-item-read-gate.ts, packages/rest/src/meta-collection-routes.ts, packages/rest/src/meta-item-read-routes.ts, packages/rest/src/meta-app-publish-gate.test.ts, packages/rest/src/meta-query-arity-doors.test.ts, packages/runtime/src/domains/dispatcher-query-arity.test.ts, packages/runtime/src/domains/meta-list-projection-parity.test.ts, packages/qa/dogfood/test/meta-read-doors-live.dogfood.test.ts + packages/runtime/src/domains/meta.ts (the dispatcher's /meta branches: metaQueryArityRefusal('list' | 'book-tree' | 'drafts' | 'state'), metaItemQueryRefusal('item' | 'layers' | 'published' | 'save'), the packageBinding closure, the rule !== 'layered-member' reader), packages/rest/src/rest-server.ts (metaItemPackageBinding, serveMetaItemLayered, META_ITEM_RESET_PARAMS, META_ITEM_PUBLISH_PARAMS), packages/rest/src/meta-item-write-routes.ts and packages/rest/src/meta-item-history-routes.ts (the write, publish and /published doors' refuseUnknownQueryParams sets), packages/rest/src/meta-collection-routes.ts (the diagnostics and _drafts doors beside the list), packages/rest/src/index.ts and packages/rest/package.json (the exports map and the entry's export list), packages/adapters/hono/src/index.ts (readDispatchQuery), packages/plugins/plugin-hono-server/src/adapter.ts (readQuery), packages/client/src/index.ts (the ?package= / ?object= builders), ui/** (read only, for the producer census), .changeset/29199-bracketed-object-package-refused.md (read for ②), PR #29046's contract review record 6570942095 and the rest CHANGELOG entries for #23072 and #23214 (precedent for ②). Inputs: card #29199 body and its four comments (triage 6574247482, claim 6574465942, os-dev-report 6575874431, claim amendment 6575888036), PR #29298 body, file list and net diff against origin/main at this head, and the check-runs on this head. The merge-base and the branch were read with git show / git grep on fetched refs; nothing was built, run, re-run or ablated here. No reviewFace path hit: record-recognisers.mjs --template --paths <file list> reports no schema or governed path and defers to the changeset's Clause-②: no (narrowing) line, so the narrowing face is the one owed and the scope floor is every path outside the prose faces; all eight are listed above.

① Derived judgments

Accept-set and public-surface changes the diff implies, each read against the cited code on origin/main and on the head:

  1. GET /meta/view and GET /meta/views with any query key that opens object[ (?object[]=NAME, ?object[0]=NAME, ?object[name]=NAME): 200 with every view the caller may see → 400 VALIDATION_ERROR, no list, on both transports. Right. Both adapters still build the query with Object.fromEntries over c.req.queries() and parse no brackets (readQuery, readDispatchQuery), so the key reaches the chain spelled as sent; the list door declares no closed set and META_QUERY_ARITY_DOORS.list names object, not object[, so neither entry gate catches it; on the merge-base step 4 read query?.object only and the key fell through to the unfiltered list, which is the card's measurement. On the head step 4 asks refuseBracketedListFilter(query, 'object', 'object', 'view') before query?.object is read, keyed on the folded metaType === 'view' (metaTypeSingular on the REST route, pluralToSingular in the dispatcher's list branch at meta.ts ~2174), so views is covered and pinned. The throw is throwListFilterRefusal's Error with status = 400, code = 'VALIDATION_ERROR', the 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 and fix(rest)!: GET /meta/app refuses a bracketed id key (?id[]=, ?id[0]=) instead of answering every app #29046 already traced through handleRouteError (REST, flat { error, code }) and answerMetaList's errorFromThrown (dispatcher). Pinned: the rest unit (three keys × three caller postures × two spellings, full sentence, no items, no bare array), the dispatcher arity test (object[] on the dispatcher's /meta/view), the parity census (?object[]=lead, ?object[0]=lead on every type × caller, both transports) and its reference row, and the dogfood over HTTP as admin and as a member.
  2. GET /meta/:type (every list type) and GET /meta/book/:name/tree with any query key that opens package[: 200 across every package → 400 VALIDATION_ERROR on both transports. Right. metaBracketedPackageRefusal(door, query) answers only where META_QUERY_ARITY_DOORS[door] names package (list and book-tree; drafts and state return undefined, pinned). RestServer's list door (meta-collection-routes.ts 644-651) and tree door (meta-item-read-routes.ts 388-395) ask it right after refuseRepeatedQueryParams and before metaItemPackageBinding(req.query?.package), and write it in the nested { error: { code, message } } their arity gate writes; the dispatcher's list (meta.ts 2174) and tree (1149) get it through metaQueryArityRefusal, which now returns repeatedOnceRefusal(...) ?? metaBracketedPackageRefusal(...), so no runtime source moved. The dev's premise that metaItemPackageBinding(raw) cannot see the key is right: it receives the VALUE of query.package (rest-server.ts 1854, typeof raw === 'string' && raw !== '' && raw !== 'all' ? raw : undefined), which is undefined for a bracketed key, so "the binding's one seam" in the order could not be taken literally and the arity decision is the one seam both transports ask before any read of package. Pinned on both transports and over HTTP (/meta/object?package[]=, ?package[0]=), with getMetaItems asserted never called on RestServer.
  3. Rule order and multiplicity: a repeated plain package still answers the multiplicity sentence first; a bracketed key beside a plain one is refused (two spellings of one filter/scope); a bracketed key sent twice arrives as an array under package[], which repeatedOnceRefusal never reads (it iterates the door's named keys only), so the bracket arm answers it with one sentence. Right, read in repeatedOnceRefusal and bracketedKeyOf; the first two are pinned.
  4. fix(rest)!: GET /meta/app refuses a bracketed id key (?id[]=, ?id[0]=) instead of answering every app #29046's and 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 answers are byte-identical. Right. listFilterRefusalMessage(key, names, got, notAFilter, 'list', \list every ${lists} you can see`)expands to exactly the merge-base literal ofthrowListFilterRefusal (Query parameter '${key}' names no ${names}; ${got}. The list was not served: ${notAFilter} is not a filter. Name one ${names}, or leave the parameter out to list every ${lists} you can see.); the idarm'sgotandnotAFiltertemplates withparam = 'id', names = 'app'expand tothe 'id' filter takes exactly one app name, spelled ?id=NAMEanda bracketed 'id' key, the merge-base literals; refuseEmptyListFilteris untouched. Every hunk in the five test files is an addition, so #29046'sid[…]` pins and 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 empty-value pins run unchanged on this head.
  5. Everything else is unchanged. Right, read in the diff and the cited code: ?object=NAME still switches and an absent object lists all (controls pinned); a bracketed object key on any type but view stays unread, as ?object= is there (rest control on app, the census's 2 probes × every type); packages[, packageId and ID[ do not match startsWith('package[') / startsWith('object['); a non-object query returns early from bracketedKeyOf; ?package=ID scopes, ?package=all and no package read every package (controls pinned); the /data filter slot and every non-/meta door are untouched.
  6. No write door's behaviour changes, and every other /meta door that reads package already refuses package[ as an unknown name, on both transports. Right, read door by door on the head: RestServer item read (metaItemQueryRefusal('item') at meta-item-read-routes.ts 461, closed set META_ITEM_QUERY_DOORS.item.accepted names package not package[), /layers (metaItemQueryRefusal('layers') at 323 before serveMetaItemLayered's binding read at rest-server.ts 4322), /published (refuseUnknownQueryParams(META_ITEM_PUBLISHED_PARAMS), history-routes 1143), PUT (refuseUnknownQueryParams(META_ITEM_SAVE_PARAMS), write-routes 281), DELETE (META_ITEM_RESET_PARAMS, 482), POST …/publish (META_ITEM_PUBLISH_PARAMS, history-routes 666), /meta/diagnostics (unknownQueryParamRefusalMessage(req.query, META_DIAGNOSTICS_PARAMS), collection-routes 337); the dispatcher's item (meta.ts 1737), /layers (1193), /published (1239) and PUT (1519) each ask metaItemQueryRefusal before the packageBinding() read, and the dispatcher serves no DELETE, /publish or diagnostics twin (read in meta.ts). The dev's measured table matches the tree; the order's "stop if it reaches a WRITE door" condition was not met.
  7. Public surface. metaBracketedPackageRefusal, refuseBracketedListFilter, bracketedKeyOf and listFilterRefusalMessage are not on packages/rest/src/index.ts, and the package's exports map has the one . entry, so none is published. No new export, error code, status, answer member or wire field. The ONE published-face movement is the rule union of MetaItemQueryRefusal: the interface is not in the entry's export type list, but it is the declared return type of the exported metaQueryArityRefusal and metaItemQueryRefusal, so it is reachable from the entry type graph (the runtime already types against it as ReturnType<typeof metaItemQueryRefusal>), and its rule discriminator now carries a fourth member, 'bracketed-parameter'. Judged under ② and ③.

Claims checked for wrong statements about the surface: the changeset's two "Now" sentences are the literals the builder and the tests pin; the PR body's door table holds against item 6; "no runtime source changed" holds (the runtime diff is two test files); "rest-server.ts untouched" holds. None found wrong.

② Semver level

.changeset/29199-bracketed-object-package-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 ?object[]=NAME TO ?object=NAME and FROM ?package[]=ID TO ?package=ID. 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, as #29046's record applied it). The level matches what the diff publishes: only @objectstack/rest ships changed source; @objectstack/runtime moves two test files 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. Skills-docs: none holds: git grep on the head finds no page under skills/, content/docs/ or apps/docs/ that spells object[] / package[] as a query key (the hits are TypeScript type notation in generated reference tables). Check Changeset and Lint · repo & tooling gates are green on this head.

The rule member and the declaration line (the dev's open question). The dev asks whether 'bracketed-parameter' on MetaItemQueryRefusal.rule is a published widening and recommends A (keep it, keep no (narrowing)). My reading: the published face is the exports map's reachable type graph (contract-review.md), and ① item 7 shows the union is reachable, so the TYPE a consumer receives from an exported function is one member wider; a consumer that switches exhaustively on rule fails to compile against this head, which is the textbook sense in which an output union widening is a change. It is NOT a widening of any accept set (no door accepts more; every move is a refusal), no transport writes rule to the wire, and the only readers in the tree are the two rule !== 'layered-member' inequalities. The precedent on this same record spelled it the other way: #23072 (which introduced MetaItemQueryRefusal and rule) and #23214 (which introduced metaQueryArityRefusal) both shipped as Clause-②: yes (narrowing) with the same patch + BREAKING + ADR-0087 frame. So the exact spelling for "a published type grows AND an accept set narrows" is yes (narrowing), option B, and A under-declares by one word. What the line protects is unchanged either way: both spellings owe this review at CONTRACT_REVIEW_TIER with ④ (reviewFace maps yes (narrowing) to the narrowing face too), check-adr-0087-registration reads the arm and the kind, the level is patch under both, the banner and marker are present, and the changeset's own prose already names the widened tag in the open. Option C is wrong on the merits: package[] is a recognised key with a dedicated remedy, and tagging it unknown-parameter would misdescribe it to the one place rule is meant for (a transport that answers the third rule elsewhere). Judged: should-fix, not must-fix — respell Clause-②: yes (narrowing) in the PR body and the changeset if this PR cuts another head for any reason; the under-declaration hides nothing, moves no gate, tier or level, and does not block landing on this head.

③ Boundary flags

  • open_questions[0] (the rule union, options A/B/C): answered under ② — B is the precise spelling, A is accepted on this head as a should-fix, C is refused. Not escalated: no maintainer ruling is needed for a declaration spelling whose gate reading is identical.
  • Deviation: file surface (rest-server.ts named, meta-collection-routes.ts + meta-item-read-routes.ts touched instead). Verified in the tree and accepted by the seat's claim amendment 6575888036 before this review; ① item 2 confirms the binding cannot see the key, so the arity decision is the seam the order's "at the binding's one seam" could only mean. Closed.
  • Deviation: runtime test files gained pins with no runtime source change. Verified: the runtime diff is dispatcher-query-arity.test.ts (+32) and meta-list-projection-parity.test.ts (+16), additions only. Closed.
  • Deviation: dogfood red-first read off the ablation legs, not a main run of the dogfood file. Coherent: each ablation arm restores main's code for that arm, and the two dogfood reds per arm are exactly that arm's two HTTP pins. Not re-run here (read-only). Accepted.
  • Deviation: origin/main moved 22 commits after the merge, none on a file this PR edits. The queue's merge-group run is the check of record for that; nothing for this record.
  • Evidence: ablation. Not re-run here. Read for coherence: the object arm reds exactly the three object[…] rows, the dispatcher object[] case, the reference row and the two dogfood view pins; the package arm reds exactly the ten bracketed-package rows, the four dispatcher list/tree cases, the reference row and the two dogfood object pins; the census rows staying green under ablation is the correct expectation (both transports then give the same unfiltered answer). Coherent with the diff. Accepted.
  • Evidence: the full dogfood battery. (narrowing) owes it; reported under the verify lock at this head as 389 files passed, 3 skipped, 3220 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 three prerequisite re-runs declared. The verdicts of record are the head's check-runs, read from the API after the last one completed: 49 of 49 completed, 36 success and 13 skipped, none failed — including all eight Test Core shards, the four Type Check jobs, every Lint job, 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. The dev's report said CI was in progress at report time; it is green now.
  • Out-of-scope finding: GET /meta/_drafts ?packageId[]= / ?type[]=. Verified in the tree as the same class: both transports read req.query?.packageId / query?.packageId and type after refuseRepeatedQueryParams / metaQueryArityRefusal('drafts') only, with no closed set (META_QUERY_ARITY_DOORS.drafts = ['packageId', 'type']), so a bracketed key is unread and the draft list is answered across every package / every type. Unmeasured, outside the card's keys, and the order did not pull it in; it is a separate card, not a FAIL of this one. Recorded for that card: the fix is metaBracketedPackageRefusal's shape generalised to the door's own names (packageId, type), asked at the same two call sites (meta-collection-routes.ts ~467 and meta.ts ~2067); ?preview[]= and ?include[]= on the list are the same adapter pass-through but read as "absent switch", a narrower consequence.
  • Precedent carried: PR fix(rest)!: GET /meta/app refuses a bracketed id key (?id[]=, ?id[0]=) instead of answering every app #29046's record 6570942095 ③ named this exact follow-up and its mechanical shape ((param, names, lists) twin at step 4; package[ read at the door entry on both transports); the diff is that shape.
  • needs:contract-review is not on the PR at the time of writing (labels: documentation, size/m, tests, tooling); nothing for this record to say about it. The PR is draft with no auto-merge armed. Author objectstack-fleet[bot], so no author note is owed.

④ Design judgment

The narrowing, read against the published contract on main: GetMetaItemsRequestSchema (packages/spec/src/api/protocol.zod.ts 240) declares packageId as one optional string — the member ?package= binds to through metaItemPackageBinding — and no object member at all; ?object= is the view switcher's transport-level declaration (one object name) and ?package= is #6877 / #22188's one-package scope (all or absence = every package); no spec text or docs page declares an array spelling of either, so the spec gives a bracketed key no meaning to take away. @objectstack/client builds ?package= with URLSearchParams.set on every /meta read (index.ts 876, 1038, 1915, 1937) and sends no object on the list; the console (ui/**, read only) has no qs library and no bracketed object / package builder (the 'object[]' literal in app-shell/src/utils/paramValueShape.ts is a value-shape name, not a query key); no docs or skill example spells either key bracketed. So no real producer loses a capability; this is the spec's own refusal class made real, the ruling line of #27484, #28548 and #29046 for a value that names nothing, not an implementation gap narrowed at the consumer.

Per hits-floor file: meta-item-read-gate.ts holds against the scenario (the two refusals are asked where each key is read, by both transports, and the shared builder reproduces the earlier literals); meta-collection-routes.ts and meta-item-read-routes.ts hold (the hand-off sits after the multiplicity gate and before the binding, in the door's own envelope dialect); the five test files pin each new answer on the transport that gives it plus the plain-spelling and absent controls, and nothing existing was edited to pass. Attempts to break it: a doubled bracketed key (one answer, ① item 3); a percent-encoded package%5B%5D (Hono decodes it before queries()); packageId[, packages[, ID[ (unmatched, consistent with those plain spellings being other names or unread); a dispatcher-only host (same decision, pinned by the arity test and the census); the _drafts door (correctly not judged, ① item 2); a bracketed key on a write door (already an unknown name, ① item 6); a non-object query (early return). Nothing breaks.

The two-dialect note stands as it did on #28602 / #29046 (the chain's thrown refusal writes RestServer's flat { error, code }, the entry arity gate the nested one; status and code identical); it is pre-existing and not moved here.

Must-fix: none. Should-fix: ② — respell the declaration Clause-②: yes (narrowing) (PR body and changeset) at the next head of this PR, if any; and file the _drafts packageId[] / type[] sibling as its own card.

Implemented-by: claude/project-thread-9szbaq
Reviewed-by: session_01ADHkmQxwoGb5fSnrFkpxmt

VERDICT: PASS

Stamped 2026-11-02T11:40Z.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-steve · session_01ADHkmQxwoGb5fSnrFkpxmt

席位核对

ACCEPT · domain:server seat 2 · os-steve · session_01ADHkmQxwoGb5fSnrFkpxmt · 2026-11-02T11:46Z · head 1ee7e4f5 · claimed by seat 2 on #29199 (claim 6574465942, amendment on the card)

The maintainer's words for this seat, verbatim: 「接手所有开发,包括跟进之前未完成的开发落地。」

  1. Shape: a draft against main, ! title, Fixes #29199, BREAKING banner and one adr-0087: marker; check-governed-merges --pr 29298: NOT governed. No packages/spec, no ui/**.
  2. Contract review: PASS at this head (6576238292). It verified three things: fix(rest)!: GET /meta/app refuses a bracketed id key (?id[]=, ?id[0]=) instead of answering every app #29046's and 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 messages are byte-identical, object[ and package[ are refused on both transports before the binding, and no write door's behaviour changes. Its should-fix 1 is about spelling. The widened MetaItemQueryRefusal.rule union is on the published face, so the precise spelling is Clause-②: yes (narrowing). The record accepts no (narrowing) on this head because no gate, tier, level, banner or ADR-0087 reading moves. The seat follows that: the spelling is corrected only if another head is cut. Should-fix 2 is filed as meta door: GET /meta/_drafts?packageId[]= / ?type[]= reads as no filter and answers every package / every type (same class as #29199) #29319 (_drafts packageId[] / type[], the same class).
  3. Acceptance against the delta:
    • (1) ?object[]= / ?object[0]= answer fix(rest)!: GET /meta/app refuses a bracketed id key (?id[]=, ?id[0]=) instead of answering every app #29046's 400.
    • (2) Every list door reading object through step 4 is covered on both transports.
    • (3) ?package[]= was measured red on main on the list door and the book tree, and is refused in the arity decision. The claim amendment declares that surface.
    • (4) Red-first pins and ablation: the object arm reds 7, the package arm reds 17, and both restore clean.
    • (5) Producer census: no real producer sends a bracketed object or package. The reviewer re-ran it independently.
  4. Evidence: full dogfood battery 389 files and 3220 tests with no failures. 64 derived gates, 0 NOT-MEASURED. CI on this head: 36 success, 13 skipped, 0 red, all completed.
  5. Serial: none; no other open PR touches these files. 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 11:47
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 2, 2026
Merged via the queue into main with commit 08610a9 Nov 2, 2026
50 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-9szbaq branch November 2, 2026 12:25
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-steve · session_01ADHkmQxwoGb5fSnrFkpxmt

Landing record · PR #29298

domain:server seat 2 · session_01ADHkmQxwoGb5fSnrFkpxmt · 2026-11-02T13:07Z · card #29199

Reading Probe Result
Merged PR #29298 state merged, merge commit 08610a931c
On main git merge-base --is-ancestor 08610a931c origin/main yes
Queue refs git ls-remote origin refs/heads/gh-readonly-queue/* matching 29298 0
Content git grep -l refuseBracketedListFilter origin/main -- packages/rest/src present

Release: ships patch for rest at the next release, with the BREAKING banner its changeset carries.


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.

meta door: GET /meta/view?object[]=NAME answers every view instead of the views of that object

1 participant