Skip to content

Commit cdfbee2

Browse files
os-zhuangclaude
andauthored
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) Part of #5206 (step 1, spec 车道)。 `api` 条目一直被产出(artifact ingest 把 `defineStack({ apis })` 映射为 `api`)、 被索引(`buildEndpointIndex`)、被执行(#5040 E5/E8),而 spec 里哪儿都没声明这个 kind。于是 `getMetadataTypeSchema('api')` 返回 undefined,`saveMetaItem` 走它自己 文档写明的「未注册 schema 的类型不经校验直接落库」分支 —— `PUT /meta/api/:name` 接受任意 JSON。这是 `declared ≠ enforced` 反着读:enforced but undeclared。 - `MetadataTypeSchema` + `DEFAULT_METADATA_TYPE_REGISTRY` 补 `api` 条目; - `BUILTIN_METADATA_TYPE_SCHEMAS` 补 `api: ApiEndpointSchema`; - `ApiEndpointSchema` 补 ADR-0010 保护信封(每个注册类型的不变量); - `api` 的最小 create seed(教 carve-out 形状与 object_operation 的两个半边); - showcase `KIND_COVERAGE` 接手 `apis` 的覆盖(它不再是「非注册表 kind」)。 旗标按证据定,不是新授权:无静态条目时 `isRuntimeCreateAllowed` 与 `assertAllowed` 都走「无注册表条目 ⇒ 可运行时创建」的兜底(两处注释都点名 `api`),所以运行时直写 本来就被接受、只是不校验。`allowRuntimeCreate: true` 把这个既有判决写下来, `allowOrgOverride: false` 同样是今天的实际取值。code-only 方案被证据否掉:它会把 今天的 200 变成 403,且 #5086 在落库前对 draft 一视同仁地拒绝,#5206 第 2 步 (PR #5279)将无 draft 可门。 `ApiEndpointSchema` 的收紧被实测否掉:同一个 schema 也解析存量行,而存量行带 `packageId` / `state`,`strictObject` 让 packages/metadata 10 条测试转红。`api` 因此 与 `view` 同列 STILL_STRIP,实测写进该列表注释,真正的修法(信封/正文分离)另立 #5309。 Fixture 逐条裁定而非批量改写:protocol-meta 与 sys-metadata-repository 里的 `api` 标本被**替换**(留着会让断言经另一条分支变绿、却仍宣称在证明「无静态条目」那条); endpoint-matcher 那条「strips storage annotations」整条重写(它钉的正是信封被丢弃 这个缺陷本身)。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB * chore: sync origin/main + 整体重生成 spec 基线;补 #5279 fixture 的落地路径 `git merge origin/main`(至 5aae790,无冲突),生成物按 os-regen 四步互保: generated 文件整体 checkout 回合并基线,再 wholesale 重生成,最后断言兄弟 PR 的条目仍在。 两处过程中发现并纠正的坑,记下来免得下一个人重踩: 1. `gen:api-surface` 读的是**构建产物**,不是源码。合并后没重建就重生成,会把 #5021(PR #5289)刚退役的 `AnimationSchema` / `ZIndexSchema` 四行**重新加 回去** —— 正是 AGENTS.md §9 的陈旧产物陷阱。重建 spec 后重生成才对 (4422 → 4418 exports)。 2. 第 2 步的 `git checkout origin/main -- <generated>` 必须用**你实际合并的那个 tip**,不是 `origin/main` 的当前值。main 在我合并与 checkout 之间又前进了, 于是把 #4938 的 http-server 生成文档拉了进来 —— 而我的源码里没有那个改动, 等于提交了一份源码产不出的生成物。改用合并基线 5aae790 后归零。 最终生成物 delta 相对合并基线只有 7 行,纯增量(ApiEndpoint 的保护信封键); #5021 的退役完好(Animation/ZIndex 确认缺席)。 `protocol-publish-drafts-endpoint-gate.test.ts`(#5279,已合入 main)的 ENDPOINT_SCHEMA 用例改了**落地路径**而非断言:该 draft 现在过不了 saveMetaItem 的 422(这正是 #5206「一处修,两面得」要的结果),所以 fixture 改为先按真实写 路径存一条合法 draft、再只污染其 body —— 让那条 backstop 分支仍然被真实覆盖, 而不是删掉用例留一条无测试的活分支。未改该 PR 的任何生产代码。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB * chore(spec): 把 `api` 纳入 liveness 治理并播种台账;重生成三份参考文档 (#5271) PM 返工第 1 轮。把 `api` 注册成 metadata type 就落入「每个 REGISTERED 类型 必须被治理」不变量,而 `check:liveness` 不在我上一轮跑过的门清单里: ✗ 1 REGISTERED metadata type(s) governed by nothing: api 走的是第一条路线(纳入 GOVERNED + 播种台账),没有用 PENDING_GOVERNANCE —— 这是治理成本最低的时点:#5040 的 E 系列执行器全部已合 main,每个键**今天** 都有真实证据路径,而不是一句承诺。datasource 的教训(#4487:无治理期积了六个 惰性键,只能靠人手找出来)就是这条路线存在的理由。 27 条属性分类:live 25、planned 2、dead 0。 逐键证据按层给到 file:line —— 匹配器(endpoint-matcher)吃 name/path/method; 执行器(endpoint-executor)吃 type/target/objectParams;策略链 (endpoint-policy + security/inbound-rate-limit)吃 authRequired/rateLimit/ cacheTtl;映射层(api-mapping)吃 inputMapping/outputMapping;OpenAPI 增强 (rest/openapi-endpoints)吃 summary/description。7 个保护信封键由门自动判 live (ADR-0010),故不写进台账。 两个 `transform` 判 **planned 而非 dead**,这个区分是有承重的:台账里的 dead 指「解析了、没有消费方」即静默 no-op;而 transform 是反过来 —— 它被解析后在 publish 与 runtime 两处**响亮拒绝**(api-mapping.ts:259),作者会被告知怎么改。 它留在词表里而不是被删,是因为接纳它需要函数注册表 + sandbox 裁决 (#5040 §3.4),那是一个待做的设计决定,不是一个可以顺手删掉的键。 无任何 `api` 键属于 proof-registry 的 bound high-risk class,因此没有一条带 `proof` —— 不为显得周全而编造。 另:`check:docs` 实测**仍红**(上一轮的同步提交并没有修好它,我从未跑过 `gen:docs`)。三份生成文档已重生成,delta 纯属本单:endpoint.mdx 的 7 个信封键, metadata.mdx / metadata-plugin.mdx 的枚举里多了 `api`。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB * chore(i18n): 重生成 platform-objects 的四个翻译 bundle (#5271) PM 返工第 2 轮。`api` 进 metadata type registry 后,platform-objects 的 metadata-forms bundle 键集多出该类型的 label/description,四个 locale 漂移。 `node scripts/check-i18n-bundles.mjs --write`(merge 模式:不覆盖任何既有翻译, 新键以源文填充)。delta 纯属本单 —— 四个 bundle 各 +4 行,只有 `api` 一个键: api: { label: "API Endpoint", description: "Declarative HTTP endpoint — …" } 非英文 bundle 里这两条暂为英文源文,这是 merge 模式的既定产物("they still need translating");`check:i18n-coverage` 的冻结基线未被突破,故两门皆绿。 手改 `.generated.ts` 不是这条链的修法。 ⚠️ 这是我第三次漏门(liveness → docs → i18n),根因是我一直按"改了哪个包就跑 哪个包的门"收工,而这三道都是**仓根**的、由被改内容触发而非由被改目录触发。 已改为按 `.github/workflows` 的 job 清单逐条过:本次 push 前跑完 44 道 CI 必跑 门(lint.yml 25 道 + spec-liveness-check.yml 4 道 + spec 生成物 13 道 + validate-deps.yml 2 道),全绿。 注:i18n 两门必须在 **full build 之后**跑 —— 抽取器经 extract config 读 `@objectstack/spec` 的 dist、并跑构建后的 CLI。未构建时它报的是九个包 "extract failed — no output",很容易被误读成配置坏了而不是缺构建。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB * chore(spec): gen:schema 推进 authorable-surface 锚点至 origin/main 28ad90e 合并 origin/main 后重跑 gen:schema,锚点 baseRev 由 1c3da1f0899 前进到 merge base 28ad90e(7907 keys,与该 commit 的 authorable-surface.json 逐行一致)。生成器写入,非手改;lag 本可保留,但 gen:schema 在 keys 漂移 (#5321 退役 109 键)时按设计刷新。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ErbEDVAg1No9gdg1pgDAGB --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 9c5abf4 commit cdfbee2

