Repository navigation
fix(metadata)!: the unscoped plural read counts an action name's holders per (object, name), so the boot pass keeps a package-less row on another object beside the holder's - #33422
Conversation
…e), so a package-less row bound to an object no package binds is the environment's own The unscoped plural read (pluralEntryKeys) folded a package-less action row into the name's one holder whatever object it was bound to: beside ALPHA's approve on account, a package-less approve on contact was folded into ALPHA's entry, the unscoped read kept one of the two rows, and the boot priming pass registered that one alone. It is the holder's customization only when the holder binds the name to the row's own object (ADR-0150 D4). Claude-Session: https://claude.ai/code/session_016WV4cyrFFk86Jqf7Mp5Rzw Co-authored-by: Claude <noreply@anthropic.com>
… cold read against boot pass, with one-object CONTROLs Claude-Session: https://claude.ai/code/session_016WV4cyrFFk86Jqf7Mp5Rzw Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check3 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 16 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 ea30ec5216bef032ae2a6b07ec2929af9a1392a4 && git checkout ea30ec5216bef032ae2a6b07ec2929af9a1392a4
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b4624440e699395d1929b1eb7d77f60b4185c481 8efae7e95f6ed203f7efbe8b4790f66370516312 && git checkout -B drift-repro b4624440e699395d1929b1eb7d77f60b4185c481 && git merge --no-ff 8efae7e95f6ed203f7efbe8b4790f66370516312
node scripts/docs-audit/affected-docs.mjs --json b4624440e699395d1929b1eb7d77f60b4185c481 |
… shape and the single-holder cell, and the package-less row's route judging and running that one item Claude-Session: https://claude.ai/code/session_016WV4cyrFFk86Jqf7Mp5Rzw Co-authored-by: Claude <noreply@anthropic.com>
…easured Claude-Session: https://claude.ai/code/session_016WV4cyrFFk86Jqf7Mp5Rzw Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cyrFFk86Jqf7Mp5Rzw Co-authored-by: Claude <noreply@anthropic.com>
…olds ALPHA's account key beside the bare slot Claude-Session: https://claude.ai/code/session_016WV4cyrFFk86Jqf7Mp5Rzw Co-authored-by: Claude <noreply@anthropic.com>
|
domain:server#5 · os-elon-musk · session_016WV4cyrFFk86Jqf7Mp5Rzw |
|
Contract-review-claim: session_016WV4d1PGptBW3QRQNmy5e6 · head 8efae7e · 2026-11-14T21:38:47Z review#3. |
Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentThe bug was two readers of one row disagreeing on its identity: the plural read keyed Implemented-by: VERDICT: PASS |
|
domain:server#5 · os-elon-musk · session_016WV4cyrFFk86Jqf7Mp5Rzw
|
Requested by Elon · project thread
Fixes #32681
Clause-②: yes (narrowing)
Metadata-protocol: none
维护者速读
action)的无包行在非限定的复数读取(pluralEntryKeys,loadMany/loadManyKeyed/DatabaseLoader的非限定读取共用)里,按(对象, 名字)数持有者:只有当名字唯一的持有包也把该名字绑在同一个对象上时,无包行才并入它(它的定制);绑在没人绑的对象上的无包行是环境自己的条目,单独保留。approve绑在account、一条无包行绑在contact时,复数读取按名字把两行并成一条、只留最后保存的那行,启动预载后注册表只剩一个;点名 ALPHA 的读取在预载后答出contact行(ALPHA 从没声明过的绑定),而冷读答 ALPHA 自己的account行。只有 BETA 绑contact、无包行在account时同理。DatabaseLoader的宿主(工作区内 0 处);库里的行不改不迁移,只改复数读取保留哪些行、预载注册哪些;同一对象上的单持有者、两个包持有同名的情形不变;回滚 = revert 本 PR。packages/metadata/src/package-slotted-store.ts的pluralEntryKeys(风险 hunk:customizes判定)与新 helperentryIdentitiesOf。pnpm --filter @objectstack/metadata exec vitest run src/loaders/database-loader.test.ts -t "counts an action name",预期 11 个用例全绿。What changes
Second half of seat ruling A on #32681 (ADR-0150 D4: an action is keyed by
(package, objectName, name)). The first half (#33318) keyedDatabaseLoader's saves and slot reads per(object, name); this one does the same for the unscoped plural read, now that #33299's keyed-entry stamping is onmain.pluralEntryKeys(packages/metadata/src/package-slotted-store.ts) is the keyMetadataManager.loadMany,MetadataManager.loadManyKeyedandDatabaseLoader's unscoped reads keep one entry per. Foraction, an entry that carries no package is now folded into the name's one holder only when that holder binds the name to the entry's own object (or both are object-less): the holders of the entry's(object, name), counted from the row'sbundle_membercolumn first, else its body's object, through a new helperentryIdentitiesOfthatpluralEntryCustomizedHoldersnow shares. Bound to an object no package binds the name to, the row is the environment's own entry and keeps a key of its own, so the boot priming pass registers it beside the holder's item and the store's(object, name)write rule files it in the bare slot.Every moved answer, FROM to TO (host wiring a
DatabaseLoader; stored rows;approvethe action name):account, a package-less row oncontactloadMany)contactkey alone (package-less saved last) / ALPHA'saccountkey alone (saved first)accountkey and thecontactkeycontactrowaccountitem, as without the passcontactalone, a package-less row onaccountcontactkey and theaccountkeyaccountrowcontactitem, as without the passaccount+ a package-less object-less row; ALPHA object-less + a package-less row onaccountThe narrowing: after the boot pass, a read naming a package no longer reaches a package-less row bound to an object that package never bound the name to.
Re-measured with #33299 merged: the fold no longer loosens a read naming BETA into ALPHA's item. In the mirror shape, a read naming BETA (which holds nothing) answers the environment's
contactrow before and after the pass, in both version orders, and never ALPHA's.Not changed: a name one package holds on one object beside a package-less row on that object (one entry, the row saved last); a name two packages hold (the package-less row keeps its own entry and
pluralEntryCustomizedHoldersnames the holder it customizes, as in #33318); the/actions/OBJECT/NAMEroute's declaration read (see Acceptance notes); types other thanaction; stored rows (no migration).Tests
packages/metadata/src/loaders/database-loader.test.ts, describe[#32681] the plural read counts an action name's holders per (object, name)…(11 cases): four moved shapes in both version orders (plural read entries, cold reads, registry keys after the realMetadataPluginboot pass, reads after the pass equal to the cold reads, no row updated); CONTROL one package on one object (both orders: one entry, the one saved last, its key); CONTROL two packages on one object (three entries, bare read refused naming both and the environment, before and after the pass).[#32791]"a package's row bound to an object and an object-less package-less row saved last" — the registry now holds ALPHA'saccountkey beside the bare slot (FROM the bare slot alone); the bare reads are unchanged.packages/runtime/src/action-package-addressing.test.ts, describe[#32681] /actions/account/approve…(4 new cases): mirror shape and single-holder cell, both save orders, over the writablesys_metadatadouble, the realMetadataPluginboot pass and the protocol: registry holds both keys, a read naming the holder answers its own item, and the package-less row's route judges that item and runs the environment's handler.Acceptance notes
/actions/account/approvein the mirror shape,/actions/contact/approvein the single-holder cell) still judges no declaration, before and after this change alike (measured both ways, the metadata fix ablated and rebuilt): its declaration read is the protocol's execution face (getMetaItemForExecution), which beside one package holder answers the environment's item on the other object (judgeBareActionReadkeeps main's answer on that face); the route then sees another object's action. That face is inpackages/metadata-protocol(area E1), where fix(metadata-protocol, metadata)!: the bare read of an action name weighs its package-less holder, and serves the one enabled holder when a disabled package registered first (ADR-0150 D3) #32551 records it as followed up by its owner seat; filed as metadata-protocol: an action's route beside a package-less row on another object judges no declaration for the holder's own object, and serves the row under the holder's envelope (ADR-0150 D4) #33456 (lease:E1). The route of the package-less row's object judges and runs that one item (pinned)._packageIdand_provenance: 'package'(the protocol's artifact envelope lookup goes by name, not(object, name)); the route's ownpackageIdis unset and the environment's handler runs. Measured in the runtime harness, also area E1, carried by metadata-protocol: an action's route beside a package-less row on another object judges no declaration for the holder's own object, and serves the row under the holder's envelope (ADR-0150 D4) #33456.(package, name), the row saved last) is unchanged here and pinned as a CONTROL.packages/metadata-protocol/src/**,packages/metadata/src/metadata-manager.tsorpackages/metadata/src/loaders/database-loader.ts.Verification
Local runs
At 8efae7e (after merging
origin/mainwith #33193):@objectstack/metadatatest 78/78 files, 2423 tests;@objectstack/objectql595/595 files, 12978 tests;@objectstack/runtime599/599 files, 10707 passed, 19 skipped; metadata and runtime typecheck exit 0; eslint on the three changed source/test files exit 0; dogfood battery 466 passed / 3 skipped files, 3944 passed / 42 skipped tests. Derived gates: 59 families, 58 exit 0, 1 NOT MEASURED (check:dual-build-cjs-loads, no full dist in the worktree; CI Build Core covers it);dispatch-gates --ranreconciles 59/59.Ablation (the
customizesline mutated back to the by-name fold throughablation-replace.mjs, metadata rebuilt,ablation-dist-preflight --absentexit 0): metadata 8 moved cases red, every CONTROL and the first half's 11 cases green; runtime 4 of 4 new cases red (registry held one key). Route answers in that state equal the fixed state's (holder's route: no declaration; package-less row's route: that item, environment's handler). Restored to the HEAD blob (git diff HEADempty), metadata rebuilt, fix marker back indist/.Generated by Claude Code