You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 3b8c724
Browse filesBrowse the repository at this point in the historyBrowse files
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)
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} -->
_Requested by **Yibo Wang** · [project
thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwBNTkQJpyt5hDhp8ZSurFMMX)_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.**
- `SchemaRegistry.getObjectExtensionsOf(packageId)` (new,
`@objectstack/objectql`): every `extend` contribution the package makes,
read back as the `objectExtensions` entry it was registered from, the
inverse of `registerObjectExtension`. `extend` is the key it is filed
under; `priority` is written only when it is not the default `200` (the
value an entry without one parses to), so a second hop exports the same
bytes as the first; `name` and every envelope key (`_packageId`,
`_provenance`, ...) are left out.
- `assemblePackageManifest` (`@objectstack/runtime`) appends those
entries (`registeredExtensionsOf`) to the per-package layer's
`object_extension` items, skipping any target an item already names: a
published extension keeps exporting as it is stored (#28574, #29903 pins
unchanged). A registry double without the method adds nothing, as
before.
**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
#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
- **The inventory pin alone could not see the defect**, and it still
cannot see half 1 alone: with half 1 ablated and half 2 in place, the
inventory test stayed green (both hops lose the field identically) while
the new #29872 dogfood assertions went red. That is why the halves ship
together and why the new assertions read the field by name.
- **Dedupe is by the item's `extend` spelling against the registry
key.** A stored row spelling a short name that the registry files under
a legacy folded key (#28858) would export twice; the save door refuses
that spelling (#28834), so no stored row carries it today. Noted, not
filed.
- **The pending
`.changeset/29903-base-export-without-pair-extension.md`** still says an
extension from any other installed package "is still folded into the
base's object in the base's export, as before", which this PR makes
false. I first corrected it, and `check-empty-changeset` refused (it
guards another PR's pending note); I restored it byte-for-byte from the
merge base instead, and this PR's changeset carries a "Supersedes, in
the same release" paragraph naming that sentence. The seat may still
prefer to correct the #29903 note on the release side.
- A code package's export (not in the whole-deployment bundle, which
skips `source: 'code'` rows) now also carries its manifest
`objectExtensions`, and a platform-owned object's export no longer folds
in plugin extensions. Both follow from the same rule.
## Verification
<details><summary>Ablations (each: mutation through
scripts/ablation-replace.mjs, anchor 1 → 0, blob restored == HEAD, `git
diff HEAD` empty)</summary>
- **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 #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.
</details>
<details><summary>Local runs (code head 2e498fb; final head 7b1145a
changes only this PR's changeset)</summary>
- 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 #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.
</details>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01WGufzebqeaeGkaTWGnN9bm
<!-- signed: domain:server#6 · os-support-ai ·
session_01WGufzebqeaeGkaTWGnN9bm -->
---------
Co-authored-by: Claude <noreply@anthropic.com>
fix(objectql, metadata-protocol, runtime): a package's export carries the `objectExtensions` it was installed with, and the extended object's owner's export does not
8
+
9
+
Clause-②: yes (widening)
10
+
Metadata-protocol: none
11
+
12
+
A package installed through `POST /api/v1/marketplace/install-local` (what `os package install` sends, and how every entry of a whole-deployment export is restored) that extends another package's object exported without its `objectExtensions`: `GET /api/v1/packages/<id>/export` answered the package's other items and no extension, while the extension's fields were folded into the owner's export instead, as if the owner had shipped them. So a deployment restored from an export lost those fields on its next export, with no error, and a `.custom` pair installed from its export no longer exported its extension.
13
+
14
+
-**The extending package's export** (and its entry in `GET /api/v1/packages/export`) now carries every extension it contributes as `objectExtensions`, however it was installed: an extension published through `/api/v1/meta/object_extension` (as before) or a manifest's `objectExtensions` entry (install-local, or a code package). A published extension is exported as it is stored; a manifest entry as it was registered, with `priority` written only when it is not the default `200`.
15
+
-**The extended object's owner's export** now carries the object as the owner declares it, without the fields, field patches or labels of ANY other package's extension. Until now only its own `.custom` pair's extension was left out; an extension from any other installed package was folded in.
16
+
-**Every served read is unchanged**: `GET /api/v1/meta/object/<name>`, the object list, the console and the data API keep serving the object with every extension folded in.
17
+
18
+
`SchemaRegistry` gains `getObjectExtensionsOf(packageId)`, which reads a package's `extend` contributions back as the `objectExtensions` entries they were registered from (the inverse of `registerObjectExtension`). `listItems`, `getAllObjects` and `foldObjectExtendersOnto` take an `onlyExtensionsOf` option beside `withoutExtensionsOf`: each object is resolved with only that package's `extend` contributions folded in. The metadata protocol's per-package layer (the package export) now passes `onlyExtensionsOf`, naming the listed package, and no longer passes `withoutExtensionsOf`, which keeps its meaning for any other caller.
19
+
20
+
Supersedes, in the same release: the `.custom` pair note for #29903 says an extension contributed by any other installed package is still folded into the base's object in the base's export. After this change it is not: the base's export leaves out every other package's extension, and that package's own export carries it.
0 commit comments