Skip to content

feat(spec): 执行器契约面 matchEndpoint? + setFallbackHandler?(#5040 E1) - #5097

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-5080-executor-contracts
Aug 4, 2026
Merged

feat(spec): 执行器契约面 matchEndpoint? + setFallbackHandler?(#5040 E1)#5097
os-zhuang merged 1 commit into
mainfrom
claude/issue-5080-executor-contracts

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #5080
Part of #5040(执行器 E 系列第 1 单,contract-first 首件)

这个 PR 做什么

纯声明,零行为变更。 只在 packages/spec/src/contracts/ 增加两个可选契约成员与一个导出类型。仓内没有任何实现体、没有任何接线;声明式 ApiEndpoint 在 v17 仍被 publish 硬拒(#4936 裁决),本单落的只是它未来得以执行所需的契约前件。E2/E3 的实现单 Blocked-by: 本单。

#5080范围收窄评论,IMetadataService.generateOpenApi? 已从范围中剔除(#5078 实测:GET /openapi.jsonpackages/rest 独占应答,在 metadata 契约上加该成员会造出 ADR-0076 禁止的第二属主),本 PR 未实现它。#5080 正文的 Blocked-by: v17 切版 已由维护者放行(见认领评论)。

两个成员

1. IMetadataService.matchEndpoint?(query: { path: string; method: string })

把一次请求的 method+path 解析为拥有该路由的 api 元数据条目,未命中返回 undefined。这正是 HTTP dispatcher 在「内建 domain 均未认领」与「答语义 404」之间空出的那一步。随之导出新类型 ApiEndpointMatch:

  • endpointApiEndpointSchema.parse 之后的形状 —— 默认值已物化,而非存储里的原始 JSON。作者漏写 authRequired 时消费端拿到的是 true(schema 默认值),因此消费端永远读不到「缺省」这个中间态,也就不可能把一个缺失的安全默认误读成放行。这是「防 AI 写元数据犯错」轴上的主要设计点。
  • params 在 17.x 恒为 {} ApiEndpointSchema.path 词表已冻结(ADR-0121),既未定义 :param 也未定义 {param},本契约刻意不发明模板语法 —— 只存在于实现里的语法就是隐藏方言(Prime Directive Add comprehensive test suite for Zod schema validation #12)。槽位现在就声明出来,是为了将来真要加路径模板时,那是词表的加法,而不是本契约的破坏性变更。

匹配维度(#5040 设计 §2)一并写进 doc-comment:method 大小写不敏感;path 去尾斜杠后整串精确比较,17.x 不做百分号解码、不做 Unicode 规整、不做大小写折叠。另注明作用域即实例(无 env 参数,与仓内其余 metadata 消费同构),以及「未命中 ≠ 故障」——读不到存储必须 throw,不得把故障伪装成 404(与 loadDiagnosed 同一区分)。

2. IHttpServer.setFallbackHandler?(handler: RouteHandler)

设计 §1 方案 C 的传输层兜底 seam。doc-comment 载入两条语义保证:

  1. 仅在全部显式注册的路由均未命中后调用。 它在结构上不可能遮蔽任何已注册路由,因此零注册顺序依赖 —— 这正是它优于备选「通配路由」方案的原因:后者的归属由插件 start() 顺序下的 first-registration-wins 决定,即 ADR-0076 D11「一条路由一个属主」要防的病灶。实现方映射到框架自身的 not-found 钩子(Hono 的 app.notFound),而非映射到一条路由。
  2. handler 收到的 req.body 可读,与 use() 中间件契约明确的「body 不填充」相反(在 use() 处解析 body 会在真正拥有它的路由 handler 之前吃掉请求流,见 packages/plugins/plugin-hono-server/src/adapter.ts:362)。这条差异正是中间件 seam 无法承载动态端点、必须新增本成员的原因:由 flow 或 create 操作支撑的声明式端点必须读 body。

另注明「重复调用是替换而非追加(只有一个兜底器,不是链)」,以及「handler 不写响应时,适配器既有的 404/405 未匹配语义保持不变」。

可选性

两者均为可选成员,消费端按仓内既有惯例以 typeof x === 'function' 探测(同 watch? / subscribe? / getRawApp?)。不实现它的 metadata 槽位占用者、无法表达 not-found 钩子的适配器,都仍然满足契约。对现有实现方无迁移动作。

契约测试

与既有 contracts 测试同风格同位置(packages/spec/src/contracts/*.test.ts):可选成员的在场/缺席探测(typeof === 'function')、以带类型字面量做的类型层形状断言。setFallbackHandler 侧另有两条行为性断言 —— 已注册路由不被兜底器遮蔽、兜底 handler 读得到 body;matchEndpoint 侧真正走一遍 ApiEndpointSchema.parse,断言作者漏写的 authRequired 在返回值里已物化为 true

生成物 regen 范围

按 AGENTS.md 的 os-regen 纪律执行(buildcheck:generatedcheck:generated --fix,只重生成被证明陈旧的那一件,不整套刷)。

api-surface.json 新增一行 ApiEndpointMatch (interface),0 breaking / 1 added。两个新成员是 interface 成员而非导出,不动其余七件生成物 —— check:generated 复跑 8/8 全绿。无本次改动之外的漂移。

验证结果(实跑)

结果
pnpm --filter @objectstack/spec test Test Files 302 passed (302) / Tests 7663 passed (7663)
pnpm --filter @objectstack/spec typecheck tsc --noEmit 无输出
pnpm --filter @objectstack/spec check:generated All 8 generated artifacts are up to date.
check:exported-any no exported type resolves to any: 1843 types + 1594 schemas across 16 entry points
check:dual-source-exports no new dual-source exports: 4243 names across 16 entry points
turbo typecheck(metadata / runtime / plugin-hono-server / rest 及其依赖) 29 successful, 29 total
eslint 四个改动文件 ✅ exit 0

补充:packages/spec/tsconfig.json 既有地 exclude**/*.test.ts(仓内记录在案的 TEST_DEBT),所以 typecheck 门读不到测试文件。为确认新增的类型层断言真的编译得过,另用一份临时 tsconfig 单独对两个测试文件跑了 tsc:新增代码零报错,仅剩 4 条改动前就存在的 TS6133 未用形参(status: function (code) 等)。临时 tsconfig 已删除。

未做的事

  • 无 runtime / hono 侧实现或接线(E2/E3,另单);
  • 未触碰 ApiEndpointSchema(词表冻结,ADR-0121);
  • 未触碰 content/docs/releases/(Prime Directive —— 发版说明由 changeset 集中编译)。

Changeset:.changeset/executor-contract-surface-e1.md(@objectstack/spec minor)。


🤖 Generated with Claude Code

https://claude.ai/code/session_01EYGdmvWP1ieZSLqvAW6uyd


Generated by Claude Code

…andler? (#5080)

Part of #5040 (E1, contract-first). Pure declaration: two OPTIONAL contract
members plus one exported type. No implementation, no wiring, zero behavior
change — declared `apis:` are still hard-rejected at publish in v17 (#4936).

IMetadataService.matchEndpoint?(query: { path, method })
  Resolves a request's method+path to the declared `api` item that owns it,
  or undefined on a miss — the dispatcher step between "no built-in domain
  claimed this" and "answer a semantic 404". New exported type
  ApiEndpointMatch:
    - `endpoint` is the ApiEndpointSchema.parse-d shape, schema defaults
      MATERIALIZED, so a consumer can never read a missing `authRequired`
      as permissive.
    - `params` is always {} in 17.x. The frozen ApiEndpointSchema vocabulary
      (ADR-0121) defines no template syntax and this contract deliberately
      does not invent one — a syntax living only inside an implementation is
      the hidden dialect Prime Directive #12 forbids. The slot is declared
      now so path templates would be an additive vocabulary change rather
      than a breaking contract change.

IHttpServer.setFallbackHandler?(handler: RouteHandler)
  The last-resort handler, invoked only after every explicitly registered
  route has missed. Structurally incapable of shadowing a registered route,
  hence zero registration-order dependency — unlike the wildcard-route
  alternative, whose ownership is decided by first-registration-wins across
  plugin start() order (the ADR-0076 D11 hazard). Second guarantee, also in
  the contract: the handler's `req.body` IS readable, in contrast with the
  use() middleware contract which explicitly does not populate it. That
  difference is why the middleware seam cannot carry dynamic endpoints.

Both members are optional and feature-detected with typeof === 'function',
matching watch? / subscribe? / getRawApp?. No migration for implementors.

Contract tests mirror the existing contracts-test style: optional-member
presence/absence probing, and type-level shape assertions via typed literals.

Generated: api-surface.json gains exactly one line, ApiEndpointMatch
(interface) — 0 breaking, 1 added. The two members are interface members,
not exports, so the other seven artifacts are untouched.
@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 4:14am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling 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/spec.

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

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

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.

@github-actions github-actions Bot added the size/m label Aug 4, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 4, 2026 04:15
@os-zhuang
os-zhuang enabled auto-merge August 4, 2026 04:15
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 4, 2026
Merged via the queue into main with commit 77be690 Aug 4, 2026
25 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-5080-executor-contracts branch August 4, 2026 04:37
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 4, 2026
…ack-ai#5089) (objectstack-ai#5110)

Implement `IMetadataService.matchEndpoint?` on `MetadataManager`, the
repo's occupant of the `metadata` slot, backed by a new pure matcher
module. E2 of the objectstack-ai#5040 endpoint-executor program; the contract text
landed in objectstack-ai#5080/objectstack-ai#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 (objectstack-ai#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 objectstack-ai#5090) and publish still rejects a non-empty `apis:`
(objectstack-ai#4936), so the whole path is structurally unreachable. `packages/spec`
untouched — the vocabulary stays frozen.

Out-of-scope findings filed: objectstack-ai#5108, objectstack-ai#5109.


Claude-Session: https://claude.ai/code/session_01EYGdmvWP1ieZSLqvAW6uyd

Co-authored-by: Claude <noreply@anthropic.com>
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/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

E1(#5040 执行器契约面):IMetadataService.matchEndpoint? / generateOpenApi? + IHttpServer.setFallbackHandler? 可选契约方法与契约测试

2 participants