Skip to content

fix(metadata-protocol): a .custom pair's published override of a dashboard, report or email template no longer wears the base's envelope in the registry's bare slot - #29640

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-39ijj8
Nov 3, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-39ijj8

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steve · project thread

Fixes #29600
Clause-②: no
Metadata-protocol: none

维护者速读

What changes

A published override of a base's dashboard, report or email template, saved in the base's .custom pair, was registered in the registry's bare slot wearing the BASE's artifact envelope (_packageId = BASE, _provenance: 'package'). The base's per-package layer, which the base's export reads, then listed the override beside the base's own item, and every registry-direct read of the base's item answered the override.

The hydrator (hydrateOverlayIntoRegistry) now declines the bare slot for a row of these three types when the row's bound package ships no artifact of the name while another package does. This is the #22057 view rule, widened per seat ruling A to dashboard, report and email_template. The rule lives in declinesBorrowedEnvelope in protocol-package-bindings.ts, a closed list of three types.

Files (5, +424 / −17):

  • packages/metadata-protocol/src/protocol-package-bindings.ts: declinesBorrowedEnvelope and its type list.
  • packages/metadata-protocol/src/protocol.ts: one guard line in the hydrator, plus the header and heal docblocks.
  • packages/objectql/src/protocol-custom-override-bare-slot-envelope.test.ts (NEW): the per-type pins. It sits in objectql because the pins need the real ObjectQL engine, the in-memory driver and the real registry, and a metadata-protocol test cannot import objectql (that would be a dependency cycle). It reads metadata-protocol from dist/, like the sibling protocol-per-package-layer-provenance.test.ts whose harness it copies. No objectql source is edited.
  • packages/objectql/src/environment-overlay-email-locale-restart.test.ts: forced edits, listed below.
  • .changeset/29600-pair-override-bare-slot-envelope.md: patch for @objectstack/metadata-protocol.

Tests

New file packages/objectql/src/protocol-custom-override-bare-slot-envelope.test.ts, 9 tests. Per type (view, dashboard, report, email_template) two tests:

  • (a) The base's per-package layer lists the base's item alone, and the pair's lists the override alone. The bare slot holds no entry for the name (name@en-US for the email template). registry.getItem answers the base's own item.
  • (b) The served reads still serve the override: getMetaItem with no package, BASE and PAIR; getMetaItems with no package and BASE; getMetaItemLayered(BASE).effective.

A control pins #4624: OTHER also ships the dashboard and owns a stored row, so its bare entry stays with OTHER's envelope and _provenance: 'package'.

Forced edits in packages/objectql/src/environment-overlay-email-locale-restart.test.ts (seat-approved, on condition that no non-test email_template registry reader moves; the audit is under Acceptance notes). These four cases pinned the registry entry of a pair's override of a SHIPPED member, which is exactly the entry this PR removes. Each now pins the served read, and keeps the registry pin on the shipped body:

  1. "DELETE by member: resetting zh-CN removes its row only, and the en-US overlay stays served — the registry included": the registry pins become PKG en / PKG zh. The served reads go through the protocol (subjectOf(protocol, 'en-US')), and after the restart subjectOf(after.protocol, 'en-US') = ADMIN en.
  2. "DELETE by member of a member NO artifact ships: its overlay leaves the registry, and the canonical overlay stays": adds the by-name read getMetaItem({ type, name, member: 'en-US' }) = ADMIN digest en; the registry entry for en-US is PKG digest en.
  3. "DELETE with no member": the registry pin on zh-CN ADMIN zh is replaced by subjectOf(protocol, 'zh-CN') = ADMIN zh.
  4. "the cluster fan-out: a peer converges on the member a write changed, and a member reset heals only that member": the peer converges through vi.waitFor on subjectOf(peer.protocol, 'zh-CN') = ADMIN zh; the registry pins are PKG zh / PKG en and stay PKG zh on both nodes after the delete.

Ablation of the guard back to view-only. The anchor line in declinesBorrowedEnvelope is replaced so that it returns false for every type. The marker is confirmed in dist/, and the restore is proved by an empty git diff HEAD and ablation-dist-preflight --absent exit 0.

Ablation reading (both files)
✓ dist/: marker present in 2 built files -- the ablation is live in the artifact the suite consumes.
     × dashboard: the base's per-package layer lists the base's item alone, the pair's the override alone
     × report: the base's per-package layer lists the base's item alone, the pair's the override alone
     × email_template: the base's per-package layer lists the base's item alone, the pair's the override alone
     × DELETE by member: resetting zh-CN removes its row only, and the en-US overlay stays served — the registry included
     × DELETE by member of a member NO artifact ships: its overlay leaves the registry, and the canonical overlay stays
     × the cluster fan-out: a peer converges on the member a write changed, and a member reset heals only that member
 Test Files  2 failed (2)
      Tests  6 failed | 44 passed (50)
DIFF-HEAD-EMPTY=yes
✓ dist/: marker absent from all 24 built files
ABSENT-EXIT=0

Acceptance notes