24 files changed

Lines changed: 874 additions & 157 deletions
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec): `api` is a declared metadata kind — `DEFAULT_METADATA_TYPE_REGISTRY` + `BUILTIN_METADATA_TYPE_SCHEMAS` (#5271, part of #5206)
6+
7+
`api` items were produced, indexed and executed while the spec declared the kind
8+
nowhere. Artifact ingest maps `defineStack({ apis })` to `api` metadata
9+
(`ARTIFACT_FIELD_TO_TYPE`), the endpoint matcher indexes them
10+
(`buildEndpointIndex`), and #5040's executor serves them — but
11+
`DEFAULT_METADATA_TYPE_REGISTRY` had no `{ type: 'api', … }` entry and
12+
`BUILTIN_METADATA_TYPE_SCHEMAS` had no `api` binding. So
13+
`getMetadataTypeSchema('api')` returned `undefined` and `saveMetaItem` took its
14+
documented "unregistered type is stored without validation" branch:
15+
`PUT /api/v1/meta/api/:name` accepted **any JSON** and answered 200. That is
16+
`declared ≠ enforced` read backwards — enforced but **undeclared**.
17+
18+
Both halves are now declared, which is one fix with two faces:
19+
20+
- **A body is validated.** The existing 422 `invalid_metadata` path applies to
21+
`api` like every other kind, with structured Zod issues naming the offending
22+
key. An endpoint with no `target`, or no `type`, is refused instead of stored.
23+
- **The type is describable.** `/meta/types` emits a real JSON Schema and a
24+
create seed for `api`, so the metadata-admin engine renders a form rather than
25+
a raw-JSON textarea, and the entry carries a real label, domain and file
26+
patterns instead of the synthesised `label: 'api'`, `filePatterns: []`
27+
placeholder a type with no registry row gets.
28+
29+
**The write door is unchanged.** `allowRuntimeCreate: true` records what the
30+
runtime already did: with no static registry entry, both write gates
31+
(`isRuntimeCreateAllowed`, `assertAllowed`) fall through to "runtime-creatable",
32+
and both name `api` in that comment. `allowOrgOverride` stays `false`, also its
33+
effective value today — an endpoint is the publishing package's outward URL
34+
contract, and a per-org fork could move `path`, flip `authRequired` or drop
35+
`rateLimit` on a URL third parties integrate against. Marking the type code-only
36+
instead (`allowRuntimeCreate: false` + `allowOrgOverride: false`) was considered
37+
and rejected: it would turn today's 200 into a 403 rather than validate it, and
38+
#5086's refusal runs before persistence for drafts too, which would leave
39+
#5206 step 2's `publishPackageDrafts` endpoint gate with no draft to gate.
40+
41+
**`ApiEndpointSchema` gains the ADR-0010 protection envelope, and stays open to
42+
unknown keys.** Every registered kind must declare the envelope its loader
43+
stamps (`_packageId` / `_provenance`), or it is dropped on every parse; that
44+
spread is added. Closing the shape against unknown keys was attempted and
45+
**measured to be unsafe**: the same schema parses stored rows as well as
46+
authored declarations (`buildEndpointIndex`, `gateApiItemsForPublish`), and a
47+
stored row carries the metadata layer's own bookkeeping (`packageId`, `state`),
48+
so `strictObject` turned 10 tests in `packages/metadata` red — the load-time
49+
backstop excluded endpoints and the publish gate reported a schema error in
50+
place of its ADR-0121 D6 verdict. `api` therefore joins `view` on the #4001
51+
campaign's `STILL_STRIP` list, with that measurement written into the list's own
52+
note, and the real fix (separating the stored envelope from the body at the
53+
metadata layer) is filed as #5309 rather than bought by teaching the authoring
54+
vocabulary two storage keys.
55+
56+
**This is a shape check, not a second servability judge.** ADR-0121's rules —
57+
the `apps/<namespace>` carve-out (D1/D2), anonymous-requires-an-armed-`rateLimit`
58+
(D6), the supported target subset, mapping and policy — stay with
59+
`validateApiEndpointDeclarations` / `identityFreeEndpointGateFailure`, which run
60+
at publish and again at load. A pin test asserts an anonymous unmetered endpoint
61+
parses green here and is still refused by the gate, so the two never grow
62+
competing opinions.
63+
64+
**Upgrade note (not purely additive).** A stored `api` row that does not satisfy
65+
`ApiEndpointSchema` is refused with 422 on its **next write**; reads and the
66+
existing load-time behaviour are unchanged (the matcher already excluded
67+
unparseable rows loudly, #5189). Every `api` declaration reachable in this repo
68+
— the two E8-migrated showcase endpoints and the two dogfood policy-fixture
69+
endpoints — was parsed against `ApiEndpointSchema` before landing this: all four
70+
clean. A live deployment's `sys_metadata` cannot be scanned from CI; an operator
71+
holding hand-written `api` rows should run `GET /api/v1/meta/diagnostics?type=api`
72+
(which now covers the type) before upgrading.
73+
74+
ADR-0088's admission test is satisfied on all three clauses: independent
75+
lifecycle (the matcher indexes and invalidates one item at a time), declarative
76+
governability (`allowRuntimeCreate` plus file patterns), and a real consumer
77+
(#5040's executor, boot-proven by #5040 E8). This does not reverse the `router`
78+
kind's retirement — `router`'s delivered forms are code contributions, whereas a
79+
single `ApiEndpoint` is a declarative artifact, exactly the "third, real
80+
delivered form" ADR-0088's own `router` row anticipated.

content/docs/references/api/endpoint.mdx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ const result = ApiEndpointSchema.parse(data);
4444
| **authRequired** | `boolean` || Require authentication |
4545
| **rateLimit** | `{ enabled: boolean; windowMs: integer; maxRequests: integer }` | optional | Rate limiting policy |
4646
| **cacheTtl** | `number` | optional | Response cache TTL in seconds |
47+
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
48+
| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. |
49+
| **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). |
50+
| **_provenance** | `Enum<'package' \| 'org' \| 'env-forced'>` | optional | Origin of the item (package \| org \| env-forced). |
51+
| **_packageId** | `string` | optional | Owning package machine id. |
52+
| **_packageVersion** | `string` | optional | Owning package version. |
53+
| **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. |
4754

4855

4956
---

content/docs/references/api/metadata.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,7 @@ Metadata query with filtering, sorting, and pagination
341341

342342
| Property | Type | Required | Description |
343343
| :--- | :--- | :--- | :--- |
344-
| **types** | `Enum<'object' \| 'field' \| 'hook' \| 'seed' \| 'mapping' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'dataset' \| 'flow' \| 'job' \| 'datasource' \| 'external_catalog' \| 'translation' \| 'email_template' \| 'doc' \| 'book' \| 'permission' \| 'position' \| 'agent' \| 'tool' \| 'skill'>[]` | optional | Filter by metadata types |
344+
| **types** | `Enum<'object' \| 'field' \| 'hook' \| 'seed' \| 'mapping' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'dataset' \| 'flow' \| 'job' \| 'datasource' \| 'external_catalog' \| 'translation' \| 'api' \| 'email_template' \| 'doc' \| 'book' \| 'permission' \| 'position' \| 'agent' \| 'tool' \| 'skill'>[]` | optional | Filter by metadata types |
345345
| **namespaces** | `string[]` | optional | Filter by namespaces |
346346
| **packageId** | `string` | optional | Filter by owning package |
347347
| **search** | `string` | optional | Full-text search query |
@@ -376,7 +376,7 @@ Metadata query with filtering, sorting, and pagination
376376

377377
| Property | Type | Required | Description |
378378
| :--- | :--- | :--- | :--- |
379-
| **type** | `Enum<'object' \| 'field' \| 'hook' \| 'seed' \| 'mapping' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'dataset' \| 'flow' \| 'job' \| 'datasource' \| 'external_catalog' \| 'translation' \| 'email_template' \| 'doc' \| 'book' \| 'permission' \| 'position' \| 'agent' \| 'tool' \| 'skill'>` || Metadata type |
379+
| **type** | `Enum<'object' \| 'field' \| 'hook' \| 'seed' \| 'mapping' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'dataset' \| 'flow' \| 'job' \| 'datasource' \| 'external_catalog' \| 'translation' \| 'api' \| 'email_template' \| 'doc' \| 'book' \| 'permission' \| 'position' \| 'agent' \| 'tool' \| 'skill'>` || Metadata type |
380380
| **name** | `string` || Item name (snake_case) |
381381
| **data** | `Record<string, any>` || Metadata payload |
382382
| **namespace** | `string` | optional | Optional namespace |

content/docs/references/kernel/metadata-plugin.mdx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ const result = MetadataBulkResultSchema.parse(data);
152152

153153
| Property | Type | Required | Description |
154154
| :--- | :--- | :--- | :--- |
155-
| **types** | `Enum<'object' \| 'field' \| 'hook' \| 'seed' \| 'mapping' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'dataset' \| 'flow' \| 'job' \| 'datasource' \| 'external_catalog' \| 'translation' \| 'email_template' \| 'doc' \| 'book' \| 'permission' \| 'position' \| 'agent' \| 'tool' \| 'skill'>[]` | optional | Filter by metadata types |
155+
| **types** | `Enum<'object' \| 'field' \| 'hook' \| 'seed' \| 'mapping' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'dataset' \| 'flow' \| 'job' \| 'datasource' \| 'external_catalog' \| 'translation' \| 'api' \| 'email_template' \| 'doc' \| 'book' \| 'permission' \| 'position' \| 'agent' \| 'tool' \| 'skill'>[]` | optional | Filter by metadata types |
156156
| **namespaces** | `string[]` | optional | Filter by namespaces |
157157
| **packageId** | `string` | optional | Filter by owning package |
158158
| **search** | `string` | optional | Full-text search query |
@@ -202,6 +202,7 @@ const result = MetadataBulkResultSchema.parse(data);
202202
* `datasource`
203203
* `external_catalog`
204204
* `translation`
205+
* `api`
205206
* `email_template`
206207
* `doc`
207208
* `book`
@@ -220,7 +221,7 @@ const result = MetadataBulkResultSchema.parse(data);
220221

221222
| Property | Type | Required | Description |
222223
| :--- | :--- | :--- | :--- |
223-
| **type** | `Enum<'object' \| 'field' \| 'hook' \| 'seed' \| 'mapping' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'dataset' \| 'flow' \| 'job' \| 'datasource' \| 'external_catalog' \| 'translation' \| 'email_template' \| 'doc' \| 'book' \| 'permission' \| 'position' \| 'agent' \| 'tool' \| 'skill'>` || Metadata type identifier |
224+
| **type** | `Enum<'object' \| 'field' \| 'hook' \| 'seed' \| 'mapping' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'dataset' \| 'flow' \| 'job' \| 'datasource' \| 'external_catalog' \| 'translation' \| 'api' \| 'email_template' \| 'doc' \| 'book' \| 'permission' \| 'position' \| 'agent' \| 'tool' \| 'skill'>` || Metadata type identifier |
224225
| **label** | `string` || Display label for the metadata type |
225226
| **description** | `string` | optional | Description of the metadata type |
226227
| **filePatterns** | `string[]` || Glob patterns to discover files of this type |

examples/app-showcase/src/coverage.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,17 @@ export const KIND_COVERAGE: Record<MetadataType, KindCoverage> = {
115115
'PERMANENT by design (ADR-0088): a runtime-created snapshot produced by Setup → Datasources → Sync (ADR-0062). A package shipping one would be stale on arrival; the showcase demos the federation flow that produces it.',
116116
issue: ISSUE.noAuthoringSurface,
117117
},
118+
// [#5271] `api` graduated from STACK_COLLECTION_COVERAGE into the registry:
119+
// it is now a real metadata kind (`DEFAULT_METADATA_TYPE_REGISTRY` +
120+
// `BUILTIN_METADATA_TYPE_SCHEMAS`), so its coverage is owned here. The notes
121+
// below moved verbatim from the old `STACK_COLLECTION_COVERAGE.apis` entry —
122+
// the proof did not change, only which manifest is responsible for it.
123+
api: {
124+
status: 'demonstrated',
125+
files: ['src/system/apis/index.ts'],
126+
notes:
127+
'Declarative ApiEndpoint metadata (object_operation + flow targets), MEASURED on a real boot rather than asserted: packages/qa/dogfood/test/showcase-declarative-endpoints.dogfood.test.ts boots the showcase through the artifact-ingestion path and proves each declared path is matched and executed (the find endpoint answers byte-identically to the built-in /data route for the same operation), that `authRequired` denies anonymous with 401, that `cacheTtl: 30` reaches the wire as Cache-Control on successes only, and that /openapi.json and GET /meta/api describe exactly what is mounted. This entry read "demonstrated … executed by the runtime dispatcher (handleApiEndpoint)" once BEFORE that was true — #4936 measured it and found a bare 404 on every declared path, which is why the waiver stood from #4936 until the #5040 executor landed. It is restored to `demonstrated` only because a real-boot test now fails if any of it stops being true (#5040 E8 / #5112). The `router` kind stays retired: code-only (ADR-0088). src/system/server/recalc-endpoint.ts remains the code-mounted HTTP counterpart.',
128+
},
118129
translation: { status: 'demonstrated', files: ['src/system/translations/index.ts'] },
119130
email_template: { status: 'demonstrated', files: ['src/system/emails/index.ts'] },
120131
doc: {
@@ -179,12 +190,10 @@ export const STACK_COLLECTION_COVERAGE: Record<string, KindCoverage> = {
179190
files: ['src/data/extensions/account.extension.ts'],
180191
notes: 'Merged into showcase_account by the ObjectQL engine at registerApp (priority overlay).',
181192
},
182-
apis: {
183-
status: 'demonstrated',
184-
files: ['src/system/apis/index.ts'],
185-
notes:
186-
'Declarative ApiEndpoint metadata (object_operation + flow targets), MEASURED on a real boot rather than asserted: packages/qa/dogfood/test/showcase-declarative-endpoints.dogfood.test.ts boots the showcase through the artifact-ingestion path and proves each declared path is matched and executed (the find endpoint answers byte-identically to the built-in /data route for the same operation), that `authRequired` denies anonymous with 401, that `cacheTtl: 30` reaches the wire as Cache-Control on successes only, and that /openapi.json and GET /meta/api describe exactly what is mounted. This entry read "demonstrated … executed by the runtime dispatcher (handleApiEndpoint)" once BEFORE that was true — #4936 measured it and found a bare 404 on every declared path, which is why the waiver stood from #4936 until the #5040 executor landed. It is restored to `demonstrated` only because a real-boot test now fails if any of it stops being true (#5040 E8 / #5112). The `router` kind stays waived: code-only. src/system/server/recalc-endpoint.ts remains the code-mounted HTTP counterpart.',
187-
},
193+
// `apis` is NOT listed here any more: as of #5271 it is a registry kind, so
194+
// its coverage lives in `KIND_COVERAGE.api` above. Leaving a duplicate row in
195+
// this manifest — whose contract is "stack collections that are NOT registry
196+
// kinds" — would mean two places to update and one of them silently wrong.
188197
connectors: {
189198
status: 'demonstrated',
190199
files: ['src/system/connectors/index.ts', 'src/automation/flows/index.ts'],

packages/metadata-protocol/src/protocol-publish-drafts-endpoint-gate.test.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -318,11 +318,30 @@ describe('publishPackageDrafts — the ADR-0121 endpoint publish gate (#5206 ste
318318
const { engine, rows } = makeStubEngine('showcase');
319319
const protocol = new ObjectStackProtocolImplementation(engine);
320320

321-
// `api` has no entry in BUILTIN_METADATA_TYPE_SCHEMAS (that half is
322-
// #5271, the spec lane), so the direct-write path stores arbitrary JSON
323-
// verbatim. Parsing is the gate's precondition: an unparseable
324-
// declaration cannot be judged and could never be served either.
325-
await saveApiDraft(protocol, 'garbage', { name: 'garbage', totally: 'not an endpoint' });
321+
// [#5271] This comment used to read "`api` has no entry in
322+
// BUILTIN_METADATA_TYPE_SCHEMAS (that half is #5271, the spec lane), so
323+
// the direct-write path stores arbitrary JSON verbatim" — and it minted
324+
// the garbage draft through `saveMetaItem`. That half has now landed:
325+
// `api` resolves `ApiEndpointSchema`, so this body is refused with a
326+
// 422 at the EARLIEST door and the draft can no longer be created at
327+
// all. That is the "一处修,两面得" outcome #5206 asked for, and it is
328+
// asserted on the spec lane's side (packages/objectql
329+
// /src/protocol-meta.test.ts, "refuses a spec-INVALID `api` item").
330+
//
331+
// The `ENDPOINT_SCHEMA` branch this case pins is therefore no longer
332+
// reachable from the Studio write path — it is exactly what the module
333+
// header calls it, a BACKSTOP, for a row that reached the store some
334+
// other way: a direct `metadata.register()`, a migration, or a row
335+
// written before #5271. Deleting the case would leave a live branch
336+
// with no test; re-spelling the body would only re-test the 422. So the
337+
// fixture PLANTS such a row instead of minting one — it saves a valid
338+
// draft through the real write path (so every bookkeeping column is
339+
// byte-for-byte what production writes) and then corrupts only the
340+
// stored body, which is the one thing the earlier door cannot police.
341+
await saveApiDraft(protocol, 'garbage', validEndpoint({ name: 'garbage' }));
342+
const planted = Array.from(rows.values()).find((r) => r.name === 'garbage' && r.state === 'draft');
343+
expect(planted, 'the valid draft must exist before it is corrupted').toBeDefined();
344+
planted!.metadata = JSON.stringify({ name: 'garbage', totally: 'not an endpoint' });
326345

327346
const res = await protocol.publishPackageDrafts({ packageId: PKG });
328347
expect(res).toMatchObject({ success: false, publishedCount: 0 });

0 commit comments

Comments
 (0)