Skip to content

feat(metadata): 端点匹配器 —— matchEndpoint 惰性索引实现(#5040 E2) - #5110

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-5089-endpoint-matcher
Aug 4, 2026
Merged

feat(metadata): 端点匹配器 —— matchEndpoint 惰性索引实现(#5040 E2)#5110
os-zhuang merged 1 commit into
mainfrom
claude/issue-5089-endpoint-matcher

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

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/ 仍是不同路径,孤零零的 / 不被削成 ""(否则查询 "" 会跟声明 / 撞上)
17.x 不解码 / 不规整 / 不折叠 原串即键。/a%2Fb/Tasks、NFD 的 /café 全部原样入键,各有测试钉住
返回 parse 后形状 每条经 ApiEndpointSchema.safeParse,默认值已物化 —— 作者省略 authRequired 时消费方拿到 true,不可能把「缺省」误读为放行
parse 失败响亮跳过 error 级点名条目 + 后果(该路由将回 404)+ 处方,绝不返回半合法形状;坏条目不牵连同批好条目
params{} 词表(ADR-0121)无模板语法,本单不发明 —— 只跑在实现里的语法就是隐藏方言(Prime Directive #12)
undefined = miss,断存储必须 throw 读不到存储时抛出,伪装成 miss(miss 会变成 404)。构建失败不缓存,下次调用重试

重复声明(#5040 设计 §1.3):两条条目声明同一 METHOD+path 时,name 字典序在前者保留路由,被弃者连同规则一并 error 级点名。确定性是因为掷硬币会让同一份部署在不同节点/不同启动上表现不同;响亮是因为「声明了却不服务」正是本仓一直在还的那笔账。

失效用的是仓内既有机制,没有新造事件系统 —— 两条 seam 覆盖的是不相交的事件集,缺一不可:

  1. invalidateListCache('api') —— 全部本地写入,包括 artifact 装载与 HMR reload 的 { notify: false } 写入(它们按构造根本不经过 watcher);索引跟 list 缓存共享同一条失效不变量:同一类的清单缓存过期了,由它建的索引当然也过期了;
  2. 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.tsMetadataManager,#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)。

⚠️ 这条 seam 只能暴露 loader 真的报出来的失败。DatabaseLoader 在自己内部就把读故障吞成 [],所以 DB 断了在这里依然看不见 —— 已单独立卡 #5108,代码注释指向它,本 PR 不修根因(那是全仓 list() 消费方共同的面,不该搭这趟车)。

范围外发现(按 Prime Directive #10 立卡,均未在本 PR 修)

验证

pnpm --filter @objectstack/metadata exec vitest run --maxWorkers=2
  Test Files  17 passed (17)
       Tests  384 passed (384)          # 新增 48 个(336 -> 384)

npx tsc --noEmit -p packages/metadata/tsconfig.json
  96 -> 92 errors,与 origin/main 基线 92 逐个持平(新增 0)
  # 首轮多出的 4 条 TS2835 是测试相对 import 缺 .js 扩展,已补齐

pnpm --filter @objectstack/spec check:generated
  ✓ All 8 generated artifacts are up to date.
  git status 干净 —— 运行期改动不移动任何生成物,符合预期

pnpm check:type-check-coverage                       OK (62/77 + 15 DEBT + 1 exempt)
check:nul-bytes / adr-anchors / route-envelope / error-code-casing /
wildcard-fallthrough / durability-log-level / slot-lookup /
service-providers / startup-registry-verdict / release-notes   全部 PASS

eslint(4 个改动文件)                                 零告警
turbo test --filter=@objectstack/objectql --filter=@objectstack/metadata-protocol
  114 files / 1810 tests passed(最近的下游消费方)

测试覆盖(48 个)逐条对应契约条款:索引构建 / 命中 / 未命中→undefined / method 大小写 / 两侧去尾斜杠 / 默认值物化 / parse 失败响亮跳过且不牵连 / 断存储 throw(且不缓存失败)/ 失效重建(含 notify:falseregisterInMemory、集群 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

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
@vercel

vercel Bot commented Aug 4, 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 Aug 4, 2026 5:01am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/l labels Aug 4, 2026
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/metadata.

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

  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/metadata)
  • content/docs/kernel/cluster.mdx (via packages/metadata)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/metadata)
  • content/docs/plugins/packages.mdx (via @objectstack/metadata)
  • content/docs/protocol/kernel/metadata-service.mdx (via @objectstack/metadata)
  • content/docs/releases/v12.mdx (via @objectstack/metadata)
  • content/docs/releases/v9.mdx (via @objectstack/metadata)

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 changed the title feat(runtime): 端点匹配器 —— matchEndpoint 惰性索引实现(#5040 E2) feat(metadata): 端点匹配器 —— matchEndpoint 惰性索引实现(#5040 E2) Aug 4, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 4, 2026 05:04
@os-zhuang
os-zhuang enabled auto-merge August 4, 2026 05:04
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 4, 2026
Merged via the queue into main with commit ecc61ab Aug 4, 2026
25 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-5089-endpoint-matcher branch August 4, 2026 05:12
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/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

E2(#5040 执行器):端点匹配器 —— 惰性索引 + 元数据事件失效,精确路径匹配,params 恒空

2 participants