Skip to content

fix(rest)!: /meta/_drafts refuses a bracketed packageId or type key instead of listing every draft - #29440

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

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-ta4g7t

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread
Fixes #29319
Clause-②: no (narrowing)
Metadata-protocol: none
Skills-docs: none — no skill or doc page teaches a bracketed packageId or type key on _drafts (git grep over skills/, content/docs/ and apps/docs/ finds none); the pages that name _drafts teach no query spelling at all.

维护者速读

What changes

?packageId[]=ID, ?packageId[0]=ID, ?type[]=TYPE and ?type[0]=TYPE reach GET /meta/_drafts under the key as spelled: both Hono adapters hand query keys through unparsed (readQuery in plugin-hono-server, readDispatchQuery in @objectstack/hono). Both transports read only query.packageId and query.type, so listDrafts was handed no package and no type, and an author who named one package's (or one type's) drafts got every draft with 200.

  • packages/rest/src/meta-item-read-gate.ts: fix(rest)!: /meta list refuses a bracketed object key on views and a bracketed package key on the list and book tree #29298's metaBracketedPackageRefusal becomes metaBracketedFilterRefusal(door, query), driven by one per-door table, META_BRACKETED_FILTERS: the list and the book tree judge package (as before), _drafts judges packageId and then type. It is the same function, generalised and renamed, not a second copy; it was module-level and never on the package index, so no export moves. The two package sentences are byte-identical (fix(rest)!: /meta list refuses a bracketed object key on views and a bracketed package key on the list and book tree #29298's pins pass unchanged). metaQueryArityRefusal already asks it after the multiplicity rule, so the runtime dispatcher's _drafts gets the refusal with no runtime source change.
    • packageId: Query parameter 'packageId[]' names no package; the 'packageId' filter takes exactly one package id, spelled ?packageId=ID. The list was not served: a bracketed 'packageId' key is not a filter. Name one package, or leave the parameter out to list the drafts of every package.
    • type: Query parameter 'type[]' names no metadata type; the 'type' filter takes exactly one metadata type, spelled ?type=TYPE. The list was not served: a bracketed 'type' key is not a filter. Name one metadata type, or leave the parameter out to list the drafts of every type.
  • packages/rest/src/meta-collection-routes.ts: RestServer's _drafts door asks the same decision right after refuseRepeatedQueryParams, before listDrafts, and writes it in the nested envelope its arity gate writes (as the list door does).
  • packages/rest/src/meta-item-read-routes.ts: the book tree's call follows the rename (one line).
  • The order is unchanged on both transports: capability gate (403), protocol probe (501), multiplicity, then the bracketed key. A bracketed key beside the plain one is refused (two spellings of one filter). MetaItemQueryRefusal.rule gains no member: the refusal reuses bracketed-parameter, so Clause-②: no (narrowing).

