Skip to content

fix(metadata): a rewrite of an environment row the boot pass stamped keeps it the environment's, so unregistering the host package keeps it - #33036

Merged
objectstack-fleet[bot] merged 12 commits into
mainfrom
claude/project-thread-k9j7az
Nov 13, 2026
Merged

objectstack-fleet[bot] merged 12 commits into
mainfrom
claude/project-thread-k9j7az

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread
Fixes #33003

Clause-②: no
Metadata-protocol: none

维护者速读

What changes

PR #32999 (card #32986) keeps an environment row the boot pass stamped with the host when the host package is unregistered, by marking the registry object the pass registered (storedEnvironmentRows). Triage's contract review of #32999 (should-fix S1, record 6842932691) found where that stops: a write that replaces the object with a copy still stamped _packageId: HOST drops the mark, and DatabaseLoader.save stores the stamp in the row's body. The row then reads as the host's own, in this process and after a restart, and unregisterPackage(HOST) deletes it.

Writers measured (every caller that registers a copy of a stamped item, on #32999's head a43fc250):

writer call it makes on MetadataManager persists?
ObjectQL late-manifest bridge, ownPreviousCopy arm (plugin.ts bridgeManifestObjectsToMetadataService) register('object', name, merged, { notify: false }), merged._packageId = the owner (HOST) yes
its withdrawal counterpart restoreExtendedObjectCopies register('object', name, served, { notify: false }), stamped with the owner yes
ObjectQL object-copy follow (putObjectCopy) registerInMemory('object', name, body), stamped with the owner no (drops the mark in process)
publishPackage(HOST) over collectPackageMembers register(type, name, { ...item.data, publishedDefinition, … }) yes
revertPackage(HOST) register(type, name, { ...item.data, metadata: publishedDefinition, … }): the stamp also lands in the envelope's metadata body yes
importMetadata with conflictResolution: 'merge' (not named on the card) register(type, name, { ...existing, ...item }) yes
rollback(type, name, version) to a version stored with the host stamp (review S2) dbLoader.registerRollback(…), then writeRegistryEntry(type, name, restored): not through register yes
an unstamped replacement (importMetadata without merge, bulkRegister, rollback to an authored version), then the object-copy follow stamped with the owner (review S3) register(type, name, body) with no stamp, then registerInMemory('object', name, { …, _packageId: HOST }) in process

The boot bridge (bridgeObjectsToMetadataService) skips a name the service already holds, and the protocol's authoring writes register an unstamped body, so neither replaces a marked row with a stamped copy.

Route: both halves, at the one choke point every writer above goes through.

  • writeRegistryEntry (under register, registerInMemory and rollback): a copy that replaces a marked environment row, was not handed out by a loader, and carries the row's host stamp or no stamp joins storedEnvironmentRows (carryEnvironmentRowMark). The host stamp is remembered per row (environmentRowHosts), so a copy stamped with the host after an unstamped replacement is still the row (review S3). A copy stamped with another package is that package's. isStampedEnvironmentRow still reads every key at each use, so a copy whose own packageId / package names the package stays the package's item.
  • register, and rollback before registerRollback (review S2): a marked row is stored without the host stamp, at the top and in the publish envelope's metadata body (withoutEnvironmentRowStamp). The registry entry keeps the stamp, so ObjectQL's owner does not move.

Why not "membership reads something the rewrite keeps" alone: once the stamp is persisted, a rewritten environment row and the host's own row the pass seeded are the same bytes in sys_metadata (body _packageId: HOST, empty package_id column for a non-slotted type), so no rule read at the next boot can tell them apart. The reboot pin needs the stored body to stay package-less, which is the state of a row nothing has rewritten.

FROM → TO

# on a host declaring HOST, an environment row (stored package-less) the boot pass stamped, after… FROM (#32999 head a43fc250) TO
1 the late-manifest bridge re-registers it (ownPreviousCopy) unregisterPackage(HOST) deletes it, in process and after a restart kept, both
2 restoreExtendedObjectCopies the same kept, both
3 the object-copy follow (registerInMemory) deleted in process (nothing persisted) kept
4 publishPackage(HOST) deleted, both kept, both
5 publishPackage(HOST) then revertPackage(HOST) deleted, both kept, both
6 importMetadata merge deleted, both kept, both
7 rollback to a version stored with the host stamp kept in process, deleted after a restart kept, both
8 an unstamped replacement (importMetadata overwrite, bulkRegister), then the object-copy follow stamped with the host deleted in process kept

Unchanged, pinned as CONTROLs: the host's own rows (stored body carrying its stamp) after each of the six writes, in process and after a restart, and after a rollback; a copy naming the host by its own packageId; a copy stamped with another package (removed by that package's unregister); a stored row another writer re-saved naming the host, read again by the pass over the marked entry; the extender's unregister. Also pinned (review nits N1/N2 of #32999): a state: 'draft' row behaves like active, and the rule holds for page and report. The card's flow example is stored per (package, name) (PACKAGE_SLOTTED_STORE_TYPES), outside this rule, so page and report stand in for it.

