Skip to content

Commit f3fd209

Browse files
docs(adr-0076): OQ#9 — correct the partition's i18n and storage entries against a boot (refs #23347) (#23570)
Refs #23347 Docs-only. One dated correction appended to ADR-0076's OQ#9 entry (`docs/adr/0076-objectql-core-tiering.md`, the `9. **dispatcher vs rest-server overlap**` item). The audit text and its verdict are left as written. `docs/adr/**` is a Tier H governed surface, so this is split off from the code PR #23569 to let that one land on its own. **Land it after #23569:** the correction says the plugin's copy is retired, and that is true once #23569 is on `main`. ## What the correction says OQ#9's partition lists `storage` and `i18n` among the routes the dispatcher bridge owns in a standard boot, and says only `discovery` and `packages` were double-mounted. Two entries do not hold against the tree and a boot: - **i18n was double-mounted too**, whenever `I18nServicePlugin` was composed. The plugin registered the same three `/api/v1/i18n/*` patterns at `kernel:ready`, after the bridge's `start()` registrations, so first-registration-wins shadowed it. Measured for #23347 on `objectstack serve` over `examples/app-showcase`, with the plugin's `I18n routes registered: …` line in the boot log: - every anonymous request on all three paths answered `401 UNAUTHENTICATED` in the dispatcher envelope, with `X-Request-Id`; - a signed-in `labels/showcase_task/zh` answered `locale: zh-CN`, the dispatcher's `resolveLocale` expansion. #23569 retires the plugin's copy. - **storage is not the bridge's.** `packages/runtime/src/dispatcher-plugin.ts` mounts no `${prefix}/storage` route. Its docblock says "NOT /storage — `@objectstack/service-storage` owns that surface … (#4087)", and its `── Storage ──` block opens with "Nothing mounted here on purpose (#4087)". As a control, the same grep spelling finds the 3 i18n mounts. The verdict (delineate, do not merge) is unchanged. ## Gates Local subset, derived by `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `189c6449b`. - **19 families, every one exit 0.** Reconciled with `--ran`: "19 derived, 19 run, 0 NOT-MEASURED". - `@objectstack/lint`'s `check:doc-formula-expressions` first exited 3 (nothing measured), because the workspace `@objectstack/lint` was not built. It was built (turbo cache hits) and then exited 0. ## 维护者速读(草稿) **改了什么** 只改了一份架构决策记录(ADR-0076)里的一条审计说明:在第 9 个开放问题后面追加一段带日期的更正,原文和结论都不动。 **为什么改** 这条说明写的是"多语言接口和文件存储接口都只由运行时调度器提供,只有两处是重复挂载的"。实测不对。多语言接口其实挂了两份:服务插件自己还挂了一份,只是永远轮不到它应答。文件存储接口则根本不归调度器,早已交给存储服务自己。代码那边的修复(#23569)已经把多余的那一份删掉。这里把记录改成和事实一致,免得后来的人或 AI 照着旧说明做判断。 **风险与代价(含回滚)** 纯文档,不影响任何运行行为。唯一的注意点是落地顺序:更正里写了"插件那份已退役",需要 #23569 先合入才成立。回滚就是删掉这一段追加文字。 **席位意见** **你要做的** 看一眼追加的那段更正是否准确。如果同意,在 #23569 合入之后批准本 PR。 --- _Generated by [Claude Code](https://claude.ai/code/session_017WLmd5in9RXu7AjyHR8fKQ)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent fa6a608 commit f3fd209

1 file changed

Lines changed: 1 addition & 1 deletion

File tree

‎docs/adr/0076-objectql-core-tiering.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,7 @@ import { SecurityPlugin } from '@objectstack/plugin-security';
274274
6. **Data-facade home** — does the `DataProtocol` impl live in the engine-adjacent transport layer / `rest`, or a small `@objectstack/protocol-data`? (It is thin and transport-shaped.)
275275
7. **Metadata package name (post-segmentation)** — **Resolved: keep `@objectstack/metadata-protocol`** (already published; renaming has ~0 benefit and real churn). The `protocol` suffix is a low-cost naming exception — the contract is in `@objectstack/spec/api`; a README note in the package should clarify impl-vs-contract.
276276
8. **Per-domain versioning** — once segmented, do capability protocols get independent version markers / a `getCapabilities()` discovery method?
277-
9. **dispatcher vs rest-server overlap** — **Resolved (audit, #2462): delineate, do not merge.** They partition the API surface by domain rather than duplicating it: in a standard boot REST owns data/meta/batch/import-export/search/email/forms/sharing/reports/approvals/security, the dispatcher bridge owns mcp/ai/graphql/keys/automation/actions/storage/i18n/analytics/health/ready. Only **discovery** and **packages** were genuinely double-mounted (first-registration-wins — the D11 smell), and the dispatcher's `/data`/`/meta`/`/ui`/`/notifications`/`/share-links`/`/security` `dispatch()` branches are **not mounted** in a standard boot. D11 worklist: (1) kill or delegate the dead callable branches; (2) give packages + discovery a single owner; (3) extract per-domain handlers; (4) unify env-resolution on the injected `KernelResolver` seam.
277+
9. **dispatcher vs rest-server overlap** — **Resolved (audit, #2462): delineate, do not merge.** They partition the API surface by domain rather than duplicating it: in a standard boot REST owns data/meta/batch/import-export/search/email/forms/sharing/reports/approvals/security, the dispatcher bridge owns mcp/ai/graphql/keys/automation/actions/storage/i18n/analytics/health/ready. Only **discovery** and **packages** were genuinely double-mounted (first-registration-wins — the D11 smell), and the dispatcher's `/data`/`/meta`/`/ui`/`/notifications`/`/share-links`/`/security` `dispatch()` branches are **not mounted** in a standard boot. D11 worklist: (1) kill or delegate the dead callable branches; (2) give packages + discovery a single owner; (3) extract per-domain handlers; (4) unify env-resolution on the injected `KernelResolver` seam. *(Correction, 2026-10-15, #23347: two entries of the partition above were read from source, and a boot contradicts them. **i18n** was double-mounted too, whenever `I18nServicePlugin` is composed: the plugin registered the same three `/api/v1/i18n/*` patterns at `kernel:ready`, after the bridge's `start()` registrations, so first-registration-wins shadowed it. Measured on `objectstack serve` over `examples/app-showcase`, the bridge answered every request on all three paths. #23347's code change retires the plugin's copy, which leaves the bridge the single owner. **storage** is not the bridge's: it has mounted no storage route since #4087, and `/api/v1/storage/*` is `service-storage`'s own surface on the same `http-server` (`packages/runtime/src/dispatcher-plugin.ts`, the bridge's docblock and its Storage block). The verdict, delineate and do not merge, is unchanged.)*
278278
- **(2) shipped**: `${prefix}/discovery` has a single deterministic owner — the dispatcher bridge cedes it to `com.objectstack.rest.api` when that plugin is registered (`kernel.hasPlugin`, new public API), keeping it only as the fallback in REST-less compositions; `/.well-known/objectstack` stays dispatcher-owned. The bridge's `/packages` family now flows through `dispatch()` (like analytics/i18n/automation/AI) instead of calling `handlePackages()` directly, so both HTTP entries into the domain (explicit mounts + the cloud hosts' catch-all) share one pipeline — the direct path used to skip kernel/identity resolution and drop `req.query` (the documented `?overwrite=true` install flag never worked over the mounted routes).
279279
- **(1) re-scoped (cross-repo finding)**: the "dead" branches are NOT safely killable — the `@objectstack/hono` `createHonoApp()` catch-all delegates every unmatched `${prefix}/*` to `dispatch()`, and both cloud hosts mount it under their plugin routes. In that composition `/share-links` is the **designed primary surface** (per-env kernels register the `shareLinks` service with `registerShareLinkRoutes:false`; the host dispatcher serves the route after kernel swap — documented in cloud's `artifact-kernel-factory` and `default-environment-plugins`), and `/notifications` has **no other HTTP owner anywhere**. `/data`/`/meta`/`/ui`/`/security` are REST-shadowed but still catch REST misses. Killing therefore waits until the catch-all is retired in favor of per-domain handlers (step 3); until then the branches are the cloud fallback fabric, not dead code.
280280
- **(1)+(3) VERDICT (2026-07-27, #2462 series #3491–#3573): the catch-all is NOT retired — it is rehabilitated, and step (1) is absorbed.** Step (3) decomposed the dispatcher the other way around: every domain body moved to `packages/runtime/src/domains/*` (+ the `action-execution` subsystem), `dispatch()` shrank to the cross-cutting gate stages (env resolution → identity → auth gate → membership → scope strip) followed by a first-match `DomainHandlerRegistry` lookup, and the legacy if-chain reached zero domain branches. The original disease was never the catch-all itself but what it delegated INTO — an unenumerable god if-chain where "declared ≠ mounted ≠ implemented" could hide (the v16 seam bug class). Delegating into a thin gates+registry pipeline is the sane terminal shape: the route table is enumerable (`DomainHandlerRegistry.list()`), every entry has an owner module and seam tests, and the cloud fallback fabric (`/share-links` as the per-env primary surface, `/notifications` as sole owner) rides the SAME pipeline verified end-to-end by `@objectstack/http-conformance` (41 cross-adapter assertions) + the dogfood suite (351) — cloud-side `objectos-runtime` (240 tests incl. the kernel-resolver strategy) passes against the decomposed dispatcher unchanged. "Kill the dead callable branches" (1) is thereby absorbed: there are no branches left to kill, only registry entries with real owners. The optional future polish — having adapters enumerate the registry into explicit per-prefix mounts so the HTTP layer prints its own route table — would require turning the gate stages into per-route middleware; deliberately NOT scheduled (cost exceeds benefit while conformance + route-parity gates hold the line).

0 commit comments

Comments
 (0)