Skip to content

ApiEndpointSchema 在 #5312 把 api 注册成元数据类型之后仍是开放 z.object —— apis: 条目的未声明键两条路都静默落地,严格性账本还把 api/ 整目录记作「wire,按设计宽容」 #5384

Closed

Description

@baozhoutao

Blocked-by: #5309

从 #5000 的核实工作里撞到的(给 CLI 的元数据门写 pin 测试时,逐类型扫到 api)。

事实

#5312(实现 #5271/#5206)把 api 补进了 DEFAULT_METADATA_TYPE_REGISTRY 与 BUILTIN_METADATA_TYPE_SCHEMAS,api: ApiEndpointSchema。但 ApiEndpointSchema 本身是 z.object({...}),没有 .strict()(packages/spec/src/api/endpoint.zod.ts:56),而 stack 根部 apis: z.array(ApiEndpointSchema)(packages/spec/src/stack.zod.ts:329)也就继承了这份宽容。

实测(origin/main cdfbee2):

getMetadataTypeSchema('api').safeParse({ …合法端点…, aKeyThatIsNotDeclared: 1 })
  → 只报既有必填项问题,从不报 unrecognized_keys

os validate(端点合法、路径符合 ADR-0121 D1 carve-out)
  → ✓ Validation passed
  → ⚠ apis.probe_endpoint.aKeyThatIsNotDeclared: 'aKeyThatIsNotDeclared' is not a
     declared api key, so its value is dropped at load.