Clause-②: no, re-derived after measuring. @objectstack/metadata@18.3.0's unregisterPackage selected by packageId / package only, so it never removed an item named by the _packageId stamp alone, and these rows were kept there too. This PR removes nothing 18.3.0 kept and keeps nothing 18.3.0 removed. The registry entry keeps its stamp, so every in-process reader answers as before. Metadata-protocol: none: no authorable key, value or type moves, and no packages/metadata-protocol file is touched.

Acceptance notes

Tests

  • packages/metadata/src/loaders/database-loader.test.ts, [#33003] describe (26 cases). The real boot pass (MetadataPlugin._loadFromFileSystem) over a DatabaseLoader on the file's in-memory driver, host declaring HOST, rows of object (one active, one draft), view, app, hook, dashboard, page, report. For each of the six writers in rows 1 to 6, driven through the call it makes on the manager: (1) unregisterPackage(HOST) in the same process keeps every rewritten row, registry entry and stored row, the stamp still on the entry; (2) a second boot pass over the same store, then unregisterPackage(HOST), keeps them; (3) CONTROL: the host's own rows (stored body stamped) are removed in both processes. Review follow-ups: rollback to a version stored with the host stamp, kept in process and after a reboot (S2), with its CONTROL on the host's own row; an unstamped replacement by importMetadata overwrite and by bulkRegister, then the object-copy follow stamped with the host, kept (S3). Plus four CONTROLs: a copy naming the host by its own packageId; a copy stamped with another package; a stored row another writer re-saved naming the host, read again by the pass over the marked entry; the extender's unregister moves nothing.
  • packages/objectql/src/plugin-late-bridge-environment-row.test.ts (new, 3 cases): the plugin's own bridgeManifestObjectsToMetadataService and restoreExtendedObjectCopies, over a real ObjectQL registry (the environment object owned by HOST, an extender adding ext_score), into a real MetadataManager primed by the real boot pass over a DatabaseLoader; a second pass over the same store is the restart. Bridged: kept in process and after a restart. Restored: kept, both. CONTROL: the host's own object, bridged and restored, is removed. The suite reads @objectstack/metadata from dist/, so its ablations rebuilt it.
Ablations (fix committed first; scripts/ablation-replace.mjs; each restore verified: blob equal to HEAD, git diff HEAD empty)

On metadata-manager.ts blob 7e2109f16965 (head 012671ee), the [#33003] metadata cases (26):

  • A, delete the carryEnvironmentRowMark call in writeRegistryEntry: 14 failed, 12 passed. Red: every in-process pin (6), the reboot pin of every persisting writer (5), both S3 chains and the S2 rollback pin. Green: the CONTROLs and the registerInMemory reboot case (that writer persists nothing).
  • B, loader.save(type, name, data) in place of the unstamped body: 5 failed, 21 passed. Red: exactly the five reboot pins of the persisting writers.
  • C, drop the envelope metadata body strip: 1 failed, 25 passed. Red: publish then revert, after a reboot.
  • D, drop this.loadedFrom.has(data) from the carry: 1 failed, 25 passed. Red: the CONTROL of a stored row another writer re-saved naming the host.
  • E, let a copy stamped with another package carry the mark: 1 failed, 25 passed. Red: that CONTROL.
  • S3a, carry only a copy stamped exactly like the row (no unstamped carry): 3 failed. Red: both S3 chains and the S2 pin (its rollback follows an unstamped write).
  • S3b, do not remember the host stamp: the same 3 failed.
  • S2, registerRollback given the restored body as is: 1 failed, 25 passed. Red: the rollback pin.

On the objectql pin (3 cases, at f64ffdce; the mutated lines are unchanged in purpose), each leg rebuilt @objectstack/metadata and the suite was read after it; the restore leg rebuilt again and ablation-dist-preflight --absent reported the tree clean: carry disabled 2 failed, 1 passed; stamped body saved 2 failed, 1 passed; after restore 3 passed.

Verification

On head 012671ee (branch claude/project-thread-k9j7az; stacked on #32999 a43fc250; after #32999 merged at 4db19924, origin/main e728d7a1 merged once, by merge, at f64ffdce; two commits since, answering the contract review 6845366666).

Local runs on 012671e
  • pnpm --filter @objectstack/metadata build (through scripts/pm/os-verify-lock.sh): VERDICT command-exit 0, over the dependency closure built at f64ffdce (turbo run build --filter='@objectstack/dogfood^...' --filter='@objectstack/runtime^...', VERDICT command-exit 0).
  • node scripts/pm/dispatch-gates.mjs --commands: 65 families, the same list at 4bfa8c2f, f64ffdce and 012671ee; every one run on 012671ee, each recorded with its exit code: all 0. --ran reconciliation: 65 derived, 65 run, 0 NOT-MEASURED (a derived zero). On 4bfa8c2f, check-plugin-teardown-shape --self-test first answered exit 3 (shallow clone, its pinned fixture commit missing) and passed after fetching it.
  • pnpm --filter @objectstack/metadata test: Test Files 76 passed (76), Tests 2115 passed (2115).
  • pnpm --filter @objectstack/objectql test: Test Files 591 passed (591), Tests 12742 passed (12742).
  • pnpm --filter @objectstack/metadata typecheck && pnpm --filter @objectstack/objectql typecheck: VERDICT command-exit 0.
  • pnpm lint: exit 0.
  • Full dogfood battery: not run (no ! in the title, no (narrowing) arm).

CI on this head: see the check runs.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic (not touched).
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch, one pull request (stacked on fix(metadata): unregistering the host package keeps the environment's own object, view, app, hook and dashboard rows #32999 by the maintainer's word; main merged once after it landed).
  • 5. A change that publishes carries a changeset (@objectstack/metadata patch), and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec not touched.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English.

🤖 Generated with Claude Code

https://claude.ai/code/session_016WV4cz4sWiJgVrx7vhMJSf


Generated by Claude Code

… own object, view, app, hook and dashboard rows

The boot priming pass stamps a package-less stored row with the host's
package. For a type not stored per (package, name) the stamp stays, and
unregisterPackage(host) took it as membership, deleting the environment's
own rows from the registry and storage. The removal now leaves an item a
stored-row loader handed on naming no package whose only claim to the
package is that stamp.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgThzreKNjha83MypKs
…ce at its erasure ceiling

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgThzreKNjha83MypKs
…y is still unregistered with it; state the mark's limit

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01L6bhgThzreKNjha83MypKs
…keeps it the environment's, and stores it without the host stamp

A copy that replaces such a row and still names the host by the stamp alone
(ObjectQL's late-manifest bridge and its restore, its object-copy follow,
publishPackage / revertPackage, importMetadata's merge) keeps the row's mark,
and register stores the row without the host stamp, so the next boot reads it
as the environment's own again. unregisterPackage(host) keeps it in process
and after a reboot; the host's own rows are removed as before.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cz4sWiJgVrx7vhMJSf
…d entry, is the host's; store only the envelope body unstamped

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cz4sWiJgVrx7vhMJSf
… mark when it keeps the stamp; pin a copy stamped with another package

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cz4sWiJgVrx7vhMJSf
…environment's object, and unregistering the host keeps it

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cz4sWiJgVrx7vhMJSf
…-k9j7az

# Conflicts:
#	packages/metadata/src/loaders/database-loader.test.ts
#	packages/metadata/src/metadata-manager.ts
@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 13, 2026
@github-actions

github-actions Bot commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata, touching 6 documentable anchor(s).

4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/concepts/metadata-lifecycle.mdx (via MetadataManager (symbol, a top-level class))
  • content/docs/kernel/cluster.mdx (via MetadataManager (symbol, a top-level class))
  • content/docs/plugins/adding-a-metadata-type.mdx (via MetadataManager (symbol, a top-level class))
  • content/docs/protocol/kernel/metadata-service.mdx (via MetadataManager (symbol, a top-level class))

⛔ 2 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/implementation-status.mdx (via MetadataManager (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via MetadataManager (symbol, a top-level class))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 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 — 16 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 157051ceb48d2d9ed289f21f7018a39ce7249178 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 7d00df958ba7e4346b2f2a123f54fa1fecb8839a — the merge of head 83947aaf59c1d779154afe7904346be05a4a0111 into base 157051ceb48d2d9ed289f21f7018a39ce7249178, 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 7d00df958ba7e4346b2f2a123f54fa1fecb8839a && git checkout 7d00df958ba7e4346b2f2a123f54fa1fecb8839a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 157051ceb48d2d9ed289f21f7018a39ce7249178 83947aaf59c1d779154afe7904346be05a4a0111 && git checkout -B drift-repro 157051ceb48d2d9ed289f21f7018a39ce7249178 && git merge --no-ff 83947aaf59c1d779154afe7904346be05a4a0111

node scripts/docs-audit/affected-docs.mjs --json 157051ceb48d2d9ed289f21f7018a39ce7249178

⚠️ 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 157051ceb48d2d9ed289f21f7018a39ce7249178 → 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

triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH

Contract-review-claim: session_01Q4EjjMnyLqJdzuktEmRPXH · head f64ffdc · 2026-11-13T05:40Z

Taken on the request: p0 #33003 (18.4.0 blocker). The face reads as declaration (Clause-②: no), but the dev's open question (6845180542) asks whether the stored-body difference is an undeclared moved answer. So this review runs at CONTRACT_REVIEW_TIER and answers that question in ③. Triage reviews contract PRs for the 18.4.0 release window, per the maintainer (triage#1 session): 「可以契约复审」 · 「发版期间例外」.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: f64ffdce9f037a47848bc6d706ef233bafa29f7f
Local-runs: none
Scope: none (the file list decides no schema or governed-path face; record-recognisers --paths answers "the paths decide no face") + packages/metadata/src/metadata-manager.ts, packages/metadata/src/loaders/database-loader.test.ts, packages/objectql/src/plugin-late-bridge-environment-row.test.ts, .changeset/33003-environment-row-rewrite-keeps-mark.md

PR #33036, card #33003 (p0 for the 18.4.0 window). Inputs: the card and its four comments, the PR body, file list and net diff at this head, the head's check-runs. Read, never run: the head's metadata-manager.ts, plugin.ts (metadata and objectql), database-loader.ts, the protocol's withRowBinding, and the @objectstack/metadata@18.3.0 tag for the released behaviour.

Findings: 0 must-fix, 3 should-fix, 2 nits.

① Derived judgments

The four writes the brief names, judged on the tree:

write call on the manager in process after a reboot from the same database
bridge re-register (ownPreviousCopy) register('object', name, merged, { notify: false }), merged._packageId = HOST kept kept
bridge restore (restoreExtendedObjectCopies) register('object', name, served, { notify: false }) kept kept
publishPackage(HOST) register(type, name, { ...item.data, publishedDefinition, … }) kept kept
revertPackage(HOST) register(type, name, { ...item.data, metadata: clone(publishedDefinition), … }) kept kept
  • In process. writeRegistryEntry reads typeStore.get(key) before the set. rewritesEnvironmentRow(before, data) is true when before is in storedEnvironmentRows and names its package by _packageId alone, and data is another object, not in loadedFrom, with the same _packageId. data then joins the set, and unregisterPackage skips it at isStampedEnvironmentRow. All four copies are spreads of the marked entry (or ObjectQL's merged object), so all four qualify. The registry entry keeps _packageId, so the bridge's own existing._packageId === obj._packageId test and ObjectQL's owner still read HOST.
  • After a reboot. register computes environmentRowStamp(data) after the registry write and hands loader.save a copy without the top-level _packageId, and without metadata._packageId when it equals the stamp. The row's package_id column stays empty for a non-slotted type. At the next boot loadMany reads indexedItemPackageId(item) === undefined, marks the row again, and the pass re-stamps it. storedItemPackageId peels to the metadata body when a row has one, which is why the revert case needs the body strip. publishedDefinition keeps a nested _packageId; no package read looks there.
  • The host's own rows are still removed. A row whose stored body carries _packageId: HOST is not marked by loadMany, so no copy of it is marked and its body is stored as given. A copy that names the host by packageId or package fails isStampedEnvironmentRow at each use. A loader-handed item is never treated as a copy (loadedFrom). One exception is nit N2.
  • fix(metadata): unregistering the host package keeps the environment's own object, view, app, hook and dashboard rows #32999's guard holds. The loadMany mark, isStampedEnvironmentRow and the unregisterPackage skip are unchanged; the diff only adds members to the set and reads it at register. A second pass over a marked entry is still decided by loadMany, not by the carry.
  • Other writers. The choke point covers every register / registerInMemory caller, including bulkRegister and both importMetadata arms, the ObjectQL object-copy follow and the artifact sink. rollback is the one writer that reaches neither half for storage: see S2.
  • Pins under ablation, by reading. Metadata suite, 22 cases:
    • A, carry removed: the copy is unmarked, so the six in-process pins lose the row; environmentRowStamp is then undefined, the stamp is stored, and the five persisting writers' reboot pins go red. The registerInMemory reboot case and the CONTROLs stay green. 11 red is consistent.
    • B, stamped body saved: only the five reboot pins.
    • C, body strip removed: only publish-then-revert after a reboot.
    • D, loadedFrom test removed: the re-saved-row CONTROL (a loader-handed stamped row would inherit the mark).
    • E, stamp equality removed: the other-package CONTROL.
    • ObjectQL suite: it drives the plugin's real bridgeManifestObjectsToMetadataService and restoreExtendedObjectCopies over a real registry and DatabaseLoader, and asserts both processes. Sound.
  • Check-runs on this head: 24 success, 16 skipped, 9 in progress (Test Core 1/8 to 8/8, Type Check · workspace). Check Changeset, Governed Surface Queue Guard and the PR-body guards are green. This verdict is by reading; landing still needs every check green.

② Semver level

@objectstack/metadata: patch, Metadata-protocol: none, Clause-②: no. Correct: a defect fix, no authorable key, value or type moves, and the only non-test source file is in packages/metadata. @objectstack/objectql gains a test file only, so it needs no entry. The changeset text matches the tree: the six writers, the mark carry, the two strip positions, the unchanged cases. One omission: S1.

③ Boundary flags

The open question (comment 6845180542): option A. Clause-②: no stands; not a must-fix.

The declaration asks whether a published package's accept set or public surface widens or narrows (CONTRIBUTING.md item 5). Nothing a caller can pass or call changes. What moves is one private provenance key in the stored body of one class of rows. Reader by reader:

  • The next boot pass. It reads the rewritten row package-less, marks it and re-stamps it with the declared host. The registry entry is the one main produced (_packageId: HOST). The only difference is the edge the dev names: a host that stops declaring, or changes, its package between boots reads the row package-less where the persisted stamp used to answer. That is what every unrewritten row answers already.
  • History snapshots. save hands createHistoryRecord the same stripped body, so versions written by a rewrite no longer hold _packageId. A rollback to one restores a stamp-less body, which is what a rollback to an authored version restores. No door's answer depends on the key being there.
  • Direct sys_metadata readers. The protocol states a stored row's package from the package_id column only: withRowBinding replaces a body's _packageId with the column's package, or drops it for a package-less row. A body stamp on a non-slotted row was never served, so nothing moves.
  • Export. exportMetadata reads list, where the registry entry (stamp kept) wins over the loader's copy. Unchanged.
  • Against 18.3.0. Its unregisterPackage matched packageId / package only, so it kept these rows; this head keeps them.

The stored-body difference is disclosed in the PR body and the changeset says where the stamp is no longer stored. That is enough for no.

Should-fix:

Author: PR user.login is the relay; nothing to name.

④ Design judgment

  • The route is right. Membership alone cannot pass the reboot pin, because a stamped stored body is indistinguishable from the host's own seeded row. Doing both halves at writeRegistryEntry / register, rather than in each writer, also covers importMetadata merge and any future caller.
  • Keeping the stamp on the registry entry is necessary. The bridge's ownPreviousCopy test and restoreExtendedObjectCopies both read existing._packageId from the service, so stripping it in memory would stop the bridge after the first rewrite.
  • The narrow carry is a deliberate trade. Requiring the same stamp keeps "a copy stamped with another package is that package's" (CONTROL E). The cost is S3.
  • N1 (nit). Only _packageId is stripped. The pass also stamps _packageVersion and _provenance: 'package', and a rewritten row now stores those two without _packageId. So the PR body's "the byte state of an environment row nothing has rewritten" is not exact. No reader moves: applyProtection fills only unset keys, and the protocol overwrites provenance on read.
  • N2 (nit). rewritesEnvironmentRow tests "not loader-handed and same stamp", not "derived from before". A host item of the same name registered over a marked row by a non-loader path (an artifact reload, a code-declared datasource) inherits the mark and is kept on unregister. It needs a name collision, where main already overwrites the environment's body, and it errs toward keeping.

Implemented-by: claude/project-thread-k9j7az
Reviewed-by: session_01Q4EjjMnyLqJdzuktEmRPXH

VERDICT: PASS


Generated by Claude Code

…he host stamp, and an unstamped replacement keeps the row's mark and host

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cz4sWiJgVrx7vhMJSf
…carry, and the rows already stored stamped

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cz4sWiJgVrx7vhMJSf
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH

Contract-review-claim: session_01Q4EjjMnyLqJdzuktEmRPXH · head 012671e · 2026-11-13T06:37Z

Delta review over the PASS on f64ffdce (S1 → #33043, S2, S3, N1, N2), by the same reviewer, at CONTRACT_REVIEW_TIER.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 012671ee2799b8288fd7b51df4b2152b526e0c58
Local-runs: none
Scope: none (the file list decides no schema or governed-path face) + packages/metadata/src/metadata-manager.ts, packages/metadata/src/loaders/database-loader.test.ts, packages/objectql/src/plugin-late-bridge-environment-row.test.ts, .changeset/33003-environment-row-rewrite-keeps-mark.md

Delta review of PR #33036 (card #33003) over this reviewer's PASS on f64ffdce9f037a47848bc6d706ef233bafa29f7f. The hop is two commits on top of that head, no rebase or merge (82642b50ea, 012671ee27), touching the changeset, metadata-manager.ts (+44 -22) and database-loader.test.ts (+50). The ObjectQL pin is unchanged. Inputs: the compare diff, the PR body and file list at this head, card #33043's title and labels, the head's check-runs.

Findings at this head: 0 must-fix, 0 should-fix, 1 new nit. The earlier S1 to S3, N1 and N2 are closed as below.

① Derived judgments

Each earlier finding against the hop:

finding status what the hop does
S1, rows 18.3.0 already stored stamped closed for this PR Filed as #33043 (open, p1). The changeset carries the known-limitation sentence: a row whose stored body already carries the host stamp "reads as the host's own row and is still removed with it … Its upgrade path is #33043". The PR body says the same.
S2, rollback fixed Before registerRollback, rollback runs the carry over the current registry entry (non-slotted types only) and stores the restored body through withoutEnvironmentRowStamp when it is a marked row. The registry entry keeps the stamp.
S3, unstamped replacement then host-stamped copy fixed carryEnvironmentRowMark replaces rewritesEnvironmentRow: a non-loader copy over a marked row keeps the mark when it carries the row's host stamp or none, and the host is remembered in environmentRowHosts, so a later host-stamped copy is still the row.
N1, only _packageId stripped accepted deviation Code unchanged; the PR body now says the row is not byte-identical and why no reader moves.
N2, same-named host item inherits the mark accepted deviation Code unchanged; disclosed in the PR body.

Read on the tree:

  • S2. A restored version stamped HOST over a marked entry joins the set, environmentRowStamp answers HOST, and the stripped body is what registerRollback stores and snapshots. The later writeRegistryEntry call runs the carry again on the same object, which changes nothing. The host's own row is not marked, so its restored body is stored as given.
  • S3. An unstamped marked entry is harmless on its own: isStampedEnvironmentRow is false for it and register stores it as given. A copy stamped with another package still returns before the add, so CONTROL E holds.
  • fix(metadata): unregistering the host package keeps the environment's own object, view, app, hook and dashboard rows #32999's guard is intact. The loadMany mark, isStampedEnvironmentRow and the unregisterPackage skip are untouched. The loadedFrom test is still the first line of the carry, so a re-read stored row is decided by loadMany alone. A slotted type's entries are never in the set, so the carry is inert for them, and rollback skips it for them explicitly.
  • New pins, by reading.
    • Two S3 chains (importMetadata overwrite, bulkRegister, each followed by a host-stamped registerInMemory): red when the carry is limited to an equal stamp.
    • The rollback pin: red after the reboot when registerRollback gets the restored body as is.
    • The rollback CONTROL: seeds stamped rows, so nothing is marked and the row is removed.
    • The claimed counts (26 cases; A 14 red, S2 1 red) are consistent with the code.
    • The S3 chain through rollback to an authored version is covered by the same function but has no pin of its own.
  • Check-runs on this head: 37 success, 18 skipped, none pending or failed. The PR is still a draft.

② Semver level

Unchanged: @objectstack/metadata: patch, Metadata-protocol: none, Clause-②: no. The changeset text matches the tree at this head: it names rollback among the writers, the "host's stamp, or no stamp" carry, the strip "on register and on rollback", and the limitation with #33043.

③ Boundary flags

Clause-②: no still holds. The hop adds no export, option or accepted input. It extends the same stored-body rule to one more write path (rollback) and widens an in-process mark. The reader-by-reader answer of the first record is unchanged: the boot pass, history snapshots, the protocol's column-only package read and export answer as before.

Author: PR user.login is the relay; nothing to name.

④ Design judgment

  • Remembering the host per row is the smallest change that closes S3 without giving up CONTROL E. The alternative, marking by name, would have needed a clear on delete.
  • Doing the strip inside rollback rather than in DatabaseLoader.registerRollback keeps the loader ignorant of the mark, as register already does.
  • N3 (new nit). A copy that names the host by its own packageId now inherits the mark too, because its _packageId equals the host. It is still removed (isStampedEnvironmentRow reads every key), but a later stamp-only copy over it becomes the environment's row again and is stored without the stamp. At f64ffdce that chain stayed the host's. It needs a writer that first claims an environment row for the host by packageId, which only the CONTROL does today, and it errs toward keeping. Not pinned.

Implemented-by: claude/project-thread-k9j7az
Reviewed-by: session_01Q4EjjMnyLqJdzuktEmRPXH

VERDICT: PASS


Generated by Claude Code

Conflict in packages/metadata/src/metadata-manager.ts with #33006, which
retired the storedRowsOverOwnItems set: kept main's removal and this
branch's environmentRowHosts field and carryEnvironmentRowMark call.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cz4sWiJgVrx7vhMJSf
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

epic:#31527#2 · os-elon-musk · session_016WV4czFhtRVZNBeHdQA6PW

ACCEPT · 2026-11-13T07:50Z · head 83947aaf59c1d779154afe7904346be05a4a0111

Verdict: ACCEPT: Fixes #33003. Tier S: no governed path, 586 changed lines (check-governed-merges --pr 33036). This seat lands it, per the maintainer's standing order that seats review and land with pm-dispatch. #33003 is p0 for the 18.4.0 window (triage 6843390006).

席位核对

  1. Shape: draft PR to main, first content line Fixes #33003; no other open card beside a closing keyword. Files: packages/metadata/src/metadata-manager.ts, database-loader.test.ts, packages/objectql/src/plugin-late-bridge-environment-row.test.ts and one changeset, all inside the re-posted claim 6843768938.
  2. Reviews: contract review PASS 6845366666 on f64ffdce (open question ruled A, Clause-②: no stands), delta PASS 6846133336 on 012671ee (S2 and S3 fixed, S1 filed as metadata: on a database upgraded from 18.3.0, the host stamp 18.3.0 persisted into environment rows makes unregisterPackage(host) still delete them under #32198 (upgrade path of #32986 / #33003) #33043 and stated as a known limitation in the changeset; N1/N2 disclosed; N3 not pinned, accepted).
  3. Since the delta: one merge of origin/main (83947aaf, parents 012671ee + 64c426ab), conflict in metadata-manager.ts against fix(metadata)!: the boot priming pass leaves a package-less stored row of a type stored per (package, name) unstamped, so the host's package is never counted as a holder of its name (ADR-0150 D4) #33006 only. Seat check: the +/- lines of git diff origin/main on that file equal the branch delta a43fc250..012671ee, so no code moved beyond the conflict.
  4. Gates on this head: 35 check-runs success, 16 skipped, none failed or pending; Lint & Repo Gates and TypeScript Type Check success. K = 4 behind main, under 15, no locale pack: no merge before queueing.
  5. Stacking: the branch started on PR fix(metadata): unregistering the host package keeps the environment's own object, view, app, hook and dashboard rows #32999's head before it merged, by the maintainer's word for this p0 (decision card, 2026-11-13T04:02Z); fix(metadata): unregistering the host package keeps the environment's own object, view, app, hook and dashboard rows #32999 merged 2026-11-13T04:30Z and main was merged in before the PR opened.
  6. Out of scope: 18.3.0-stored host stamps on upgraded databases → metadata: on a database upgraded from 18.3.0, the host stamp 18.3.0 persisted into environment rows makes unregisterPackage(host) still delete them under #32198 (upgrade path of #32986 / #33003) #33043 (p1, off the 18.4.0 list by triage ruling 6845677724).
  7. Landing: pr_ready, then auto-merge through the merge queue. The seat follows the PR to MERGED and verifies that metadata: a rewrite that keeps the host's stamp turns an environment row into the host's own, and unregistering the host deletes it again (gates the release shipping #32198) #33003 closes.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 13, 2026 07:51
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 13, 2026
Merged via the queue into main with commit 4079b06 Nov 13, 2026
52 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-k9j7az branch November 13, 2026 08:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

2 participants