Repository navigation
fix(runtime): the @objectstack/hono catch-all's PUT /meta honours If-Match, If-None-Match and ?mode=draft - #22206
Conversation
…h and ?mode=draft The runtime dispatcher's PUT /meta/:type/:name (the only answer behind the @objectstack/hono catch-all) handed saveMetaItem no parentVersion and no mode, so a stale If-Match wrote, If-None-Match: * over an existing row wrote, and a ?mode=draft save landed active. RestServer's precondition parser moves to @objectstack/rest's metaSaveRequestOptions, which now also reads ?mode=draft, and both doors call it. Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU Co-authored-by: Claude <noreply@anthropic.com>
…If-None-Match and ?mode=draft RestServer's PUT handler and the dispatcher driven exactly as the @objectstack/hono catch-all drives it, each over its own real sqlite store: a stale token and If-None-Match: * over a row are refused 409 and write nothing, a draft save leaves the active row untouched, an unguarded save is unchanged, and the dispatcher's fallback writer refuses a pin it cannot carry. Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU Co-authored-by: Claude <noreply@anthropic.com>
…r the /meta save preconditions Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU Co-authored-by: Claude <noreply@anthropic.com>
…ledger the moved draft switch refuseRepeatedQueryParams unwraps one ?mode occurrence encoded as an array, so the shared mapping must read the query after it, as the inline read did. The census of rest-server.ts draft switches loses its PUT row because that switch is now read in meta-save-request.ts. The parity test's unhonourable header list is typed so tsc accepts it. Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 2 package(s): 28 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 9 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 34 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 94c193505b7fea7efa6791b7b65c9318cdbc4dc2 && git checkout 94c193505b7fea7efa6791b7b65c9318cdbc4dc2
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0e9371f0c6104922e19b9bdaabc993b4d3dbca61 88ac87541f94f9a5ac5ac83c35b88999460a5cbf && git checkout -B drift-repro 0e9371f0c6104922e19b9bdaabc993b4d3dbca61 && git merge --no-ff 88ac87541f94f9a5ac5ac83c35b88999460a5cbf
node scripts/docs-audit/affected-docs.mjs --json 0e9371f0c6104922e19b9bdaabc993b4d3dbca61
|
…Options export widens its public surface Clause-②: yes (widening) for the rest changeset; the runtime changeset stays patch with Clause-②: no. Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: ① Derived judgmentsInputs, read 2026-10-08T06:40Z to 06:50Z: card #22141 (body and all six comments: triage 6052589425, claim 6052823060 as amended, reports 6053751704 and 6054009746, REWORK 6053835755, ACCEPT 6054035642), PR #22206 (body, 8-file list, net diff
② Semver level
③ Boundary flagsThe dev's flags (report 6053751704
Check-runs on Implemented-by: VERDICT: PASS |
Resolves the one conflict, in packages/rest/src/rest-server.ts, with PR 22185. This branch removes the private metaSavePreconditionPin and its docblock (moved to meta-save-request.ts as metaSaveRequestOptions); main keeps that function unchanged and adds metaItemPackageBinding with its docblock directly below it. Both intents are kept: metaSavePreconditionPin stays removed, and metaItemPackageBinding with its docblock stays unchanged. No other edit rides this commit. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU
…Match, If-None-Match and ?mode=draft (objectstack-ai#22341) Fixes objectstack-ai#22221 Clause-②: no ## What this adds One test file, no production code: `packages/qa/http-conformance/src/hono-meta-save-preconditions.conformance.test.ts`. It composes the real `createHonoApp` over a `LiteKernel` and sends every request through `app.request(...)`. So the `@objectstack/hono` catch-all's hand-off to `dispatch()` (`packages/adapters/hono/src/index.ts:739`) is in the path, which no test held before. Four rows. Each asserts the status, the envelope, and the stored rows, read back from `sys_metadata` after the request: | row | request through the catch-all | asserted | |:--|:--|:--| | control | `PUT /api/v1/meta/view/case_grid` twice, no header | `200`, `success: true`, receipts `state: 'active'`; active row `v2`, no draft row | | stale `If-Match` | `PUT` with `If-Match` set to the first receipt's `version` | `409`, `success: false`, `error.code` `METADATA_CONFLICT`; active row still `v2`; the current token then saves `v3` | | `If-None-Match: *` | a first write with it (`200`), then the same over the existing row | `409`, `success: false`, `METADATA_CONFLICT`; active row still `first` | | `?mode=draft` | `PUT ?mode=draft` over an active row | `200`, receipt `state: 'draft'`; active row unchanged, draft row `staged`; `GET ...?state=draft` answers `staged`, and the plain `GET` still answers the active row | **File choice (the seat's cut): a new file beside the read-side twin, not rows added to it.** `hono-meta-item-read-gate.conformance.test.ts` is about the read door, and it answers from a stub protocol (a `vi.fn` over a plain object) that keeps no version and no draft. The write rows need a real store. So they live in their own file, and each file stays about one door. There is no `package.json`, `turbo.json` or `scripts/cross-package-test-inputs.mjs` change: every import is a dependency this package already declares (H5 below). There is no changeset: `@objectstack/http-conformance` is `private: true`, and the diff is one test file. ## Readings (the PM's H1 to H5, measured on `origin/main` `28bff18d`) - **H1, the line under test: holds.** The read path: - the catch-all calls `dispatcher.dispatch(method, subPath, body, queryParams, { request: c.req.raw }, prefix)` (`packages/adapters/hono/src/index.ts:739`); - that reaches `handleMetadataRequest`'s `PUT` branch, which calls `metaSaveRequestOptions({ headers: _context.request?.headers, query })` (`packages/runtime/src/domains/meta.ts:1366`; the function is in `packages/rest/src/meta-save-request.ts`); - the answer is spread into `protocol.saveMetaItem`. The function that reads each one: - `If-Match` and `If-None-Match`: `metaSaveRequestOptions`, through its `requestHeader()` helper. That helper calls `Headers.get` on the raw `Request` the catch-all hands on as `context.request`. - `?mode=draft`: also `metaSaveRequestOptions`, but from `http.query`. That is the catch-all's `queryParams` argument (flattened from the URL), **not** the `Request`. See H4. - **H2, the harness: the twin's shape is reused, its store is not.** Same shape: `LiteKernel`, then `createHonoApp({ kernel, prefix: '/api/v1', cors: false })`, then `app.request`. Same single stubbed seam: `HttpDispatcher.prototype.timedResolveExecutionContext`. Two differences, both deliberate: - **Store.** The twin's stub protocol cannot refuse or stage a save. What I added: `ObjectQLPlugin`'s built-in assembly (`registerProtocol` at its default). It registers a real `ObjectStackProtocolImplementation` and the `sys_metadata*` objects. It runs over `SqliteWasmDriver({ filename: ':memory:' })`, handed in as a `driver.memory` service. The boot logs `Schema sync complete {"synced":5}`. Both packages were already in this package's `devDependencies`, because the integration suite boots them. - **Identity.** The twin keys the principal on a request header. Here the stub answers one author (`manage_metadata`) for every request, so no row depends on the request to know who the caller is. Otherwise, ablating the `{ request }` hand-off would also strip the caller. Every row, the control included, would then go red for the wrong reason. - **H3, the four rows through the real app: hold.** See the table above. 4 passed. - **H4, the ablation: partly falsified.** Passing `{}` turns only the two header rows red. The `?mode=draft` row stays green under it, because `mode` rides the `queryParams` argument of the same call, not `context.request`. So the draft row gets its own ablation of the hand-off: the query argument. The control stayed green in every leg. Table below. - **H5, cross-package test inputs: nothing to add.** `pnpm check:cross-package-test-inputs` exits 0: `OK: 30 package(s) read outside themselves, all declared ... 2854 test file(s) import a workspace sibling by bare specifier over 378 package pair(s), every one declared.` The new file imports only by bare specifier, over pairs that are already declared. ## Ablations (one per row, never committed) The subject is `packages/adapters/hono/src/index.ts`, HEAD blob `8c32e89e8fc2`. `vitest.config.ts`'s anchored alias points `@objectstack/hono` at that SOURCE, so no `dist/` leg applies. Each leg went red while `packages/adapters/hono/dist` was untouched, which proves the source is what runs. How each leg ran: - through `node scripts/ablation-replace.mjs` in WRAP mode, under `os-verify-lock.sh`; - the anchor hit x1 before and x0 after, and the mutation was counted on disk; - the file was then restored, proven by its blob (`8c32e89e8fc2`, equal to HEAD) and an empty `git diff HEAD`. I wrote down the expected direction of each leg before running it. Each leg landed as predicted. | leg | mutation at `:739` | control | stale `If-Match` | `If-None-Match: *` | `?mode=draft` | |:--|:--|:--|:--|:--|:--| | head | none | green | green | green | green | | A | `{ request: c.req.raw }` → `{}` (blob `3900c478d42e`) | green | **red**: `200, success: true` where `409` was expected | **red**: `200` where `409` was expected | green | | A' | the same argument → `{ request: new Request(c.req.raw.url, { method: c.req.method }) }`, a `Request` with no headers (blob `effd7d0f0b20`) | green | **red** | **red** | green | | B | `queryParams` → `{}` in the same call (blob `b64245cd76d5`) | green | green | green | **red**: receipt `state: 'active'` where `'draft'` was expected | Each red reproduces the defect objectstack-ai#22141 described, on the wire: a guarded save was answered `200` and written, or a draft went live. ## Tier `Test Core`, on every PR. `ci.yml`'s `Test Core (N/6)` shards run `pnpm turbo run test` over the affected packages. This package's `test` script is a plain `vitest run` (one config, no project split), so the new file runs whenever `@objectstack/http-conformance` is affected. `Test Core` is one of the seven required contexts. ## Verification (head `e66efff7`, base `28bff18d`) - **Build.** `pnpm --workspace-concurrency=2 --filter '@objectstack/http-conformance^...' build` (the dependency closure): exit 0. A grep confirms that `@objectstack/runtime`'s `dist/` carries `metaSaveRequestOptions({ headers: _context.request?.headers, query })`. - **Tests.** `pnpm --filter @objectstack/http-conformance test`: `Test Files 9 passed (9)`, `Tests 106 passed (106)`. - **Typecheck.** `pnpm --filter @objectstack/http-conformance typecheck`: exit 0, `check:test-typecheck: OK ... 3 file(s) / 27 error(s) / 10 pinned signature(s) held`, ledger unchanged. The new file is in that program: `tsc --listFiles -p tsconfig.test.json` lists 9 of the package's 9 test files, the new one included. A file the ledger does not list may carry no error. - **Gates.** I ran the order's 71 commands and recorded each exit code before any pipe: 70 exited 0. - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands`, re-derived at head, gives 53 commands (1 path, 206 changed lines), a subset of the 71. - The `--ran` reconciliation: `✓ dispatch-gates --ran: 53 derived famil(ies) accounted for — 52 run, 1 NOT-MEASURED`, 0 unrun. - **NOT MEASURED: `pnpm check:dual-build-cjs-loads`, reason: exit 3, `PREREQUISITE NOT MET`.** 34 packages are unbuilt in this worktree. This diff adds one test file to a private package that has no `build` script, so it emits nothing that gate reads. CI runs the gate in full. - **Lint.** `pnpm lint` (`eslint . --no-inline-config`, the whole repo) at `e66efff7`: exit 0, no findings. - **Upstream.** `origin/main` is 1 commit past the base (`e36ee535`). It touches 10 files under `service-storage`, `plugins/organizations` and `qa/dogfood`, none of them in this suite's dependency closure, so I did not merge `main`. ## Acceptance notes - **H4's reading was half right, and the missing half matters to anyone who reads the catch-all.** The `{ request: c.req.raw }` argument carries the two precondition headers. The `queryParams` argument carries the lifecycle. A row now goes red if either one is lost. - **Boot noise, not a finding.** Each boot prints two `[sql-driver] DATABASE_ERROR` lines (`no such table: sys_setting`, `no such table: _objectstack_sequences`) at `kernel:ready`. They come from platform-migration probes against tables this minimal composition does not provision. They print before the first request, and no row depends on them. - The catch-all's `409` still carries no `currentVersion` as data (PR objectstack-ai#22206's open question H6). These rows assert only `status`, `success` and `error.code`, so they neither pin nor block any answer to that question. Written by session `session_01RWZbGvPFcRKvUqASZtunCU`. --- _Generated by [Claude Code](https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU)_ Co-authored-by: Claude <noreply@anthropic.com>
Fixes #22141
Clause-②: yes
What this changes
Behind
@objectstack/hono's${prefix}/*catch-all,PUT /meta/:type/:nameis answered by the runtime dispatcher's/metadomain (handleMetadataRequest). That branch handedsaveMetaItemnoparentVersionand nomode. So through the catch-all a staleIf-Matchwrote (200, not 409),If-None-Match: *over an existing row wrote, and a?mode=draftsave landed on the ACTIVE row. A draft went live without a publish, and the client read a 200.RestServer's private precondition parser moves to@objectstack/restasmetaSaveRequestOptions(packages/rest/src/meta-save-request.ts). It now also reads?mode=draft.RestServer'sPUTdoor and the dispatcher'sPUTbranch both call it, and spread itsrequest(parentVersionandmode, each present only when asked) intosaveMetaItem. It reads a FetchHeaders(get) or a plain header record, so it takes the rawRequestthe catch-all handsdispatch()and the record theplugin-hono-serveradapter handsRestServer.If-None-Matchother than*) is400 VALIDATION_ERROR, after the capability gate and before anything is written, as onRestServer. The dispatcher alone has a fallback writer (metadata.saveItem(type, name, item), used when the protocol has nosaveMetaItem). It cannot carry a pin or a lifecycle, so a save that asks for one is refused501 NOT_IMPLEMENTEDthere instead of written unguarded or active.RestServeranswers are unchanged. One ordering moved: its multiplicity guard (refuseRepeatedQueryParamsforforce/package/mode) now runs just before the shared read, because the guard unwraps a single-element?modearray in place and the mapping must see the unwrapped string. A request that carries both a malformed pin and a repeated parameter now gets the repeated-parameter400first. Both are400 VALIDATION_ERROR.Clause-②: yes).@objectstack/rest's package entry now exportsmetaSaveRequestOptionsand the typesMetaSaveRequestHttp,MetaSaveRequestMembersandMetaSaveRequestOptions. That widens its public surface, so its changeset isminorwithClause-②: yes (widening), the shape the read-side twin of this parity declared when it publishedcreateMetaItemReadGate.@objectstack/runtime's public surface does not change: its changeset stayspatchwithClause-②: no.@objectstack/honoitself is unchanged: the catch-all already handsdispatch(){ request: c.req.raw }, so the headers were oncontext.requestall along (H3). The fix ships in@objectstack/runtime(patch) and@objectstack/rest(minor).Readings (on
origin/main6ed0c0f3, this branch's base)H1, reach. In this repository
objectstack serve/os devcomposecreateRestApiPluginandcreateDispatcherPlugin. The dispatcher plugin mounts no/metaroute (its explicit mounts are/notifications*,/keys,/analytics/*,/i18n/*,/health,/ready,/discovery, plus AI routes), so/metawrites there areRestServer's.packages/adapters/now holds onlyhono, andcreateHonoAppis called by no app, example or package outside tests. Whether the hosted runtime'sPUT /metareaches this catch-all, and on which cloud pin: NOT MEASURED from this session (objectstack-ai/cloud is not readable here). The grade is the seat's.H2, reproduced on
main, through the realcreateHonoApp. A scratch probe (not committed) mountedcreateHonoApp({ kernel, prefix: '/api/v1', cors: false })from this checkout's adapter source over aLiteKernelcarrying a realObjectStackProtocolImplementationon ObjectQL + better-sqlite3:memory:, with identity stubbed tomanage_metadata. Requests went throughapp.request. Same probe,main'smeta.tsthen this branch's:mainPUT /api/v1/meta/view/case_grid(v1, then v2)PUTwithIf-Match= v1's token (stale)v3 staleMETADATA_CONFLICT, active rowv2PUTwithIf-None-Match: *over an existing rowsecondMETADATA_CONFLICT, active rowfirstPUT ?mode=draftstate: 'active', active rowstaged, no draft rowstate: 'draft', active rowv2, draft rowstagedH3, where. Confirmed:
handleMetadataRequest'sPUTbranch read neither headers normode. The catch-all'sdispatcher.dispatch(method, subPath, body, queryParams, { request: c.req.raw }, prefix)already carries the FetchRequest, socontext.request.headers.get(...)reaches them. No adapter change is needed.H4, one mapping. Dependency direction measured:
@objectstack/runtimedepends on@objectstack/rest(dependencies), andrestdoes not depend onruntime. So the shared parser lives in@objectstack/rest, beside the/metaread chain the runtime already imports from there. Nopackages/specedit.H5, every
/metawrite verb. Measured on both doors (catch-all rows driven throughdispatch()with the catch-all's arguments, every header and parameter sent):RestServerPUT /meta/:type/:nameIf-MatchparentVersion; stale → 409METADATA_CONFLICTRestServerIf-None-Match: *parentVersion: null; over a row → 409. Non-*or besideIf-Match→ 400VALIDATION_ERRORRestServer?mode=draftmode: 'draft'RestServerDELETE /meta/:type/:name(reset)If-Match→parentVersion;?state=draft→state;If-None-Matchnot readMETHOD_NOT_ALLOWED(Allow: GET, HEAD, PUT), no protocol callPOST /meta/:type/:name/publishROUTE_NOT_FOUND, no protocol callPOST /meta/:type/:name/rollbackROUTE_NOT_FOUND, no protocol callPOST /meta/_migrate-storedThe three verbs the catch-all does not serve answer loudly and write nothing. The comment on the 405 branch records that mounting a real
DELETEthere "needs its own card".RestServer's rows where a parameter is not read stay as they are (not widened).H6, same answer. Partly holds. Same status, same
code, same refusal sentence, and the draft receipt saysstate: 'draft'on both doors. Not byte-identical envelopes: each transport answers in its own dialect, and the spec declaresMetadataConflictErrorSchemaas "the REST door's flat ADR-0112 dialect".RestServer's 409 is{ error: SENTENCE, code: 'METADATA_CONFLICT', currentVersion: TOKEN }. The catch-all's is{ success: false, error: { code: 'METADATA_CONFLICT', message: SENTENCE, httpStatus: 409 } }(measured through the realcreateHonoApp).currentVersionis not carried as data on the catch-all. That is an open question for the seat (see the report), not decided here.H7, neighbour.
rest-server.ts's?packagereads (thePUTdoor'spackageRaw/packageIdlines and the publish door's) are untouched. Edits there are the import, the removed private parser, and thePUTdoor's precondition / guard / spread lines.Pins (
packages/runtime/src/domains/meta-save-preconditions-parity.test.ts)Each door over its own real store (better-sqlite3
:memory:, the realsys_metadata*objects, a realObjectStackProtocolImplementation). The store is read after every write. TheRestServerleg calls its registeredPUThandler. The catch-all leg repeats the catch-all's four statements over a real FetchRequest(path below the prefix, JSON body, query flattened from the URL, the rawRequestascontext.request) into the realHttpDispatcher.dispatch(). It cannot importcreateHonoApp: this package cannot depend on@objectstack/hono(that package depends on it), andpackages/adapters/hono's suite aliases@objectstack/runtimeto a stub. The H2 probe above is the real-createHonoAppreading.Per door: control (an unguarded save writes the active row, last writer wins); a stale
If-Match→ 409METADATA_CONFLICT, nothing written, and the current token still saves;If-None-Match: *writes the first row and over an existing row → 409, nothing written;?mode=draftstages a draft and leaves the active row untouched; both unhonourable pins → 400VALIDATION_ERROR, nothing written. Plus one side-by-side row (same status, code and token-masked sentence on both doors), and the dispatcher's fallback writer (three asks → 501 andsaveItemnever called; control: an unguarded save still reaches it).Reverse verification and ablations (fix committed first; each leg restored from
HEADand proven by blob hash and an emptygit diff HEAD; the subject resolves by relative source import and@objectstack/restby the runtime vitest alias to source, so nodist/leg applies):main'smeta.tsin the treeRestServer's 5 rows and both controls green.headers: undefinedIf-None-Match: *, the 400 row, the side-by-side row, 2 fallback header rows.RestServergreen; the catch-all's draft row green.query: undefinedRestServergreen.Ablations went through
node scripts/ablation-replace.mjs(anchor hit x1 → x0, blobe90328952e70→18cb795a909f/88f9509a61df, restored toe90328952e70== HEAD,git diff HEADempty).Verification
All at this branch's head
bba46774(git rev-parse --short HEAD), base6ed0c0f3. Heavy runs went throughscripts/pm/os-verify-lock.sh.pnpm --filter '@objectstack/runtime^...' build(dependency closure), then@objectstack/restand@objectstack/runtimerebuilt at head: exit 0.dist/checked to carrymetaSaveRequestOptions.pnpm --filter @objectstack/rest typecheck: exit 0.pnpm --filter @objectstack/runtime typecheck: exit 0. The test layer printedcheck:test-typecheck: OK — 27 file(s) / 190 error(s) / 68 pinned signature(s) held, ledger unchanged. The new test file is in that program: it was refused once, for one error, before the fix inbba46774.pnpm --filter @objectstack/rest exec vitest run --project local: 261 files passed, 4929 passed, 326 skipped.pnpm --filter @objectstack/runtime exec vitest run --project local: 335 files passed, 4736 passed, 19 skipped. Therepoprojects (test:repo) were not run locally; they are left to CI.pnpm lint, each exit recorded.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsre-derived at head gives the same 64 (8 paths, 653 changed lines, under the 5000 threshold).--ranreconciliation:64 derived famil(ies) accounted for — 63 run, 1 NOT-MEASURED, 0 unrun. 63 exit 0, andpnpm lint(eslint . --no-inline-config, whole repo) exit 0 with no findings.pnpm check:dual-build-cjs-loads, reason: exit 3PREREQUISITE NOT MET. It reads every package'sdist/, and 38 packages are unbuilt in this worktree. Declared narrowing in its place: therequireentries of the two packages whose shipped code changed load at head.require('packages/rest/dist/index.cjs')gives 34 exports includingmetaSaveRequestOptions, andrequire('packages/runtime/dist/index.cjs')gives 312 exports. CI runs the full gate.origin/mainis 6 commits past the base (13aea189). None of them touchesmeta.ts,rest-server.ts,rest/src/index.ts,query-multiplicity.ts,http-dispatcher.tsormetadata-protocol/src/protocol.ts, so no merge ofmainwas taken.6d5f6a5f. Only.changeset/22141-rest-meta-save-request-options.mdchanged (patch→minor,Clause-②: no→Clause-②: yes (widening)), so the code verification above stands.dispatch-gates --commandsfor that path derives 20 gates, and all 20 exit 0 at6d5f6a5f(--ran: 20 run, 0 NOT-MEASURED, 0 UNRUN). The branch union is unchanged (the same 64). Also exit 0:check:type-check-coverage,check:type-check-debt, andcheck-changeset-no-major.mjs --base origin/main --eventover this body, whose level axis readsClause-②: yesand finds@objectstack/rest: minor.Acceptance notes
PUT ?package=allthrough the catch-all binds the row to a package literally namedall. The dispatcher readsquery?.package || undefined, whileRestServertreatsalland empty as the env-local overlay. Measured on the real store at this branch's head: catch-all → rowpackage_id: 'all';RestServer→package_id: null. This is the same write-parity family, but the shared fix would editRestServer's?packageread, the region finding(metadata-protocol): a second package-less draft save of a package-owned item is refused 409 METADATA_CONFLICT with no If-Match — the save door's head read and the repository's draft package inheritance read different rows #22128's claim may edit. finding(metadata-protocol): a second package-less draft save of a package-owned item is refused 409 METADATA_CONFLICT with no If-Match — the save door's head read and the repository's draft package inheritance read different rows #22128 is not addressed here.currentVersionas data (H6). Open question in the report.RestServer'sPUTtreats any?modevalue other thandraftas an active save (no closed value set). Unchanged here: a row whereRestServeritself does not refuse stays as it is./metadomain does not judge query multiplicity. The catch-all flattens its query to one string per name, so a repeated?modethere is read as its last value.meta-save-request.ts's header records that a door calls the mapping after its own multiplicity gate.meta-draft-read-door-census.test.tsloses itsPUT ?mode=draftrow: thatwriteswitch is no longer read inrest-server.ts. Its header now says where it went and which tests hold it.Written by session
session_01RWZbGvPFcRKvUqASZtunCU. Round 1 changed only theClause-②declaration (the@objectstack/restchangeset regrade and this body), with no code change.