Skip to content

fix(core,plugin-dev,runtime): every in-memory fallback and dev stub self-describes honestly (#4058 step 1) - #4082

Merged
os-zhuang merged 1 commit into
mainfrom
claude/plugin-dev-analytics-stub-cleanup-ht12kj
Jul 30, 2026
Merged

fix(core,plugin-dev,runtime): every in-memory fallback and dev stub self-describes honestly (#4058 step 1)#4082
os-zhuang merged 1 commit into
mainfrom
claude/plugin-dev-analytics-stub-cleanup-ht12kj

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

#4058 的第一步(分类),不动任何门。#4062 落地后的自然延续。

为什么先做这一步

#4058 真正要回答的是"其他 dispatcher 域要不要照 analytics 那样加 handlerReady 门"。这个问题之前答不了,因为 plugin-dev 里所有实现共用一个 _dev: true,被归一化成 { status: 'stub', handlerReady: false } —— 它把"真能用的内存实现"和"编造答案的假实现"宣布为同一种东西。分不开,就没法逐域取舍。

盘的过程中还捞出一条比 plugin-dev 更严重、且不在 D12 原始清单里的:内核自己的 fallbackcreateMemoryCache / Queue / Job / I18n / Metadata,由 ObjectKernel 自动注册)带的是 _fallback: true —— 这个 key 没有任何 consumer 认识readServiceSelfInfo 也不认。于是两个 discovery builder 都把它们报成完整可用的 available。D12 要修的那个谎,在内核自己的兜底实现上一直没被修到。

分类规则

判据只有一条:答案是不是真的

  • degraded —— 真干活,能力打折cache / queue / job / file-storage / search / i18n / metadata / workflow / realtime。storage 真的存进去也真的取得回来,search 真的建索引真的匹配,metadata 真的注册真的列得出。把它们说成 stub 是低估,会让 consumer 放弃一个其实能用的能力。每条的 message 点名缺的是什么 —— 包括容易漏看的两处:memory-jobschedule() 永远不会自己触发(没有 timer,只有 trigger() 真跑),workflow stub 不校验任何状态机(每次 transition 都接受)。
  • stub —— 答案是编的ai(编造文本)/ automation(不跑 flow 直接报成功)/ notification(声称已送达,实际什么都没发)/ data(收下写入,一行不存)/ auth(一律放行为固定 dev admin)/ security.*(allow-all)。这些绝不能被当成能力 —— D12 里记的"已经误导过 agent"就是 AI stub。

handlerReady 回答的是另一个问题(有没有 HTTP handler 真的在服务):所有 stub 为 false,加上没有 HTTP 面的 degradedcache / queue / job / realtime —— realtime 是 D12 立的先例)。

分类写在一张表里(DEV_STUB_SELF_INFO),因为这次改动的产物就是这张表,它应该能被一眼 review。

其他

  • plugin-dev 不再覆盖实现自带的自述:被它包装的内核 fallback(createMemoryCache 等)比这个插件更清楚自己是什么,applySelfInfo 见到已有 __serviceInfo 就不动。
  • 唯一一处 duck-type 读旧标记的 consumer(app-plugin 里的 i18n 诊断日志 svcAny._fallback || svcAny._dev)改读 readServiceSelfInfo
  • content/docs/kernel/services.mdx 的"可选服务"示例原来用 getServices().has('analytics') + 虚构的 analytics.track():既不是真契约,又恰好示范了 D12 要 consumer 改掉的"只判存在"。换成真契约,并补了一段"presence is not capability"说明怎么读 __serviceInfo
  • ADR-0076 D12 的 fakes 清单补上内核 fallback 这条,并标注 step 1 已落地、step 2(各域的门)仍开着。

影响面

不改任何路由/门/dispatch 行为 —— 每个 dispatcher 域解析服务的方式和之前完全一样。变的是 discovery 输出:以前谎称 available 的内核 fallback 现在报 degraded 并说明原因;以前被一律打成 stub 的 storage/search/metadata 等现在报 degraded_dev 仍被 readServiceSelfInfo 认识,第三方带这个标记的 stub 不受影响。

测试

