Skip to content

fix(cli,runtime): a named artifact that is missing is a broken instruction, not an empty boot — and the ordering claims say what the kernel resolves (#4110 follow-up, #4131) - #4138

Merged
os-zhuang merged 4 commits into
mainfrom
claude/os-serve-artifact-startup-cgy5aq
Jul 30, 2026
Merged

fix(cli,runtime): a named artifact that is missing is a broken instruction, not an empty boot — and the ordering claims say what the kernel resolves (#4110 follow-up, #4131)#4138
os-zhuang merged 4 commits into
mainfrom
claude/os-serve-artifact-startup-cgy5aq

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

#4110 / #4085 的跟进。一条原则贯穿全部改动:「平台可以没有应用」是关于能力的陈述,它不授权把「缺失的、被指名的输入」猜成「你是想空启动」。

原本这个 PR 只是修一条假注释(#4131 第一步)。推演「裸 os serve 是否该合流到 os start 的空启动」这个问题时,我发现 #4110 自己在 artifact 一侧留了同类的洞,于是一并修掉 —— 都是同一条判据。

一、我在 #4110 留的洞:被指名的 artifact 缺失会静默空启动

#4110 让「artifact 不存在」在整条链路上都不致命(loadArtifactBundle 记日志返回 null、MetadataPlugin 空状态启动)。对约定位置 <cwd>/dist/objectstack.json 是对的 —— 那只是「还没编译」。但 OS_ARTIFACT_PATH / { artifactPath } 按设计跳过存在性检查(交给 loader 惰性校验),于是宽容度也漏给了它们:

OS_ARTIFACT_PATH=/nope os serve
→   No objectstack.config.ts found — booting from artifact (default host)...
→   ✓ Server is ready

整个启动输出里那个路径出现 0 次(实测)。serve 的静默启动窗口会丢弃 loader 那句平静的 ENOENT 行 —— 因为 #4110 把它从 warn(会被 #4084 缓存回放)改成了 log(被丢弃)。#4110 之前这个 case 是大声死掉的,所以这是我引入的退步。

判据是「被指名 vs 约定」,不是 errno。 createDefaultHostConfig(无 config 的启动,artifact 就是整个部署)现在对「被指名的本地 artifact 不存在」抛错,指名路径指名它的来源。loader 的宽容度不动,createStandaloneStack 完全不碰 —— #4085 修好的 config 启动路径不受影响。

二、serve 的「找不到」现在说出它找过哪里

两种真实情况是文件名拼错cwd 不对,后者更常见。现在给出 config 绝对路径、artifact 绝对路径、以及 OS_ARTIFACT_PATH is not set.,并指向 objectstack start(那才是有意的无应用启动)。

行为不变:仍然报错退出。os serve 是被要求去加载某个东西的;凭空变出一个零对象平台会把错误藏在一个跑着的服务器后面 —— 正是 #4085 的形状。

三、这条报错原本会被截断(测试逮到的,不是 review 逮到的)

新加的 e2e 第一次跑就红了:输出只剩前两行。this.exit(1) 走到 oclif 的 process.exit不等管道 stdout 排空,多次 console.log 的尾巴丢失 —— 丢掉的恰好是「去哪儿找」。这就是 #4012 的形状。两处 pre-flight 报错都改成一次 write

其中端口占用那条的验证状态我说明白:靠构造保证(一次 write),没有 driven test —— 触发它需要生产模式下占用端口,我没为它写 e2e。

四、#4131 第一步:顺序声明说实话(含一条钉错东西的测试)

createStandaloneStack 的注释声称 defaultDatasourcePlugin数组位置 "MUST precede ObjectQLPlugin: its start() connects the default driver"。两处都不对:连接在 init();而依赖图会把 ObjectQLPlugin 拓扑提前到它前面(实测 resolved init:objectql #10、default-datasource #16)。

更值得修的是:standalone-stack.test.ts 里那条测试断言的正是这个数组下标,理由写着「schema sync needs the driver」—— 和假注释同一个错误信念。有人把真契约破坏掉(把 connect 从 init() 挪到 start()),它照样绿。现在改成钉真正定序的那条声明依赖:删掉数组位置它不会响,删掉 dependencies 它会响。

真实契约(phase 切分 + 声明依赖)本来就记在 default-datasource-plugin.ts"Ordering — phase, not list position" 里,那段还记着当初教会我们这件事的「shipping a server with no tables」。#4131 记录的是病根:同一子系统被同类顺序 bug 咬过两次,两次的修法都是写注释

验证

packages/runtime/src/standalone-stack.test.ts —— 18 passed(12 → 18):

  • 被指名的 OS_ARTIFACT_PATH 缺失 → 抛错且指名路径
  • 显式 artifactPath 缺失 → 抛错;
  • requireArtifact: false依然抛错(「artifact 可选」≠「忽略你指名的那个」);
  • 控制组:无人指名时保留原有 No artifact source available 报错(守卫不得吞掉它);
  • 控制组:http(s):// 不做文件系统检查。

packages/cli/test/serve-no-artifact.e2e.test.ts —— 4 passed(驱动真实 os serve,全文件无 os compile):无 artifact 的应用能启动并出现在已启动插件集合里 / 裸 export default {} 能启动 / 无 config 无 artifact 时拒绝并说出找过哪里且不曾 ready / 无法注册的 app 不拖垮平台且打印原因。

反向验证:撤掉 default-host.ts 的守卫 → 3 条新测试失败、其余 15 条不变;截断那条则是 e2e 先红后绿。

自动化:pnpm build 全量通过(含 tsc);@objectstack/runtime 926 passed、@objectstack/cli 881 passed;改动文件 ESLint 无告警。base 已合入最新 main(含 #4133#4116 ledger 的修复),本地 check:wildcard-fallthrough 在合并后的树上 ✓ 7 yielding / 0 ratcheted / 5 exempt

未纳入

不新增 --empty 之类用户面开关:OS_BOOT_EMPTY=1 已存在、os start 已覆盖「有意的无应用启动」。裸 os serve 合流到空启动 —— 理由见第二节。

🤖 Generated with Claude Code

https://claude.ai/code/session_01Ve9HidCGRGtS2UjNPUGHSV

…he order the kernel actually resolves (#4131)

The comment on `defaultDatasourcePlugin`'s slot claimed the position was
load-bearing — "MUST precede ObjectQLPlugin: its start() connects the default
driver … ObjectQLPlugin.start() runs boot schema-sync right after". Both halves
are wrong, and reading the slot as a guarantee is how #4085 happened:

- the connect happens in `init()`, not `start()`;
- the position cannot deliver that order anyway. `DefaultDatasourcePlugin`
  declares a hard dependency on ObjectQL, so `resolveDependencies` HOISTS
  ObjectQLPlugin ahead of it. Measured on a real serve boot:
  `com.objectstack.engine.objectql` inits at #10,
  `com.objectstack.runtime.default-datasource` at #16 — the reverse of what the
  slot reads as.

What actually orders them is the phase split (Phase 1 completes before any
`start()`) plus that declared dependency — the contract already documented on
the plugin itself, under its "Ordering — phase, not list position" note, which
records the no-tables boot that taught it to us. This comment now points there
instead of contradicting it two files away.

Comment only; no behaviour changes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ve9HidCGRGtS2UjNPUGHSV
@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 30, 2026 11:33am

Request Review

@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/cli, @objectstack/runtime.

30 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/skills-reference.mdx (via packages/cli)
  • content/docs/api/client-sdk.mdx (via @objectstack/cli, packages/runtime)
  • content/docs/api/data-flow.mdx (via @objectstack/cli)
  • content/docs/api/environment-routing.mdx (via @objectstack/cli)
  • content/docs/api/error-catalog.mdx (via @objectstack/cli)
  • content/docs/api/index.mdx (via @objectstack/runtime)
  • content/docs/api/wire-format.mdx (via @objectstack/runtime)
  • content/docs/automation/hook-bodies.mdx (via packages/cli, @objectstack/runtime)
  • content/docs/concepts/north-star.mdx (via packages/runtime)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/runtime)
  • content/docs/deployment/backup-restore.mdx (via @objectstack/cli)
  • content/docs/deployment/cli.mdx (via @objectstack/cli)
  • content/docs/deployment/index.mdx (via @objectstack/runtime)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/runtime)
  • content/docs/deployment/self-hosting.mdx (via @objectstack/cli)
  • content/docs/deployment/single-project-mode.mdx (via @objectstack/runtime)
  • content/docs/deployment/vercel.mdx (via @objectstack/runtime)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/cli, @objectstack/runtime)
  • content/docs/kernel/runtime-services/data-service.mdx (via packages/cli)
  • content/docs/kernel/runtime-services/index.mdx (via packages/cli)
  • content/docs/permissions/authentication.mdx (via @objectstack/cli, @objectstack/runtime)
  • content/docs/permissions/authorization.mdx (via packages/runtime)
  • content/docs/plugins/packages.mdx (via @objectstack/cli, @objectstack/runtime)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/cli)
  • content/docs/protocol/kernel/realtime-protocol.mdx (via @objectstack/cli)
  • content/docs/releases/implementation-status.mdx (via @objectstack/cli, @objectstack/runtime)
  • content/docs/releases/v16.mdx (via @objectstack/cli)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

@github-actions github-actions Bot added documentation Improvements or additions to documentation tooling size/s labels Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

CI 红在 ESLint job,但挂的不是 ESLint —— 是同一 job 里的 pnpm check:wildcard-fallthrough 步骤:

✗ wildcard fall-through guard (#4116)
  packages/adapters/hono/src/index.ts:all `${prefix}/storage/*`
    DECLARED but not found by the scan. Moved, renamed or deleted? Update MOUNTS.

这是 base 分支的失败,不是本 PR 造成的。 本 PR 的全部改动是 packages/runtime/src/standalone-stack.ts 的一段注释加一个空 changeset(+23 / −3),packages/adapters/hono/src/index.ts 与 main 逐字节相同。在干净的 origin/main(detached、working tree 无任何本地差异)上跑同一个脚本,输出完全一致。

成因是 #4122(引入这个 guard,并在 MOUNTS 里登记 /storage/*)与 #4112 / #4087(按设计退役了那个 mount)的合并竞态。#4133 已经在修这条(其正文第二节就是删掉这条 ledger 条目),所以我不动它 —— 那是别人在飞的改动。

本 PR 其余检查在同一轮里是绿的:TypeScript ✅ Build Core ✅ Temporal Conformance ✅ Changeset ✅ PR Size ✅ Auto Label ✅ filter ✅ docs-flag ✅(Test Core / Dogfood 仍在跑)。

#4133 落地后我会把 main 合进来重跑,届时如果还红就是本 PR 的问题,我自己修。


Generated by Claude Code

claude added 2 commits July 30, 2026 11:29
…ction, not an empty boot (#4110 follow-up)

#4110 made an absent artifact non-fatal all the way down — right for the
conventional `<cwd>/dist/objectstack.json`, which is simply "not compiled yet".
But `OS_ARTIFACT_PATH` / `{ artifactPath }` skip the existence check by design
(the loader was to validate lazily), so that tolerance reached them too:

    OS_ARTIFACT_PATH=/nope os serve
    → No objectstack.config.ts found — booting from artifact (default host)...
    → ✓ Server is ready

with the missing path named NOWHERE in the output — serve's boot-quiet window
drops the loader's calm ENOENT line, which #4110 had moved from `warn` (buffered
and replayed by #4084) to `log` (discarded). Before #4110 this died loudly.

The distinction that matters is NAMED vs CONVENTIONAL, not the errno.
`createDefaultHostConfig` — the boot with no config, where the artifact IS the
deployment — now rejects a named local artifact that does not exist, naming the
path and which source named it. The loader keeps its tolerance, so the
config-boot path #4085 fixed is untouched, and `createStandaloneStack` is not
touched at all.

Two more corrections in the same spirit:

- `serve`'s "config not found" refusal never said WHERE it looked. The two things
  that actually happen are a typo'd filename and the wrong working directory, and
  the second is the common one. It now names the config path, the artifact path,
  and that OS_ARTIFACT_PATH is unset — and still refuses rather than inventing a
  zero-object platform, pointing at `objectstack start` for a boot that is
  app-less on purpose.

- That refusal was being TRUNCATED. `this.exit(1)` unwinds to oclif's
  `process.exit`, which does not drain a piped stdout, so a diagnostic split
  across several `console.log` calls loses its tail: measured, only the first two
  lines survived — i.e. exactly the "where to look" part went missing. Both of
  serve's pre-flight refusals emit one write now. The e2e added here caught it;
  review had not.

Also fixes a test that pinned the wrong thing: `the DefaultDatasourcePlugin
precedes ObjectQLPlugin (schema sync needs the driver)` asserted an ARRAY INDEX
with a rationale the kernel does not implement — the connect happens in `init()`,
and the dependency graph hoists ObjectQLPlugin ahead of the datasource plugin
(measured: 6 slots earlier). It now pins the declared dependency that actually
orders the two inits, which deleting the array position cannot break and
deleting the declaration does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ve9HidCGRGtS2UjNPUGHSV
@os-zhuang os-zhuang changed the title docs(runtime): the ordering note in createStandaloneStack describes the order the kernel actually resolves (#4131) fix(cli,runtime): a named artifact that is missing is a broken instruction, not an empty boot — and the ordering claims say what the kernel resolves (#4110 follow-up, #4131) Jul 30, 2026
#4110 follow-up)

The artifact resolution-priority line reads as one fall-through list, so
`--artifact` / `OS_ARTIFACT_PATH` look like they degrade to
`<cwd>/dist/objectstack.json` when they do not resolve. They never did: a named
source is used as given, without an existence check.

That gap is what let #4110's "an absent artifact is not fatal" reach a case it
was never meant to cover, so the doc now states the rule it always followed —
named is used as-is and must exist, the fall-through is for the conventional
locations only — alongside the fail-loud behaviour restored in this PR.

Worth recording where the invariant was already written down: `os start`'s
`resolveArtifactSource` deliberately does NOT exit on a missing `--artifact`,
with the comment "Defer to serve.ts which already prints a precise error". #4110
silently removed that error; this PR restores what `os start` documents it
depends on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ve9HidCGRGtS2UjNPUGHSV
@os-zhuang
os-zhuang marked this pull request as ready for review July 30, 2026 11:51
@os-zhuang
os-zhuang merged commit 1e38158 into main Jul 30, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/os-serve-artifact-startup-cgy5aq branch July 30, 2026 11:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants