feat(spec,core,runtime)!: 声明式 apis: 响亮拒绝 + ApiRegistry 整面退役 (#4936, #4939) - #5065
Conversation
… ApiRegistry family (#4936, #4939) The declarative `apis:` surface was zero-execution end to end while reporting perfect health. Metadata loaded fine — `GET /api/v1/meta/api` returned every declared endpoint with every key — but no route was ever mounted for a declared path, so a request died at Hono's `notFound` (a bare 404, not the dispatcher's semantic one), and the `handleApiEndpoint` branch behind it called a `matchEndpoint` method no implementation in this repo has ever provided. Every key on `ApiEndpointSchema` was therefore declared != enforced, `authRequired` included — a security semantic that parsed green and gated nothing. Per the maintainer verdict (2026-08-04, #4936), this takes the third route: keep the vocabulary, refuse the authoring. - spec: a non-empty `apis:` is rejected on `ObjectStackDefinitionSchema` — the one choke point `defineStack`, metadata artifact ingestion, `os validate`, the lint scorer and `EnvironmentArtifactSchema` all run through, so no path can forget to check. The rejection carries its own prescription and names #5040 (the executor) as the live tracker. Empty/absent still pass. `ApiEndpointSchema` itself is untouched: retiring an industry-stable endpoint shape would only mean re-introducing it identically later. - runtime: `handleApiEndpoint`, its now-orphaned private `callData` delegate (tsc TS6133 found it) and the `/__api-endpoint` ledger + legacy-prefix entries are deleted, so the absence is loud instead of grep-able dead code. - spec/core (#4939): the second, unrelated endpoint declaration shape retires whole — `ApiEndpointRegistration`/`ApiRegistry`/`ApiRegistryEntry` and their value schemas (12 JSON-Schema defs, 67 authorable keys), the ~500-line `ApiRegistry` service, `createApiRegistryPlugin`, and hono's unread `useApiRegistry` option. It was composed only in `packages/core/examples/`, so `requiredPermissions` promised gateway enforcement no gateway performed. `ConflictResolutionStrategy` survives, moved to `api/router.zod` — two independent ratchets (spec sync-retirement, objectui parity) pin it. - showcase: declares no endpoints (both definitions preserved, commented, with the rationale); the coverage manifest's "demonstrated ... executed by the runtime dispatcher" claim is corrected to a waiver — it was the exact advertise-what-you-do-not-deliver claim Prime Directive #10 forbids. - the #4910/#5006 endpoint-level `rateLimit` tracking pointers now name #5040, since #4936 closes here. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYGdmvWP1ieZSLqvAW6uyd
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 5 package(s): 120 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…ublic method
`HttpDispatcher.handleApiEndpoint()` carried no `private` modifier, so deleting
it removes a symbol from `@objectstack/runtime`'s public surface even though the
method returned `{ handled: false }` on every call it ever received. Record it
as breaking with that nuance stated, rather than letting a `minor` imply the
symbol survived. Also drops the `@objectstack/client` entry: that change is a
comment in a `.test.ts`, which never ships.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYGdmvWP1ieZSLqvAW6uyd
The APIS_NO_EXECUTOR_GUIDANCE const landed between the two doc-comment blocks that both belong to ObjectStackDefinitionSchema, orphaning the first. Move the const above them — no behaviour change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYGdmvWP1ieZSLqvAW6uyd
The test claimed the prescription does not name #4936 as the tracker but only asserted #5040 was present — true even with a stale issues/4936 link beside it. Assert the set of issue URLs in the message is exactly {5040}. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYGdmvWP1ieZSLqvAW6uyd
…1 D1 renames paths on restore ADR-0121 (accepted 2026-08-04, after this branch opened) namespaces endpoint paths as `<runtime-prefix>/apps/<namespace>/<subpath>`. The rejection message said definitions "stay valid"; that is true of every key except `path`, so it is now stated precisely, with the FROM -> TO. The showcase's commented endpoints carry the same note — they would be rejected under D1 if uncommented verbatim. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EYGdmvWP1ieZSLqvAW6uyd
与 ADR-0121(#5064,本 PR 开工后合入)的对账开工中途
一处因它而必须修正的措辞(已在
showcase 里注释保留的两个端点同样加了这条警告 —— ADR-0121 §D1 已点名它们回迁后的形态是 二次 merge 后的复验(
|
| 项 | 结果 |
|---|---|
pnpm turbo run typecheck(全仓) |
124/124 |
spec check:generated |
8/8 up to date |
@objectstack/spec test |
7651 passed (302 files) |
| runtime / core / showcase / client / hono test | 82 / 27 / 11 / 17 / 13 files,全绿 |
composeStacks 不再静默丢弃顶层键)与本 PR 同改 stack.zod.ts,merge 无冲突,合并后 compose-stacks-key-loss.test.ts 与本单新增 pin 测试同时全绿。
Generated by Claude Code
Generated by Claude Code
范围外发现,已另开单:#5078摘除
差别在于:这条路由并非无人服务 —— 单里明确标注了我没有实测的那一点:两个属主在真实 composition 下谁先接到请求 —— 这决定它是纯死代码还是用户可见 404,建议按 ADR-0076 结尾那句起真实 boot Generated by Claude Code Generated by Claude Code |
CI 全绿(
|
| 门禁 | 结果 |
|---|---|
| TypeScript Type Check(含 spec 八件生成物 + api-surface) | ✅ |
| Spec property liveness | ✅ |
| Test Core (1/3, 2/3, 3/3) | ✅ |
| Dogfood Regression Gate (1/3, 2/3, 3/3) | ✅ |
| Dogfood Verify CLI | ✅ |
| Temporal Conformance (live PG + MySQL) | ✅ |
| Build Core / Build Docs | ✅ |
| ESLint / Check Changeset / Check PR Size | ✅ |
| No other open PR may claim the same issue | ✅ |
关于交办时提示的 Validate Package Dependencies(OSV 每日公告打红,修复 #5039):该 job 本次 run 未出现在检查列表中 —— 期间合入的 caf144a(ci(deps): OSV batch 2026-08 — undici 7.29.0, hono 4.12.34, fast-uri 3.1.5)与 70c0769(OSV 逃生口机械门禁)已经落地,本 PR 因此不受该问题影响,无需与 main 对照排除。
Draft 状态保持,等待维护者/PM 复核后再决定转 ready。
Generated by Claude Code
Generated by Claude Code
Fixes #4936
Fixes #4939
按维护者 2026-08-04 00:20Z 裁决(#4936 评论,改良第三路)执行,⛔ 不重议的四件均已照办:硬拒路线、
ApiEndpointSchema词表保留、#4939 无条件退役、跟踪指针落 #5040。为什么是「拒绝」而不是「退役」
声明式
apis:是装载全绿、执行为零的整面。元数据侧完美:defineStack收下、GET /api/v1/meta/api原样返回两个端点。执行侧一次都没响过 —— 声明的 path 从未挂载,请求死在 Hono 的notFound(那个裸{"error":"Not found"},不是 dispatcher 的语义 404);其后的handleApiEndpoint分支调用的matchEndpoint,全仓没有任何实现提供过,恒等于{ handled: false }。于是
ApiEndpointSchema每个键都是 declared ≠ enforced ——— 包括authRequired,一个解析全绿、把关为零的安全语义。这是 ADR-0049 要杀的 false compliance,不是欠债。裁决选了保留词表、拒绝授权:端点词表是行业极稳定形状,退役只会原样再引入一遍;响亮拒绝同样杀谎,还保住词表与元数据投资。
五件套落地
1. 非空
apis:硬拒(breaking)拒绝点落在
ObjectStackDefinitionSchema的apis字段本身 —— 这是defineStack、metadata plugin 的 artifact 装载(ObjectStackDefinitionSchema.parse)、os validate、lint scorer、EnvironmentArtifactSchema共同且唯一的必经闸门,所以不存在「某条路径忘了检查」。空数组 / 缺省照旧通过。真实 CLI 上的实测(把 showcase 端点临时放回去跑
objectstack validate):处方按裁决落 #5040(活跟踪位),#4936 作为裁决出处随本 PR 关闭。
2. 执行残骸摘除
handleApiEndpoint整体删除;tsc随即用 TS6133 指出它是 dispatcher 私有callData代理的唯一调用者,一并删除(其余数据路径本来就直接走actionExec.callData(deps, …),是 D11③ 的正规形状)。/__api-endpoint从LEGACY_CHAIN_PREFIXES与ROUTE_LEDGER移除。3. #4939 无条件退役
registry.zod.ts整个家族(12 个 JSON-Schema def、67 个 authorable key)、packages/core的ApiRegistry+createApiRegistryPlugin+ 两者测试 + example、API_REGISTRY.md,以及 hono 那个从未被读的useApiRegistry选项。它只在packages/core/examples/装配过,所以requiredPermissions以现在时承诺的 gateway 校验从来没有 gateway 执行 —— 两套声明形状就此收敛为一套。ConflictResolutionStrategy保留,移入api/router.zod.ts。它被两条互相独立的 ratchet 钉死为./api导出:仓内spec/src/automation/sync-retirement.test.ts(#4738 的「第四个亲戚」),以及跨仓 objectui 的offline-nav-performance-spec-parity.test.ts—— 后者的useOffline正是因为这个名字被占用才改的名。退役它是一次跨仓变更,不在本单裁决范围内。退役套件按
spec-property-retirementskill 走完:#2978 manifest ratchet 先开火(12 条,自证「整 def 删除」路线),再按 gate (c) 路径 3(def 不再 emit)删 authorable 基线;生成物一律整体重生成。4. showcase 处置
两个端点注释保留 + 缘由(便于 #5040 落地时原样恢复),
apis: [];coverage.ts那条demonstrated … executed by the runtime dispatcher (handleApiEndpoint)改为 waiver —— 它正是 Prime Directive #10 明禁的失实覆盖声明。test/gap-fill.test.ts的断言反转而非删除:删掉只会让端点悄悄漂回来,而现在漂回来会直接打死objectstack validate。5. #5006 四处指针 → #5040(
dispatcher-plugin.ts、inbound-rate-limit.ts、stack-server.zod.ts+ 生成 mdx、http-protocol.mdx),只改跟踪位,不动 seam 逻辑。另修两处顺带被本变更证伪的记载:ADR-0088
router行(原文把apis:记作「executed byhandleApiEndpoint」)加了带日期的更正,ADR 决定本身不变;PROTOCOL_MAP.md的registry.zod.ts行删除(其死链被 spec 自己的 gate 抓到)。验证
merge
origin/main(含 #5045 把 runtime src 接进 turbo typecheck)之后全部重跑:pnpm turbo run typecheck(全仓)check:generated八件@objectstack/spectest@objectstack/runtimetest@objectstack/coretest@objectstack/example-showcasetestobjectstack validate(showcase)api-surface.jsondelta 精确为 −25 / +0,零残留,幸存者ApiEndpointSchema与ConflictResolutionStrategy均在。新增 pin 测试两组:
apis-no-executor.test.ts(拒绝含处方 + 指针为 #5040 + 空/缺省通过 + 词表零折腾的反空洞断言)、registry-retirement.test.ts(15 个退役名消失 +ConflictResolutionStrategy值域逐个保真 + 单一属主)。Changeset 已加,
major语义,带 FROM → TO 与一行修法。⛔ 未触碰content/docs/releases/。CI 知会
Validate Package Dependenciesjob 目前被 OSV 每日公告(fast-uri/hono/undici)打红,修复 #5039 在飞 —— 若本 PR 该 job 红,请先与main对照,非本变更引入。🤖 Generated with Claude Code
https://claude.ai/code/session_01EYGdmvWP1ieZSLqvAW6uyd
Generated by Claude Code