⚠️ 与本 PR 无关的既有失败:runtimedatasource-autoconnect.test.ts > makes the federated object queryable…。单独跑必现(3/3),跑整包时偶尔能过;ext_note 读到 6 行(单独跑时 12 行)而非 2 行。与本改动无关(改动前后一致,回退 driver-memory#4048 前也一样),main 的 CI 是绿的。已另开 issue 记录。


Generated by Claude Code

…elf-describes honestly (#4058 step 1)

ADR-0076 D12 gave services one standard way to say "I am not the real thing"
(`__serviceInfo`), but the producers never converged on it, and the result was
wrong in both directions:

- The kernel's OWN fallbacks (createMemoryCache/Queue/Job/I18n/Metadata) carried
  `_fallback: true` — a marker NO consumer recognized, `readServiceSelfInfo`
  included — so both discovery builders reported them as fully `available`.
  They were absent from D12's inventory and were its worst case.
- plugin-dev marked every implementation with the same `_dev: true`, normalized
  to `{ status: 'stub', handlerReady: false }`. That declared a working
  in-memory search index exactly as fake as an AI stub returning invented text.

The single marker is why #4058's real question — should the dispatcher gate
other domains on `handlerReady`, as #4000 did for analytics? — could not be
answered: the two kinds of implementation in there were indistinguishable.
This change makes them distinguishable, and changes nothing else.

The classification, now one reviewable table (`DEV_STUB_SELF_INFO`):

- `degraded` — really does the work, with reduced capability. `file-storage`
  really stores and returns the bytes, `search` really indexes and matches,
  `metadata`/`workflow` really record and read back, `realtime` really delivers
  in-process, and the wrapped kernel fallbacks describe themselves. Their
  answers are true answers; each `message` names what is missing — including
  the ones that are easy to miss (memory-job's `schedule()` never fires: no
  timer; the workflow stub validates no state machine).
- `stub` — the answer is fabricated: `ai` (invented text), `automation`
  (success without running the flow), `notification` (claims delivery, delivers
  nothing), `data` (accepts writes, stores none), `auth` (waves everyone
  through as an admin), `security.*` (allow-all).

`handlerReady: false` answers the separate question — no HTTP handler serves
this — and is set for every `stub` plus the surfaceless `degraded` ones
(`cache` / `queue` / `job` / `realtime`, the D12 realtime precedent).

Also: the one duck-typed consumer of the ad-hoc markers (an i18n diagnostic in
app-plugin) now reads `readServiceSelfInfo`, and plugin-dev never overwrites a
self-description an implementation already carries — a wrapped kernel fallback
knows better than this plugin what it is.

No routing, gating, or dispatch behavior changes: every dispatcher domain
resolves services exactly as before. Discovery output does change, which is the
point — a kernel fallback that claimed `available` now reports `degraded` and
says why. `_dev` stays understood by `readServiceSelfInfo`, so third-party stubs
carrying it keep working.

#4058 step 2 (which domains adopt the `handlerReady` gate) stays open, now with
the two classes separable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UvcnzFr5XBuvvAd7mosG54
@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 8:56am

Request Review

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/core, @objectstack/objectql, @objectstack/plugin-dev, @objectstack/runtime.

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

  • content/docs/ai/actions-as-tools.mdx (via @objectstack/core)
  • content/docs/ai/knowledge-rag.mdx (via @objectstack/core)
  • content/docs/ai/natural-language-queries.mdx (via @objectstack/core)
  • content/docs/api/client-sdk.mdx (via packages/runtime)
  • content/docs/api/index.mdx (via @objectstack/runtime)
  • content/docs/api/wire-format.mdx (via @objectstack/runtime)
  • content/docs/automation/hook-bodies.mdx (via @objectstack/runtime)
  • content/docs/automation/webhooks.mdx (via @objectstack/core)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/objectql)
  • content/docs/concepts/north-star.mdx (via packages/core, packages/runtime)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/runtime)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql)
  • content/docs/deployment/index.mdx (via @objectstack/runtime)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/core, @objectstack/objectql)
  • content/docs/deployment/production-readiness.mdx (via @objectstack/runtime)
  • content/docs/deployment/single-project-mode.mdx (via @objectstack/runtime)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql, @objectstack/runtime)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/runtime)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/core)
  • content/docs/kernel/runtime-services/examples.mdx (via @objectstack/core)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/core, @objectstack/objectql)
  • content/docs/kernel/services.mdx (via @objectstack/core, @objectstack/objectql)
  • content/docs/permissions/authentication.mdx (via @objectstack/core, @objectstack/objectql, @objectstack/runtime)
  • content/docs/permissions/authorization.mdx (via packages/core, packages/runtime)
  • content/docs/plugins/anatomy.mdx (via @objectstack/core)
  • content/docs/plugins/development.mdx (via @objectstack/core)
  • content/docs/plugins/index.mdx (via @objectstack/core, @objectstack/objectql)
  • content/docs/plugins/packages.mdx (via @objectstack/core, @objectstack/objectql, @objectstack/plugin-dev, @objectstack/runtime)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/core)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/runtime)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/core, @objectstack/objectql, @objectstack/runtime)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/core, @objectstack/runtime)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/core)
  • content/docs/protocol/objectql/query-syntax.mdx (via packages/objectql)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql)
  • content/docs/releases/implementation-status.mdx (via @objectstack/core, @objectstack/objectql, @objectstack/runtime)
  • content/docs/releases/v12.mdx (via @objectstack/core)
  • content/docs/releases/v15.mdx (via @objectstack/core)
  • content/docs/releases/v9.mdx (via @objectstack/objectql)

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.

@os-zhuang
os-zhuang marked this pull request as ready for review July 30, 2026 09:08
@os-zhuang
os-zhuang merged commit 45dc446 into main Jul 30, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/plugin-dev-analytics-stub-cleanup-ht12kj branch July 30, 2026 09:08
os-zhuang pushed a commit that referenced this pull request Jul 30, 2026
…cile with #4082 (#4058 step 1)

