Repository navigation
fix(rest): the dataset door judges a holder whose own address a row no one module owns contends, so the name refuses 409, not 503 (ADR-0150 D4) - #33491
Conversation
…ended instead of answering 503 (ADR-0150 D4) A row two or more holders of a dataset name could own (a package-less row, or an App pair's undeclared row in the registry's bare slot) makes each holder's own exact read refuse `ambiguous-in-closure`, naming itself among the holders. The admission arm read that as "cannot be judged" and the door answered 503 for every caller. Such a holder is now judged on its own item as the list scoped to it serves it, never picked, and named in the 409 when the caller is admitted to it. Also: one exported package-scoped refusal recogniser for the door and the rule; the undetermined 503 carries its cause for the operator's log. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cz4f7zEQZ2C9GTG382
…ause; docs and changeset Real-arrangement pins over the registry and protocol: an App pair row no module owns, both orders, bare read, a module's ?package= and ?callerPackage=, single door and batch; CONTROLs for a package-less row (declined, unchanged) and for one holder (unchanged). The analytics page says what the door answers for such rows and that the 503 logs its cause. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cz4f7zEQZ2C9GTG382
📓 Docs Drift CheckThis PR changes 1 package(s): 25 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 10 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 0d77ea3d3bd1f82e3e25bd938b80e5c3b66d527d && git checkout 0d77ea3d3bd1f82e3e25bd938b80e5c3b66d527d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f068cde84d272d25c451003996f3752bd016001b b89bfcd08d3ae799053b7ff46f1aeca750baf94d && git checkout -B drift-repro f068cde84d272d25c451003996f3752bd016001b && git merge --no-ff b89bfcd08d3ae799053b7ff46f1aeca750baf94d
node scripts/docs-audit/affected-docs.mjs --json f068cde84d272d25c451003996f3752bd016001b
|
|
domain:server#5 · os-elon-musk · session_016WV4cz4f7zEQZ2C9GTG382 Area notes for the reviewer:
|
|
Contract-review-claim: session_016WV4d1PGptBW3QRQNmy5e6 · head 7eae0de · 2026-11-14T22:36:35Z review#3. |
|
domain:server#5 · os-elon-musk · session_016WV4czXwx6JtQPvacrzzUw
|
Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
…ddress widening The door's own sentence for an ambiguity narrowed to one admitted holder no longer counts holders or speaks of another package; pins assert the hidden holder is neither named nor implied (body keys, text, counts). The widening is pinned at ?package= and ?callerPackage= a module, both registration orders: the uncontended holder is not a candidate there and is never reached across to. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cz4f7zEQZ2C9GTG382
…den holder Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4cz4f7zEQZ2C9GTG382
|
Contract-review-claim: session_016WV4d1PGptBW3QRQNmy5e6 · head b89bfcd · 2026-11-14T22:51:46Z review#3 (delta). |
Contract reviewServed-tier: This reviews the delta ① Derived judgments
② Semver levelUnchanged: ③ Boundary flags
Implemented-by: VERDICT: PASS |
|
domain:server#5 · os-elon-musk · session_016WV4cz4f7zEQZ2C9GTG382
|
Requested by Elon · project thread
Fixes #32948
Fixes #32826
Clause-②: yes (widening)
Metadata-protocol: none
维护者速读
POST /analytics/dataset/query(含批量接口每一条)在"一行无主的存储行挡在 App 两个模块各自地址上"时,不再对所有调用者答 503;改为按该模块自己的数据集判准入:能查的模块在 409 里点名,不能查的不点名,一个都不能查答 404,被挡住的模块永不执行。另外:仍然答 503 的情形,服务端日志写明是哪些持有包判不了、为什么;拒绝识别函数合并成一份;文档补齐。yes (widening);该放宽只在裸名读取上发生,在某个模块自己的?package=/?callerPackage=上,另一个包的持有项不在被拒候选里,不会被跨过去执行(答 404,或 409 只点名该模块)。只剩一个可查持有包的 409 文案不提、不计数任何被隐藏的持有包。回滚即 revert 本 PR。packages/rest/src/analytics-dataset-admission-order.ts(isOwnAddressContention与judge两处,风险在"被争用的持有包永不执行");packages/rest/src/analytics-routes.ts里的listHolder(只取该持有包自己的那一项);packages/rest/src/analytics-routes.test.ts的[#32948]钉子。pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2 src/analytics-routes.test.ts -t '32948|32826',预期 10 个用例全部通过。What changes
Premise check first. The card's literal composition does not answer
503onmain(ba35dfd78) or on PR #32808's landing head (c50d10c00): a package-less dataset row beside the App's two modules is declined at each module's own address for a dataset (epic ruling 6817768147, the[#32568]pin already in this file), so?package=APP.customis judged over the modules as if the row were not there (both admitted409naming both, one admitted runs it, none404). Measured over seven stagings (the row in the registry's bare slot with_provenance: 'org', stampedsys_metadataor'', before or after the modules; a storedsys_metadatarow withpackage_idnull; ametadataservice copy stamped'',sys_metadataor unstamped), with and without the pair installed, both orders: each module's own exact read serves its own dataset. That composition is now a CONTROL pin.The 503 the card describes is real in a sibling form, the one the #32804 review (N5) said "can be staged with the registry's own
registerItem": an App pair's row that names no module, hydrated into the registry's bare slot (the registry binds it to no package; stampAPP.custom). Either module could own it, so it is no one holder's customization (ADR-0150 D4; epic ruling on #31699 F2), and each module's own exact read refusesambiguous-in-closure, read as that module and naming both. The admission arm read every per-holder refusal as "cannot be judged", so onmainthe bare read, a module's?package=and a module's?callerPackage=all answered503for every caller, in both orders, single door and batch alike.Now
admitRefusedDatasetHoldersrecognises that one shape (isOwnAddressContention:ambiguous-in-closure,callerPackageIdthe holder itself, only package candidates, the holder among them) and judges such a holder on its own dataset as the list scoped to it serves it (getMetaItems({ type: 'dataset', packageId, names: [name] }), which withholds the row; only an item stamped with that holder counts). That is howquery_datasetjudges every holder. A contended holder is never picked or run; admitted, it is named in the409; not admitted, it is not named. Every other refusal of a holder's own read stays undetermined, as before.Moved answers (refusal arm only, plugin-wired admission):
503 SERVICE_UNAVAILABLE409 RESOURCE_CONFLICTnaming those holders503409naming that holder only, nothing run503404 NOT_FOUND, byte-equal to a missing name's503200, that holder runs (the one-admitted rule of #32292, now reached): the widening arm?package=or?callerPackage=a contended module503404 NOT_FOUND, byte-equal to a missing name's; that holder is not a refused candidate there, so it is never reached across to?package=or?callerPackage=503409naming that module onlyAn ambiguity narrowed to one contended holder gets its own sentence instead of the vocabulary's "is held by 1 packages … address it by its package", which would be wrong twice (a count of one, and a remedy that answers the same refusal). The sentence speaks of that one package and the row only:
The dataset "NAME" cannot be read at package "ID": a stored row of the name stands beside that package's item and is not saved under it, so nothing is run until the row is saved under a package (ADR-0150 D4).It names, counts or implies no other holder: a holder the caller may not query stays hidden, and the refusal body carries onlycodeandmessage(no candidates, reason or details). Seat amendment (2) on this PR replaced an earlier wording that said the row was one "no one package owns".#32826 (loose ends of PR #32804), folded in:
SchemaRegistryandObjectStackProtocolImplementation(the[#32948]pins). The package-less form is declined, already pinned by[#32568]. The first-read form that never reaches the per-holder catch (the App pair address itself refusing, two modules holding the name) is pinned by the#32501describe'sjudgedOverAdmission, which landed with fix(metadata-protocol)!: a pair address reads the package it names: its envelope is that package's, and an App pair two of whose modules hold the name is refused naming both (ADR-0150 D5) #32808. No staging measured here still reaches the per-holder undetermined path through a real refusal; the protocol double that pins it now says which shape it stands for.503's cause. The undetermined answer carries a cause naming each holder that could not be judged and why (its own read refused, with the reason; its address served nothing; its admission was unresolved; contended but not listed). The door'slogErrorprints it. The body is unchanged and names no holder.isPackageScopedRefusalis exported fromanalytics-dataset-admission-order.tsand used by the door and the rule; the copy inanalytics-routes.tsis gone.content/docs/data-modeling/analytics.mdxsays a409reached with some holders unjudged names only the judged ones. The changeset half points at a releasedCHANGELOG.mdentry and is split out (Acceptance notes).ambiguous-in-namespacestays in the rule's ambiguity set, with the reason in the comment: only a namespace address is refused with it, the dataset doors never name a namespace, and listing it keeps an ambiguity from ever being judged asoutside-closure.Tests
packages/rest/src/analytics-routes.test.ts, ten new cases:[#32948]in the#32501describe (real registry and protocol; analytics double admitting by object;both()drives the single door and a one-entry batch): the contention pin, both orders, on the bare read,?package=a module and?callerPackage=a module, every admission cell; the widening pin with a third package registered first and last (bare read); the module-address pin, both orders, at?package=a module and?callerPackage=a module with that third package present: admitted to it alone answers the missing name's404byte for byte, admitted to it and one module answers409naming that module only; every one-module409cell (contention pin and module-address pin) also asserts the hidden holder is neither named nor implied: body keys exactlycodeandmessage(batch entrystatusanderror), no hidden id in the text, no holder count, and none ofpackages,holders,other,another,both,each,either,no one,ambiguous,pickin the sentence; CONTROL: a package-less row beside the two modules,?package=APP.customjudged as with no row; CONTROL: one module, the pair row is its customization, served, admission not asked.[#32826]in the#32292describe: the undetermined503logs an error whose cause names both unjudged holders and the refusal reason; the body names neither.Red on
main, green here, one ablation per pin. Each ablation leg went throughscripts/ablation-replace.mjs(anchor 1 → 0, blob moved, restored blob equal to HEAD,git diff HEADempty). The tests import the rule and the door from source, so no rebuild between legs. Leg M putorigin/main's blobs of both source files (05e9c422,662e822c) under this head's tests and restored them withgit checkout HEAD -- PATH, blob-checked.ABLATE_32948_NOJUDGE(contention not judged)ABLATE_32948_RUNNABLE(a contended holder runnable)ABLATE_32826_NOCAUSE(no cause)Leg M, A2 and A4 were rerun at
cfc3b3dd3over the ten cases: M 7 failed / 3 passed (every module-address cell503); A2 4 failed / 6 passed; A4 4 failed / 6 passed; each restored blob-equal to HEAD withgit diff HEADempty. A1 and A3 rows are from head7eae0dea1and were not rerun for the new pin.Acceptance notes
200) at?package=MODULEand?callerPackage=MODULEtoo. There the read refuses the module's name over the two modules only; the third package's holder is not a refused candidate, so the one-admitted rule never sees it. Running it would serve another package's dataset at a module's exact address, a pick that address does not make. The pin asserts what the door answers:404(byte-equal to a missing name's) for the caller admitted to that holder alone,409naming only the module for the caller admitted beside it. Moving that to200would be a further widening and a seat call; it is not in this PR.packages/rest/CHANGELOG.mdentry. Its pointer "so the row above applies to it" points at the503row, and it has no row for a409reached with some holders unjudged. That amendment is a docs-only PR to the released entry, owned by the area session.@objectstack/core's vocabulary. The structuredreasonandcandidatesare the vocabulary's.GET /meta/dataset/NAME?package=MODULE) also says "address it by its package", which answers the same refusal there. The text is in@objectstack/coreand@objectstack/metadata-protocol; this PR does not touch it.Verification
Local, at head
cfc3b3dd3(source and tests;b89bfcd08after it touches only the changeset and the docs page):pnpm --filter @objectstack/rest test: Test Files 411 passed (411), Tests 8397 passed, 332 skipped (lock verdictcommand-exit 0).pnpm --filter @objectstack/rest typecheck: exit 0.eslint --no-inline-configover the three touched source and test files: exit 0.node scripts/pm/dispatch-gates.mjs --commandsderived 95 families against merge base34c05229fat head7eae0dea1; 93 exit 0; 2 exit 3 PREREQUISITE NOT MET, NOT MEASURED here and left to CI (pnpm --filter @objectstack/spec run check:skill-examplesneeds the client SDK packages built;pnpm check:dual-build-cjs-loadsneeds every package'sdist/).--ranreconciliation: 95 derived, 93 run, 2 NOT-MEASURED, 0 UNRUN. Those 95 were read at7eae0dea1; for the amendment the 27 docs, changeset and prose families among them were rerun overb89bfcd08's tree, all exit 0, andnode scripts/check-nul-bytes.mjsOK. No dogfood battery: no!in the title and no(narrowing)arm.Generated by Claude Code
🤖 Generated with Claude Code