Bar from the ruling: no protocol read moves. Every reader was measured before (main 0ab4a09) and after (this branch) with the same harness: real ObjectQL, the in-memory driver and the protocol from dist/. The base is registered by registerApp, the pair installed by installPackage, and the override is a draft save into the pair declaring the override, then the publish. S is the shipped body, O the override; the envelope is label / _packageId / _provenance.

Reader view dashboard / report / email_template page / app / dataset / doc / book
bare slot entry none → none O/BASE/package → none O/BASE → O/BASE
registry.getItem (bare, BASE, PAIR) unchanged O → S unchanged
registry.listItems(BASE) unchanged [S, O] → [S] unchanged
per-package layer, BASE [S] → [S] [S, O] → [S] [S, O] → [S, O]
per-package layer, PAIR [O] → [O] [O] → [O] [] → []
per-package layer, unscoped unchanged [O, O] → [S, O] unchanged
getMetaItem (none, BASE, PAIR) O → O O → O O → O
getMetaItems (none, BASE) [O] → [O] [O] → [O] [O] → [O]
getMetaItemLayered(BASE).effective O → O O → O S → S
GET /me/apps n/a n/a O → O (app)

Only the bold cells move, and none of them is a protocol served read. translation was not measurable through the pair (the save is refused NOT_OVERRIDABLE in this harness). flow and action are outside the card.

email_template non-test registry readers, per the condition on the forced edits:

Reader Reads Moves?
plugin-email template-loader.ts the protocol first (by-name read with member); the registry only when no protocol is wired no: the protocol read serves O before and after
cli organization-ownership.ts TemplatePort stored rows plus getMetaItemsForExecution no: neither reads the bare slot
objectql registry direct (getItem, listItems) the registry yes, O → S (the intended change, bold above)
protocol by-name / list / layered stored rows via findServedOverlayRow and the list merge no

The plugin-email suite (48 files) and the cli ownership tests pass on the fixed head, as listed under Verification.

Follow-up: #29613 holds page, app, dataset, doc and book (row-served override path first, then the guard), and the two class-b findings: the pair's per-package layer is empty for those types, and their layered effective serves S while getMetaItem serves O.

Verification

