Repository navigation
Commit e909aa0
fix(cli): os dev -a and os start --artifact serve the named artifact beside a cwd objectstack.config.ts — one artifact precedence for start, dev and the serve child (#21549)
Fixes #21501
Clause-②: no
## What changed
The ruling on the card is one precedence, written once: explicit flag >
env (`OS_ARTIFACT_URL` / `OS_ARTIFACT_PATH`) > `dist/objectstack.json` >
a cwd `objectstack.config.ts`. Triage amended it in `5966519064`: a cwd
config joins the boot when the resolved artifact is its own compiled
output, because a host config's compiled file cannot carry its code
plugins. This PR builds that order in five parts.
- **The one resolver lives in
`packages/cli/src/utils/artifact-precedence.ts`.**
- `resolveArtifactBootSource()` holds the ladder: `--artifact` >
`OS_ARTIFACT_URL` > `OS_ARTIFACT_PATH` > `CWD/dist/objectstack.json` >
`HOME/dist/objectstack.json` (`os start` only) > unresolved, which is
the cwd config. `os start` and `os dev` both resolve through it.
- `cwdConfigJoinsBoot()` answers the last rung, as amended: the config
joins only when the resolved artifact is its own compiled output. The
supervisors' `Config:` row and the `serve` child both read this one
predicate.
- `start.ts` drops its private `resolveArtifactSource` and its inline
`OS_ARTIFACT_URL` handling. `dev.ts` drops its inline ladder, which had
no `OS_ARTIFACT_URL` rung.
- A structural pin refuses any read of `OS_ARTIFACT_PATH` or
`OS_ARTIFACT_URL` in `start.ts` or `dev.ts`.
- **The `serve` child boots the supervisor's answer beside a config**
(`serve.ts`).
- Before, the child read `OS_INTERNAL_ARTIFACT_PATH` only when the cwd
held no config. With a config present, the config boot ran instead, and
its standalone stack re-derived the artifact from the environment.
- Now the config joins only when the answer is the config's own compiled
output. Any other answer boots alone, exactly as from a directory with
no config. When the config joins, the config boot is handed the answer
explicitly.
- **A config's compiled output, recognised where the command compiled
it** (`artifact-precedence.ts`, `internal-artifact-channel.ts`,
`dev.ts`, `serve.ts`).
- The conventional `CONFIGDIR/dist/objectstack.json` still counts. So
does the path the supervising command itself compiles the config to.
- `os dev` under a local `OS_ARTIFACT_PATH` compiles the cwd config INTO
that path, so it declares that path to the child on a second private
variable of the same channel, `OS_INTERNAL_CONFIG_OUTPUT_PATH`. That
variable is set only when declared and is owned by the parent.
- `isConfigCompiledArtifact(path, configPath, compiledTo)` recognises
either place, so a host config compiled to a named path still composes
its plugins.
- **The honest ready banner** (`serve.ts`, `utils/format.ts`). On a
config boot, the child's ready banner names what actually loaded:
- `Artifact: dist/objectstack.json` when a non-host config's standalone
stack served the app from a compiled bundle. The proof is the stack's
own AppPlugin over that bundle, and the path comes from the runtime's
own `resolveDefaultArtifactPath` over the same explicit input;
- `Config: objectstack.config.ts` for a host config (its `plugins` hold
code) or a config with no bundle loaded.
- No ready-banner row names a file the boot did not load. No `os start`
stale-artifact warning is added, per triage.
- **`os dev` flag over env** (`internal-artifact-channel.ts`). A
`resolved` channel decision removes `OS_ARTIFACT_URL` from the child
env. Without a flag, `dev` treats a reference the way `start` does: it
hands no channel down, prints a redacted `Artifact: ...
(OS_ARTIFACT_URL)` row, and does not compile, watch or run the staleness
check.
- **Docs**: in `content/docs/deployment/cli.mdx`, the `os dev` options
row lists `-a`'s env equivalents as `OS_ARTIFACT_URL` /
`OS_ARTIFACT_PATH`, as the resolver's ladder says. That is the only docs
edit. The PM declares this docs path to `domain:devx`.
What does not change:
- A bare `os dev`, a bare `os start` in a project, and the documented
`os start --artifact ./dist/objectstack.json` all name the config's own
compiled output, so the config still joins on those paths. The showcase
is a host config, and it still boots itself.
- A direct `os serve` is untouched, since no supervisor channel is
involved.
- `packages/runtime`'s own fallback ladder is not edited.
## Measured at the public door
Two artifacts differ in one served value, the label of object
`fx_widget`. The label was read back through `GET
/api/v1/meta/object/fx_widget`, booted through the built entry
`bin/run.js`.
| boot | before (base `550f4cc2fd`) | after |
|---|---|---|
| leg 1: `os dev -a ALPHA`, beside a config whose `dist/` holds BRAVO |
Widget BRAVO | Widget ALPHA |
| leg 2: `os start --artifact ALPHA`, same directory | Widget BRAVO |
Widget ALPHA |
| leg 2: `os start --artifact ALPHA`, config but no `dist/` | Widget
CONFIG | Widget ALPHA |
| leg 2 control: `os start --artifact ALPHA`, no config | Widget ALPHA |
Widget ALPHA |
| `os start --artifact ALPHA`, beside a host config (plugin instance in
`plugins`) | Widget CONFIG | Widget ALPHA |
| `OS_ARTIFACT_URL=file://.../BRAVO.json os dev -a ALPHA` | Widget BRAVO
| Widget ALPHA |
| `OS_ARTIFACT_PATH=ALPHA os start --artifact ./dist/objectstack.json`
(dist = BRAVO) | Widget ALPHA | Widget BRAVO |
| `os start --artifact ./dist/objectstack.json` beside its config
(documented path) | Widget BRAVO | Widget BRAVO |
| bare `os start` beside a host config, `dist/` = BRAVO: ready-banner
row | `Config: objectstack.config.ts` (served CONFIG; the supervisor row
said `Artifact: dist/objectstack.json`) | `Config:
objectstack.config.ts`, served CONFIG |
| bare `os start` beside a non-host config, `dist/` = BRAVO:
ready-banner row | `Config: objectstack.config.ts` (served BRAVO) |
`Artifact: dist/objectstack.json`, served BRAVO |
| `OS_ARTIFACT_PATH=build/named.json os dev` beside a host config:
plugin roster | marker absent at the round-1 head (ablations D and E
below) | marker present, `Config: objectstack.config.ts` |
**`Artifact:` banner.** Before the fix, the supervisor printed
`Artifact:` from its own resolution before spawning. The child then
printed `Loading objectstack.config.ts...` and a ready-banner `Config:`
row, so one screen named two sources. After the fix:
- the child boots exactly the supervisor's answer;
- beside a config it does not load, the child says so;
- the supervisors print `Config:` only when `cwdConfigJoinsBoot` says
the config takes part;
- the child's ready banner names the config or the bundle it actually
loaded.
**Env leg.** `OS_ARTIFACT_URL` already outranked a cwd config. Two
flag-over-env violations were in scope and are now fixed and pinned: the
`os dev` reference case and the twin-plus-`OS_ARTIFACT_PATH` case.
`OS_ARTIFACT_PATH` beside a config now boots that artifact alone under
`os start`. Under `os dev`, it is the path dev compiles the config to,
so the config joins.
**Raise rule.** No deploy was measured serving a different stack this
way. The shipped runtime image and the scaffolded `Dockerfile` copy only
the artifact into `/srv/app`, with no config beside it.
## Pins
- `packages/cli/test/artifact-flag-precedence.integration.test.ts`
(integration tier) runs 10 cases over the source entry. All boots happen
in `beforeAll`.
- leg 1, leg 2 with and without `dist/`, and the leg 2 no-config
control;
- a host config beside a named artifact. This is read through the boot's
plugin roster, because the source entry runs `NODE_ENV=development`,
where the dev metadata door serves the channel's artifact even with the
config loaded;
- a bare `os start` beside a host config with a differing `dist/`: the
ready banner says `Config:`, and the roster marker is present (its
positive control);
- a bare `os start` beside a non-host config: the ready banner says
`Artifact: dist/objectstack.json`;
- `os dev` under `OS_ARTIFACT_PATH=build/named.json` beside a host
config: the config is compiled there and still composes its plugins;
- `dev -a` under `OS_ARTIFACT_URL`;
- the documented path under an exported `OS_ARTIFACT_PATH`.
- `packages/cli/src/commands/artifact-child-env.pin.test.ts`:
- the ladder over `resolveArtifactBootSource`;
- `cwdConfigJoinsBoot` and `isConfigCompiledArtifact`, including the
command's own compile path;
- the channel's ownership of both private variables, and its removal of
an outranked `OS_ARTIFACT_URL`;
- the structural no-second-ladder pin.
- The `serve-banner-config-row.test.ts` and
`format.config-artifact-row.test.ts` unit pins cover the new bundle row.
## Reverse verification
All mutations ran through `scripts/ablation-replace.mjs` in WRAP mode on
the committed tree, with literal anchors. The subject runs from `src/`
through `bin/run-dev.js`, so there is no `dist/` leg.
| ablation | predicted red | observed |
|---|---|---|
| A: the whole serve fix | leg 1, leg 2, leg 2 no-dist, host config,
documented path | exactly those 5 red |
| A1: `configJoins` ignores the channel | host config | 1 red |
| A2: config boot not handed the answer | documented path under
`OS_ARTIFACT_PATH` | 1 red |
| B: channel keeps `OS_ARTIFACT_URL` | flag over env, channel unit pin |
2 red |
| C1: ready banner never names the bundle | bare non-host banner
(integration) and the banner-row unit pin | 2 red, 14 green |
| C2: banner names what the supervisor resolved, not what loaded | bare
host-config banner | 1 red, 9 green |
| D: channel never hands down the compile path | named-path host case
and the channel unit pin | 2 red, 41 green |
| E: predicate ignores the command's compile path | named-path host case
and the predicate unit pin | 2 red, 41 green |
Every run ended with `ok restored: blob == HEAD` and an empty `git diff
HEAD`. Ablations A to B ran at round 1 (`f5c0a890b7`), and C1 to E at
round 2.
## Verification (final commit `f634c5bf09`)
- `pnpm --filter @objectstack/cli exec vitest run --project unit`: 251
files, 3690 tests passed.
- `pnpm --filter @objectstack/cli typecheck` passed, including
`check:test-typecheck: OK` with the debt ledger unchanged.
- Integration: `artifact-flag-precedence.integration.test.ts` and
`dev-no-watch.pin.test.ts`, 18 of 18 passed;
`artifact-child-env.pin.test.ts`, 33 of 33.
- `dispatch-gates --commands --repo objectstack-ai/objectstack` derived
97 families at `f634c5bf09`. All 97 ran with exit 0, after a full `turbo
run build`. `--ran` reconciliation: 97 derived, 97 run, 0 NOT-MEASURED,
0 UNRUN, with the zero derived from recorded exit codes.
- `pnpm lint`: a proven narrowing, not the repo-wide run.
- The population comes from `eslint.config.mjs`'s own globs, and none of
the 10 changed `.ts` files is ignored.
- `eslint --no-inline-config --format json` over the 10 files reports 10
results, 0 errors, 0 warnings.
- The config has no `parserOptions.project` and reads only two untouched
baseline JSONs, so untouched files' verdicts cannot move.
- Main was merged in at `c4528fad62` (`10454b3afa`). Main has not moved
under `serve.ts`, `dev.ts` or `start.ts` since.
## Acceptance notes
- **The supervisor's pre-boot `Artifact:` row on the config-joins path
is unchanged.** On a bare `os start` beside a host config, `os start`
still prints `📦 Artifact: dist/objectstack.json` before it spawns, and
that file is not what boots (non-dev). The child's ready banner after
the boot now says `Config: objectstack.config.ts`, which is the row
triage named for this. The supervisor cannot tell a host config from a
non-host one without loading the config. Dropping or rewording its row
would change every bare `os dev` / `os start` banner, so this round does
not do it, and the seat decides.
- `os dev`'s handling of `OS_ARTIFACT_PATH` is pre-existing. It compiles
into that path when the file is missing (or under `--compile`), and its
watch loop rebuilds there. This PR only makes the child recognise that
file as the config's output.
- The round-1 merge commit `f5c0a890b7` carries no trailer pair. Every
other commit carries the model-free pair.
---
_Generated by [Claude
Code](https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 7d674df commit e909aa0
12 files changed
Lines changed: 1225 additions & 212 deletions
File tree
- .changeset
- content/docs/deployment
- packages/cli
- src
- commands
- 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
181 | 181 | | |
182 | 182 | | |
183 | 183 | | |
184 | | - | |
| 184 | + | |
185 | 185 | | |
186 | 186 | | |
187 | 187 | | |
| |||
Lines changed: 223 additions & 25 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
16 | | - | |
17 | | - | |
18 | | - | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
19 | 26 | | |
20 | 27 | | |
21 | 28 | | |
| |||
37 | 44 | | |
38 | 45 | | |
39 | 46 | | |
| 47 | + | |
40 | 48 | | |
41 | 49 | | |
| 50 | + | |
42 | 51 | | |
43 | | - | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
44 | 57 | | |
45 | 58 | | |
46 | 59 | | |
| |||
99 | 112 | | |
100 | 113 | | |
101 | 114 | | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
102 | 151 | | |
103 | 152 | | |
104 | 153 | | |
| |||
140 | 189 | | |
141 | 190 | | |
142 | 191 | | |
143 | | - | |
| 192 | + | |
144 | 193 | | |
145 | 194 | | |
146 | 195 | | |
| |||
150 | 199 | | |
151 | 200 | | |
152 | 201 | | |
| 202 | + | |
| 203 | + | |
153 | 204 | | |
154 | 205 | | |
155 | 206 | | |
| |||
161 | 212 | | |
162 | 213 | | |
163 | 214 | | |
164 | | - | |
| 215 | + | |
165 | 216 | | |
166 | 217 | | |
167 | 218 | | |
168 | 219 | | |
169 | | - | |
| 220 | + | |
| 221 | + | |
170 | 222 | | |
171 | | - | |
| 223 | + | |
| 224 | + | |
172 | 225 | | |
173 | | - | |
| 226 | + | |
174 | 227 | | |
175 | 228 | | |
176 | 229 | | |
177 | 230 | | |
178 | | - | |
| 231 | + | |
179 | 232 | | |
180 | 233 | | |
181 | | - | |
182 | | - | |
183 | | - | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
184 | 238 | | |
185 | | - | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
186 | 242 | | |
187 | | - | |
| 243 | + | |
| 244 | + | |
188 | 245 | | |
189 | | - | |
190 | | - | |
191 | | - | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
192 | 262 | | |
193 | 263 | | |
194 | | - | |
| 264 | + | |
195 | 265 | | |
196 | | - | |
| 266 | + | |
197 | 267 | | |
198 | 268 | | |
199 | 269 | | |
200 | 270 | | |
201 | 271 | | |
202 | 272 | | |
203 | | - | |
| 273 | + | |
| 274 | + | |
204 | 275 | | |
205 | 276 | | |
206 | | - | |
| 277 | + | |
207 | 278 | | |
208 | | - | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
209 | 342 | | |
210 | 343 | | |
211 | | - | |
212 | | - | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
213 | 352 | | |
214 | 353 | | |
215 | 354 | | |
| |||
311 | 450 | | |
312 | 451 | | |
313 | 452 | | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
0 commit comments