#4082 landed the classification half of #4058 while this branch was open, and did
it better: `DEV_STUB_SELF_INFO` is one reviewable table, it covers the kernel's
OWN fallbacks too (the `_fallback: true` marker no consumer recognized — filed
from this branch as #4089, now fixed at the source), it drops the blanket
`_dev: true` from plugin-dev rather than keeping it as a provenance tag, and it
sets `handlerReady: false` on the surfaceless `degraded` fallbacks
(`cache`/`queue`/`job`/`realtime`) — a refinement this branch missed.

So this branch keeps only what #4082 explicitly left open: step 2, the gate.

Dropped from this branch as superseded:
- the per-stub `devDegraded`/`devStub` markers and their tests (main's table wins);
- retiring the `automation` / `notification` / `ai` stubs. Step 1 classes them
  `stub`, so the gate already answers their HTTP surface as an empty slot without
  deleting anything — verified end to end below. Whether the fabricating class
  should be registered at all is the wider evaluation in #4093 (A tier), which is
  where that deletion belongs alongside `data`/`auth`/`security.*`.

Kept: `isServiceServeable` + the six domain gates, the discovery `routes`/
`features` alignment, the metadata-protocol route suppression, their tests, and
the ADR/docs/changeset text — the ADR annotation now reads as step 2 on top of
step 1 instead of re-claiming the classification.

The gate is compatible with step 1's table by construction, because it reads
`handlerReady` and not `status`: `file-storage` / `i18n` / `metadata` / `workflow`
declare `degraded` with `handlerReady` defaulting to true and keep serving, while
the surfaceless `degraded` fallbacks have no dispatcher domain to gate.

Verified against a real plugin-dev slate after the merge: `automation` /
`notification` / `ai` are registered and classed `stub` ⇒ no route advertised,
`GET /automation` and `GET /notifications` unhandled, `POST /ai/chat` 404, and
`GET /ai/agents` still answers `{ agents: [] }`; `file-storage` / `i18n` /
`workflow` / `metadata` keep their routes and serve. runtime 923, plugin-dev 8,
objectql 1183, metadata-protocol 99, core 415 all pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018UNGqBQcdJ2RYHtWgntJ9B
os-zhuang added a commit that referenced this pull request Jul 30, 2026
…lerReady, not slot occupancy (#4058 step 2) (#4086)

#4000 made the `/analytics` domain execute ADR-0076 D12's third conclusion
("consumers treat only `handlerReady: true` as a real capability"). Every other
service domain still gated on "is the slot occupied", so an occupant that
self-declares `handlerReady: false` was called like a real implementation and its
fabricated answer went out as a 200. #4082 (step 1) made the two kinds of
implementation distinguishable; this is the gate that reads the distinction.

One predicate — `isServiceServeable` in `runtime/src/service-serveable.ts`, which
absorbed `isAnalyticsServiceServeable` — is read by the `/analytics`,
`/automation`, `/notifications`, `/ai`, `/storage` and `/i18n` domains, the
route-mount gate, discovery's `routes`/`features`, and the metadata-protocol
builder's route advertisement. A slot whose occupant declares
`handlerReady: false` gets exactly the answer an empty slot gets: the domain's
own 404, or the explicit 501 `/storage` and `/i18n` use. Same predicate on the
advertisement and the handler, so the two cannot disagree.

`handlerReady`, not `status`, is the test, and that is what makes the rule safe
to generalize: an implementation that really works declares `degraded`, which
defaults `handlerReady` to true, so `file-storage` / `i18n` / `metadata` /
`workflow` keep serving. Only the fabricating class is answered as empty.

`discovery.services.*` stays presence-gated: a registered stub still reports
`{ enabled: true, status: 'stub', handlerReady: false }` with no `route`, which
says strictly more than collapsing it to `unavailable` would.

`/ai` improves for the stub case. Being truthy, such an occupant used to fall
through to the `!routes` 503 ("AI service routes not yet initialized") and lose
the `GET /ai/agents` empty-list answer the console polls for on every
navigation. Both are restored.

Deliberately unchanged:

- `/data`, `/meta`, `/auth` and the security path are NOT gated. Their dev stubs
  back the dev stack's own core loop, and gating them would 404 the dev stack
  itself. Step 1 labels them honestly either way.
- No stub is deleted. #4058 preferred retiring the `automation` / `notification`
  / `ai` stubs too, but once step 1 classes them `stub` this gate already makes
  their HTTP surface equal to an empty slot — verified end to end — so "should
  the fabricating class occupy a slot at all" became separable. It is evaluated
  as a class in #4093, together with `data` / `auth` / `security.*`.
- The metadata-protocol route suppression covers only the six dispatcher-owned
  slots. Elsewhere the route belongs to the plugin that registers the service, so
  `handlerReady` says nothing about whether THAT route is mounted; suppressing it
  would be a guess. A test pins that boundary.

Filed out of scope (Prime Directive #10): #4087 (the `/storage` bridge calls
`upload(file, {request})` against the contract's `upload(key, data, options?)` —
pre-existing, reproduced during end-to-end verification) and #4093 (whether the
"stub fills the slot" design should be retired wholesale). #4089 (the kernel
fallbacks' unrecognized `_fallback` marker, filed from this branch) was fixed at
the source by #4082 and is closed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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