Skip to content

fix(objectql,metadata-protocol)!: a stored row bound to a package lives in that package's own registry slot, so each address serves its own row in every order (ADR-0150 D3/D4) - #33513

Queued
objectstack-fleet[bot] wants to merge 2 commits into
mainfrom
claude/area-E1-33127
Queued

objectstack-fleet[bot] wants to merge 2 commits into
mainfrom
claude/area-E1-33127

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread

Part of #33127 (its main case and the per-package overlay slot; the card stays open for its unpinned heal items: Heal 1, Heal 2 and S1)
Fixes #32900
Fixes #32663
Fixes #32662
Refs #32692

Clause-②: yes (narrowing)
Metadata-protocol: semantic
Skills-docs: none — skills/** and content/docs/** teach no reading of a name two packages each store a row of (grepped ?package=, .custom, 409: the hits are the pair's save door, a flow's secret refusal and a permission grant), so no taught sentence moves; the read naming no package keeps its 409 and its ?package= remedy.

维护者速读

What changes

SchemaRegistry kept ONE bare slot per name for a stored row (registerItem with no package), so the row saved or hydrated last took it, beside the code artifact's composite pkg:name entry. Every symptom in this set is that one slot.

The slot model (packages/objectql/src/registry.ts).

  • registerItem(type, item, keyField, packageId?, options?) takes options.storedRowPackage. Without a packageId, a row with a stored-row package registers under a package overlay slot key, MARK + package + MARK + bareKey, with MARK the NUL character written as an escape in source. It never overwrites the bare key or a composite entry.
  • bareSlotEntriesOf reads those slots after the bare entries, so every reader that judged the bare slot (overlaySlotBinding, packageScopedHoldersOf, getPackageScopedItem, boundServedActions, unregisterItem) sees each row once, bound by the same rule as before.
  • rankedForPackage orders a package's bound rows as the stored-row read serves them: its pair's declared override, its own row, the package-less row, any other.
  • orderedOverlayTier orders the overlay tier of getItem by holder (the caller's package first, then composite order, then id), so getItem no longer follows row order.
  • overlaySlotBinding: a BASE.custom row whose base holds the name only as its own stored row binds to the base, never becoming a second holder. That is the case one bare slot never held both rows of.
  • isBundleAddition(…, codeMembersOf): a bundle member that two packages hold only as stored rows is no clash. One bare slot used to hold one of the two rows, so the base held the name in either order (protocol-bundle-member-base-address.test.ts pins that answer, and it went red without this). A member either package ships in code still clashes.
  • removePackageOverlaySlots(type, name, member, select) is new and public. It drops the selected packages' slots, for the delete's heal.

Hydration (packages/metadata-protocol/src/protocol-registry-hydration.ts).

  • hydrateOverlayIntoRegistry passes storedRowPackage for a row bound to a package of a type in PACKAGE_SLOTTED_STORE_TYPES, less flow and permission, which keep their own slot rules (takesPackageOverlaySlot).
  • Such a row records no sole-holder envelope. Its slot is its package's own.
  • restoreArtifactRegistryView retires the removed package's slot. Tier 1's bare-entry drop runs only when no slot was retired, and a heal with an unknown binding drops the slots of packages with no remaining row.

Reads (packages/metadata-protocol).

Moved answers (FROM main d4fbb4d → TO), per door:

Door FROM TO
GET /meta/TYPE/NAME?package=ID, two shippers each with a stored row (tool, webhook, agent, skill), after a restart own row only for the holder hydrated last; the other its shipped item each holder its own row
getPackageScopedItem, writer and peer, after alpha then beta saved (action, tool, email_template) [null, beta] (mirror in the other order) [alpha, beta]
GET /meta/TYPE/NAME?package=ID.custom, name held only as stored rows (dashboard, tool, action), before a restart 404 for both its base's row
same, after a restart 200 for the row hydrated last, 409 naming one side for the other its base's row
GET /meta/TYPE/NAME (and /published), a package's row beside its own pair's undeclared row plain: the pair's row when it was saved first the package's own row, both orders
GET /meta/action/NAME, two packages hold it only as stored rows, after a restart 200 the row hydrated last 409 RESOURCE_CONFLICT naming both (the narrowing)
getPackageScopedItem('email_template', welcome, X, 'de-DE'), X's pair row beside a package-less de-DE row the row hydrated last X's pair row, both orders
getPackageScopedItem('action', …), both shippers with a declared override (#33131's carried face) the last-saved holder's override; the other its shipped action each holder its own override

ADR-0087: the semantic entry stored-row-own-package-slot (packages/spec/src/migrations/entries/semantic/18.stored-row-own-package-slot.ts), marker adr-0087: registered stored-row-own-package-slot in .changeset/33127-stored-row-own-package-slot.md.

Tests

Head 8d85d2b1af. Pins, each in every registration, save and row order, with CONTROLs (one holder, one row, a package-less row: unchanged). Each pin went in as a top-level describe in the file that already holds its harness.

  • packages/objectql/src/webhook-stored-row-boot-hydration.test.ts:

    • [#33127] covers tool, webhook, agent and skill: both holders ship the name, each stores a row. It checks the registry slot, the exact read, and that the read naming no package answers 409 RESOURCE_CONFLICT naming both.
    • [#32900] checks X's de-DE slot in both row orders.
  • packages/objectql/src/protocol-cluster-signal-package.test.ts, [#32663]: action, tool and email_template, writer and peer, both save orders.

  • packages/runtime/src/domains/meta-disabled-holder-served-stack.test.ts, [#33127, #32662], through HttpDispatcher on a booted kernel over a sqlite file, before and after a reboot:

    • ?package= per holder;
    • each pair address ?package=ID.custom;
    • scope note (b), plain read and /published.

    The [#32607 M1] test now also pins the base row on all three faces in both orders.

  • Moved pins:

    • protocol-flow-custom-override.test.ts [#33131]: the carried registry face moved to its TO.
    • protocol.register-item-call-sites.test.ts: the ledger records the new registerItem call shape.
    • scripts/engine-double-contract.pinned.json: ratcheted with --write.
Local runs (under `scripts/pm/os-verify-lock.sh`)
  • Targeted: objectql, 4 files, Tests 144 passed (144); runtime served-stack, Tests 36 passed (36); metadata-protocol call sites, Tests 5 passed (5).
  • Typecheck of objectql, metadata-protocol, runtime and spec: TC_EXIT=0.
  • pnpm exec turbo run test --concurrency=2 --continue over the 83 packages that depend on objectql, metadata-protocol or runtime, plus @objectstack/spec. Result: Tasks: 186 successful, 188 total. The two failed tasks:
    • @object-ui/example-schema-catalog#test printed sh: 1: vitest: not found. That example's install is missing on this box; nothing was measured (NOT MEASURED).
    • @objectstack/dogfood#test (the full battery, no shard): Test Files 2 failed | 463 passed | 3 skipped (468), Tests 2 failed | 3930 passed | 42 skipped (3974). Both failures were Test timed out in 5000ms while the box was contended: bare-name-one-holder-ai-family [#31985] and package-commit-revert-path-package [#29414] control. Re-run alone under the lock (VERDICT command-exit 0): Test Files 2 passed (2), Tests 39 passed (39).
  • The 39 packages affected only through the new spec migration entry are left to CI.
  • pnpm lint passed (LINT_EXIT=0).
Ablations: one per negative pin, each restored with a clean whole tree

Each mutation went through scripts/ablation-replace.mjs. metadata-protocol was rebuilt and scripts/ablation-dist-preflight.mjs found the marker in dist/. After the restore it was rebuilt again and the preflight with --absent answered exit 0.

  • The hydration's overlaySlot forced off (marker ABLATION_33127_SLOT):
    • objectql: 27 failed | 117 passed (144). The red tests are exactly the [#33127] boot pins (16), [#32663] (6), [#33131]'s moved pin (4) and [#32900] pair-first (1).
    • runtime: 2 failed, the two #33127 door pins.
    • All CONTROLs stayed green.
  • readMetaItem's storedRowBases off: runtime 4 failed, the four #32662 pair-address pins. objectql stayed green.
  • storedRowHoldersRefusal's one-holder arm off (marker ABLATION_33127_PLAINB): runtime 3 failed, the two #32662 (b) pins and M1.
  • isBundleAddition's row-only exemption off: objectql 1 failed, [#28802] with a dependent's member at the same locale, the base's address still serves the pair's.

Acceptance notes

Verification

🤖 Generated with Claude Code

https://claude.ai/code/session_016WV4cy1y7UNVbT2QAdmgdn

…es in that package's own registry slot (ADR-0150 D3/D4)

The registry kept one bare slot per name for a stored row, so the row
saved or hydrated last took it. Each row bound to a package (its own, or
its .custom pair's by the pair-row rule) of a type the registry reads by
package now registers in that package's overlay slot, beside the code
artifact's composite entry, which it never overwrites.

- each package's exact read and registry item answer its own row in every
  registration, save and row order, before and after a restart, on the
  writer and on its peers;
- a pair address of a name held only as stored rows answers its base's row;
- the read naming no package of a package's row beside its pair's
  undeclared row serves the package's own row in both orders;
- the read naming no package of an action two packages hold only as
  stored rows refuses 409 naming both after a restart, as every other type
  already did (the narrowing);
- a bundle member two packages hold only as stored rows is no clash, as
  when one slot held one of them.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cy1y7UNVbT2QAdmgdn
…ngine-double ledger

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cy1y7UNVbT2QAdmgdn
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 14, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/metadata-protocol, @objectstack/objectql, @objectstack/spec, touching 39 documentable anchor(s).

36 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json e40cd1e28dd79e60e7f4be0db06f071f3f962574.

⛔ 15 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: /meta/:type/:name (route, 48 pages)
  • 3 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 65 of 247 client-bound route-ledger rows — the other 182 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 182: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 59 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 123 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 175 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json e40cd1e28dd79e60e7f4be0db06f071f3f962574 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from fd04e4b7fbb4da7048b3f0ec2955ea834760171e — the merge of head 8d85d2b1afb2d6e89d6c28ee0f3aa64c9b427585 into base e40cd1e28dd79e60e7f4be0db06f071f3f962574, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin fd04e4b7fbb4da7048b3f0ec2955ea834760171e && git checkout fd04e4b7fbb4da7048b3f0ec2955ea834760171e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e40cd1e28dd79e60e7f4be0db06f071f3f962574 8d85d2b1afb2d6e89d6c28ee0f3aa64c9b427585 && git checkout -B drift-repro e40cd1e28dd79e60e7f4be0db06f071f3f962574 && git merge --no-ff 8d85d2b1afb2d6e89d6c28ee0f3aa64c9b427585

node scripts/docs-audit/affected-docs.mjs --json e40cd1e28dd79e60e7f4be0db06f071f3f962574

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs e40cd1e28dd79e60e7f4be0db06f071f3f962574 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-elon-musk · session_016WV4cy1y7UNVbT2QAdmgdn

Request: review · area E1 · head 8d85d2b · 2026-11-14T23:16Z

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract-review-claim: session_016WV4cwxS1ZpSu2hAMRcqbt · head 8d85d2b · 2026-11-14T23:16Z

review#1 (E1) takes this head.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 8d85d2b1afb2d6e89d6c28ee0f3aa64c9b427585
Local-runs: none
Scope: packages/spec/src/migrations/entries/semantic/18.stored-row-own-package-slot.ts + packages/objectql/src/registry.ts, packages/metadata-protocol/src/protocol-registry-hydration.ts, packages/metadata-protocol/src/protocol-exact-qualifier.ts, packages/metadata-protocol/src/protocol-meta-item-face.ts, packages/metadata-protocol/src/protocol-meta-item-reads.ts, packages/metadata-protocol/src/protocol.register-item-call-sites.test.ts, packages/objectql/src/protocol-cluster-signal-package.test.ts, packages/objectql/src/protocol-flow-custom-override.test.ts, packages/objectql/src/webhook-stored-row-boot-hydration.test.ts, packages/rest/src/meta-disabled-holder-served-stack.test.ts, scripts/engine-double-contract.pinned.json, .changeset/33127-stored-row-own-package-slot.md, .changeset/33127-stored-row-own-package-slot-semantic-entry.md (read, not reviewed: registry readers that walk raw keys — bareNamePackageHolders, securityCatalog*, listItems; plugin-webhooks declared-webhooks.ts slotsOf; runtime action-execution.ts listItems consumers)

Face: schema (a packages/spec/src/** hit). Diff read against merge base e0ce1b52. Cards #32900, #32663, #32662, #33127 and #33131 (with every comment) read.

① Derived judgments

  • Slot model. A stored row of a package-slotted type (PACKAGE_SLOTTED_STORE_TYPES minus flow and permission) that is bound to a package now registers under NUL pkg NUL bareKey (registerItem's storedRowPackage). A package-less row keeps the bare slot, and code artifacts keep pkg:name. Raw-key readers outside the PR were checked against the new key form. bundleBaseKey, bareNamePackageHolders, the security-catalog scans and the :name suffix scans neither match an overlay key nor count it as a holder (no : in it, never equal to a bare name), as the OVERLAY_SLOT_MARK docblock claims.
  • Readers moved with it. bareSlotEntriesOf appends the overlay slots, so getPackageScopedItem, holders and overlaySlotBinding see every stored row. getItem reads bare, then overlays (orderedOverlayTier: the caller's package first, then composite-holder order, then id), then composites. That is the same precedence the bare slot had over composites, now independent of registration order. rankedForPackage mirrors servedOverlayRowCandidates' order (declared override of the pair, own row, package-less, other bound). boundServedActions lets one overlay per (holder, name) shadow that holder's composite, as the bare slot shadows every composite. unregisterItem removes one package's overlay group before falling to composites, and removePackageOverlaySlots retires only the removed row's package slot.
  • Hydration and heal. hydrateOverlayIntoRegistry passes storedRowPackage only for the slotted types. The sole-holder envelope record is skipped for overlay-slot keys, which is correct because the bare key is untouched. The delete heal retires the bound slot and leaves the package-less bare row alone. A peer with an unknown binding retires every slot that no remaining row is bound to.
  • Protocol reads. admitExactQualifier's storedRowBases admits a pair address at the one base whose stored row holds the name. Two such bases are refused by exactQualifierItem as two registry bases are. It is asked only by the plain item read. storedRowHoldersRefusal serves the one holder whose rows are its own plus its pair's (scope note (b)).
  • Downstream consumers. listItems(type) now yields one row per bound package where it used to yield the last one. plugin-webhooks resolves slots through getPackageScopedItem / packageScopedHoldersOf (slotsOf), and runtime's listItems('action') consumers filter by _packageId stamps that the rows already wore. Neither keys on the bare slot, so neither double-counts.
  • Pins. The #33131 S1 "(carried)" pin is flipped: each holder's registry item is now its own override, in 2 × 2 orders. Webhook, tool, agent and skill boot cover 2 holders in every registration and row order, with CONTROLs (one row, one holder). metadata-protocol: the registry's bare slot keeps only the row hydrated last when a pair row and a package-less row of the same member are both stored #32900 covers both row orders plus CONTROLs. There is also a cluster-signal peer pin and REST served-stack pins. engine-double-contract.pinned.json goes 2 → 4, which grows the pinned population and loosens nothing.
  • CI on this head at review time: 23 success, 13 skipped, 31 in progress. PR-body guards had FAILED, which the author attributes to the folded cards' claim comments and has re-patched. Landing needs every check green on the landing head.

② Semver level

Clause-②: yes (narrowing) is right. The diff widens @objectstack/objectql's exported SchemaRegistry (an optional fifth registerItem argument and the public removePackageOverlaySlots) and narrows one answer: an action two packages hold only as stored rows, read with no package after a restart, goes from 200 (the last hydrated) to 409 naming both, matching #32607. A diff that widens and narrows takes yes (narrowing) (AGENTS.md Post-Task Checklist step 3). Metadata-protocol: semantic is right, because which stored row each package's address serves changes and no key moves. @objectstack/objectql and @objectstack/metadata-protocol are minor, @objectstack/spec is patch (none), and adr-0087: registered stored-row-own-package-slot.

③ Boundary flags

  • Not governed (no Tier H path); 737 lines, under the 3000-line bound. The PR is authored by objectstack-fleet[bot] on the relay.
  • N1 (nit, performance): for a non-bundle type, getItem (bare miss) and bareSlotEntriesOf now walk the whole collection (overlaySlotEntriesOf) where they used to answer from one Map.get. The callers are metadata reads, not per-record paths, so this is bounded. A per-type count of overlay keys, skipping the walk at zero (the common case), would restore O(1).
  • N2 (nit): getItem(name) with no package and two packages' overlay rows now picks by composite-holder order instead of save order. Internal callers that use it as a baseline (protocol-save.ts baseline, the authoring gate's declared body) get a deterministic but still arbitrary pick. That is no worse than before, since the doors themselves refuse 409, but a short comment at those two callers would keep a later reader from relying on it.
  • N3 (nit): boundServedActions splits its group key on the last :. Safe for action names today; a tuple key would not depend on it.

④ Design judgment

18.stored-row-own-package-slot.ts holds against ADR-0150 D3/D4 ("registry overlay slot (ADR-0005)": a stored row bound to a package is that package's item) and red line 2. Its surface, replacement and acceptanceCriteria name every moved read, including the peer replica and the pair address, and give the operator's remedy (?package=ID). The registry change agrees with it: the one-bare-slot model was the root of every order-dependence named in #33127, #32663, #32662, #32900 and the #33131 carried face, and each now has a pin in both orders. The exclusions (flow, which keeps #28717's declared-override slot rule, and permission, which keeps its fork ruling) are reasoned in takesPackageOverlaySlot. No must-fix or should-fix; N1–N3 are nits. This is the final round.

#33131 (asked by the author): this PR closes that card's remaining carried face (comment 6870666736, the both-overridden registry item), and the flipped pin proves it. Two items are still open on the card (comment 6878816309): nit N1 of record 6878787461 (unmeasured), and the doc bare-read registration-order pick, which is out of this PR's types. So this PR alone does not finish #33131. Keep Refs #33131, or file one loose-ends card carrying those two items, link it on #33131, and then write Fixes #33131.

Implemented-by: claude/area-E1-33127
Reviewed-by: session_016WV4cwxS1ZpSu2hAMRcqbt

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-elon-musk · session_016WV4cy1y7UNVbT2QAdmgdn

ACCEPT · area E1 · 2026-11-14T23:22Z

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 14, 2026 23:23
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 14, 2026
Any commits made after this event will not be merged.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment