Repository navigation
fix(rest)!: /meta list refuses a bracketed object key on views and a bracketed package key on the list and book tree - #29298
Conversation
…meta list and book tree Claude-Session: https://claude.ai/code/session_01ADHkmPfJsJeWNxeaZkvyEM Co-authored-by: Claude <noreply@anthropic.com>
…bracketed package key on the list and book tree Claude-Session: https://claude.ai/code/session_01ADHkmPfJsJeWNxeaZkvyEM Co-authored-by: Claude <noreply@anthropic.com>
…er HTTP Claude-Session: https://claude.ai/code/session_01ADHkmPfJsJeWNxeaZkvyEM Co-authored-by: Claude <noreply@anthropic.com>
…/meta list and book tree Claude-Session: https://claude.ai/code/session_01ADHkmPfJsJeWNxeaZkvyEM Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 20 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 6 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 19 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 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
|
|
domain:server#2 · os-steve · session_01ADHkmQxwoGb5fSnrFkpxmt Contract reviewServed-tier: ① Derived judgmentsAccept-set and public-surface changes the diff implies, each read against the cited code on
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
The ③ Boundary flags
④ Design judgmentThe narrowing, read against the published contract on Per The two-dialect note stands as it did on #28602 / #29046 (the chain's thrown refusal writes Must-fix: none. Should-fix: ② — respell the declaration Implemented-by: VERDICT: PASS Stamped 2026-11-02T11:40Z. |
|
domain:server#2 · os-steve · session_01ADHkmQxwoGb5fSnrFkpxmt 席位核对ACCEPT · The maintainer's words for this seat, verbatim: 「接手所有开发,包括跟进之前未完成的开发落地。」
Generated by Claude Code |
|
domain:server#2 · os-steve · session_01ADHkmQxwoGb5fSnrFkpxmt Landing record · PR #29298
Release: ships Generated by Claude Code |
Requested by Steve · project thread
Fixes #29199
Clause-②: no (narrowing)
Metadata-protocol: none
Skills-docs: none — no skill or doc page teaches a bracketed
objectorpackagekey (git grepoverskills/,content/docs/andapps/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)时答 400VALIDATION_ERROR并点名该键;/meta列表与 book tree 收到括号写法的package(?package[]=ID、?package[0]=ID)时同样答 400。以前这些键被忽略,返回全部视图或跨全部包的列表。?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[]=IDand?package[0]=IDreach the/metadoors under the key as spelled: every adapter hands query keys through unparsed (readQueryinplugin-hono-server,readDispatchQueryin@objectstack/hono). The view switcher readsquery.objectand the package binding readsquery.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.tsrefuseBracketedAppIdFilterbecomes one helper keyed by parameter,refuseBracketedListFilter(query, param, names, lists). Step 3 asks it foridonappand step 4 now asks it forobjectonview. Theidanswer is byte-identical; the sentence builder is nowlistFilterRefusalMessage, 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 readpackage(the list and the book tree) a key that openspackage[is refused as data, rulebracketed-parameter, in the same sentence frame.metaQueryArityRefusalasks it after the multiplicity rule, so the runtime dispatcher's list and tree get it with no runtime source change.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 saysThe tree was not servedandread the tree across every package.packages/rest/src/meta-collection-routes.ts(list) andpackages/rest/src/meta-item-read-routes.ts(book tree):RestServer's two doors ask the same decision right afterrefuseRepeatedQueryParams, before the binding is read, and write it in the nested envelope their arity gate writes.Measured: which doors does each seam serve (order item 2, 3).
?object[]=?package[]=on mainGET /meta/view,/meta/views(list chain step 4)GET /meta/:type, any other type?object=)GET /meta/book/:name/treeGET /meta/:type/:name,/layersPUT/DELETE /meta/:type/:name,POST …/publish(write doors)GET /meta/diagnosticsStep 4 is the only list step that reads
object, keyed on the foldedview(viewsis covered). The write doors were measured before any change (a scratch probe driving eachRestServerroute with?package[]=; the dispatcher's save and item read ask the same closed sets throughmetaItemQueryRefusal), 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 ofquery.package, which isundefinedfor a bracketed key, so it cannot see the key. The seam both transports ask before every arity-door read ofpackageis the arity decision, which is where the refusal sits;rest-server.ts(the binding's home) is untouched.Producer census (order item 5).
@objectstack/clientbuilds?package=withURLSearchParams.set(bothmeta.getItemssurfaces) and sends noobjecton the list; the console (ui/**, read only) has no qs library and no bracketedobject/packagebuilder; 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]'])onviewandviewsfor three caller postures asserts 400,code, the full sentence and no list; a bracketed key beside a plainobjectis 400; controls:?object=leadswitches, noobjectlists all, and a bracketedobjectkey onappis not read (200).packages/rest/src/meta-query-arity-doors.test.ts: the decision as data forpackage[],package[0],package[id]on the list and the tree (beside a plainpackagetoo); a repeated plainpackagestill answers the multiplicity sentence first; not judged on_drafts,stateorpackages[;RestServer's list and tree answer the nested 400 andgetMetaItemsis never called; controls: the plainpackagestill scopes, none reads every package.packages/runtime/src/domains/dispatcher-query-arity.test.ts: the dispatcher's list and tree refusepackage[]/package[0]with the same code and sentence before any read, and its list chain refusesobject[].packages/runtime/src/domains/meta-list-projection-parity.test.ts: probes?object[]=lead,?object[0]=lead,?package[]=crmon the list census (every type × caller, both transports),?package[]=crmon the item, tree and layers censuses; the "reference moves" test assertsRestServer's 400 forobject[]andpackage[].packages/qa/dogfood/test/meta-read-doors-live.dogfood.test.ts(booted showcase over Hono), appendeddescribe:/meta/view?object[]=/?object[0]=showcase_taskand/meta/object?package[]=/?package[0]=com.example.showcaseanswer 400VALIDATION_ERRORnaming the key, no items, as the admin and as a member; controls:?object=showcase_taskis that object's views and fewer than all,?package=com.example.showcaseis 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:
Tests 13 failed | 58 passed (71)— the threeBRACKETED view object[…]cases (expected 200 to be 400) and the ten bracketed-packagecases. Every control passed.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.Ablation: each refusal, put back, reds only its own pins
Through
scripts/ablation-replace.mjs(wrap mode),pnpm --filter @objectstack/rest build, thenablation-dist-preflight.mjsbefore reading any run.if (metaType === 'view') refuseBracketedListFilter(query, 'object', 'object', 'view');replaced by a marker statement. Anchor 1 → 0, blob894bc70c→aae79d48; marker present in 2 built files. rest3 failed | 32 passed (35)(the threeobject[…]cases); runtime2 failed | 1178 passed (1180)(the dispatcherobject[]case and the reference test); dogfood2 failed | 17 passed (19)(/meta/view?object[]=and?object[0]=).metaBracketedPackageRefusal's door check replaced by an unconditionalreturn undefined. Anchor 1 → 0, blob894bc70c→2a7a9483; marker present in 2 built files. rest10 failed | 26 passed (36)(the ten bracketed-packagecases); runtime5 failed | 1175 passed (1180)(the four dispatcher list/tree cases and the reference test); dogfood2 failed | 17 passed (19)(/meta/object?package[]=and?package[0]=).Each restore: blob back to HEAD
894bc70c,git diff HEADempty, rebuild exit 0, marker absent from all 6 built files, whole tree clean.Acceptance notes
packagekey also reads as "every package" on the list (every type) and on the book tree.meta-item-read-gate.tsand, ifpackage[]red, the binding's home (rest-server.ts). The fix lives inmeta-item-read-gate.tsplus three-line door hand-offs inmeta-collection-routes.tsandmeta-item-read-routes.ts;rest-server.tsis untouched (see "Measured" for why the binding cannot see the key). The runtime parity and dispatcher arity tests gained pins; no runtime source changed.object[]refusal is thrown inside the list chain after the per-caller gate, soRestServerwrites the flat{ error, code }body fix(rest)!: GET /meta/app refuses a bracketed id key (?id[]=, ?id[0]=) instead of answering every app #29046'sid[]refusal writes. Thepackage[]refusal is answered at the door entry, wherepackageis read, soRestServerwrites the nested{ error: { code, message } }its arity gate writes. Status andcodeare identical in both.GET /meta/_draftsreads?packageId=and?type=with no closed set, so a bracketedpackageId[]ortype[]very likely reads as every package or every type;?preview[]=drafton 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 fix34997b8fplus a merge oforigin/main), in a worktree onclaude/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 0pnpm --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--no-inline-configoverpackages/rest,packages/runtime,packages/qa/dogfood: exit 0, no findingsFull dogfood battery (the narrowing owes it)
pnpm --filter @objectstack/dogfood testunder 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, at1ee7e4f5): 64 commands, all exit 0.--ranreconciliation: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 nodist/; built, then115 published require entry point(s) across 70 package(s)load) andcheck: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)).test:repo(the vitestrepoprojects, CITest Core). The onerepo-project file this diff edits,meta-list-projection-parity.test.ts, was run directly (above).origin/mainhas moved 22 commits since the merge; none touches a file this PR edits (git diff --name-onlyover the range), so the queue's re-run against current main is the remaining check.Generated by Claude Code