Repository navigation
Commit 28bff18
fix(cli): os serve and seven sibling readers read a multi-package config's package-owned keys through its package bodies (#22321)
Fixes #22288
Clause-②: no
## What this changes
A `composeStacks([…], { manifest: 'preserve' })` config carries every
package-owned key (`requires`, `tiers`, `analyticsCubes`, `flows`,
`objects`, …) once, inside the body of the package that declared it. Its
top level carries none of them. `os serve` read the capability providers
to mount from the top-level `requires` only (`serve.ts:2847` on
`6729e107`), so a capability a package declares was not mounted. The
card named two sibling readers. The enumeration this PR adds found the
same top-level read in seven more readers in `packages/cli`. All of them
are fixed here.
Every reader now uses one rule: `resolveStackCollection`
(`utils/stack-collections.ts`, the rule #22285 gave the build
preflight), which takes the top-level value when the stack carries one
and otherwise every package body's. Or it folds the stack with
`authoringRuleUnionStack` where the stack is loaded. A stack with no
`packages[]` reads exactly as before. `serve` imports only
`stack-collections.ts` (core plus spec), so it stays off
`artifact-packages.ts` and `@objectstack/lint`.
| reader | file | change |
|---|---|---|
| `os serve` providers (`requires`) | `commands/serve.ts` |
`stackDeclaredCapabilities(config)`, a new one-line export over
`resolveStackCollection(…, 'requires')` |
| `os serve` `tiers` | `commands/serve.ts` |
`resolveStackCollection(config, 'tiers')` |
| `os serve` analytics cubes | `commands/serve.ts` | the top level first
(its array, then the legacy `cubes` spelling), then the bodies |
| `os serve` declared-flow count (automation line) | `commands/serve.ts`
| `resolveStackCollection(config, 'flows').length` |
| `os migrate plan` / `apply` providers |
`utils/schema-migration-plugins.ts` |
`stackDeclaredCapabilities(config)` |
| `os generate` missing-capability hint | `utils/scaffold-wiring.ts` |
`declaredCapabilities` reads by the same rule; JSDoc rewritten (below) |
| `os generate types` / `client` / `migration` | `commands/generate.ts`
| the four readers fold the loaded config on entry |
| `os doctor` | `commands/doctor.ts` | folds once where it normalizes
the config |
| `os diff` | `commands/diff.ts` | folds each side where it is loaded |
| `os migrate meta` data-migration advice | `commands/migrate/meta.ts` |
folds at the one call of `pendingDataMigrations` |
## Measured, through the doors
Each door was run with bounded boots/runs of two-package fixtures and
their one-package controls. BEFORE is `6729e107`, AFTER is this branch.
The fixtures are the ones in the pins below.
| door | two packages, before | two packages, after | one package
(control, before = after) |
|---|---|---|---|
| `os serve` (config, no artifact), package `requires: ['automation']` |
`Info: Optional service not present: automation`; banner 31 plugins, no
`AutomationServicePlugin` | `Plugin loaded:
com.objectstack.service-automation`; banner 32 plugins incl.
`AutomationServicePlugin` | `Plugin loaded:
com.objectstack.service-automation` |
| `os serve`, package `requires: ['ai']` (no open-edition provider) | `✓
Server is ready` | exit 1 `✗ Capability "ai" resolves to
@objectstack/service-ai, which is not available in the open edition …` |
the same exit 1 line |
| `os dev`, package `analyticsCubes` | `[Analytics] Service started with
0 cubes: (none)` | `… with 1 cubes: prb_note_cube` | `… with 1 cubes:
prb_note_cube` |
| `os dev`, package screen flow, no automation | no flow line | `⚠
Flows: 1 flow(s) declared but the automation engine is not enabled …` |
the same line |
| `os serve`, package `tiers` without `auth` | ignored:
`com.objectstack.auth` mounted, ready | exit 1 `✗ This stack mounts no
auth, …` | the same exit 1 |
| `os migrate plan`, host plugin hard-depending on automation, package
`requires: ['automation']` | exit 1 `✗ [Kernel] Dependency
'com.objectstack.service-automation' not found for plugin
'com.probe.connector'` | exit 0, `Composed AutomationServicePlugin for
requires: ['automation'] …` | exit 0, same note |
| `os generate flow done --object prb_ticket`, app package declares
`['automation','triggers']` | `⚠ It will not run yet:
objectstack.config.ts does not require 'automation', 'triggers'` and a
`requires:` line to add | `✓ Reaches the stack`, no warning | no warning
|
| `os generate types --dry-run` | 0 record interfaces | 2 | 2 |
| `os generate migration --format sql --dry-run` | 0 `CREATE TABLE` | 2
| 2 |
| `os doctor` | no metadata check ran; `✅ Environment is healthy` |
circular-dependency and unused-object checks ran (2 unused) | the same 2
|
| `os diff --json`, one object added to the service package | `total: 0`
| `total: 1`, `prb_extra` added | `total: 1` |
| `os migrate meta --from 16 --json`, a `file` field in the service
package | `dataMigrations: []` | `adr-0104-file-references` |
`adr-0104-file-references` |
## The PM readings (H1–H5)
- **H1, reproduced, and narrower than the card says.** Bare `os serve`
on a two-package config reads `[]` (table, row 1). **`os dev` and `os
start` reach the same line of code, but they do NOT reproduce the
defect.** Both boot a compiled artifact: `os dev` always compiles, and
`os start` auto-compiles when no artifact exists.
`createStandaloneStack` resolves the artifact's `packages[]`
(`resolveArtifactCollections`), and `mergeBootConfig` lays the resolved
`requires` over the top level. Measured on `6729e107` with the same
two-package fixture: `os dev` and `os start` each log `Plugin loaded:
com.objectstack.service-automation` once and `Optional service not
present` zero times. So does `os serve` with a built
`dist/objectstack.json` beside the config. The `requires` defect is
therefore confined to a config boot with no compiled artifact. Triage
graded p1 on `os dev` / `os start` reach. On this measurement, the
`requires` half does not reach them. The `tiers`, `analyticsCubes` and
flow-count halves do reach `os dev` (table, rows 3–5), because the
artifact path does not carry those keys.
- **H2.** Serve calls the existing rule (`resolveStackCollection`) and
does not copy it. The import stays off `artifact-packages.ts`.
- **H3, measured, with two narrowings rather than one.** (1) A
capability a package declares with no installed provider now stops an
`os serve` config boot. This is table row 2, and the same exit 1 the
one-package app always gave. `os dev` / `os start` already stopped
there, since the artifact path made those tokens "declared". (2) A
package's own `tiers` now apply on `os serve`, `os dev` and `os start`
(row 5). The claim named one narrowing. Both are named in the changeset.
Nothing in this repo boots a multi-package config whose packages declare
`requires` or `tiers`. `examples/app-multi-package` declares neither,
and no test boots a `preserve` config with either.
- **H4.** Both readers read `[]` on a multi-package config, and both are
fixed (rows 6 and 7). The `os generate` hint now reads the list a server
mounts. Its JSDoc keeps the `defineStack` half true. On a one-package
stack the list is its top-level `requires`, which is also what
`defineStack`'s trigger rule reads. On a multi-package stack,
`defineStack` judged each package against its own `requires` when the
config loaded. A refusal there is the `load-failed` branch, reached
before the hint reads anything, so the union is what decides whether the
item runs.
- **H5.** `test/normalized-call-sites.test.ts` gains a second table. It
enumerates every member read of a package-owned key off a stack-named
receiver in `packages/cli/src`. The key set comes from
`packageOwnedCollectionKeys()` (now exported from
`stack-collections.ts`, derived from the two schemas, 37 keys, envelope
keys excluded). Every read is classified. A `resolved` row carries
`evidence`: code that must be present in the file (comments and strings
blanked), which proves the fold. A `top-level` row carries the reason.
53 reads in 15 files after the fix: 6 `top-level` rows (the build
preflight's `config.requires` ×2, serve's `config.analyticsCubes` first
leg and `config.docs`, `collect-docs`' artifact `stack.docs`,
`scaffold-wiring`'s presence probe) and 12 `resolved` receivers. The
grammar's bounds are written in the file: a stack held under another
name, a computed key, a member chain, and a cast whose type has
parentheses. None of those spellings reads a package-owned key in `src`
today.
## Pins, and the tier each runs in
- `test/serve-package-declared-capabilities.test.ts`: **integration tier
(per-PR)**, `runServe(` boots. Covers `automation` mounted on two
packages, the one-package control, the `ai` narrowing, cubes plus the
flow line, and the `tiers` narrowing. 5 boots, about 60s under a shared
box. Not named `*.e2e.test.ts`, so it gates PRs.
- `test/package-owned-command-readers.test.ts`: **integration tier
(per-PR)**, spawns `os doctor`, `os diff --json` and `os migrate meta
--json`, two packages vs one.
- `src/utils/schema-migrate.requires-providers.integration.test.ts`:
**integration tier**, a second case where a package declares `requires`,
on the real kernel boot.
- `test/package-owned-readers.test.ts`: **unit tier**.
`stackDeclaredCapabilities`, the `os generate` hint and the
types/migration generators against real `composeStacks` output, each
paired with one `defineStack`.
- `test/normalized-call-sites.test.ts`: **unit tier**, the enumeration,
with a scanner self-test (read forms, writes skipped, envelope keys and
prose ignored) and a positive control on the real tree.
## Ablation (one-time; nothing permanent left behind)
Run with `scripts/ablation-replace.mjs` (WRAP mode, the anchor must hit,
restore on EXIT/INT/TERM), at `6a30bcff0`. No build leg: the scanner
reads `src` and the spawned CLI runs `src` through tsx
(`bin/run-dev.js`).
- **serve.ts back to `Array.isArray((config as any).requires) ? … :
[]`**: anchor 1→0, blob `7a588141d660`→`4bf8b068950a`. Three tests red:
`no read is unclassified`, naming `commands/serve.ts ::
config.requires`; `two packages: the service package's automation is
mounted`; and the `ai` narrowing. The one-package control and the
cubes/tiers cases stayed green. Restored: blob `7a588141d660` == HEAD,
`git diff HEAD` empty.
- **scaffold-wiring.ts back to a top-level-only read**: two hint tests
red in `package-owned-readers.test.ts`. Restored: blob `3ec828a90838` ==
HEAD, `git diff HEAD` empty.
Direction observed: red, as expected.
## Verification
Everything below ran at the merged head `a58d828b0`. That is this branch
merged with `origin/main` at `dc4a5c630`, which brought #22304's
`serve.ts` change. The merge was clean, and all four edits to `serve.ts`
were checked present after it. The dists were rebuilt after the merge.
- `pnpm --filter @objectstack/cli typecheck`: exit 0. That is `tsc
--noEmit`, then `check:test-typecheck: OK … 3 file(s) / 28 error(s) / 6
pinned signature(s) held`, so no new signature.
- `pnpm --filter @objectstack/cli exec vitest run --project unit`: 267
files, 3943 tests passed.
- `pnpm --filter @objectstack/cli exec vitest run --project
integration`: 97 files, 922 passed, 2 skipped. This tier is owed locally
because the diff adds integration-tier files and touches the serve boot
path.
- Gate union: `node scripts/pm/dispatch-gates.mjs --repo
objectstack-ai/objectstack --commands` derives the same 67 commands as
the dispatch order lists. All 67 ran and exited 0. The `--ran`
reconciliation reads `67 derived, 67 run, 0 NOT-MEASURED, 0 UNRUN`. On
the pre-merge tree, `check:dual-build-cjs-loads` and
`check:i18n-coverage` first refused with `PREREQUISITE NOT MET` because
some packages had no dist. Those dists were built and both gates re-ran
green.
- `pnpm lint` (full, `eslint . --no-inline-config`): exit 0.
- Before the merge, at `d12d93891`: the unit tier (267/3943), the
integration tier (96 files, 919 passed, 2 skipped), full lint and the
gate union were all green as well.
## Acceptance notes (observations, not filed)
- **Artifact-only boots.** `createStandaloneStack`'s result carries
`requires`, `objects`, `manifest`, `permissions`, `positions` and
`i18n`, but not `tiers`, `analyticsCubes` or `flows`. An `os start` with
an artifact and no config therefore reads those three as empty even for
a one-package artifact. Inferred from the code, not measured. That is a
different shape from this card (not multi-package specific). Carrier:
none.
- **Doc embed lint.** `collectAndLintDocs`' comment says a doc already
on a package body "is also in `docs` above, where `lintMetadataEmbeds`
has judged it". That was true of the additive shape, but the 2026-09-22
addendum emits a body's docs only in the body, so a package body's
inline docs may not reach the embed lint. Inferred, not measured.
Carrier: none.
- **Legacy spellings kept as they were.** Serve's `cubes` fallback, and
`os generate`'s `data?.objects` fallback, are consumer-side spellings
that predate this card. They are kept byte-for-byte in precedence, not
endorsed.
- **Wiring lines.** On a multi-package config, the `os generate` "Not
wired" advice still says `inside defineStack({ … })` without naming
which package. The `requires:` line it prints is the union of what the
packages declare plus what is missing, so it never drops a declared
token.
- **#22190**, the producer-side member of this family, is not addressed
here and remains open.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent fe98cc6 commit 28bff18
14 files changed
Lines changed: 998 additions & 34 deletions
File tree
- .changeset
- packages/cli
- src
- commands
- migrate
- utils
- test
| 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 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
3 | 3 | | |
4 | 4 | | |
5 | 5 | | |
| 6 | + | |
6 | 7 | | |
7 | 8 | | |
8 | 9 | | |
| |||
214 | 215 | | |
215 | 216 | | |
216 | 217 | | |
217 | | - | |
218 | | - | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
219 | 225 | | |
220 | 226 | | |
221 | 227 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
| 13 | + | |
13 | 14 | | |
14 | 15 | | |
15 | 16 | | |
| |||
2312 | 2313 | | |
2313 | 2314 | | |
2314 | 2315 | | |
2315 | | - | |
| 2316 | + | |
| 2317 | + | |
| 2318 | + | |
| 2319 | + | |
| 2320 | + | |
| 2321 | + | |
| 2322 | + | |
2316 | 2323 | | |
2317 | 2324 | | |
2318 | 2325 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1334 | 1334 | | |
1335 | 1335 | | |
1336 | 1336 | | |
1337 | | - | |
| 1337 | + | |
| 1338 | + | |
| 1339 | + | |
| 1340 | + | |
| 1341 | + | |
| 1342 | + | |
1338 | 1343 | | |
1339 | 1344 | | |
1340 | 1345 | | |
| |||
2245 | 2250 | | |
2246 | 2251 | | |
2247 | 2252 | | |
2248 | | - | |
| 2253 | + | |
| 2254 | + | |
| 2255 | + | |
2249 | 2256 | | |
2250 | 2257 | | |
2251 | 2258 | | |
| |||
3482 | 3489 | | |
3483 | 3490 | | |
3484 | 3491 | | |
3485 | | - | |
| 3492 | + | |
| 3493 | + | |
| 3494 | + | |
3486 | 3495 | | |
3487 | 3496 | | |
3488 | 3497 | | |
| |||
3653 | 3662 | | |
3654 | 3663 | | |
3655 | 3664 | | |
3656 | | - | |
| 3665 | + | |
| 3666 | + | |
| 3667 | + | |
3657 | 3668 | | |
3658 | 3669 | | |
3659 | 3670 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
| 22 | + | |
22 | 23 | | |
23 | 24 | | |
24 | 25 | | |
| |||
1065 | 1066 | | |
1066 | 1067 | | |
1067 | 1068 | | |
1068 | | - | |
| 1069 | + | |
| 1070 | + | |
| 1071 | + | |
| 1072 | + | |
| 1073 | + | |
| 1074 | + | |
| 1075 | + | |
1069 | 1076 | | |
1070 | 1077 | | |
1071 | 1078 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
105 | 105 | | |
106 | 106 | | |
107 | 107 | | |
| 108 | + | |
| 109 | + | |
108 | 110 | | |
109 | 111 | | |
110 | 112 | | |
| |||
2844 | 2846 | | |
2845 | 2847 | | |
2846 | 2848 | | |
2847 | | - | |
2848 | | - | |
2849 | | - | |
| 2849 | + | |
| 2850 | + | |
| 2851 | + | |
| 2852 | + | |
| 2853 | + | |
| 2854 | + | |
| 2855 | + | |
| 2856 | + | |
| 2857 | + | |
2850 | 2858 | | |
2851 | 2859 | | |
2852 | 2860 | | |
| |||
2915 | 2923 | | |
2916 | 2924 | | |
2917 | 2925 | | |
2918 | | - | |
2919 | | - | |
2920 | | - | |
2921 | | - | |
| 2926 | + | |
| 2927 | + | |
| 2928 | + | |
| 2929 | + | |
| 2930 | + | |
| 2931 | + | |
2922 | 2932 | | |
2923 | 2933 | | |
2924 | 2934 | | |
| |||
4790 | 4800 | | |
4791 | 4801 | | |
4792 | 4802 | | |
4793 | | - | |
| 4803 | + | |
| 4804 | + | |
| 4805 | + | |
| 4806 | + | |
| 4807 | + | |
| 4808 | + | |
| 4809 | + | |
4794 | 4810 | | |
4795 | 4811 | | |
4796 | 4812 | | |
| |||
5303 | 5319 | | |
5304 | 5320 | | |
5305 | 5321 | | |
| 5322 | + | |
| 5323 | + | |
| 5324 | + | |
5306 | 5325 | | |
5307 | 5326 | | |
5308 | | - | |
| 5327 | + | |
5309 | 5328 | | |
5310 | 5329 | | |
5311 | 5330 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
6 | 6 | | |
7 | 7 | | |
8 | 8 | | |
9 | | - | |
| 9 | + | |
10 | 10 | | |
11 | 11 | | |
12 | 12 | | |
| |||
48 | 48 | | |
49 | 49 | | |
50 | 50 | | |
51 | | - | |
52 | | - | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
53 | 54 | | |
54 | 55 | | |
55 | 56 | | |
| |||
104 | 105 | | |
105 | 106 | | |
106 | 107 | | |
107 | | - | |
108 | | - | |
109 | | - | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
110 | 111 | | |
111 | 112 | | |
112 | 113 | | |
113 | 114 | | |
114 | 115 | | |
115 | 116 | | |
116 | | - | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
117 | 133 | | |
118 | | - | |
119 | | - | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
120 | 137 | | |
121 | 138 | | |
122 | 139 | | |
| |||
0 commit comments