feat(metadata): 端点匹配器 —— matchEndpoint 惰性索引实现(#5040 E2) - #5110
Merged
Conversation
Implement `IMetadataService.matchEndpoint?` on `MetadataManager`, the repo's occupant of the `metadata` slot, backed by a new pure matcher module. E2 of the #5040 endpoint-executor program; the contract text landed in #5080/#5097 and is implemented here literally. - METHOD -> exact-path -> parsed-endpoint index, built lazily on the first call. `method` upper-cased; `path` compared as a whole string after trimming exactly one trailing slash (both sides, never a lone `/`). No percent-decoding, no Unicode normalization, no case folding in 17.x. `params` is always `{}` — the frozen ADR-0121 vocabulary defines no template syntax and this does not invent one. - Every stored item goes through `ApiEndpointSchema.safeParse`, so the answer carries materialized defaults (an omitted `authRequired` comes back `true`). An item that fails to parse is skipped and named at `error` level; it never disturbs the good items around it. - Duplicate METHOD+path claims resolve deterministically: the lexicographically-first `name` keeps the route and the discarded claimant is named at `error` level with the rule (#5040 design §1.3). - `undefined` is a miss; a store that cannot be read THROWS, so an outage never masquerades as a 404 (ADR-0110 D3's distinction, applied to the plural read via a private `listForIndex`). A failed build is not cached. - Invalidation reuses the existing mechanisms only: `invalidateListCache('api')` covers every local write including the `{ notify: false }` artifact-ingest / HMR path, and a `subscribe('api')` watcher covers cluster peer replay. Zero HTTP behavior change: nothing calls `matchEndpoint` yet (the dispatcher seam is #5090) and publish still rejects a non-empty `apis:` (#4936), so the whole path is structurally unreachable. `packages/spec` untouched — the vocabulary stays frozen. Out-of-scope findings filed: #5108, #5109. 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
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 7 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
marked this pull request as ready for review
August 4, 2026 05:04
os-zhuang
enabled auto-merge
August 4, 2026 05:04
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #5089
Part-of #5040(E 系列第 2 单)
做了什么
给
metadata槽位的实际占位者 ——MetadataManager(packages/metadata,不是卡面文字里的packages/runtime;见下「口径修正」)—— 实装IMetadataService.matchEndpoint?,背后是一个新的纯匹配模块packages/metadata/src/endpoint-matcher.ts。契约文本(#5080 / #5097 落地)即规格,逐字实现:method大小写不敏感ApiEndpointSchema.method本就是大写枚举,所以只有查询侧会被改写 —— 这正是意图)path去尾斜杠后整串精确/x//与/x/仍是不同路径,孤零零的/不被削成""(否则查询""会跟声明/撞上)/a%2Fb、/Tasks、NFD 的/café全部原样入键,各有测试钉住ApiEndpointSchema.safeParse,默认值已物化 —— 作者省略authRequired时消费方拿到true,不可能把「缺省」误读为放行error级点名条目 + 后果(该路由将回 404)+ 处方,绝不返回半合法形状;坏条目不牵连同批好条目params恒{}undefined= miss,断存储必须 throw重复声明(#5040 设计 §1.3):两条条目声明同一 METHOD+path 时,
name字典序在前者保留路由,被弃者连同规则一并error级点名。确定性是因为掷硬币会让同一份部署在不同节点/不同启动上表现不同;响亮是因为「声明了却不服务」正是本仓一直在还的那笔账。失效用的是仓内既有机制,没有新造事件系统 —— 两条 seam 覆盖的是不相交的事件集,缺一不可:
invalidateListCache('api')—— 全部本地写入,包括 artifact 装载与 HMR reload 的{ notify: false }写入(它们按构造根本不经过 watcher);索引跟 list 缓存共享同一条失效不变量:同一类的清单缓存过期了,由它建的索引当然也过期了;subscribe('api', …)—— 集群对端的写入,attachClusterPubSub只把它经notifyWatchersLocal回放,不走 (1)。零行为变更(结构性不可达)
17.x 没有任何东西会调到
matchEndpoint:挂载 seam 是 #5090 的文件面(本 PR 完全没碰 dispatcher-plugin / plugin-hono-server / http-dispatcher),publish/validate 对非空apis:仍然硬拒(#4936)。测试直接驱动服务而不走 publish —— 这正是 #5040 设计选定的验收姿态:代码被充分锻炼,但在真实组合里零暴露。packages/spec一个字没改,词表冻结。口径修正(需 PM 知悉)
#5089 卡面写的是「
packages/runtime内实现matchEndpoint」,但仓内implements IMetadataService的唯一类是packages/metadata/src/metadata-manager.ts的MetadataManager,#5040 设计 §2 与 §6 子单表也都把 E2 落在packages/metadata(engine 域)。落点按设计表执行,卡面那句按笔误处理。一处必须解释的新增私有读
契约要求「读不到存储必须抛出」,而
MetadataManager.list()的姿态是故意相反的:它 warn 掉失败的 loader 继续服务能服务的部分(部分可用的元数据面仍应尽力工作)。这个姿态对matchEndpoint是错的 —— 它的undefined会变成 HTTP 404,一次「零声明」的静默空读会把每一条已声明端点变成「没人声明这条路由」。所以新增了私有的
listForIndex(type):跟list()同源(registry + loaders,按 name 去重),但不包try/catch。它不是第二个list(),是list()的失败姿态为唯一一个「答案是可用性判断而非尽力列举」的调用方翻了个面 —— 与loadDiagnosed对单数读所做的完全同源(ADR-0110 D3)。DatabaseLoader在自己内部就把读故障吞成[],所以 DB 断了在这里依然看不见 —— 已单独立卡 #5108,代码注释指向它,本 PR 不修根因(那是全仓list()消费方共同的面,不该搭这趟车)。范围外发现(按 Prime Directive #10 立卡,均未在本 PR 修)
DatabaseLoader把存储读故障吞成空结果 —— ADR-0110 D3 的 miss/outage 之分在复数读路径上不成立 #5108 ——DatabaseLoader.loadMany/exists/stat把存储读故障吞成[]/false(loadMany连 warn 都没有)。ADR-0110 D3 的 miss/outage 之分在复数读路径上不成立;权限/策略清单读成空,按消费方姿态不同要么静默 fail-open 要么静默 fail-closed。listCache/ registry —— 收到广播的节点最长 30s 继续服务旧定义 #5109 —— 集群对端的元数据写入只经notifyWatchersLocal回放,不失效本节点的listCache/ registry,收到广播的节点最长 30s 继续服务旧定义;与cluster-semantics.mdx§5 自述的 "consumed by peers to invalidate their local caches" 不符。同文件内的applyRepoEvent是做对了的那条路径,可作修法样板。验证
测试覆盖(48 个)逐条对应契约条款:索引构建 / 命中 / 未命中→undefined / method 大小写 / 两侧去尾斜杠 / 默认值物化 / parse 失败响亮跳过且不牵连 / 断存储 throw(且不缓存失败)/ 失效重建(含
notify:false、registerInMemory、集群 watcher-only 三条路径)/ 重复声明确定性(含乱序同解)/params === {}/ 前缀不算命中 / 并发首调共享一次读。变更文件
packages/metadata/src/endpoint-matcher.ts(新)packages/metadata/src/endpoint-matcher.test.ts(新)packages/metadata/src/metadata-manager-match-endpoint.test.ts(新)packages/metadata/src/metadata-manager.ts(+111).changeset/endpoint-matcher-lazy-index.md(minor)草稿 PR,未入队,等 PM 复核。
🤖 Generated with Claude Code
https://claude.ai/code/session_01EYGdmvWP1ieZSLqvAW6uyd
Generated by Claude Code