Repository navigation
fix(rest,runtime,metadata-protocol)!: /published, REST /state, _drafts and the event-door absence body answer a disabled package's item as the by-name read does - #24247
Conversation
…s and the event-door absence body answer a disabled package's item as the plain read does Claude-Session: https://claude.ai/code/session_01Y9zBTp9ePwgEEJXeqyLG4N Co-authored-by: Claude <noreply@anthropic.com>
…ed package's item Claude-Session: https://claude.ai/code/session_01Y9zBTp9ePwgEEJXeqyLG4N Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y9zBTp9ePwgEEJXeqyLG4N Co-authored-by: Claude <noreply@anthropic.com>
…ta-disabled-doors-second-half
…owner check, with its outage pin Claude-Session: https://claude.ai/code/session_01Y9zBTp9ePwgEEJXeqyLG4N Co-authored-by: Claude <noreply@anthropic.com>
… the engine-double ledger records it Claude-Session: https://claude.ai/code/session_01Y9zBTp9ePwgEEJXeqyLG4N Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 11 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 8 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 39 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 62acb721a77e20d33ddfaddf35150ff519fa5461 && git checkout 62acb721a77e20d33ddfaddf35150ff519fa5461
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin cbfa5e43354f430e35c4f9f24732e59848174889 ff82c7a1bffa1edba88137d7a1056749abaac079 && git checkout -B drift-repro cbfa5e43354f430e35c4f9f24732e59848174889 && git merge --no-ff ff82c7a1bffa1edba88137d7a1056749abaac079
node scripts/docs-audit/affected-docs.mjs --json cbfa5e43354f430e35c4f9f24732e59848174889
|
Contract reviewServed-tier: Inputs: card #24210 (body; every comment, 6239527781 through 6244976750, including triage 6239967422, ruling 6241893667, claims 6243792589 and 6244976750, the first half's landing 6243759173 and the second half's os-dev-report 6244647143), PR #24223's record 6243449822 and ACCEPT 6243458372, the #7557 pin's docblock, PR #24247's body and file list, the net diff Check-runs at read time (2026-10-18): every run ① Derived judgmentsA.
B. REST G6,
C. REST G7,
D. REST G8,
E. Dispatcher
F. The pins. G. Accept-set and public-surface changes the diff implies, each judged. (1) ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对 ·
|
| Item | Reading |
|---|---|
| Shape | Draft PR → main. The body opens Part of #24210 with no closing keyword. The card stays open for the third dispatch (non-object ?layers=true and /layers, now in meta-item-read-routes.ts after slice 13b's PR #24240 landed). |
| Scope | 12 files (+527 / −20), claim 6243792589 as re-declared in 6244976750. listDrafts (protocol.ts); REST G6 (eventDoorRefusal, the /diff branch), G7 (/state/:field) and G8 (/published); the dispatcher's /published (meta.ts); five pins; the gate-prescribed engine-double-contract.pinned.json rows; the objectQLProvider census; three changesets. Nothing in G4, G5 or the class head. Not governed (check-governed-merges.mjs --pr 24247: 0 of 12). |
| Doors | Read in the diff and the record's table. _drafts omits a disabled package's drafts on both transports at the producer (package never hidden), while the batch publish, discard and closure verbs keep reading the repository. The event and diff doors answer the plain read's nested 404 body, recognised by thrownAnswerIsBareNotFound. REST /state refuses 404 OBJECT_PACKAGE_DISABLED byte-equal to the by-name read, and the schema is never read; this closes the first half's interim transport split. Non-object /published takes the missing name's exits on both stores, both transports. |
| H1 | Half falsified by the dev and measured live: the layered envelope of a runtime-saved item carries no packageId. The owner is the plain read's kept envelope packageId, the same field (extractProtection → _packageId) and predicate (isPackageDisabled) the by-name read asks. No new method; getMetaItemLayered untouched (ruling Q1 A). |
| Review face | Narrowing (Clause-②: no (narrowing)). The CONTRACT_REVIEW_TIER record 6245098698 on this head is PASS. The seat verified in the diff the listDrafts filter and its docblock's resolution-reader carve-out, the withheld / isPackageDisabled guard on /published, the isObjectPackageDisabled check on /state, and the thrownAnswerIsBareNotFound → sendMetaItemAbsent fork on the event and diff doors. |
| Prose face | Three changesets, sentence by sentence. Each is minor with BREAKING, Clause-②: no (narrowing) and the adr-0087 not-required (no-migration-prescription) marker. Each sentence matches the diff. Not stated in the runtime changeset: the dispatcher's /published now answers the plain read's own 503 / 400 as themselves (errorFromThrown(e, 404)) rather than falling through to the snapshot. The PR body names it (What changed, item 5), and it matches REST's twin and the dispatcher's item read. Accepted without a changeset edit. No sentence is false. |
| Pins and ablation | READERS +2 API rows, +1 resolution row (repo.listDrafts, enforces: false) and a control. The new REST door pin runs on the real protocol and real SchemaRegistry, with an outage arm. The alternate-door pin checks the body against the plain read. The dispatcher pin covers both exits. Ablations (a)–(g) each redden only their own case; every restore is clean. |
| Gates | Dev union at 98dd4f8141: 78 derived, 78 run, 0 NOT MEASURED. Typecheck scripts of all four touched source packages exit 0. CI on this head at this read: 40 check-runs, 34 success, 6 skipped (each on the EXPECTED_SKIPS roster), 0 failed. |
Disposition:
open_questions[0](the/publishedowner): A, the plain read's envelopepackageId; field identity verified (record ①.D).open_questions[1](_drafts?packageId=for a disabled package): A. While a package is disabled, Studio's package sheet hides its Pending changes section. Re-enable restores it, and the batch doors still act. This is ruling item 6's one answer; a hint on the sheet would be objectui's call.deviations: accepted. The two gate-prescribed rows are re-declared in 6244976750. Seven pushes to the own branch;origin/mainmerged by a merge commit; the dispatcher measured throughcreateHonoApp's fetch handler, as the first half was.- Named, nothing owed (record ③.5–③.6): a host with no engine slot withholds nothing on REST
/published(the degenerate-host shape the list and by-name reads carry); the ungated types' event and diff doors.
Landing: ordinary queue (pr_ready + automerge_enable through the relay); every check on this head is green or a rostered skip.
Generated by Claude Code
|
Queue kick · Removed from the merge queue at 2026-10-18T03:21Z ( Build Core, every Test Core shard and the type-check lanes fail on that one error. Cause: this PR's, a semantic merge conflict, not a flake. #23050 slice 13b (PR #24240, Act: the dev merges Generated by Claude Code |
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 39093149214 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 分类: 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
…ta-disabled-doors-second-half
…f doors after slice 13b moved the item read's catch Claude-Session: https://claude.ai/code/session_01Y9zBTp9ePwgEEJXeqyLG4N Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Inputs: card #24210 (body; every comment, 6239527781 through 6245117166, including triage 6239967422, ruling 6241893667, claims 6243792589 and 6244976750, the first half's landing 6243759173 and the second half's Why this head and not the earlier record. The earlier record 6245098698 passed
Check-runs at read time (2026-10-18T03:47:58Z): 39 runs; 29 ① Derived judgmentsA. The import fix and the moved catch (the head move's own judgment).
B.
C. REST G6,
D. REST G7,
E. REST G8,
F. Dispatcher
G. The pins (every hunk byte-identical to the earlier head). H. Accept-set and public-surface changes the diff implies, each judged. (1) What each door answers now (a disabled package's item | triage's answer | a missing name), on this head:
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
席位核对 ·
|
| Item | Reading |
|---|---|
| Why again | Dequeued at 2026-10-18T03:21Z (CI_FAILURE, queue kick 6245148853). The merge group failed @objectstack/rest's build with TS2304 thrownAnswerIsBareNotFound at rest-server.ts (3646) and (6419). #23050 slice 13b (PR #24240, 646303eae) dropped that import as unused when it moved the item read's catch into meta-item-read-routes.ts; this PR's two readers of it stayed in rest-server.ts. The texts merged cleanly and the build did not. |
| Head move | Two commits. 07b1e218a5 merges origin/main at cbfa5e43; git show --remerge-diff is empty, so no conflict was resolved. ff82c7a1bf restores thrownAnswerIsBareNotFound to the ./error-response.js import (rest-server.ts:374, +3 / −2). The seat diffed both net diffs against their own merge-bases: exactly one hunk differs, that import. Every other hunk, the three changesets included, is byte-identical to the head ACCEPT 6245106275 read. |
| Moved catch | Read at this head: the plain item read's catch (meta-item-read-routes.ts, after 13b) still forks on thrownAnswerIsBareNotFound and still emits sendMetaItemAbsent. That function stays defined in rest-server.ts (now exported for the new module), and the PR's event and diff doors call it there. One recogniser and one emitter serve both modules, so the byte-equality claim and its pins still hold after the move. |
| Review face | Narrowing (Clause-②: no (narrowing)). A head move that touches code is not covered by the earlier record, so this head has its own CONTRACT_REVIEW_TIER record, 6245404399: PASS. Its central claims (interdiff, empty remerge-diff, the import site, the moved catch's fork and emitter) were re-verified by the seat in the tree. |
| Prose face | Unchanged since ACCEPT 6245106275: three changesets, byte-identical. The record names two prose gaps the PR body already covers: the dispatcher /published answers its 503 / 400 as themselves, and REST /published answers 503 for a wired-but-rejecting engine slot. Accepted without a changeset edit, as before. No sentence is false. |
| Gates | Dev rework report 6245490394 at ff82c7a1bf: 78 derived, 78 run, 0 NOT MEASURED, 0 unrun. The original TS2304 was reproduced by ablating the import (restored, blob == HEAD). Full build 76/76; @objectstack/rest build with DTS; typecheck exit 0 for rest, runtime and metadata-protocol; suites green for rest (6034 passed), runtime (6413), metadata-protocol (20641) and the #7557 pin (15/15). |
| CI | At this read: 47 check-runs, 39 success, 8 skipped, 0 failed. The skips are the six path or opt-in skips ACCEPT 6245106275 read, plus the body-edit re-runs of Auto Label and Check PR Size, whose first runs on this head succeeded. All eight are on the EXPECTED_SKIPS roster. Build Core, every Test Core shard and every Type Check lane are green: the exact families the queue run failed. |
Disposition: unchanged from ACCEPT 6245106275 (open_questions A and A; deviations accepted). The rework report adds no open_questions and two deviations, both accepted: the worktree was re-created from the remote branch, and the PR's ready state was left as the seat set it. The PR body's Verification gains a paragraph naming the new head (read back identical by the dev).
Landing: ordinary queue (automerge_enable through the relay; the PR is already ready for review). Not governed (check-governed-merges.mjs --pr 24247: 0 of 12).
Generated by Claude Code
Part of #24210 (the second half: it closes
/publishedon both transports, REST/meta/object/NAME/state/FIELD,GET /meta/_draftson both transports, and the event and diff doors' absence body). Non-object?layers=trueand/layersstay on the card for after #23050 slice 13b lands (they sit in G4, which is in flight), so #24210 remains open.Clause-②: no (narrowing) (owes one
CONTRACT_REVIEW_TIERreview before the queue)Implements triage 6239967422 (absence for a non-object type;
OBJECT_PACKAGE_DISABLEDfor an object; no new or generalised code) and the seat's ruling 6241893667 (Q1 A door side, Q2, item 6) under claim 6243792589, dispatched by thedomain:engineseat 1 (session_01JtSQQ3EWsbNPzyYMug1tXy), plus the first half's ACCEPT 6243458372 open question A (the event and diff doors' absence is byte-identical with the plain read).What changed
Since PR #24223 the by-name read answers a disabled package's non-object item as absent, as the list omits it. Four more doors still read past that answer. Each now asks the same disabled-owner question and takes the exit a missing name takes.
GET /meta/_drafts, both transports: the producer.listDrafts(packages/metadata-protocol/src/protocol.ts) omits every draft whose rowpackageIdnames a disabled package, by the shared predicateisItemOfDisabledPackage(packagenever hidden). Neither door is edited. The package batch doors (publish-drafts,discard-drafts, the batch closure) read the repository's ownlistDrafts, not this method, so a disabled package's drafts still publish and discard (pinned as a resolution reader)./history,/audit,/diff: the absence body.eventDoorRefusal(the class member) and the/diffbranch catchfetchCurrentMetaDocument's withheld-item throw and answer it throughsendMetaItemAbsent, recognised bythrownAnswerIsBareNotFound, the same predicate the plain read's catch asks. The answer is now the plain read's nested body,{"error":{"code":"RESOURCE_NOT_FOUND","message":"Metadata item not found or access denied."}}, instead of the flat{"error":"…","code":"RESOURCE_NOT_FOUND"}.fetchCurrentMetaDocumentis not edited. A deleted item keeps its answer.GET /meta/object/:name/state/:field. Before reading the schema, the door asks the engine registry'sisObjectPackageDisabled(name). For such an object it reads the protocol's by-name read, whose404 OBJECT_PACKAGE_DISABLEDthe door's own catch renders, byte for byte whatGET /meta/object/:nameanswers. The schema is never read for that name. This is the dispatcher twin'sdisabledObjectAnswershape. It closes the interim transport split the first half named.GET /meta/:type/:name/published, every type butobjectandpackage. Before either exit (the stored published row, or the code/package snapshot), the door reads the plain read's kept protection envelopepackageIdand asks the engine registry'sisPackageDisabled. A disabled owner takes the missing name's exits: no overlay, no snapshot,404 NOT_FOUND. The registry is reached throughwiredEngineOrLoud, so a failed engine is a loud503, and a host with no engine withholds nothing. Objects keep the layered read's own refusal./publishedtwin (packages/runtime/src/domains/meta.ts): the same question throughdeps.getObjectQLandisServiceItemOfDisabledPackage. A disabled owner answers the missing name's404 RESOURCE_NOT_FOUND"Not found". A throw of that plain read is the item read's answer (errorFromThrown(e, 404)).The producer
getMetaItemLayeredis not touched (ruling Q1 A: it keeps serving its resolution reader, the security catalog echo). There is no new public method, spec key, error code or enforcement change.Mechanism hypotheses (measured before any edit)
/published's owner: half falsified. On a livepnpm dev:crm --fresh, the layered envelope of a runtime-saved item carries nopackageId, and itseffectivecarries no_packageId. The stored row'spackage_idis grafted only by the plain read (readMetaItem). So the owner comes from the plain read's kept envelopepackageId(the field the first half's echo fix reads, and the one the protocol's predicate asks of the resolved item), asked of the registry'sisPackageDisabled. A REST door has no registry predicate of its own, so G8 reads theobjectQLProviderslot (see Acceptance notes)._drafts: holds.repo.listDraftsrows carrypackageId. The protocol method has exactly two callers, the two_draftsdoors. The three batch doors call the repository directly. So the producer filters, and neither door is edited./state: holds. The G7 handler readsql.registry.getObjectdirectly. The dispatcher'sdisabledObjectAnswershape ports as-is, and no schema is read for the name.sendMetaItemAbsentis reachable fromeventDoorRefusaland the/diffbranch. The deleted item keeps its answer on all three doors (pinned and measured).Doors, measured over HTTP
REST:
pnpm dev:crm -- --freshon a real socket. Before isorigin/main2bcd406a2(full build); after isd78cc5fd(the fix commit; later commits change only tests, changesets and a ledger). Dispatcher: the real store (SqliteWasmDriver) and the real protocol (ObjectQLPlugin) undercreateHonoApp, driven through its fetch handler (scratch harness, not committed). Writable packageapp.probe-24210bholds dashboardprobe_dash(saved twice), pageprobe_page, appprobe_app(saved twice), objectprobe_gadgetand a draftprobe_draft_dash. Control packageapp.control-24210bholds the same shapes.probe_gonewas saved and deleted./meta/dashboard/probe_dash/history,/audit,/diff;/meta/app/probe_app/history,/diffRESOURCE_NOT_FOUND, flat body/meta/dashboard/probe_gone/history,/diff(deleted)/meta/dashboard/probe_dash/published,/meta/page/probe_page/published,/meta/app/probe_app/publishedNOT_FOUND, = missing nameRESOURCE_NOT_FOUND, = missing name/meta/object/probe_gadget/publishedOBJECT_PACKAGE_DISABLED/meta/object/probe_gadget/state/status?from=openOBJECT_PACKAGE_DISABLED, =GET /meta/object/probe_gadgetGET /meta/_draftsprobe_draft_dashctrl_draft_dashlistedGET /meta/_drafts?packageId=app.probe-24210b{"drafts":[]}{"drafts":[]}ctrl_dash/history,/diff,/published;ctrl_page/published;ctrl_gadget/state/statusByte equality on REST, compared over status, headers (minus date, request id, timing) and body: the disabled
probe_dashby-name read, its/history,/auditand/diff, andprobe_app's/historyand/diffall equalGET /meta/dashboard/no_such_dash_x. The three disabled/publishedreads equalGET /meta/dashboard/no_such_dash_x/published. On the dispatcher, both disabled/publishedreads answer{"success":false,"error":{"code":"RESOURCE_NOT_FOUND","message":"Not found","httpStatus":404}}, the missing name's bytes. Enabled readings after equal enabled readings before on both transports, after normalising timestamps, hashes and actor ids. Re-enabling restores every row on both transports.Callers of
GET /meta/_drafts(objectui at the pinned.objectui-sha5e15928b, read-only clone)PackagesPage.tsxpackage sheet (_drafts?packageId=)publish-drafts/discard-draftsdoors themselves still act on those drafts; re-enable and the section returnsusePendingDrafts,HomePage,DraftChangesPanel,usePublishAllDrafts404 NO_DRAFT(first half)ObjectHooksPanel,PackageOwdOverviewPanel,StudioDesignSurface(listDrafts({ packageId, type }))No caller must tell "disabled" from "missing".
Pins
packages/objectql/src/package-disable-enforcement.test.ts, the Package disable and uninstall never reach the metadata/data layer: a disabled package's objects still serve rows, and uninstall leaves 7 orphanedsys_metadatarows #7557 table: two API-reader rows (listDrafts(),listDrafts({ packageId }), which must stop), one resolution-reader row (the repository'slistDrafts, which the batch doors read and must keep serving), and the control (the healthy package's two drafts still listed). The re-enable case walks the same rows.packages/rest/src/meta-disabled-package-doors.test.ts(new), on the real protocol and a realSchemaRegistry:/publishedon both exits served, then equal to the missing name (status and body), then served again after re-enable, with a control package andgetPublishednever asked for a withheld item;/stateserved, then refused404 OBJECT_PACKAGE_DISABLEDbyte-equal toGET /meta/object/:name,getObjectnever asked the name, a control and a missing name, then served again; the slot outage case (a rejecting provider answers503 SERVICE_UNAVAILABLE, an absent one serves).packages/rest/src/meta-alternate-door-read-gates.test.ts: the withheld gated dashboard and app now assert the event and diff doors' body is the plain read's own, byte for byte, and the deleted-item case asserts its/diffbody is unchanged.packages/runtime/src/domains/meta-disabled-package-doors.test.ts: the dispatcher/publishedof a non-object item on both exits (served, then byte-equal to the missing name with neither store asked, then served again), and a control.packages/rest/src/objectql-slot-consumer-census.test.ts: the census counts the fourth consumer,/published, which passes its decidable test throughwiredEngineOrLoud, and §3 cites its outage pin.git grepof the absence sentence,_drafts/listDrafts,/published,disablePackage/isPackageDisabledacross every test. No other pin asserts the old answers. The suites of every touched package plushttp-conformancewere run (below).Verification
Rework (merge-queue removal,
CI_FAILURE), headff82c7a1bf: mergedorigin/maincbfa5e4335as07b1e218a5(a merge commit). #23050 slice 13b (646303eae) had droppedthrownAnswerIsBareNotFoundfromrest-server.ts's imports, and the event and diff doors here read it (TS2304 atrest-server.ts3646 and 6419). It is re-imported from./error-response.js, the only source edit, reproduced red without it. Atff82c7a1bf: fullpnpm build76/76 and@objectstack/restbuild exit 0;rest,runtimeandmetadata-protocoltypecheck exit 0; suitesrest295 / 6034,runtime413 / 6413,metadata-protocol244 / 20641, the #7557 pin 15/15;dispatch-gates78 derived, 78 run, all exit 0,--ran0 NOT-MEASURED, 0 UNRUN. The readings below are the previous head's.Head
98dd4f8141, after mergingorigin/main(7715d049e, a merge commit; no rebase, no force-push) with a fullpnpm build(76/76).0268f810; the one later commit changes only the new REST test'sfinddouble and the engine-double ledger, and its file was re-run at98dd4f8141, 3/3):@objectstack/rest295 files, 6032 passed (326 skipped);@objectstack/runtime412 / 6313 (19 skipped);@objectstack/objectql437 / 8244;@objectstack/metadata-protocol244 files (3 skipped) / 20641;@objectstack/http-conformance14 / 150.typecheck(tsc --noEmit, pluscheck:test-typecheckwhere the script carries it), each exit 0 at98dd4f8141:metadata-protocol,objectql,runtime,rest.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(no paths) derived 78 commands at98dd4f8141, all exit 0 (unshallowed clone).--ransays: "78 derived, 78 run, 0 NOT-MEASURED, 0 UNRUN". At the previous head two families went red on the new test's double,check:engine-double-contract(an unrecorded pinned double) andcheck:objectql-double-limit(a limit-blindfind). Both are fixed in98dd4f8141.scripts/ablation-replace.mjswith the restore provenblob == HEAD:listDrafts' predicate replaced by an always-true marker.metadata-protocolwas rebuilt, andablation-dist-preflightfound the marker indist/. The two_draftsAPI rows and the control went red (2 failed / 13 passed). After the restore and rebuild,--absentgave a dist reading of PASS (its tree reading exited 3 only on the untracked scratch harness, since removed), and the run was 15/15.eventDoorRefusal's fork disabled: only the withheld-item case went red (1 / 427). (c) The/difffork disabled: the same case only (1 / 427)./state's disabled check: only the/statecase (1 / 2). (e) REST/published's withheld verdict: only the/publishedcase (1 / 2)./publishedcheck: only its case (1 / 9)./publishedreading the slot with a swallowing.catch: only the outage case (1 / 3).Acceptance notes
/history,/auditand/diffhandlers' calls), G7, G8 and the class membereventDoorRefusal. Nothing in G4, G5 or the class head.meta-collection-routes.tsis not edited, and neither is the dispatcher's_drafts(the producer carries it).scripts/engine-double-contract.pinned.jsongains three rows for the new test file's pinned double. That ischeck:engine-double-contract's own prescription (--write) for new pinned coverage, a mechanical ledger entry with no source change.objectQLProviderconsumer. G8 reads the engine registry, which no REST/metadoor read before. It goes throughwiredEngineOrLoud, and the census pin is updated to count it.createHonoApp's fetch handler (app.request), thepackages/qa/http-conformanceshape, not over a socket. The scratch harness sat untracked underpackages/qa/http-conformanceand was removed before the gates ran.page,flow,report, …) still serve a disabled package's item's events and diff. No per-caller gate exists there to bypass, as the first half's record (③.7) named. This half does not change them.Generated by Claude Code