Measured on main before any edit (27c1c55). Real Hono request parsing on both transports (RestServer on a HonoHttpServer, driven with app.fetch; the dispatcher on a raw Hono app with readDispatchQuery's exact query shape), an author caller, four seeded drafts (two packages × two types), brackets sent raw and percent-encoded:

Request Both transports on main
no query 200, 4 drafts (control)
?packageId=crm / ?type=view / both 200, 2 / 2 / 1 drafts (controls)
?packageId[]=crm, ?packageId[0]=crm 200, 4 drafts, listDrafts({})
?type[]=view, ?type[0]=view 200, 4 drafts, listDrafts({})
?packageId[]=crm&type[]=view 200, 4 drafts
?packageId=crm&packageId[]=hr 200, 2 drafts: the bracketed key silently dropped

Producer census. @objectstack/client's listDrafts builds both keys with URLSearchParams.set; the console builds ?packageId=${encodeURIComponent(…)} (usePendingDrafts.ts, PackagesPage.tsx); neither has a qs library; no docs or skill example spells either key bracketed. No real producer sends them.

Tests

  • packages/rest/src/meta-query-arity-doors.test.ts (appended describe): the decision as data for packageId[], packageId[0], packageId[id], type[], type[0], type[name] (and beside the plain key); a repeated plain packageId still answers the multiplicity sentence first; not judged on the list or the tree, nor for types[], packageIds[] or package[] on _drafts; RestServer's _drafts answers the nested 400 and listDrafts is never called; controls: the plain spellings still narrow, none reads every draft.
  • packages/runtime/src/domains/dispatcher-query-arity.test.ts (appended describe): the dispatcher's _drafts refuses packageId[], packageId[0], type[], type[0] with the same code and sentence before listDrafts; beside the plain key too; control: the plain spellings reach listDrafts.
  • packages/qa/dogfood/test/meta-read-doors-live.dogfood.test.ts (booted showcase over Hono, appended describe): /meta/_drafts?packageId[]= / ?packageId[0]=com.example.showcase and ?type[]= / ?type[0]=view answer 400 VALIDATION_ERROR naming the key, no list, as the admin; controls: ?packageId= and ?type= answer 200 with only matching rows, and a member is still refused 403 at the capability gate first.

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

Red first on main (a2fcede, pins at b748eaa)

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

Ablation: each refused key, and the RestServer hand-off, put back, reds only its own pins

Through scripts/ablation-replace.mjs (wrap mode), pnpm --filter @objectstack/rest build, then ablation-dist-preflight.mjs packages/rest MARKER (marker present in 2 built files) before reading any run. Baseline on the fixed head first: rest 49 passed, runtime 43 passed, dogfood file 24 passed.

  1. packageId. The _drafts table row's param: 'packageId' renamed to a marker, so the key is not judged. Anchor 1 → 0, blob 977baa3c → 69581359. rest 5 failed | 44 passed (49) (the three packageId as-data cases, the two RestServer packageId cases); runtime 3 failed | 40 passed (43) (packageId[], packageId[0], beside-the-plain-key); dogfood 2 failed | 22 passed (24) (packageId[], packageId[0]).
  2. type. The row's param: 'type' renamed to a marker. Anchor 1 → 0, blob 977baa3c → 36e7b636. rest 5 failed | 44 passed (49) (the three type as-data cases, the two RestServer type cases); runtime 2 failed | 41 passed (43) (type[], type[0]); dogfood 2 failed | 22 passed (24) (type[], type[0]).
  3. RestServer hand-off. The _drafts door's call asks an unknown door instead of 'drafts'. Anchor 1 → 0, blob 824a9f54 → 2d929242. rest 4 failed | 45 passed (49) (the four RestServer door cases); runtime 43 passed (43) (the dispatcher asks the decision through metaQueryArityRefusal, untouched); dogfood 4 failed | 20 passed (24) (the showcase is served by RestServer).

Each restore: blob back to HEAD, git diff HEAD empty; then a rebuild, all three markers absent from all 6 built files, whole tree clean (git status --porcelain empty).

Acceptance notes

  • Premise. Holds as the card states it, on both transports, raw and percent-encoded; and beside a plain ?packageId= the bracketed key was silently dropped.
  • File surface vs the claim. As claimed, plus the one-line rename at the book tree's call in meta-item-read-routes.ts, the other caller of the function this PR generalises. packages/runtime/src/domains/meta.ts needed no edit: its _drafts already asks metaQueryArityRefusal('drafts', …). No ui/**, no packages/spec/**.
  • Main-red seen on the final head, not this change. flow-runas.dogfood.test.ts (3) and declarative-endpoint-anonymous-guest.dogfood.test.ts (1) red with this PR's source set back to main's (see Verification); reported to the seat.
  • Same class, still not fixed here. ?preview[]=draft on the list reads as no preview and ?include[]= as no inclusion (fix(rest)!: /meta list refuses a bracketed object key on views and a bracketed package key on the list and book tree #29298's note). Not measured here; outside this card's keys.

Verification

Every local reading below is at HEAD ee0e30c9 (the fix 359c7496 plus two merges of origin/main, the last taken after dfbe34d8), in a worktree on claude/project-thread-ta4g7t. Families CI runs are cited from this PR's check-runs in the report, not re-run here.

Package suites and typecheck (HEAD ee0e30c)
  • pnpm --filter @objectstack/rest test: VERDICT command-exit 0 · Test Files 370 passed (370) · Tests 7043 passed | 332 skipped (7375)
  • pnpm --filter @objectstack/rest typecheck && … runtime typecheck && … dogfood typecheck: VERDICT command-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: VERDICT command-exit 0 · Tests 1185 passed (1185)
  • lint: root eslint with --no-inline-config over packages/rest, packages/runtime, packages/qa/dogfood: exit 0, no findings
  • node scripts/check-platform-checklist.mjs --changed-from origin/main (a module export was renamed): OK, 12 anchors judged, 0 lost
Full dogfood battery (the narrowing owes it)

pnpm --filter @objectstack/dogfood test under the verify lock, after the @objectstack/dogfood^... closure build:

  • HEAD ee0e30c9: Test Files 2 failed | 391 passed | 3 skipped (396) · Tests 4 failed | 3244 passed | 28 skipped (3276). The four reds are flow-runas.dogfood.test.ts (three: the member's flow trigger answers 404 RECORD_NOT_FOUND) and declarative-endpoint-anonymous-guest.dogfood.test.ts (one). They are not this change: with this PR's three packages/rest/src files set back to origin/main's (blob-identical to the merged main), rest rebuilt and the fix's sentence absent from dist/, the same two files answer the same Tests 4 failed | 8 passed (12); restored, git status --porcelain empty. Neither file touches /meta; between the two heads main took dfbe34d8, which changed the flow start doors (not bisected). Reported to the seat as a main-red, not fixed here.
  • HEAD 88dc5779 (the same diff on the previous main): VERDICT command-exit 0 · Test Files 393 passed | 3 skipped (396) · Tests 3248 passed | 28 skipped (3276).
Derived gate set

node scripts/pm/dispatch-gates.mjs --commands (from the merge-base, at ee0e30c9): 64 commands, all exit 0. --ran reconciliation with each exit code recorded: 64 derived famil(ies) accounted for — 64 run, 0 NOT-MEASURED (a DERIVED zero). On the previous head two first-pass exit 3s were prerequisites, each re-run green: check-plugin-teardown-shape.mjs --self-test (its pinned fixture commit fetched on this shallow clone) and check:dual-build-cjs-loads (7 packages outside the dogfood closure built).

  • Not run locally: test:repo (the vitest repo projects, CI Test Core). The one repo-project file this diff could reach, meta-list-projection-parity.test.ts, was run directly (above).

Generated by Claude Code

…meta/_drafts

The decision as data, RestServer's _drafts door, the runtime dispatcher's
twin and the booted showcase over HTTP. Red on main: the bracketed key reads
as no filter and every draft is listed.

Claude-Session: https://claude.ai/code/session_01ADHkmSq83zsshBxiLpFJdE
Co-authored-by: Claude <noreply@anthropic.com>
The arity doors' bracketed-package decision becomes one per-door table,
metaBracketedFilterRefusal: the list and the book tree judge `package`,
_drafts judges `packageId` and `type`. metaQueryArityRefusal asks it, so the
runtime dispatcher's _drafts gets it with no runtime change; RestServer's
_drafts door asks it right after its multiplicity gate.

Claude-Session: https://claude.ai/code/session_01ADHkmSq83zsshBxiLpFJdE
Co-authored-by: Claude <noreply@anthropic.com>
@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 13 documentable anchor(s).

29 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 329121397b395e688cff1ca9e845e07ba53c652a.

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

What this run could not see
  • 5 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 329121397b395e688cff1ca9e845e07ba53c652a → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 329121397b395e688cff1ca9e845e07ba53c652a

⚠️ 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 329121397b395e688cff1ca9e845e07ba53c652a → 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: ee0e30c96c3c8aba7fbb570fd5e9b00e58a80ccb
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-query-arity-doors.test.ts, packages/runtime/src/domains/dispatcher-query-arity.test.ts, packages/qa/dogfood/test/meta-read-doors-live.dogfood.test.ts + packages/runtime/src/domains/meta.ts (the dispatcher's _drafts branch at 2033-2093: mayReadPendingDrafts, resolveProtocol, metaQueryArityRefusal('drafts', query) at 2067, queryRefusalAnswer at 971, the query?.type / query?.packageId reads), packages/rest/src/meta-item-read-gate.ts on the merge-base (the retired metaBracketedPackageRefusal body, repeatedOnceRefusal, META_QUERY_ARITY_DOORS, bracketedKeyOf, listFilterRefusalMessage, refuseBracketedListFilter, throwListFilterRefusal, MetaItemQueryRefusal.rule), packages/rest/src/index.ts and packages/rest/package.json (the entry's export list and the exports map), packages/client/src/index.ts (listDrafts at 2198-2206), ui/** (read only: usePendingDrafts.ts 107, PackagesPage.tsx 560 / 701 / 755, data-objectstack/src/metadata-client.ts 1191), content/docs/**, apps/docs/**, skills/** and packages/spec/src/api/protocol.zod.ts 2265-2300 (the _drafts declaration: a response schema only), .changeset/29319-bracketed-drafts-filter-refused.md (read for ②), PR #29298's record 6576238292, ruling A #25105, main-red #29426 and PR #29365's file list (read for ③). Inputs: card #29319 body and its three comments (triage 6576358498, claim 6576407231, os-dev-report 6580395483), PR #29440 body, file list and net diff against the merge-base dfbe34d8 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. record-recognisers.mjs --paths over the seven files reports no schema or governed path and defers to the changeset's Clause-②: line, which carries (narrowing), so the narrowing face is the one owed and the scope floor is every path outside the prose faces; all six are listed above.

① 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/_drafts with any query key that opens packageId[ or type[ (?packageId[]=ID, ?packageId[0]=ID, ?type[]=TYPE, ?type[0]=TYPE, ?packageId[id]=, ?type[name]=): 200 with every draft the caller may read becomes 400 VALIDATION_ERROR, no list, on both transports, before any binding. Right. Both adapters hand the key through as spelled (the seat's readings table measured every spelling, raw and percent-encoded, at 200 n=4 with listDrafts({}) on both transports on main 27c1c555). On the head, RestServer's door (meta-collection-routes.ts 468-477) asks metaBracketedFilterRefusal('drafts', req.query) right after refuseRepeatedQueryParams(req, res, META_QUERY_ARITY_DOORS.drafts) and before the metadata: GET /api/v1/meta/_drafts answers 200 {"drafts":[]} for a misspelled type or an unknown package, where the list and diagnostics doors refuse the same mistake 400 #29180 refuseUnknownMetaListType read of req.query?.type (480) and the listDrafts({ packageId, type }) call (487); it writes the nested { error: { code, message } } its multiplicity gate writes. The dispatcher's _drafts (meta.ts 2067) asks metaQueryArityRefusal('drafts', query), which on the head returns repeatedOnceRefusal(...) ?? metaBracketedFilterRefusal(door, query), before its own query?.type / query?.packageId reads (2073-2080), and answers through queryRefusalAnswer as deps.error(message, 400, { code: 'VALIDATION_ERROR' }). No runtime source line moved; the runtime diff is one test file, additions only. Pinned on both transports (six keys as data; four keys on RestServer with listDrafts asserted never called; four keys on the dispatcher with listDrafts asserted never called) and over HTTP on the booted showcase (four keys as admin, drafts asserted empty).
  2. The order on both transports is unchanged: capability gate (403), protocol probe (501), multiplicity, then the bracketed key, then metadata: GET /api/v1/meta/_drafts answers 200 {"drafts":[]} for a misspelled type or an unknown package, where the list and diagnostics doors refuse the same mistake 400 #29180's unknown-type verdict, then listDrafts. Right, read at meta-collection-routes.ts 447-487 and meta.ts 2047-2080. A repeated plain packageId beside type[] answers the multiplicity sentence first (pinned as data); a bracketed key beside the plain one is refused (two spellings of one filter; pinned as data and on the dispatcher). The dogfood control pins that a member is still refused 403 at the capability gate before the bracketed key is judged.
  3. fix(rest)!: /meta list refuses a bracketed object key on views and a bracketed package key on the list and book tree #29298's two package sentences are byte-identical after the rename. Right. The retired body built listFilterRefusalMessage(key, 'package', "the 'package' filter takes exactly one package id, spelled ?package=ID", "a bracketed 'package' key", tree ? 'tree' : 'list', tree ? 'read the tree across every package' : 'list across every package'). The table rows expand names = 'package', the template the '${param}' filter takes exactly ${takes} with PACKAGE_ID_TAKES = 'one package id, spelled' and takes = '… ?package=ID', a bracketed '${param}' key, served = 'list' | 'tree', and the same two unscoped literals, so every argument is the merge-base's literal; listFilterRefusalMessage itself is untouched. rule stays 'bracketed-parameter'. fix(rest)!: /meta list refuses a bracketed object key on views and a bracketed package key on the list and book tree #29298's pins in meta-query-arity-doors.test.ts (the three package[…] keys on the list and the tree, the "not on _drafts, state" case at 176-177), in dispatcher-query-arity.test.ts and in the dogfood file run unchanged on this head: every hunk in the three test files is an addition.
  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 untouched. Right. The diff of meta-item-read-gate.ts has four hunks (two JSDoc links, the metaQueryArityRefusal tail, and the helper's body); refuseBracketedListFilter, bracketedKeyOf, throwListFilterRefusal, refuseEmptyListFilter and the list chain's id / object steps are not in it.
  5. Nothing else is judged. Right. META_BRACKETED_FILTERS names package for list and book-tree only (as before), packageId then type for drafts, and no row for state (META_BRACKETED_FILTERS[door] ?? [] returns undefined there, where the retired body's includes('package') did). bracketedKeyOf matches startsWith('param['), so types[, packageIds[, package[ on _drafts and packageId[ / type[ on the list and the tree stay unread (pinned as data). ?packageId=ID and ?type=TYPE still narrow, absent ones list every draft (pinned on RestServer with the exact listDrafts arguments, on the dispatcher, and over HTTP with the rows asserted to match). The /meta/:type list, the book tree, state, every by-name door, /data and every non-/meta door are untouched.
  6. Public surface. metaBracketedPackageRefusal was never on packages/rest/src/index.ts (the entry exports metaQueryArityRefusal, metaItemQueryRefusal, META_QUERY_ARITY_DOORS and their siblings from meta-item-read-gate.js, not the bracket helper), the package's exports map has the one . entry, and git grep metaBracketedPackageRefusal over the whole head tree returns nothing, so the rename is a module-internal move with no consumer, not a published-surface break. metaBracketedFilterRefusal is likewise not exported from the entry. MetaItemQueryRefusal.rule is the same four-member union on the head as on the merge-base; no new export, error code, status, answer member or wire field. check-platform-checklist reported 12 anchors judged, 0 lost (the dev's reading; Lint · repo & tooling gates is green on this head).

Claims checked for wrong statements about the surface: the changeset's two "Now" sentences are the literals the builder and the tests pin; "no runtime source change" holds; "the _drafts door asks it after its multiplicity gate" holds; "one extra line in meta-item-read-routes.ts" holds (the book tree's call at 389). None found wrong.

② Semver level

.changeset/29319-bracketed-drafts-filter-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 ?packageId[]=ID TO ?packageId=ID and FROM ?type[]=TYPE TO ?type=TYPE. The PR title carries !, and the PR body carries the same two declaration lines line-initial. Under ruling A #25105 (row ④: every non-protocol change, REST fix!: included, is patch, and "a non-protocol narrowing ships as patch but keeps its BREAKING banner and its ADR-0087 disposition") and AGENTS.md Post-Task Checklist step 3, patch + banner + marker is the right frame, as #29046's and #29298's records applied it. The level matches what the diff publishes: only @objectstack/rest ships changed source; @objectstack/runtime moves one 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.

The declaration arm. #29298's record spelled yes (narrowing) as the precise form for that PR because MetaItemQueryRefusal.rule, reachable from the entry type graph, gained a member there. Here it gains none (① item 6), and no export, type or wire field widens, so no (narrowing) is the exact spelling for this diff, not an under-declaration; the two records are consistent. Skills-docs: none holds: git grep on the head finds no page under skills/, content/docs/ or apps/docs/ that spells a _drafts query at all, bracketed or plain. Check Changeset and Lint · repo & tooling gates are green on this head.

③ Boundary flags

④ Design judgment

The narrowing, read against the published contract on main: packages/spec/src/api/protocol.zod.ts declares GET /meta/_drafts by its response only (ListDraftsResponseSchema, 2265-2300); the request's two filters are declared where they are consumed, as one optional string each (@objectstack/client's listDrafts(opts?: { packageId?: string; type?: string }) and the kernel's listDrafts({ packageId, type }) argument both transports build), and META_QUERY_ARITY_DOORS.drafts already names each as single-valued (#23214: a repeat is refused). No spec text, docs page or skill declares an array spelling of either, so the spec gives a bracketed key no meaning to take away. Producer census, read on the head: @objectstack/client builds both keys with URLSearchParams.set (index.ts 2200-2203); the console (ui/**, read only) builds ?packageId=${encodeURIComponent(...)} at usePendingDrafts.ts 107, PackagesPage.tsx 560 / 701 / 755 and data-objectstack/src/metadata-client.ts 1191, has no qs import, and sends no type on _drafts; content/docs/**, apps/docs/** and skills/** spell no _drafts query at all. So no real producer loses a capability; this is the spec's own refusal class made real, the ruling line of #27484, #28548, #29046 and #29298 for a value that names nothing.

Per hits-floor file: meta-item-read-gate.ts holds against the scenario (one table, one loop, asked where each door's keys are read by both transports; the package rows reproduce the earlier literals byte for byte; drafts judges packageId before type, matching the door's own META_QUERY_ARITY_DOORS order, so a request carrying both answers for the first as the multiplicity rule does); meta-collection-routes.ts holds (the hand-off sits after the multiplicity gate, before the #29180 type verdict and the binding, in the door's own envelope dialect, and listDrafts is never reached); meta-item-read-routes.ts holds (the rename only); the three 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 (arrives as an array under packageId[], which repeatedOnceRefusal never reads, so the bracket arm answers it with one sentence); a percent-encoded packageId%5B%5D (Hono decodes before queries(), measured by the seat's readings); ?packageId=crm&packageId[]=hr (refused, pinned on the dispatcher; before, the bracketed key was silently dropped); a key that merely starts alike (types[, packageIds[, unmatched, pinned); a bracketed key with an empty value (?type[]= is a key, judged on the key, not the value); the state door (no row, undefined); a non-object query (early return in bracketedKeyOf); a member caller (403 first, pinned over HTTP); a kernel without listDrafts (501 before the arity decision on both transports, unchanged). Nothing breaks.

The two-dialect note stands as it did on #28602 / #29046 / #29298 (the #29180 verdict thrown into handleRouteError 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: none.

Implemented-by: claude/project-thread-ta4g7t
Reviewed-by: session_01ADHkmQxwoGb5fSnrFkpxmt

VERDICT: PASS

@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-02T15:15Z · head ee0e30c9 · claimed by seat 2 on #29319

The maintainer's words for this seat, verbatim: 「接手所有开发,包括跟进之前未完成的开发落地。」 There is no maintainer ruling on this card. It was filed by seat 2 as should-fix 2 of PR #29298's contract review, which is the same class as #29199.

  1. Shape: a draft against main, with a ! title and Fixes #29319. Clause-②: no (narrowing), a patch for rest, a BREAKING banner and one adr-0087: marker. check-governed-merges --pr 29440 says NOT governed. No packages/spec, no ui/**.
  2. Contract review: PASS at this head (6580712643), with no must-fix and no should-fix. The reviewer confirmed five things:
  3. Evidence:
  4. 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 15:17
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 2, 2026 15:17
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 2, 2026
Merged via the queue into main with commit 186a83a Nov 2, 2026
50 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-ta4g7t branch November 2, 2026 16:02
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#2 · os-steve · session_01ADHkmQxwoGb5fSnrFkpxmt

Landing record · PR #29440

domain:server seat 2 · session_01ADHkmQxwoGb5fSnrFkpxmt · 2026-11-02T16:18Z · card #29319

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

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/_drafts?packageId[]= / ?type[]= reads as no filter and answers every package / every type (same class as #29199)

1 participant