Skip to content

fix(objectql, metadata-protocol, runtime): a package's export carries the objectExtensions it was installed with, and the owner's export folds no other package's extension - #29948

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

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-fh6dvf

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Requested by Yibo Wang · project thread

Fixes #29872

Clause-②: yes (widening)
Metadata-protocol: none

维护者速读

  • 改了什么:通过 install-local 安装(或代码包里声明)的扩展包,导出时现在会带上它自己的 objectExtensions;被扩展对象的拥有者(基础包)导出时,不再把任何其他包的扩展字段并进自己的对象(此前只排除了它自己的 .custom 配对包)。/meta/object、控制台和数据接口读到的对象不变,仍是合并后的。
  • 为什么改:整部署导出恢复到新环境后,每个包都是 install-local 装进来的;它们再次导出时丢掉扩展字段且不报错,第二跳就静默丢数据(卡片复现 E12)。ADR-0146 D8 第 3 条:包导出只带自己的东西,字段只出现一次。
  • 风险与回滚:只影响包导出读的 per-package 层与导出拼装;registry 新增一个只读方法和一个可选参数,已发布的 withoutExtensionsOf 语义不变。回滚即 revert 本 PR。
  • 先看:packages/runtime/src/domains/packages.ts(registeredExtensionsOf,按目标去重)、packages/objectql/src/registry.ts(getObjectExtensionsOf 与 isLeftOutExtension)、packages/metadata-protocol/src/protocol-meta-list-reads.ts(per-package 层改传 onlyExtensionsOf)。
  • 两分钟验证:pnpm --filter @objectstack/runtime exec vitest run --maxWorkers=2 src/domains/packages-export-base-showcase-boot.integration.test.ts -t 29872,预期 2 passed:扩展包导出带 objectExtensions,基础包导出与安装扩展包之前逐字节相同,第二跳再导出不变。
  • 需要你做的:无,席位落地。

What changes