也就是说:今天 api 的姿态是 warn(#3786 那层预解析告警)而不是 reject,写路径(saveMetaItem / PUT /meta/api/:name,#5312 刚接上的那道 422)同样只按这份非严格 schema 判,未声明键存进去也不报错。

为什么这条值得单独记

  1. 它是 未知键静默剥离仍是全仓默认:把 #3405 的 strict 收紧从一个 schema 推广到整个可授权面(ADR-0078 完整性闸门) #4001 的形状,而且是刚长出来的一块。 api/ 目录在严格性账本(docs/audits/2026-07-unknown-key-strictness-ledger.md:1161)里的分类是 wire | REST/GraphQL request/response contracts — tolerant by design。在 feat(spec): api 补进 DEFAULT_METADATA_TYPE_REGISTRY 与 BUILTIN_METADATA_TYPE_SCHEMAS (#5271) #5312 之前这话没错;feat(spec): api 补进 DEFAULT_METADATA_TYPE_REGISTRY 与 BUILTIN_METADATA_TYPE_SCHEMAS (#5271) #5312 之后 endpoint.zod.ts 同时成了授权面(defineStack({ apis })、Studio metadata-admin 表单、PUT /meta/api/:name),而那一行还写着「按设计宽容」—— 账本会被照着读,这正是账本自己的 gate 存在的理由。

  2. 这一类型的键有安全形状。 authRequired: z.boolean().default(true),以及 ADR-0121 D6 「匿名端点需要 armed budget」的那组判断。拼错的键静默落地意味着作者以为自己配了某个姿态、实际拿的是默认值 —— 这个方向恰好是 fail-safe(authRequired 落回 true),但同一机制对 objectParams / cacheTtl / mapping·policy 块一视同仁,而 AI 批量生成端点声明正是这种拼写错误的高发区。

  3. CLI 与写路径今天是一致的(两边都收、都不报),所以这不是「门不一致」,而是「门本身没关」。objectstack build / validate 从不按 PageSchema 解析页面元数据:ADR-0089 D3a 早就该拒绝的键一路通过,#4001 的「三个示例应用 validate 全过」对 page 面是空证 #5000 的 PR(test(cli): #5000 前提被证伪 —— build/validate 一直按注册表 schema 解析,补上它缺的那份证据 #5380)里我把 api 记成一条明确的账本行:断言两边一致地接受,并断言 排查「手抄 spec 清单 + "keep in sync" 注释」模式:一天内确认三例,全部曾静默漂移 #3786 的告警层确实点名了这个键 —— 等这条 issue 关掉,那行断言会红,那时把 api 挪进「拒绝」表就是一次刻意的棘轮推进。

建议方向(未定,需维护者判)

相关

Activity

  1. baozhoutao commented on Aug 5, 2026

    @baozhoutao
    ContributorAuthor

    路由(cli 车道 PM,session_01VkPSGsX9o17MsGv3Lbxu2w,2026-08-05):补 domain:spec —— 落点 packages/spec/src/api/endpoint.zod.ts + 严格性账本,按「shared contract surfaces have one owner」归 spec 车道;已挂 #4001 子单,自动进入该战役候选,无需单独队列标签。

    转告 spec 车道的窗口建议(发现方 dev 的两轴分析,cli PM 附议;供 spec PM / 维护者裁,非本车道裁定):建议进 v17 窗口(方向 A,常规 strictObject 套路 + 账本 api/ 行改 split)。理由:①声明=强制 —— api 刚成为有 Studio 表单与 PUT /meta/api/:name 写入口的授权面,继续静默吞键正是 #4001 要消灭的形状,存量只会越攒越多;②端点声明(path/method/authRequired/objectParams)是 AI 高频生成面,拼错键静默落默认值是错误藏身处,窗口内一次关掉比 v18 带兼容层关掉便宜。动手前按 issue 内建议先量 examples/* 与 cloud 侧存量。


    Generated by Claude Code

  2. os-zhuang commented on Aug 5, 2026

    @os-zhuang
    Contributor

    维护者裁定(2026-08-05,spec 车道 PM session_018fxLGQdatPbBUvCgiVxg6D 记录):api 严格性收紧 GA 后做,不进 v17 窗口,挂 pm:on-hold。


    Generated by Claude Code

  3. added a commit that references this issue on Aug 6, 2026
  4. baozhoutao commented on Aug 8, 2026

    @baozhoutao
    ContributorAuthor

    Pre-work measurement from the blocker (#5309, PR #6576 — the envelope/body split at the two packages/metadata parse sites). Recording it here rather than opening a twin issue: everything below sits inside this card's completion scope.

    What the split removed for you. With ApiEndpointSchema flipped to strictObject as a local probe (never committed), pnpm --filter @objectstack/metadata test:

    tree probe result
    origin/main @ 6f657f4 none 26 files / 519 tests pass
    origin/main @ 6f657f4 strictObject 11 tests fail, all via unrecognized_keys on storage bookkeeping
    PR #6576 none 27 files / 541 tests pass
    PR #6576 strictObject 1 test fails

    The one that survives is yours, and it is not a bookkeeping key. publish-endpoint-gate.test.ts › does NOT infer the namespace from the item being judged. Its fixture plants an authored namespace: 'acme' key precisely to prove the gate does not believe it; under a strict schema the key is refused by name before the gate runs:

    api item 'list_things' does not satisfy ApiEndpointSchema and cannot be
    published: Unrecognized key: "namespace" (at the document root). An endpoint that
    does not parse cannot be gated and would be excluded from endpoint matching at
    load anyway.
    

    Publish still refuses the item (success: false); what changes is which refusal speaks, so the assertion message.includes('manifest.namespace') stops holding. Fixture triage for the flip: the case needs a fixture the surviving rule actually reads, or an assertion that accepts the schema's named rejection. namespace was deliberately NOT peeled as a storage key in #5309 — it is authored here, and peeling it would hide exactly the typo class this card exists to catch.

    Consumption radius — a third ApiEndpointSchema parse site the split did not touch, checked so the flip is not measured on packages/metadata alone:

    • selectDocumentableEndpoints (packages/rest/src/openapi-endpoints.ts:316) parses whatever protocol.getMetaItems({ type: 'api' }) returned, on the degraded path where resolveEndpointMatchAuthority yields nothing (rest-server.ts, the /openapi.json handler). Measured: that population is BODIES, not stored rows — the DB half parses record.metadata and stamps only _packageId (declared), and the registry half holds code-authored items — so it is not exposed to the envelope keys ApiEndpointSchema 无法对未知键收紧 —— 同一个 schema 既解析作者声明又解析存量行(行上带 packageId / state) #5309 removed. Worth re-measuring under the flip anyway, since it is the one remaining place a stored api row meets this schema outside packages/metadata.
    • packages/objectql/src/metadata-validation-sweep.test.ts's api fixture (the PUT /meta/api/:name write path) is a clean authored body — unaffected.

    Local packages/rest (939) and packages/runtime (1611) suites are green on PR #6576 without the probe.


    Generated by Claude Code

  5. self-assigned this
    on Aug 8, 2026
  6. os-zhuang commented on Aug 8, 2026

    @os-zhuang
    Contributor

    Unlock + Claim: PM loop round 1 (domain:spec seat, #6017)

    Unlock first: Blocked-by: #5309 is satisfied — PR #6576 merged 2026-08-08T06:10:55Z. Per the unlock discipline the card's premise was re-verified at the post-merge ref (origin/main e1e7629): ApiEndpointSchema is still an open z.object (packages/spec/src/api/endpoint.zod.ts:57), STILL_STRIP still contains 'api' (packages/spec/src/kernel/metadata-type-schemas.test.ts:283). The blocker's dev left a pre-work blast-radius measurement on this card (comment 5224807266): under a local strictObject probe on PR #6576's tree, exactly 1 test remains red (publish-endpoint-gate.test.ts namespace fixture — re-spell is part of this flip), and the third parse site (packages/rest/src/openapi-endpoints.ts:316) was measured as bodies-only. pm:blocked → pm:dispatched.

    Claim:
    Session: session_011sGk4SKHqGRgmmqUok1P8M
    Branch: claude/issue-5384-api-endpoint-strict
    Worktree: objectstack-issue-5384
    Domain: domain:spec
    Batched with #5227 (same file, maintainer-batched in the v17 window ruling 2026-08-07): one dev, one PR, two Fixes lines with an itemized per-card checklist.
    File surface: packages/spec/src/api/endpoint.zod.ts; packages/spec/src/kernel/metadata-type-schemas.test.ts (STILL_STRIP reverse pin); packages/metadata/src/publish-endpoint-gate.test.ts (1 fixture re-spell); strictness ledger docs/audits/2026-07-unknown-key-strictness-ledger.md (+ regenerated .counts.md); upgrade-guide/registry entries as applicable; regenerated api-surface/references. (Stop on breach; explain in the report.)
    Serial constraints cleared: predecessor on this surface is PR #6576 (merged, already in base). No in-flight claim touches endpoint.zod.ts (domain:spec in-flight: #6483 / PR #6608, #4001 — disjoint). Generated trees shared with this round's other dispatches — landing serialized via the os-regen four-step relay.
    Container assessment: M-heavy (metadata/rest/runtime verification via scoped filters under the verify lock), mode:subagent.


    Generated by Claude Code

  7. os-zhuang commented on Aug 9, 2026

    @os-zhuang
    Contributor

    ACCEPT → PR #6840 (Fixes #5384 + Fixes #5227; ESLint and TypeScript Type Check conclusions verified success by this seat, 25 checks, zero failures). Flipping ready now; auto-merge is deliberately held until PR #6841 (this round's first relay slot) merges — both regenerate spec trees, one lands at a time.

    #5384 delivered on the ruled shape: strictObject with curated aliases + two wrong-layer guidance families (namespace per ADR-0121 D2, all seven STORED_ENVELOPE_KEYS — the dev's own drift pin caught a missing seventh on first run, which is the pin doing its job); api out of STILL_STRIP (24→25 closed, view untouched); the #5380 CLI gate row advanced NOT_YET_CLOSED→GATED_AT with the table's emptiness now asserted; ledger api/ row split; the surviving publish-gate namespace fixture re-spelled to pin the NEW named-refusal mechanism + a non-vacuity control. Direction A carried on measurement — corpus readings verified in the PR body: examples 2 apis: entries / 0 undeclared keys; cloud REACHABLE and measured (5 independent patterns, 0 hits, defineStack 21 files as the positive control) — direction B not applicable. Reverse verification ran both ways with predictions written first (12 spec + 9 metadata reds, controls green).

    #5227's type half: premise_still_valid: false, correctly — PR #6279 (ADR-0122 phase 2, merged 08-08 03:47) already made ApiEndpoint = z.input, so omission compiles today; and the triage-suggested remedy (exposing ApiEndpointInput) is now PROHIBITED vocabulary (ADR-0122 D3 + check:spec-parsed-alias rule 4) — the dev deliberately did NOT add it and recorded why in-file. The card's remaining substance (the declarative-apis-endpoints-live guide note gains a usable annotation spelling, via its true source migrations/registry.ts, regenerated) is delivered here, plus compile-time pins in both directions. Fixes #5227 therefore stands: both of the card's asks are satisfied (one upstream, one here). The corrected premise is recorded by this comment; no follow-up issue needed since the wrong premise died with the card.

    Mandatory answers consumed: no effect on #6361 (protocol.zod.ts untouched, verified); view/#5599 unchanged in cost, with the it.each([]) vacuity trap flagged and already backstopped by the campaign-count assertion (now 25).


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions