Repository navigation
ApiEndpointSchema 在 #5312 把 api 注册成元数据类型之后仍是开放 z.object —— apis: 条目的未声明键两条路都静默落地,严格性账本还把 api/ 整目录记作「wire,按设计宽容」 #5384
Description
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 判,未声明键存进去也不报错。
为什么这条值得单独记
-
它是 未知键静默剥离仍是全仓默认:把 #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 存在的理由。 -
这一类型的键有安全形状。
authRequired: z.boolean().default(true),以及 ADR-0121 D6 「匿名端点需要 armed budget」的那组判断。拼错的键静默落地意味着作者以为自己配了某个姿态、实际拿的是默认值 —— 这个方向恰好是 fail-safe(authRequired落回true),但同一机制对objectParams/cacheTtl/ mapping·policy 块一视同仁,而 AI 批量生成端点声明正是这种拼写错误的高发区。 -
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挪进「拒绝」表就是一次刻意的棘轮推进。
建议方向(未定,需维护者判)
- A. 按 未知键静默剥离仍是全仓默认:把 #3405 的 strict 收紧从一个 schema 推广到整个可授权面(ADR-0078 完整性闸门) #4001 的常规套路把
ApiEndpointSchema转成strictObject(带 surface / history / aliases / guidance),并更新账本api/那一行为 split(endpoint.zod.tsauthorable,其余 wire)。需要先量存量:examples/*与 cloud 侧是否有apis:条目会因此变红。 - B. 若判定端点声明需要保留某些透传键(例如给 connector 的扩展),那就把它们声明出来,而不是靠整个 schema 不严格。
相关
- 未知键静默剥离仍是全仓默认:把 #3405 的 strict 收紧从一个 schema 推广到整个可授权面(ADR-0078 完整性闸门) #4001(严格性推广;本条属于它的完成范围,故作为子单)
- [spec]
api补进 DEFAULT_METADATA_TYPE_REGISTRY 与 BUILTIN_METADATA_TYPE_SCHEMAS(#5206 第 1 步,拆单) #5271 /api不在 metadata 类型注册表里 —— Studio 直写路径完全不校验端点,publishPackageDrafts 也没有 E7 门 #5206 / feat(spec):api补进 DEFAULT_METADATA_TYPE_REGISTRY 与 BUILTIN_METADATA_TYPE_SCHEMAS (#5271) #5312(把api注册进来的那批) objectstack build/validate从不按 PageSchema 解析页面元数据:ADR-0089 D3a 早就该拒绝的键一路通过,#4001 的「三个示例应用 validate 全过」对 page 面是空证 #5000 / PR test(cli): #5000 前提被证伪 —— build/validate 一直按注册表 schema 解析,补上它缺的那份证据 #5380(发现路径与那条账本断言)- ADR-0121(D1 carve-out、D6 匿名端点)
Activity
test(cli): classify the newly registered `api` type in the metadata g…
baozhoutao commentedon Aug 5, 2026
路由(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
os-zhuang commentedon Aug 5, 2026
维护者裁定(2026-08-05,spec 车道 PM
session_018fxLGQdatPbBUvCgiVxg6D记录):api 严格性收紧 GA 后做,不进 v17 窗口,挂pm:on-hold。ApiEndpointSchema无法对未知键收紧 —— 同一个 schema 既解析作者声明又解析存量行(行上带packageId/state) #5309 已实测裸strictObject挂 10 条 metadata 测试 —— 真修法是存量信封键(packageId/state)与作者正文分离,横跨packages/spec+packages/metadata,量级不适合 GA 前窗口;api存量增速有限,代价可控。ApiEndpointSchema无法对未知键收紧 —— 同一个 schema 既解析作者声明又解析存量行(行上带packageId/state) #5309 一单先行、本单随后实施:信封/正文分离(ApiEndpointSchema无法对未知键收紧 —— 同一个 schema 既解析作者声明又解析存量行(行上带packageId/state) #5309)→ApiEndpointSchemastrictObject → 摘出 STILL_STRIP → 台账api/行改判。Generated by Claude Code
test(cli): objectstack-ai#5000 前提被证伪 —— build/validate 一直按注册表 schema …
baozhoutao commentedon Aug 8, 2026
Pre-work measurement from the blocker (#5309, PR #6576 — the envelope/body split at the two
packages/metadataparse 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
ApiEndpointSchemaflipped tostrictObjectas a local probe (never committed),pnpm --filter @objectstack/metadata test:origin/main@ 6f657f4origin/main@ 6f657f4strictObjectunrecognized_keyson storage bookkeepingstrictObjectThe 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 authorednamespace: '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:Publish still refuses the item (
success: false); what changes is which refusal speaks, so the assertionmessage.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.namespacewas 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
ApiEndpointSchemaparse site the split did not touch, checked so the flip is not measured onpackages/metadataalone:selectDocumentableEndpoints(packages/rest/src/openapi-endpoints.ts:316) parses whateverprotocol.getMetaItems({ type: 'api' })returned, on the degraded path whereresolveEndpointMatchAuthorityyields nothing (rest-server.ts, the/openapi.jsonhandler). Measured: that population is BODIES, not stored rows — the DB half parsesrecord.metadataand stamps only_packageId(declared), and the registry half holds code-authored items — so it is not exposed to the envelope keysApiEndpointSchema无法对未知键收紧 —— 同一个 schema 既解析作者声明又解析存量行(行上带packageId/state) #5309 removed. Worth re-measuring under the flip anyway, since it is the one remaining place a storedapirow meets this schema outsidepackages/metadata.packages/objectql/src/metadata-validation-sweep.test.ts'sapifixture (thePUT /meta/api/:namewrite path) is a clean authored body — unaffected.Local
packages/rest(939) andpackages/runtime(1611) suites are green on PR #6576 without the probe.Generated by Claude Code
os-zhuang commentedon Aug 8, 2026
Unlock + Claim: PM loop round 1 (
domain:specseat, #6017)Unlock first:
Blocked-by: #5309is 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/maine1e7629):ApiEndpointSchemais still an openz.object(packages/spec/src/api/endpoint.zod.ts:57),STILL_STRIPstill 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 localstrictObjectprobe on PR #6576's tree, exactly 1 test remains red (publish-endpoint-gate.test.tsnamespace 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_011sGk4SKHqGRgmmqUok1P8MBranch:
claude/issue-5384-api-endpoint-strictWorktree:
objectstack-issue-5384Domain:
domain:specBatched with #5227 (same file, maintainer-batched in the v17 window ruling 2026-08-07): one dev, one PR, two
Fixeslines 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 ledgerdocs/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:specin-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
os-zhuang commentedon Aug 9, 2026
ACCEPT → PR #6840 (
Fixes #5384+Fixes #5227; ESLint and TypeScript Type Check conclusions verifiedsuccessby 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:
strictObjectwith curated aliases + two wrong-layer guidance families (namespaceper ADR-0121 D2, all sevenSTORED_ENVELOPE_KEYS— the dev's own drift pin caught a missing seventh on first run, which is the pin doing its job);apiout ofSTILL_STRIP(24→25 closed,viewuntouched); the #5380 CLI gate row advanced NOT_YET_CLOSED→GATED_AT with the table's emptiness now asserted; ledgerapi/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 2apis:entries / 0 undeclared keys; cloud REACHABLE and measured (5 independent patterns, 0 hits,defineStack21 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 madeApiEndpoint= z.input, so omission compiles today; and the triage-suggested remedy (exposingApiEndpointInput) is now PROHIBITED vocabulary (ADR-0122 D3 +check:spec-parsed-aliasrule 4) — the dev deliberately did NOT add it and recorded why in-file. The card's remaining substance (thedeclarative-apis-endpoints-liveguide note gains a usable annotation spelling, via its true sourcemigrations/registry.ts, regenerated) is delivered here, plus compile-time pins in both directions.Fixes #5227therefore 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 theit.each([])vacuity trap flagged and already backstopped by the campaign-count assertion (now 25).Generated by Claude Code
test(spec): the first api/ file group's test titles state each cited …