fix(rest): 两个端点契约面只宣告匹配器实际会服务的集合 (#5224) - #5487
Conversation
#5224) `GET /meta/api` and `GET /openapi.json` enumerated declared `api` items through `protocol.getMetaItems` (ObjectQL SchemaRegistry + `sys_metadata`). Whether a declared route is SERVED is decided by a different reader entirely — `IMetadataService.matchEndpoint` -> `EndpointMatcher` -> `MetadataManager.listForIndex('api')`, the manager's registry plus its registered loaders (`filesystem`, `memory` on dev and serve). A real showcase boot measured them disagreeing. A row written through `PUT /api/v1/meta/api/e8_backdoor` returned 200; the route answered 404 to anonymous and authenticated callers alike; `/meta/api` listed it; and `/openapi.json` published `/api/v1/apps/showcase/backdoor` with `security: []`, describing an endpoint needing no credentials that does not exist. That document is what SDKs, codegen and AI clients generate from, so the fiction propagates into everything built on it (Route & surface ownership Rule 4, ADR-0076 D12). Both faces now ask the matcher, per declaration, and announce only what comes back — the shape that adds no second truth source, since the matcher's own judgement (parse, the ADR-0121 identity-free publish gates, duplicate resolution) is consulted rather than restated. The match is confirmed by NAME, so the loser of a duplicate route claim is not announced either. A store outage propagates instead of collapsing into an empty served set, and every omission is named at `error`. `matchEndpoint`'s own data source is untouched. `?preview=draft` is exempt: it answers "what is PENDING", by construction not the served set. A host that wires no metadata service keeps the old behaviour and says so once, rather than having a verdict invented for it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016FNvXhtSdnEGEfLEsMmvxh
…faces-match-matcher
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 11 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
|
越界发现,已按 Prime Directive #10 单独立项(均未指派,查重含已关闭 issues,无命中):
两条都不在本 PR 里修。 Generated by Claude Code |
…up ratchet)
`ctx.getService<any>('metadata')` added a 16th erasure site to a file
grandfathered for 15. The slot has a contract — `IMetadataService` — so the
lookup carries it, and `RestServer`'s provider field takes `unknown`, which
`isEndpointMatchAuthority` narrows anyway.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016FNvXhtSdnEGEfLEsMmvxh
…sumed The comment promised a 5xx. Measured, that failure arrives as 400 — an unrecognised error lands on `mapDataError`'s terminal fallback, which is a pre-existing classification on every error this route reports. The propagation is the fact worth stating; the status is someone else's bug and is filed as one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016FNvXhtSdnEGEfLEsMmvxh
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 31015336942 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Fixes #5224
前提重核(STEP 0,真实 boot 实测,先于任何编码)
在
origin/main(含 #5464577cd27be)上重跑了 issue body 的三条探针 —— 断链完全复现,前提成立:对派发词里点名的两处漂移的实测结论:
api补进 DEFAULT_METADATA_TYPE_REGISTRY 与 BUILTIN_METADATA_TYPE_SCHEMAS (#5271) #5312 /api不在 metadata 类型注册表里 —— Studio 直写路径完全不校验端点,publishPackageDrafts 也没有 E7 门 #5206 step 2(PR fix(metadata-protocol): publishPackageDrafts 对apidraft 跑 ADR-0121 端点发布门 (#5206 step 2) #5279)之后,直写PUT /meta/api/{name}依旧成功,version哈希与 body 记录的一模一样(sha256:9ad721f4...)。原因也查清了:api现在是注册表条目,且带allowRuntimeCreate: true(packages/spec/src/kernel/metadata-plugin.zod.ts:760)—— 运行时创建是被声明允许的,所以不需要换合法写入路径复测。[EndpointMatcher] ... EXCLUDED一行都没有 —— 那条 row 压根没走到门口。E7b 依然正确必要;断链在它下面一层。同一时刻两条 stack artifact 端点服务正常(
/apps/showcase/tasks鉴权 200 / 匿名 401),确认「不误伤」这一侧有真实基线。改动(方向裁定:面只宣告匹配器实际会服务的集合)
两个面在宣告前问匹配器,只宣告问回来的那一条。
packages/rest/src/served-endpoints.ts——selectServedEndpoints(items, authority, logger)。它不复刻匹配器的判据(parse、ADR-0121 identity-free 发布门、重复路由裁决),而是逐条调用已有契约方法IMetadataService.matchEndpoint。这是「不引入第二真相源」的唯一形状:判据只有一处,改了那一处两个面自动跟随。name确认,不是「有人占这条路由就算」:重复声明由匹配器裁决出唯一 owner,落败方不该被宣告。重复规则因此仍然只写在buildEndpointIndex一处。matchEndpoint读不到 store 时抛错,这里原样上抛。吞成「什么都没声明」是同一类谎言的反向。error,每次调用聚合成一行):说明它的路由 404、以及怎样把它变成真的(走 stack artifact 或publishPackage)。RestServer新增最后一个可选构造参数metadataServiceProvider,rest-api-plugin接线;解析链与resolveProtocol/resolveI18nService同款(多租户先问本请求的 kernel)。⛔matchEndpoint/EndpointMatcher的取数逻辑一行未动,packages/runtime未动 —— 需要的只读接缝已经存在,不必新开。两处刻意保留原行为,都写在测试里
GET /meta/api?preview=draft不过滤:这个面回答的是「什么在待发布」,按定义就不是服务集合,过滤会把它想展示的东西清空。codegen / SDK 读的是不带参数的那条。GET|PUT|DELETE /meta/api/{name}不过滤:一条不被服务的声明仍然要能被看到和删掉。为什么
/meta/:type只对api特判对其他每一个 type,「列出来」和「生效」是同一个事实,由列出它的那个 reader 自己回答完了。
api不是:一条声明式路由由matchEndpoint服务,那是另一个 reader、另一个可见范围,并且是该判据的唯一持有者。所以特判的理由不是「api 特殊」,而是「api 是唯一一个服务判据不在本路由视野内的 type」。反向验证(方向先判后跑)
预判:撤掉收窄 → 面①③ 变红;面② 本来就绿、且应当保持绿,因为它是「不误伤」的守卫 —— 一个会随修复变色的守卫是在钉修复而不是钉不变量。跑完与预判一致:
写这份报告时抓到一条本来会绿得没有意义的用例:原先那条「documents the matcher's parsed value, so
authRequiredis materialized」前后都绿 —— 因为 enrichment 自己也 parse,默认值两条路径都补得出来,它根本区分不了改动。已改写成真正有判别力的形状:让两个 reader 对同一条路由持有不同状态(枚举侧summary: 'STALE'+authRequired: false,匹配器侧是真值),断言文档描述的是匹配器那一份。单独复跑确认它在撤掉收窄后确实变红。测试
packages/rest/src/served-endpoints.test.ts(10 例)—— 收窄语义:保留 / 丢弃 / 重复落败方 / 无 name 或 method 或 path / 空输入 / outage 上抛 / 日志形状。packages/rest/src/rest-endpoint-surfaces-served-only.test.ts(12 例,各自}, 60_000))—— 用真实MetadataManager而不是 stub,因为面①(manager 真的看不见)和面③(E7b 门真的排除)只有对真匹配器才有意义。日志里能看到真实的[EndpointMatcher] ... EXCLUDED ... ADR-0121 D6 ...那一行。pnpm --filter @objectstack/rest test→ 47 files / 698 tests passed(合入origin/main(含 chore(ci): job 级if:读 needs.*.outputs.* 必须显式点名状态函数 (#5343) #5477、fix(spec): OpenAPIcomponents.schemas不再是空的 ——lazySchemaProxy 撞上typeof === 'object',并补上产物自洽门禁 #5459 —— 后者同改openapi-endpoints.ts)之后复跑仍绿)。packages/qa/dogfood/test/showcase-declarative-endpoints.dogfood.test.ts→ 14 passed(它正是「两条 stack artifact 端点必须出现在 openapi 文档里」的既有 pin)。npx tsc --noEmit(packages/rest):只剩package-routes.ts那两条既有 TS2345,新文件零错。node scripts/check-nul-bytes.mjsOK;另对本 PR 全部改动文件自查[\x00-\x08\x0b\x0c\x0e-\x1f],无命中。打完补丁后的真实 boot 复测(同一套探针)
契约点:隐藏 vs 标注(按裁定实现了隐藏,但这里是代价)
/openapi.json这一侧没有第二种选择:一个 operation 要么在要么不在,「declared but not served」在 codegen 眼里表达不出来。/meta/api这一侧本可以标注(items 已经带_diagnostics),选择隐藏是为了和 OpenAPI 对称。代价说清楚:
api的注册表条目声明了allowRuntimeCreate: true,所以运行时创建一条api是被允许的,而它永远不会被服务 —— 收窄之后作者在列表里也看不到自己刚存的那条了(单条 GET/DELETE 仍可达,日志也点名了它)。这不是本 PR 造成的破损:那条路由在改动前后都是 404,改的只是面不再谎报。真正的矛盾是「注册表允许运行时创建 / 匹配器永远看不见运行时创建的条目」,已按 Prime Directive #10 另行立项,也就是 issue 里的方向一。范围
packages/rest/src/**+ 测试 + changeset。未动matchEndpoint/EndpointMatcher取数逻辑,未动写入口,未动packages/runtime。Generated by Claude Code