Head c58da12 (c58da128a9b58e5d34788a9cabe48c1a87c1590f) (main merged in). All heavy runs went through scripts/pm/os-verify-lock.sh, and each VERDICT is command-exit 0.

  • Full build ./packages/** and ./examples/**: 79/79 tasks.
  • @objectstack/metadata-protocol test: 308 files passed (4 skipped), 22257 tests passed (26 skipped).
  • @objectstack/objectql test: 542 files, 10517 tests passed.
  • Typecheck of metadata-protocol plus 11 dependents (cli, client, cloud-connection, objectql, plugin-approvals, plugin-security, rest, runtime, service-automation, service-messaging, service-package): 12/12 tasks.
  • Dogfood, 17 files touching email templates, metadata reads, package customization and overrides: 17 files, 138 tests passed.
  • Cross-package pair-override suites: runtime 6 files / 59 tests (including packages-export-base-unchanged-per-type.integration.test.ts from fix(runtime): the base's package export leaves out its pair's dashboard and report overrides (#29563) #29597), cloud-connection 3 / 27, cli 2 / 118 (organization-ownership apply and withdrawal-pair), plugin-email 48 / 785.
  • eslint --no-inline-config on packages/metadata-protocol and packages/objectql: exit 0.
  • dispatch-gates --commands: 62 derived, 62 run, every one exit 0; dispatch-gates --ran reports 0 NOT-MEASURED, 0 UNRUN.
Dogfood files
  • test/email-template-materialization.dogfood.test.ts
  • test/email-template-overlay-survives-boot.dogfood.test.ts
  • test/email-template-two-locales-survive-boot.dogfood.test.ts
  • test/meta-read-doors-live.dogfood.test.ts
  • test/package-commit-revert-path-package.dogfood.test.ts
  • test/public-form-withdrawal-walled.dogfood.test.ts
  • test/showcase-public-form-redirect.dogfood.test.ts
  • test/showcase-public-form-walled-intake.dogfood.test.ts
  • test/showcase-public-form-withdrawal-layers.dogfood.test.ts
  • test/showcase-public-form-withdrawal.dogfood.test.ts
  • test/email-template-boot-sweep.test.ts
  • test/email-template-dependent-unshipped-locale-read.dogfood.test.ts
  • test/email-template-override-unshipped-locale.dogfood.test.ts
  • test/managed-content-sealed.dogfood.test.ts
  • test/view-container-cross-package-default.dogfood.test.ts
  • test/package-first-authoring.dogfood.test.ts
  • test/wall-authoring-trail-doors.dogfood.test.ts
Gate reconciliation
Run reconciliation — 62 derived, 62 run, 0 NOT-MEASURED, 0 UNRUN.
✓ dispatch-gates --ran: 62 derived famil(ies) accounted for — 62 run, 0 NOT-MEASURED (a DERIVED zero — all 62 recorded an exit code and none of them is 3).

Generated by Claude Code

…override never takes the base's envelope in the registry's bare slot

The hydrator registered a published .custom override of a base's dashboard,
report or email template under the bare name, wearing the base's artifact
envelope (the artifact lookup, scoped to the pair, fell back to the base's).
The base's per-package layer then listed the override beside the base's own
item. Such a row now stays out of the bare slot, as a view row already did
(#22057); every protocol read of these types answers it from its row.

Claude-Session: https://claude.ai/code/session_01ADHkmQWkCUmHFTi97EGNu1
Co-authored-by: Claude <noreply@anthropic.com>
…rved answer, not the registry entry

Forced by the #29600 fix: a .custom pair's override of a member the package
ships is served from its row and no longer registered under the package's
envelope, so four cases that read the registry entry as the served answer now
read the protocol (the execution list and the by-name read, which the send and
every door take) and pin the registry's entry as the package's own member.

Claude-Session: https://claude.ai/code/session_01ADHkmQWkCUmHFTi97EGNu1
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m 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 2, 2026
@github-actions

github-actions Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/automation/email-templates.mdx (via email_template (literal, a string literal in BORROWED_ENVELOPE_DECLINED_TYPES))
  • content/docs/concepts/metadata-lifecycle.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class), email_template (literal, a string literal in BORROWED_ENVELOPE_DECLINED_TYPES))
  • content/docs/deployment/environment-variables.mdx (via email_template (literal, a string literal in BORROWED_ENVELOPE_DECLINED_TYPES))
  • content/docs/deployment/validating-metadata.mdx (via email_template (literal, a string literal in BORROWED_ENVELOPE_DECLINED_TYPES))

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

  • content/docs/releases/v16.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via ObjectStackProtocolImplementation (symbol, a top-level class))
  • content/docs/releases/v18/18-2.mdx (via email_template (literal, a string literal in BORROWED_ENVELOPE_DECLINED_TYPES))

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
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 101 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 — 10 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 d280265b849a1586447005e49e27f316fda2a2d5 → packageMentionDocs.

Which tree this was computed on

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

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

⚠️ 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 d280265b849a1586447005e49e27f316fda2a2d5 → 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-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1

维护者速读

席位意见:与席位裁决 A 相符。邮件模板的注册表读方我设了前置条件,开发逐个核对过:plugin-email 先读协议,cli 读存储行加 getMetaItemsForExecution,声明模板落行的旧路径在 main 上已经移除。

席位核对

ACCEPT · domain:server seat 5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1 · 2026-11-02T23:31Z · head c58da128a9 · Fixes #29600 (re-scoped by seat ruling A on the card)

  1. Shape: a draft against main, Fixes #29600, with a patch changeset for @objectstack/metadata-protocol. Clause-②: no, Metadata-protocol: none. Not governed. +424/−17 across 5 files.
  2. Against the order: ruling A. The guard covers dashboard, report and email_template, a closed list judged on the canonical type, and applies only when the row's bound package ships no artifact of the name while another package does. The finding(metadata-protocol): on an unscoped kernel, a package-bound stored row of a view name two packages ship is hydrated into the registry's bare slot, so a by-name read naming the other package serves that row's body under its own _packageId #22057 view limb is unchanged. The loadMetaFromDb boot hydration keeps a third inline copy of the overlay→registry rule with an UNSCOPED artifact lookup (ADR-0048 gap) #4624 two-shipper entry is kept, with a control pin. Readers were measured before and after: only registry-direct reads move, and only for the three types; every protocol served read is unchanged (table in the body). The email_template non-test registry readers were audited per the seat's condition.
  3. Accepted deviations: four forced edits in environment-overlay-email-locale-restart.test.ts. Its registry-entry assertions for a shipped member's pair override now read the protocol, and the test asserts that the registry answers the base's member. Each edit is named in the body. The new pin file sits in packages/objectql/src/ because metadata-protocol tests cannot import objectql (dependency cycle); the seat approved this before the edit.
  4. Gates: reported on c58da128a. metadata-protocol: 22,257 tests. objectql: 542 files / 10,517 tests. Typecheck: 12/12 dependents. Dogfood, run locally: 17 files / 138 tests, including email-template materialization. runtime, cloud-connection, cli and plugin-email suites: green. eslint: exit 0. Derived gates: 62/62 exit 0, 0 UNRUN. Ablating the guard to view only turns 3 tests red, or 6 including the email-locale file, and the restore is proven clean. The seat readies the PR only when no check-run on this head is red or running.
  5. Out of scope: page, app, dataset, doc, book and the two class-b readings go to metadata-protocol: page, app, dataset, doc and book serve a published override only from the registry bare slot; build the row-served path, then take them out of it #29613. PR fix(runtime): the base's package export leaves out its pair's dashboard and report overrides (#29563) #29597's export filter stays as a belt-and-braces guard.
  6. Landing: pr_ready + automerge_enable through the relay.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-steve · session_01ADHkmQKwKFCp1m6Pb6Q8c1

Hand-off note · seat 5 signs off · 2026-11-03T00:00Z

Seat 5 goes off duty at the maintainer's order, so the next domain:server seat takes this PR to MERGED.

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/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

1 participant