Repository navigation
Commit 87cd458
fix(objectql): the data door serves a residual top-level object under the stack's manifest id (#22701)
Fixes #22615
Clause-②: no
## What was wrong
A stack that carries `packages[]` registers each package body under that
package's id. A top-level object that no body declares is the stack's
residual. The metadata door registered the residual under the stack's
`manifest.id`, and the boot warned that "every door will report that id
as their owner". The engine's `manifest` service registered the bodies
only (`resolveArtifactPackageOrder` answers `packages[]` when the key is
present). So `GET /api/v1/meta/object` listed the residual object
`acme_note` under `manifest.id`, while `GET /api/v1/data/acme_note`
answered `404`. Both boot doors behaved this way: the artifact boot and
the config boot (`os serve objectstack.config.ts`).
## What changes
- **`packages/objectql/src/plugin.ts`**: the `manifest` service now
registers the residual's objects. It reads them from `unclaimedTopLevel`
(`@objectstack/metadata`), which is the residual rule's own answer. The
metadata door, the config door and `os validate` already read that same
function, so there is no second rule and no second list. The objects are
registered:
- after the bodies, in the order the metadata door uses;
- under the stack's `manifest.id`;
- through `registry.registerObject`, the same call a body's objects go
through in `registerApp`.
No package record is created for that id, because the residual is not a
package. When the id names a body (a composed stack keeps one member's
manifest), that body's record is unchanged. When a late artifact (after
`start()`) carries a residual, its objects are copied to the metadata
service along with the bodies' objects.
- **Two classes stay listed and are not served.** Registering either one
would make a boot fail where the stack boots today. Both were measured
failing on a kernel, at `39ef9df4`, before this handling existed:
- **An object whose name another package already owns.** The registry
refuses a second owner (`OBJECT_OWNERSHIP_CONFLICT`, 422). The data door
keeps serving that other package's object.
- **An object with a field that names a picklist no package body
declares.** The boot's picklist audit at `kernel:ready` fails
(`INVALID_METADATA`, 422), because the stack's top-level picklists are
registered as metadata only. This class is decided from the stack's
bodies, never from the registry, which is still filling at this point.
The engine prints one warning naming each such object and the reason.
The remedy it gives is the residual warning's own: declare the object
inside the `packages[]` entry of the package that owns it, together with
every picklist its fields name. To add fields to another package's
object, use `objectExtensions`. Any other registry refusal, such as a
field with no valid `type`, still fails the boot, as it does for a body.
- **`packages/metadata/src/plugin.ts`**: comments only. The residual
rule's header and docblocks now describe the engine half. The residual
warning text is unchanged byte for byte.
- **`.changeset/22615-objectql-residual-objects-served.md`**:
`@objectstack/objectql` patch.
## Pins
- **`packages/cli/test/serve-config-boot-residual.integration.test.ts`**
(an existing two-boot harness, `domain:cli`, test file only). Every
`acme_*` object that `/meta/object` lists is now read back on
`/data/NAME`, through both boots.
- Row 4: the residual object answers `200` on both boots.
- Controls: the bodies' own objects answer `200` on every row, and so
does a single-package stack.
- The header paragraph saying this file does not pin the data door is
replaced.
- **`packages/objectql/src/manifest-service-residual-objects.test.ts`**
(9 cases) covers:
- the residual object resolves and is owned by `manifest.id`;
- no package record is created for that id;
- when `manifest.id` names a body, the residual takes that id and the
body's record keeps its own objects;
- a normally composed stack registers nothing extra;
- `packages: []` with a top-level object;
- a residual object whose picklist a body declares is served with that
list's options;
- the two unserved classes: the boot completes, the other package keeps
the name, and the warning names each object;
- a late artifact's residual reaches the metadata service.
## Measured
- **Premise on `origin/main`.** Run at `77b8611b`, which is `cb3bb933`
plus the pin commit only, through `--project integration`, rows 4 and
control. Row 4's new case failed: `Expected ... "acme_note:200" /
Received ... "acme_note:404"` on the artifact boot. The config-boot
reading was equal to it (`toEqual(artifact)` passed), so both boots
answered `404`. Result: 1 failed, 4 passed, 9 skipped.
- **Fixed.** The full file at `577b0757`: `Tests 14 passed (14)`. After
merging `main`, at `e7fba19b`: `Tests 14 passed (14)`.
- **Ablation** (`node scripts/ablation-replace.mjs`). The anchor was `if
(residual) this.registerUnclaimedTopLevelObjects(ctx, residual,
ordered);` and it was replaced by a marker. The anchor count went 1 to 0
and the blob changed from `7010e04df780` to `6abbdd6b25e2`. Result:
`Tests 7 failed | 2 passed (9)`. The 2 that passed are the two controls
(no package record, normally composed). The file was then restored: blob
equal to HEAD and `git diff HEAD` empty. The unit file imports
`./plugin.js` from source, so this mutation needed no rebuild.
- **Load closure.** `@objectstack/objectql`'s root entry now imports
`@objectstack/metadata`'s root entry, which adds `chokidar`, `glob`,
`js-yaml` and `readdirp` to what `import('@objectstack/objectql')`
loads. Measured: 208 modules before, 214 after.
`@objectstack/objectql/core` is unchanged: `pnpm
check:lean-entry-closure` holds its admitted set of 15 packages, and its
6 denied names are absent. `unclaimedTopLevel` is published only from
that root entry. A lean subpath for it would be a new entry export, so
it is not part of this PR.
## Acceptance notes
- `packages/runtime/src/app-plugin.ts` (`registerUnclaimedTopLevel`'s
docblock, about lines 787 to 799) still says the `manifest` service
"registers the package bodies and never the top level". After this PR it
also registers the residual's objects. This is a comment in the
`domain:cli` lane, left untouched here (carrier: none).
- A residual object's owner id has no package record. So `GET
/api/v1/packages` does not list a `manifest.id` that names no body, and
the object routes to the default datasource rather than to a
`defaultDatasource` on the stack manifest. Not measured on a boot.
- Before this PR, `ingestReloadedObjects` already registered every
reloaded object on `metadata:reloaded`, residual ones included. So a
`--dev` boot answered `404` before its first reload and served the
object after it. From boot onward the answer is now the same. This was
read from the code, not measured on a reload.
- Ordering: another package that registers an object with the same name
LATER in the same boot (for example a marketplace rehydrate at
`kernel:ready`) now meets the registry's ownership refusal, as it would
against a body-declared object. Not measured.
- The residual's other item types (views, docs, picklists, actions and
the rest) are still metadata only in the engine registry, as they were
before. Not measured on any door.
## Gates
Everything below was read at `e7fba19b`, the head of this PR, unless
another commit is named.
- **Gate list.** `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 66 commands for this diff. That
includes all 51 named in the dispatch, plus 15 more that the changeset
and the test files bring in. All 66 were run and every one exited 0. The
`--ran` reconciliation reports 66 derived, 66 run, 0 NOT-MEASURED and 0
UNRUN.
- **Two first runs measured nothing and were re-run:**
- `node scripts/check-engine-split-ratio.mjs --days 90` exited 2 because
the clone was shallow. After `git fetch --shallow-since=2026-07-05`, it
exited 0.
- `pnpm check:dual-build-cjs-loads` exited 3 (PREREQUISITE NOT MET)
because 9 packages were unbuilt. After `turbo run build
--filter='./packages/**'`, it exited 0.
- **Also run:** `pnpm check:startup-registry-verdict`, exit 0.
- **Package checks:**
- `@objectstack/objectql`: `typecheck` passed, and the full vitest run
passed `Test Files 396 passed (396)`, `Tests 7767 passed (7767)`.
- `@objectstack/metadata`: `typecheck` passed at `577b0757` (the merge
did not touch it), and vitest passed `58 passed (58)` files, `871 passed
(871)` tests.
- `@objectstack/cli` integration: the file above, `14 passed (14)`. The
`packages/cli` change is an integration-tier test file, so only that
tier was run here.
- **ESLint.** `eslint --no-inline-config --format json` over the 4
changed TypeScript files counted 4 files, 0 errors and 0 warnings.
`eslint.config.mjs` never turns on type-aware linting (no
`parserOptions.project`), so this diff cannot change the result for any
file it does not touch. The repo-wide `pnpm lint` is left to CI.
- **NUL bytes.** `pnpm check:nul-bytes` exited 0.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01JfJfBUC3cQ6hhgm9MQK76T)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 26aa999 commit 87cd458
5 files changed
Lines changed: 455 additions & 15 deletions
File tree
- .changeset
- packages
- cli/test
- metadata/src
- objectql/src
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
40 | 40 | | |
41 | 41 | | |
42 | 42 | | |
43 | | - | |
44 | | - | |
45 | | - | |
46 | | - | |
47 | | - | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
48 | 51 | | |
49 | 52 | | |
50 | 53 | | |
| |||
325 | 328 | | |
326 | 329 | | |
327 | 330 | | |
| 331 | + | |
| 332 | + | |
328 | 333 | | |
329 | 334 | | |
330 | 335 | | |
| |||
364 | 369 | | |
365 | 370 | | |
366 | 371 | | |
367 | | - | |
| 372 | + | |
| 373 | + | |
368 | 374 | | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
369 | 383 | | |
370 | 384 | | |
371 | 385 | | |
372 | 386 | | |
373 | 387 | | |
374 | | - | |
| 388 | + | |
375 | 389 | | |
376 | 390 | | |
377 | 391 | | |
| |||
455 | 469 | | |
456 | 470 | | |
457 | 471 | | |
| 472 | + | |
| 473 | + | |
458 | 474 | | |
459 | 475 | | |
460 | 476 | | |
| |||
493 | 509 | | |
494 | 510 | | |
495 | 511 | | |
| 512 | + | |
| 513 | + | |
| 514 | + | |
| 515 | + | |
| 516 | + | |
| 517 | + | |
496 | 518 | | |
497 | 519 | | |
498 | 520 | | |
| |||
519 | 541 | | |
520 | 542 | | |
521 | 543 | | |
| 544 | + | |
522 | 545 | | |
523 | 546 | | |
524 | 547 | | |
| |||
540 | 563 | | |
541 | 564 | | |
542 | 565 | | |
| 566 | + | |
543 | 567 | | |
544 | 568 | | |
545 | 569 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
247 | 247 | | |
248 | 248 | | |
249 | 249 | | |
250 | | - | |
251 | | - | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
252 | 253 | | |
253 | 254 | | |
254 | 255 | | |
255 | 256 | | |
256 | 257 | | |
257 | 258 | | |
258 | 259 | | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
259 | 268 | | |
260 | 269 | | |
261 | 270 | | |
| |||
366 | 375 | | |
367 | 376 | | |
368 | 377 | | |
369 | | - | |
| 378 | + | |
| 379 | + | |
370 | 380 | | |
371 | 381 | | |
372 | 382 | | |
| |||
1410 | 1420 | | |
1411 | 1421 | | |
1412 | 1422 | | |
1413 | | - | |
1414 | | - | |
| 1423 | + | |
| 1424 | + | |
| 1425 | + | |
1415 | 1426 | | |
1416 | 1427 | | |
1417 | 1428 | | |
| |||
0 commit comments