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 b62260c
Browse filesBrowse the repository at this point in the historyBrowse files
fix(objectql): an item registered through the registry path is served with its package's _packageVersion (#22724)
Fixes#22689
Clause-②: no
## What was wrong
`_packageVersion` is declared on the spec's protection envelope
(`packages/spec/src/kernel/metadata-protection.zod.ts`: "Owning package
version."). The metadata plugin's artifact loader stamps it through
`applyProtection(item, { packageId, packageVersion })`. The registry
load path, `ObjectQL.registerApp(manifest)` into
`SchemaRegistry.registerItem` / `registerObject`, called
`applyProtection(item, { packageId })` and stamped the id alone. The
comment above that call said both paths "produce identical lock state".
They did not: `GET /api/v1/meta/app` served a registry-registered app
with `_packageId` and no `_packageVersion`, so a console waiting on an
upgrade could not tell the new app from the old one a stale kernel still
serves (the upgrade case behind objectstack-ai/objectui#12087).
Premise checked on `origin/main` `61bea242`: both `applyProtection`
sites in `registry.ts` passed `{ packageId }` only. Measured before the
change: the served app had `_packageVersion` undefined (ablation A below
is that reading).
## How the version reaches the registry
The caller that holds a package's manifest stamps each item **before**
handing it to the registry, with the artifact loader's own call,
`applyProtection(item, { packageId, packageVersion })`.
`applyProtection` writes a key only while it is unset, so the registry's
own id-only stamp leaves the caller's envelope as stamped. **The
registry's public signatures are unchanged**
(`SchemaRegistry.registerItem`, `registerObject` and `registerApp`,
signature and JSDoc as on `main`), and both of its `applyProtection`
calls still pass `{ packageId }` alone. No version table, no second
manifest list, no lookup in the registry's installed-package record
(which would stamp a tenant row merely bound to a package with a version
it was never shipped at), no implicit context and no new export.
- `ObjectQL.registerApp(manifest)`, the private `registerPlugin` and the
private `registerMetadataCollections` stamp through two module-local
helpers in `engine.ts`:
- `stampPackage` stamps apps, collection items, view items, seed data
and extensions in place, as the registry already mutates them on `main`;
- `stampObjectPackage` stamps a shallow **copy** of each authored object
definition, so a payload registered twice (the ADR-0130 D7 pin) is never
mutated. The test pins this: "the authored object definition is left
unstamped".
- Both do nothing when `manifest.version` is absent. A nested plugin's
items carry the parent's version, as they carry its id.
- `ObjectQLPlugin`'s residual registration
(`registerUnclaimedTopLevelObjects`, landed from `main` in PR #22701
after dispatch) stamps a shallow copy of each residual object with
`unclaimedTopLevel(stack)`'s `(ownerId, ownerVersion)` before
`registerObject`, the pair the metadata door stamps the residual with.
**`Clause-②: no`, by measurement:** the declaration closure that
`@objectstack/objectql`'s `.` and `./core` entries reach is
byte-identical to `origin/main` (`e5899a67d6`): 3 of 3 closure files and
6 of 6 dist declarations. A determinism control (head versus a rebuilt
head) is identical. A positive control carrying round 0's trailing
parameter differs in 12 places, so the instrument sees a widening.
## Callers that have no version, and stamp the id alone
`applyProtection` never invents a version, so these are unchanged:
- `SchemaRegistry.registerKind` and `registerItem('plugin', manifest,
'id')`: no package id at all.
- `ObjectQLPlugin.loadMetadataFromService`, `ingestReloadedObjects` and
the per-object refresh, and `MetadataFacade.register`: they pass the
item's own `_packageId`. An item that came through the artifact loader
already carries `_packageVersion`, and `applyProtection` keeps it.
- The metadata-protocol stored-row paths (hydration, write-through,
rollback): `registerItem` with no package id, or `registerObject` of a
`_provenance: 'org'` row under its binding. These are tenant-authored,
not shipped by a package version.
- plugin-security's built-ins (`registerBuiltinPositions` /
`registerBuiltinCapabilities`): the plugin's own id.
- `ObjectQL.registerObject(schema, packageId, namespace)`: the public
proxy, unchanged.
The comment above `registerItem`'s `applyProtection` call now says this
instead of "identical lock state".
## Files outside the claimed surface
The claim named `registry.ts`, the registry's callers inside
`packages/objectql/src/`, tests there, and one changeset. Two more files
changed. Both are test pins that asserted the asymmetry this card
closes, and both went red with the change:
-
`packages/runtime/src/standalone-stack-seeder-declaration-copy.test.ts`:
"the registry copy carries no `scope` (and no `_packageVersion`)". It
now expects the registry copy to carry `_packageVersion: '3.0.0'`, the
version the door copy carries.
- `packages/runtime/src/standalone-stack-security-registrar.test.ts`
(the control case):
`expect(got.capability[0]._packageVersion).toBeUndefined()`. It now
expects `'2.0.0'`. The comment says `scope` is the one key that still
tells the raw copy from the door's.
## Tests (round 1, head `73fc84cbed`)
- New `packages/objectql/src/registry-package-version-stamp.test.ts`:
- an app and an object registered through `ObjectQL.registerApp` are
served with `_packageVersion` through the real list door;
- an upgrade (1.0.0, then 1.1.0) is served with the new version;
- a nested plugin's object and a collection item carry the owning
version;
- an id-only caller stamps the id alone;
- the authored object definition is left unstamped;
- CONTROL: the artifact loader door
(`MetadataPlugin._parseAndRegisterArtifact`, unchanged) serves an
identical envelope.
- `manifest-service-residual-objects.test.ts`: the residual object
carries the stack's version (`1.0.0`).
- `engine.test.ts`: one manifest gained `version: '1.2.0'`, and its pin
asserts `objectContaining({ name, _packageId, _packageVersion: '1.2.0'
})`. The other call-shape pins are `main`'s.
- `pnpm --filter @objectstack/objectql` vitest (`--project local`, two
foreground shards): 397 files and 7,773 tests passed, 0 failed.
Typecheck exit 0, with the test layer's pinned debt held.
- `@objectstack/runtime`: typecheck exit 0. Vitest in two shards: 348
files, 0 failed.
- **Round 0's CI red** on `7539a404` (`Test Core (6/6)`,
`artifact-load-path.test.ts`'s ADR-0130 D7 pin) was caused by this
card's first caller-side stamp mutating the authored object in place. It
is fixed by stamping a copy, and the D7 test is green and unchanged.
## Reverse verification
Both legs used `scripts/ablation-replace.mjs` from the committed head,
with each restore proven (blob equal to HEAD, `git diff HEAD` empty):
- **A:** `stampPackage`'s stamp removed. Predicted and observed: 5 red
(the new file's app, upgrade, nested and control cases, plus the
`engine.test.ts` version pin); the id-only, residual and D7 cases green.
- **B:** the residual stamp removed. Predicted and observed: only the
residual case red.
## Gates
All 70 families that `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` derives for this diff ran at
`73fc84cbed`, and each exited 0. `--ran`: 70 derived, 70 run, 0
NOT-MEASURED, 0 UNRUN. `check:dual-build-cjs-loads` was first recorded
as PREREQUISITE NOT MET in a recreated worktree and re-run after a full
build, exit 0. A narrowed `eslint --no-inline-config` over the 8 changed
`.ts` files reports 0 errors and 0 warnings. Repo-wide lint is CI's. CI
on `73fc84cbed`: 31 success, 3 skipped.
## Line budget
+213 / -34 across 9 files, against a suggested 150:
- code: `engine.ts` +49/-18, `plugin.ts` +14/-4, `registry.ts` +10/-3
(comments only);
- tests: the new file +112, and +15/-9 elsewhere;
- changeset: +13.
The new test file carries the cross-door control and the copy pin.
## Acceptance notes
- `SchemaRegistry.updatePackageManifest` (the in-memory half of `PATCH
/packages/:id`) can change a package record's `version` without
restamping the items already registered. They keep the version they were
installed with. This is a read-only inference; it was not exercised.
- The objectui follow-up objectstack-ai/objectui#12098 consumes this
field. The rebuild-pending read is cloud's (objectstack-ai/cloud#2760)
and is not in this PR.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01JfJfBUC3cQ6hhgm9MQK76T)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
An app, object or other item registered through `ObjectQL.registerApp` is now served with its package's `_packageVersion`, as the artifact loader already serves it
6
+
7
+
Clause-②: no
8
+
9
+
`_packageVersion` ("Owning package version") is declared on the protection envelope, and the metadata plugin's artifact loader stamps it. The registry load path stamped only `_packageId`: `ObjectQL.registerApp(manifest)` called `SchemaRegistry.registerItem` and `registerObject` with the package id alone. So `GET /api/v1/meta/app` served such an app with no version, and a console waiting for an upgrade could not tell the new app from the old one that a stale kernel still serves.
10
+
11
+
`registerApp` now stamps `manifest.version` on every item it registers: owned objects, object extensions, apps, every metadata collection, seed datasets, and the objects, app and collections of a nested plugin, which carry their owning package's version. A multi-package stack's residual top-level objects, which the engine registers under the stack's own id, now carry the stack's version too, as the metadata door stamps them. Each item is stamped before it reaches the registry, with the artifact loader's own call, `applyProtection(item, { packageId, packageVersion })`. The registry's stamp knows the id only, and it keeps a version that is already set. No signature changes: `SchemaRegistry.registerItem`, `registerObject` and `registerApp` keep theirs.
12
+
13
+
A caller that passes only a package id still stamps the id alone. No version is looked up or invented. Those callers are the metadata-service re-sync and the facade, which pass along the stamp the artifact loader already put on the item; the stored-row hydration, whose rows are tenant-authored; and the security catalog's built-ins.
0 commit comments