Skip to content

fix(metadata-protocol): a .custom pair's published override of a page, app, dataset, doc or book is served from its row by every read - #29707

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/project-thread-3dleug
Nov 3, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/project-thread-3dleug

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Requested by Warren · project thread

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

维护者速读

  • 改了什么:.custom 配对包对基础包页面、应用、数据集、文档、文档书的已发布覆盖,所有读取都从覆盖自己的存储行提供,和仪表盘、报表一样;注册表裸槽位不再放一份穿着基础包信封的副本。/me/apps 改读协议的有效列表。
  • 为什么改:这五种类型不接受环境覆盖,读取把配对包的覆盖行也当成被封存的覆盖拒掉,覆盖只能靠裸槽位副本提供。真实启动(有元数据服务)时按名读取返回基础包原件,列表却返回覆盖,分层读取和配对包的按包层从来看不到覆盖,违背 ADR-0146 D8。
  • 风险与回滚:按名读取(真实启动)、分层读取的 effective、按包层会变化,全部朝 D8 方向,席位已批准;已经返回覆盖的读取不变。回滚即还原本 PR,无需迁移,重启即按新规则重新水合。
  • 先看:packages/metadata-protocol/src/protocol-lock-state.ts 的 declinesStoredRow;protocol-package-bindings.ts 的两个类型清单;plugin-hono-server 的 servedAppsOf。
  • 两分钟验证:pnpm --filter @objectstack/objectql exec vitest run src/protocol-custom-override-bare-slot-envelope.test.ts,预期 24 个用例全绿(其中五种类型各两种启动形态、重启前后)。
  • 需要你做的:无,席位落地

What changes

A .custom pair's published override of a base's page, app, dataset, doc or book is now served from its stored row by every read, the way a dashboard's or report's already is. The registry's bare slot no longer holds a copy of it wearing the base's envelope.

These five types are sealed (ADR-0131 D6: allowEnvironmentOverlay: false). Every read asked declinesStoredRow, which declined their stored rows as overlays of managed content (isUnservedSealedOverlay), and that included the pair's declared override. The only place the override was served from was the bare slot. With a metadata service (every real boot), the by-name read answered the service's copy of the base's body before it reached the registry.

Why

ADR-0146 D8 item 2: every read that serves a base's name serves the .custom item that declares it. Item 3: the pair's per-package layer carries the overriding item, and the base's carries the base unchanged. Measured on main 744211fc (table below), these five types broke both items, and on a real boot the by-name read and the list disagreed about the same name.

The card's two hypotheses reconcile: the #29600 harness has no metadata service, so its by-name read fell to the bare slot (override); the #29649 real-boot reading has one, so its by-name read answered the shipped body. Both readings are reproduced below, one harness run each way.

Tests

Before and after, per type. Identical for page, app, dataset, doc and book. S = the shipped body, O = the pair's published override. Before = main 744211fc, the #29600 harness, without and with a metadata service holding the code package's bodies. After = this branch, both shapes, and after a restart.

Reader Before, no metadata service Before, with one (a real boot) After (both, and after a restart) ADR-0146 D8
getMetaItem (no package, BASE, PAIR) O S O item 2
getMetaItems (no package, BASE) [O] [O] [O] item 2 (unchanged)
getMetaItemLayered(BASE).effective S S O item 2
per-package layer, PAIR [] [] [O] item 3
per-package layer, BASE [S, O] [O] [S] item 3
registry bare slot O wearing BASE O wearing BASE none
registry.getItem (bare, BASE, PAIR) O O S registry-direct
registry.getAllApps (app) [S, O] [S, O] [S] registry-direct
GET /me/apps (app) the registry's list the registry's list the protocol's served list: O item 2

Each moved reading is seat-approved as code aligning to D8 (no protocol change, so Clause-②: no and Metadata-protocol: none).

New and extended pins:

Ablation (both mechanism lines back to main's behaviour)

The fix was committed first. servesSealedOverrideRow was mutated to return false (marker ABLATION29613) and the five types were removed from BORROWED_ENVELOPE_DECLINED_TYPES (marker ABLATION29613B), grep -c = 2 on disk, then pnpm --filter @objectstack/metadata-protocol build.

objectql protocol-custom-override-sealed-row-served (now folded into the shared file):
  × page/app/dataset/doc/book, with no metadata service: every reader serves the override, at once and after a restart
  × page/app/dataset/doc/book, with a metadata service (a real boot): every reader serves the override, at once and after a restart
  Tests  10 failed | 5 passed (15)      <- the 5 CONTROL cases stay green, by design
metadata-protocol protocol-package-bindings.sealed-override:
  Tests  6 failed | 14 passed (20)
restore: git checkout HEAD -- protocol-package-bindings.ts; DIFF-HEAD-EMPTY=yes; rebuilt;
  ablation-dist-preflight --absent: tree clean against HEAD, ABSENT-EXIT=0

The preflight's presence leg printed a refusal about an untracked path at that moment instead of a marker count. The 10 objectql reds read the protocol from dist/, which is the evidence the mutation reached it.

/me/apps ablation (ablation-replace.mjs, servedAppsOf(ctx) replaced by undefined, source-imported): 2 failed | 2 passed (the two served-list cases red; the two registry cases green by design); restored, blob equals HEAD.

Package suites (on the branch before the merge of main)
@objectstack/objectql test            Test Files 544 passed (544)  Tests 10616 passed (10616)  exit 0
@objectstack/metadata-protocol test   Test Files 309 passed | 4 skipped (313)  Tests 22282 passed | 26 skipped  exit 0
@objectstack/plugin-hono-server test  Test Files 39 passed (39)  Tests 476 passed (476)  exit 0

After the merge of main (closure rebuilt): runtime packages-export-base-unchanged-per-type.integration.test.ts, packages-customizations.test.ts, packages-ensure-customization.test.ts: 56/56, exit 0. typecheck for metadata-protocol, objectql, plugin-hono-server: exit 0.

#29649's showcase-boot export pin: pairServed widened to all eight types (f40dd0e68); 8/8 green, ablation 5 red.

Acceptance notes

Verification

Local subset on head 90267e800 (after the merge of main), derived with node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (8 paths), each exit captured before any pipe, then reconciled:

✓ dispatch-gates --ran: 65 derived famil(ies) accounted for — 64 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3).
  • NOT MEASURED: pnpm check:dual-build-cjs-loads, reason: PREREQUISITE NOT MET (it reads every package's dist/, and only this change's closure is built here). CI's Lint & Repo Gates runs it.
  • node scripts/check-plugin-teardown-shape.mjs --self-test first exited 3 (its pinned fixture commit was outside the shallow clone); after git fetch origin 621a4876 it exits 0.
  • pnpm lint: exit 0.
Recorded exit codes
node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-comment-mask-adoption.mjs :: exit 0
node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-dts-emitted.mjs --self-test :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-engine-split-ratio.mjs --self-test :: exit 0
node scripts/check-issue-citations.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0
node scripts/check-tenant-audit-census.mjs :: exit 0
node scripts/check-tenant-audit-census.mjs --self-test :: exit 0
node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0
node scripts/docs-audit/check-affected-docs.mjs :: exit 0
node scripts/docs-audit/check-drift-comment.mjs :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm check:authz-resolver :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:dispatcher-error-vocabulary :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:dts-closure :: exit 0
pnpm check:dual-build-cjs-loads :: exit 3
pnpm check:durability-log-level :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:error-status-conformance :: exit 0
pnpm check:filter-alias-parity :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:issue-citations :: exit 0
pnpm check:keyed-text-bounds :: exit 0
pnpm check:lean-entry-closure :: exit 0
pnpm check:logger-receiver-detach :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:objectql-double-limit :: exit 0
pnpm check:org-identifier :: exit 0
pnpm check:page-declaration-shape :: exit 0
pnpm check:platform-object-tenancy-census :: exit 0
pnpm check:published-files :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:registry-log-declared :: exit 0
pnpm check:rest-log-spy-declared :: exit 0
pnpm check:route-envelope :: exit 0
pnpm check:slot-lookup :: exit 0
pnpm check:sourcemap-no-sources-content :: exit 0
pnpm check:system-context-census :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0
pnpm lint :: exit 0

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic.
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes.
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: its generated artifacts are regenerated and committed. (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.
  • The Before Submitting list in CONTRIBUTING.md is done.

Generated by Claude Code

@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 3, 2026
@github-actions

github-actions Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/metadata-protocol, @objectstack/plugin-hono-server, touching 18 documentable anchor(s).

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

  • content/docs/api/client-sdk.mdx (via getBookTree (sdk, the bare tail of client method meta.getBookTree, bound to GET /api/v1/meta/book/:name/tree), meta.getBookTree (sdk, the route ledger binds it to GET /api/v1/meta/book/:name/tree, selected by route anchor /book/:name/tree))
  • 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), getMetaItems (symbol, a method of interface ServedAppLister), email_template (literal, a string literal in BORROWED_ENVELOPE_DECLINED_TYPES))
  • content/docs/deployment/environment-variables.mdx (via getMetaItems (symbol, a method of interface ServedAppLister), 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))
  • content/docs/frontend/data-objectstack.mdx (via /app/:appId/bundle (route, bridged from symbol getMetaItems — its route source's handler names it))
  • content/docs/kernel/services-checklist.mdx (via getMetaItems (symbol, a method of interface ServedAppLister))
  • content/docs/ui/apps.mdx (via /book/:name/tree (route, bridged from symbol getMetaItems — its route source's handler names it))
  • content/docs/ui/forms.mdx (via /forms/:slug (route, bridged from symbol getMetaItems — its route source's handler names it))
  • content/docs/ui/public-data-collection.mdx (via /forms/:slug (route, bridged from symbol getMetaItems — its route source's handler names it))

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

  • content/docs/releases/v14.mdx (via /book/:name/tree (route, bridged from symbol getMetaItems — its route source's handler names it))
  • 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/v17/17-5.mdx (via /book/:name/tree (route, bridged from symbol getMetaItems — its route source's handler names it))
  • content/docs/releases/v17/17-6.mdx (via /forms/:slug (route, bridged from symbol getMetaItems — its route source's handler names it))
  • content/docs/releases/v17/17-7.mdx (via /forms/:slug (route, bridged from symbol getMetaItems — its route source's handler names it))
  • content/docs/releases/v18/18-2.mdx (via email_template (literal, a string literal in BORROWED_ENVELOPE_DECLINED_TYPES), /forms/:slug (route, bridged from symbol getMetaItems — its route source's handler names it))

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 — 18 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 c9636f1e50e48a2fee5205f0a9978c2a63f2dc07 → packageMentionDocs.

Which tree this was computed on

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

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

⚠️ 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 c9636f1e50e48a2fee5205f0a9978c2a63f2dc07 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions github-actions Bot added size/l and removed size/m labels Nov 3, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#4 · os-warren · session_01JQwxbacSXJQqAztgt4UMrS

席位核对

ACCEPT · domain:server#4 · head bdfd50d5da015bd6877318bf4c8169f5b14ab996 · 2026-11-03T03:02Z · seat review of record at TIER_DEFAULT for the whole PR. Clause-②: no and Metadata-protocol: none stand as the seat's route ruling of 01:13Z allowed: the moved readings are ADR-0146 D8 alignment and are listed per type in the changeset. No contract review is owed. Not governed: check-governed-merges --pr 29707 0 of 9 paths, +476/−32.

  1. Shape: draft, base main, Fixes #29613. 9 files: declinesStoredRow (protocol-lock-state.ts) admits a page, app, dataset, doc or book row that is the shipping package's declared .custom override (isServedOverrideRowOf against packagedArtifactOwner); the five types join BORROWED_ENVELOPE_DECLINED_TYPES beside the new closed SEALED_ROW_SERVED_OVERRIDE_TYPES (protocol-package-bindings.ts, both lists pinned); GET /me/apps reads the protocol's served list when the protocol service is present (current-user-endpoints.ts); three comment-only docblock corrections in protocol.ts; tests; one patch changeset (@objectstack/metadata-protocol, @objectstack/plugin-hono-server). Inside the claim surface (6592687187).
  2. Premise measured: before and after per type, without and with a metadata service, and after a restart (PR body table). Ablation reds: 10 objectql, 6 unit, 2 /me/apps.
  3. /me/apps parity: it calls getMetaItems({ type: 'app' }), the same call shape GET /meta/app makes (packages/runtime/src/domains/meta.ts:536), and that read applies nav contributions (protocol.ts:4804), so the endpoint keeps what registry.getAllApps() gave it. A failed protocol read warns once and falls back to the registry; pinned.
  4. Serial with packages: a published dashboard or report override removes the item from the base's export, where ADR-0146 D8 item 3 says the base export is unchanged #29649: packages: a published dashboard or report override removes the item from the base's export, where ADR-0146 D8 item 3 says the base export is unchanged #29649 landed first (PR fix(runtime): the base export serves the base's own document while a .custom override of it is live #29687, 33ee9aa1). On the merged head its showcase-boot export pin, the per-type export pin and the customization pins pass 64/64, and the base export stays byte-identical for all eight types. The dev widened that pin's pairServed to all eight types (f40dd0e68, test-only; ablation 5 red). The seat accepts the edit: it pins what this PR delivers.
  5. Gates on this head: check-runs pinned to bdfd50d5: 52 runs, 37 success, 15 skipped, none failed (Lint & Repo Gates success, which runs check:dual-build-cjs-loads). Dev local: dispatch-gates --ran 65 families accounted for, 64 run, 1 NOT MEASURED (dual-build-cjs-loads, prerequisite: every dist; covered by CI above); pnpm lint 0; typecheck 0 for metadata-protocol, objectql and plugin-hono-server.
  6. Landing: not governed, under 3000 lines, no contract review owed ⇒ the seat lands it (pr_ready + automerge_enable). At MERGED: landing record on 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 (closed by Fixes), pm:dispatched removed; then [maintainer] refactor(metadata-protocol): split src/protocol.ts (33,065 lines, +10,169 in 30 days) along its ObjectStackProtocolImplementation seams, one move-only slice per PR #25467's next cut may start.

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

1 participant