The defect (measured on origin/main 21510bb). assemblePackageManifest built a package's objectExtensions from the object_extension items of the protocol's per-package layer. install-local (and a code package) registers a manifest's objectExtensions straight into the registry as 'extend' contributors (ObjectQL.registerApp) and files no item, so such a package exported no extension. Its fields were folded into the OWNER's export instead (the per-package layer left out only the owner's .custom pair since #29930). A deployment restored from an export therefore lost every extension field on its next export, and a .custom pair installed from its export no longer re-exported its extension.

Half 1: the extender's export carries its extensions, however they were installed.

Half 2: the owner's export carries its object without ANY other package's extension (the seat's carrier note 6605292329).

  • listItems / getAllObjects / foldObjectExtendersOnto take an onlyExtensionsOf option: only that package's own extend contributions fold, every other package's is left out. Both options live in one ObjectExtensionFoldOptions and one predicate (isLeftOutExtension).
  • The per-package layer (readFlattenedMetaItems, layer: 'per-package') passes onlyExtensionsOf naming the listed package to both the registry list and the overlay-row fold, in place of withoutExtensionsOf naming its pair.
  • withoutExtensionsOf is kept with its meaning: it shipped in the published @objectstack/objectql@18.3.0 tarball (5 hits in dist/index.js, measured with npm pack), so removing it would be a narrowing of a published surface. No in-repo caller passes it any more; its unit pin stays.

Served and effective reads are untouched: neither option is passed outside the per-package layer, and the controls in every new pin read GET /meta/object/NAME and GET /meta/object with every extension folded in.

Tests

  • packages/objectql/src/registry-extension-field-patches.test.ts: the packages export: a read-only base's own export carries its .custom pair's object extension merged into the object, where ADR-0146 D8 item 3 says the base exports unchanged #29903 describe now pins onlyExtensionsOf (owner listed with neither the pair's nor a third package's extension; an extender folds its own only; an un-extended object is the cached object; byte-equal to a registry with no extender; the stored-body fold) and keeps a withoutExtensionsOf case. A new describe pins getObjectExtensionsOf: a manifest entry through ObjectQL.registerApp comes back as written and parses under the strict ObjectExtensionSchema; a stored extension through registerObjectExtension comes back with its own priority and re-registers on a fresh registry to a byte-equal served object.
  • packages/runtime/src/domains/packages-export-base-showcase-boot.integration.test.ts (the shared showcase-boot harness; new top-level describe): an extender registered the way install-local registers it exports its entry; the base's export is byte-identical to its export before the extender was installed, with the pair's extension published beside it, and after a restart; the pair exports its own item only; an extender installed from its own export re-exports the same bytes (the second hop).
  • packages/qa/dogfood/test/package-export-bundle-round-trip.dogfood.test.ts: the first describe asserts the install-local extender's objectExtensions by name and in full on the source bundle AND the target bundle, and that the base's object carries the extender's field on neither; the third describe asserts the pair installed on the target from its entry re-exports its extension. The same-inventory pin stays and is green.

Acceptance notes

Verification

Ablations (each: mutation through scripts/ablation-replace.mjs, anchor 1 → 0, blob restored == HEAD, `git diff HEAD` empty)
  • A, half 1 (packages.ts: drop the registeredExtensionsOf append) against the runtime integration file -t 29872: 2 failed. the extender's export carries its manifest entry: expected undefined to deeply equal [ { extend: 'showcase_task', …(1) } ], and the second-hop test the same. Restored.
  • A on dogfood (runtime resolves from src there): the round-trip file 2 failed / 11 passed: source: com.dogfood.exportext: expected { extends: [], objectCarries: false } to deeply equal { …(2) } and [#29872] target: the pair installed from its entry re-exports its extension: expected undefined to deeply equal [ { …(3) } ]. Restored.
  • B, half 2 (protocol-meta-list-reads.ts: onlyExtensionsOf never set; marker ablated-29872-b; metadata-protocol rebuilt, ablation-dist-preflight found the marker in dist) against the runtime integration file -t '29872|29903': 2 failed / 1 passed. expected [ 'organization_id', …(11) ] to not include 'acme_billing_code' and the packages export: a read-only base's own export carries its .custom pair's object extension merged into the object, where ADR-0146 D8 item 3 says the base exports unchanged #29903 the base's object with the extension live equality. Restored, rebuilt, preflight --absent exit 0.
  • C, registry (isLeftOutExtension's onlyExtensionsOf clause → false) against registry-extension-field-patches.test.ts -t 29872: 4 failed / 4 passed (the four onlyExtensionsOf cases). Restored.
Local runs (code head 2e498fb; final head 7b1145a changes only this PR's changeset)
  • Build: pnpm --filter '@objectstack/dogfood^...' build (closure), then objectql + metadata-protocol + runtime rebuilt: VERDICT command-exit 0.
  • pnpm --filter @objectstack/objectql test: 547 files / 10827 tests passed.
  • pnpm --filter @objectstack/metadata-protocol test: 311 passed, 4 skipped files / 22320 passed, 26 skipped tests.
  • pnpm --filter @objectstack/runtime test: 538 files / 8599 passed, 19 skipped tests.
  • typecheck for objectql, metadata-protocol, runtime (incl. each check:test-typecheck): exit 0.
  • Dogfood package-export-bundle-round-trip.dogfood.test.ts: 13/13.
  • eslint --no-inline-config over the 8 changed source/test files: exit 0 (the repo-wide pnpm lint is CI's Lint & Repo Gates).
  • dispatch-gates --commands (67 families), each run and its exit recorded: 67 exit 0 after three reruns. check-plugin-teardown-shape --self-test first exited 3 (shallow clone; its pinned fixture commit fetched, then 48 cases pass); check:dual-build-cjs-loads first exited 3 (seven unbuilt dists: client-react, embedder-openai, knowledge-memory, knowledge-ragflow, service-cluster-redis, service-knowledge, studio; built, then exit 0); check-empty-changeset first exited 1 on my edit of the packages export: a read-only base's own export carries its .custom pair's object extension merged into the object, where ADR-0146 D8 item 3 says the base exports unchanged #29903 note (see Acceptance notes; restored, then exit 0). The changeset gates (check-empty-changeset, check-changeset-no-major, check-adr-0087-registration) were rerun on 7b1145a: exit 0. dispatch-gates --ran: 67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WGufzebqeaeGkaTWGnN9bm

…ts manifest objectExtensions, and an owner export folds only its own extensions

Claude-Session: https://claude.ai/code/session_01WGufzebqeaeGkaTWGnN9bm
Co-authored-by: Claude <noreply@anthropic.com>
…nlyExtensionsOf; changeset (#29872)

Claude-Session: https://claude.ai/code/session_01WGufzebqeaeGkaTWGnN9bm
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Nov 3, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 7b1145ab6d56f676c27faefc9405b4c8df5cfdab
Local-runs: none

Face: declaration (Clause-②: yes (widening) on the PR body and changeset). No published schema and no governed path in the committed tree (9 files). Read: the full diff, card #29872 and its triage audit 6600517038, the seat's carrier note 6605292329, dev report 6609855338, and the per-package layer's other reader (packages/rest/src/form-routes.ts, views only).

① Derived judgments

② Semver level

'@objectstack/objectql': patch, '@objectstack/metadata-protocol': patch, '@objectstack/runtime': patch. New exported method, interface and optional option on SchemaRegistry ⇒ widening; Metadata-protocol: none ⇒ patch (AGENTS.md Post-Task Checklist step 3). Consistent with both declaration lines.

③ Boundary flags

Implemented-by: claude/project-thread-fh6dvf
Reviewed-by: session_01WGufzibcN9oHa8kDbTTnL5

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#6 · os-support-ai · session_01WGufzibcN9oHa8kDbTTnL5

席位核对

Verdict: ACCEPT at head 7b1145ab6d56f676c27faefc9405b4c8df5cfdab · 2026-11-03T16:44Z

  • Shape: 9 files (objectql registry, metadata-protocol per-package layer + fold, runtime export assembly + integration test, dogfood round trip, one changeset); no published schema, no governed path ⇒ not Tier H. Declaration face (Clause-②: yes (widening)): TIER_DEFAULT review PASS 6609898569 at this head.
  • Premise measured: the dev reproduced it on main 21510bba (an install-local extender exported no objectExtensions, and its field folded into the owner's export). Both halves of carrier note 6605292329 shipped together, which the acceptance notes show is required: half 1 alone stays green on the inventory pin.
  • Gates: objectql 10827, metadata-protocol 22320, runtime 8599 pass; typecheck x3 and eslint exit 0; dogfood round trip 13/13; dispatch-gates 67/67 exit 0 (dev report 6609855338). Ablations red at all three seams, restored. Landing waits for every check on this head to be green or an expected skip.
  • Accepted deviations: harness-pinned branch claude/project-thread-fh6dvf; body line 2 links the card thread (session attribution rule); the packages export: a read-only base's own export carries its .custom pair's object extension merged into the object, where ADR-0146 D8 item 3 says the base exports unchanged #29903 changeset note is superseded by a paragraph rather than edited (guarded by check-empty-changeset). The signed trailer was appended by the seat through the relay.
  • Landing: on all-green at this head, the seat sends pr_ready + automerge_enable through the relay.

@github-actions

github-actions Bot commented Nov 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/concepts/metadata-lifecycle.mdx (via SchemaRegistry (symbol, a top-level class))
  • content/docs/deployment/environment-variables.mdx (via SchemaRegistry (symbol, a top-level class))
  • content/docs/kernel/services-checklist.mdx (via SchemaRegistry (symbol, a top-level class))
  • content/docs/plugins/adding-a-metadata-type.mdx (via SchemaRegistry (symbol, a top-level class))
  • content/docs/protocol/kernel/plugin-spec.mdx (via SchemaRegistry (symbol, a top-level class))

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

  • content/docs/releases/v17/17-4.mdx (via SchemaRegistry (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
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 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; 103 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 — 42 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 6a4737abae46e1d6847d7abfe430e5d62cd927f0 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 6a4737abae46e1d6847d7abfe430e5d62cd927f0

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

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

Projects

None yet

1 participant