Skip to content

fix(metadata-protocol): publishPackageDrafts 对 api draft 跑 ADR-0121 端点发布门 (#5206 step 2) - #5279

Merged
os-zhuang merged 3 commits into
mainfrom
claude/issue-5206-publish-drafts-endpoint-gate
Aug 4, 2026
Merged

fix(metadata-protocol): publishPackageDrafts 对 api draft 跑 ADR-0121 端点发布门 (#5206 step 2)#5279
os-zhuang merged 3 commits into
mainfrom
claude/issue-5206-publish-drafts-endpoint-gate

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Part of #5206 (step 2, engine half; step 1 tracked in #5271)

问题

protocol.publishPackageDrafts 是 Studio「全部发布」按钮的真实入径(ADR-0033 / ADR-0067 D2)。在此之前,它唯一的按类型前置检查是对象命名空间前缀规则(validateObjectNamespacePrefix,d.type === 'object' 才跑),于是一条 api draft 不经任何一道门就被提升为 active —— 与 #5189MetadataManager.publishPackage 上修掉的是同一形状、另一条路。

安全后果早已被 #5203 的装载期兜底挡住:端点匹配器在建索引时用同一个 firstFailure 重判每一条存量条目,没过门的被排除出索引并 error 点名。所以本 PR 修的是拒绝得太晚:ADR-0121 的原文是「publish 拒绝」,作者应当在 publish 当场拿到点名 key 的处方,而不是到装载期日志里才发现自己的端点在答 404。

判据只有一份

新增的私有方法 gateApiDraftsForPublish 调用 @objectstack/spec/api 导出的 validateApiEndpointDeclarations(#5203 转公开)—— 就是 stack schema 跑的那个函数、publishPackage 跑的那个函数、装载期兜底跑的那个 firstFailure。拒绝文案直接用门函数自己的 message(已包含端点名、越界的 key 和改法),本包不复述任何一条「什么算可服务」的规则。

publishPackage 不同,这条路有身份:包的 manifest.namespace 本来就为对象前缀规则读过了(this.engine?.registry?.getPackage?.(…)?.manifest?.namespace),所以这里跑的是全量门,命名空间门(ADR-0121 D1/D2)包含在内。命名空间门以「包声明了 namespace」为条件 —— 门函数自己的前置判据本身就是一条判据,对「压根没有 namespace」的包跳过它,等于给最不可能过编译期的那批包留一个洞。对象前缀规则对无 namespace 的包网开一面,是因为一个裸对象名只是命名气味;一个无命名空间的端点是一个无主 URL

失败姿势:我找到的是哪一种,以及我镜像了它

任务要求「沿用该函数既有的 per-item 失败姿势(即命名空间前缀违规所用的那种),不要发明新的批次语义」。我找到的既有姿势是:

  • 判定发生在提升任何东西之前的 pre-flight 阶段;
  • 一条违规就让整批不落地 —— success: falsepublishedCount: 0published: [],同批的健康 draft 保持 draft 态;
  • 但报告粒度是 per-item:每条违规在 failed[] 里各占一项,带 type / name / error / code,点名到具体条目。

也就是说「per-item」说的是报告粒度,「整批拒绝」说的是落地语义,两者并存。端点门原样加入同一个 pre-flight,行为完全一致 —— 这既是 ADR-0067 D2 的「一次 commit 不能落一半」,也正是 #5189 在另一条路上的同一姿势(一份 validationErrorsitemsPublished: 0)。

唯一的组织性变化:原先命名空间违规是在 if (pkgNamespace) 块里直接 return,现在两类违规先汇入同一个 preflightViolations 再统一 return。于是作者一次往返就能同时看到对象命名违规和端点违规,而不是修完一类再撞下一类。

gateApiDraftsForPublish 的实现是 #5189 gateApiItemsForPublish 的逐段镜像,包括「命名空间门只报一次、不归属到任何单条端点(name: '')、并追加本入径专属的 remedy 文案」这一处理 —— 后者对应 metadata-manager 里的 PUBLISH_NAMESPACE_REMEDY 常量,是既有模式而非本 PR 的发明。remedy 只回答「那我该去哪儿设」,不改变判据(拼接形式为 ${issue.message} ${REMEDY},门的原文一字不动)。

边界(写明而非默认)

  • 判定范围是本批被提升的 draft,与紧邻它的对象前缀规则一致。与同包已 active 的端点撞车不在此拦截 —— 匹配器对全库重复声明有确定性裁决并 error 点名(buildEndpointIndex);把范围扩到整包 active 集合意味着「因为你没在发布的东西而拒绝这次发布」,那是另一份契约,不是一个 bug 修复。
  • listDrafts 之后消失的 draft 行跳过而不是伪造成门失败,由提升循环报它真正的 no_draft
  • 读 store 失败则抛。「读不到 draft」绝不能被回答成「门过了」(ADR-0110 D3 的同一条区分)。
  • body 连 ApiEndpointSchema 都不满足的,codeENDPOINT_SCHEMA。解析是判定的前置条件而非第六道门:判不了的形状也服务不了(匹配器在装载期同样会响亮跳过它),放它过去等于铸一条永远答 404 的路由。这与 MetadataManager.publishPackage 不经 E7 端点门 —— ADR-0121 D6「匿名须限流」在 Studio 发布路径上不成立 #5189 在另一条路上的裁决一致。

装载期兜底(#5203)原样保留,未移除也未削弱 —— publish 是更早的那道门,不是最后那道门的替代品。测试文件末尾专门钉住了这一配对:publish 现在拒绝的那个 body,正是 identityFreeEndpointGateFailure 也拒绝的 body;哪天有人放松了一侧,这两条断言里恰好会翻掉一条。

为什么走台账而不是 pin(记录,非辩护)

check:engine-double-contract 判定本 PR 新增测试里的 fake engine 未 pin:它的 delete() 没有走 assertEngineDeleteDispatch。该门禁的立意是 #4434 —— 一个比 ObjectQL.delete 宽松的假引擎,让一条死掉的 REST 路由带着绿套件发布。

处方是「加 @objectstack/objectql 到 devDependencies,再用该判定函数打开 fake 的 delete」。这条路被实测证明在本包不可用:@objectstack/objectqldependencies 里就有 @objectstack/metadata-protocol(workspace:*),反向加边必然成环。按处方加边、pnpm install,然后:

$ pnpm exec turbo run build --filter=@objectstack/metadata-protocol --dry
• turbo 2.10.7
 WARNING  Circular package dependency detected: @objectstack/objectql, @objectstack/metadata-protocol
  x Cyclic dependency detected:
  |     @objectstack/objectql#build, @objectstack/metadata-protocol#build

turbo 直接拒绝构建图。边与 lockfile 已还原,本 PR 未新增任何 devDependency

这与 scripts/engine-double-contract.baseline.jsonsys-metadata-repository.history-counters.test.ts(#4867)、sys-metadata-repository.draft-drain.test.ts(#4981)两条 MEASURED 条目记录的是同一个环、同样的测法;两条的 closes 都指向 #4987(把判定函数下沉到双方共同依赖的包),台账原文并写明该 devDependency 路线对本包及其五条同族条目一并关闭。本文件是撞上这条已记录路线的第六个。#4987 复核尚未落地,其归属包(metadata-core 抑或 spec/contracts)留给它自己的 PR 决定,本 PR 不代选。

因此新增一条 baseline 条目(纯增量:7 行插入、0 行删除,未重排/改写/下调任何既有条目)。

这条是 DEBT,不是豁免。 它记录的是「这个 double 结构上比契约松」,不主张「这份松弛当前无害」—— 这正是本台账 $comment 写死的区分。条目 why 中记入了探针事实(把 fake 的 delete 换成抛错后确认:delete 确被调用,且只以标量 { where: { id } } 形式出现,故「删掉该方法绕开扫描」亦不成立),但按 #4867 / #4981 两条同族条目的同一句判断,那是关于本文件的论据、不是关于契约的论据,所以仍为 DEBT 而非 EXEMPT

范围

验证

代码部分在 git merge origin/main(合并至 0f2fdcde1,无冲突)之后、并重建依赖链(AGENTS.md §9)后复跑。

$ pnpm check:engine-double-contract
OK  self-test: separates engine doubles from driver doubles, accepts only the
    producer's predicate (direct or one helper deep), rejects unused imports,
    hand-mirrored guards and look-alikes, and proves discovery reaches the real tree.
check-engine-double-contract: OK — 22 pinned, 32 in the DEBT ledger, 1 exempt.

$ pnpm --filter @objectstack/metadata-protocol test -- --maxWorkers=2
 Test Files  42 passed (42)
      Tests  388 passed (388)

$ pnpm --filter @objectstack/metadata-protocol exec vitest run \
    src/protocol-publish-drafts-endpoint-gate.test.ts --maxWorkers=2
 Test Files  1 passed (1)
      Tests  12 passed (12)

$ pnpm exec eslint packages/metadata-protocol/src/protocol.ts \
    packages/metadata-protocol/src/protocol-publish-drafts-endpoint-gate.test.ts
(clean, exit 0)

本包没有 typecheck script(scripts/check-type-check-coverage.mjs 里的 DEBT 台账条目),所以按 tsc --noEmit -p packages/metadata-protocolorigin/main 基线对比:

origin/main 基线:  63 errors
本分支:            63 errors
delta:             0   (两份输出 diff 完全一致)
本 PR 改动的文件中的 error 数: 0

顺带记录一条与本 PR 无关的观察:该 DEBT 台账为本包记的是 errors: 28,实测(origin/main 上)为 63,而 ratchet 只校验条目存在且为正、从不复测。已按 Prime Directive #10 单独立单 #5278(finding 标签、未指派),未在本 PR 中改动该台账

新增回归测试

packages/metadata-protocol/src/protocol-publish-drafts-endpoint-gate.test.ts(12 例):

  • api draft 违反自由身份门(D6:authRequired: false 无 armed 预算)→ publish 拒绝并点名越界的 key;
  • rateLimit 存在但 enabledtrue 仍拒绝(D6 不是存在性检查);
  • 合法 api draft 照常发布;anonymous + armed 预算也照常发布(D6 可满足,不是禁令);
  • 落在 carve-out 之外的 path、同批 METHOD+path 重复声明,均拒绝;
  • body 不满足 ApiEndpointSchemaENDPOINT_SCHEMA;
  • 包无 namespace → D2 前置判据只报一次、不归属单条端点;
  • 整批拒绝:坏 api draft 的健康同批兄弟保持 draft 态;
  • 对象命名违规 + 端点违规合并在同一份报告;
  • api draft 完全不受影响;
  • fix(metadata,spec): publish 门兜底 —— publishPackage 接门 + 匹配器装载期拒绝(#5040 E7b) #5203 装载期兜底仍在,未被替代。

🤖 Generated with Claude Code

https://claude.ai/code/session_01Pbu27iNUfQCHeuS551Rqo7

claude added 2 commits August 4, 2026 15:10
…pi` drafts in publishPackageDrafts (#5206 step 2)

`protocol.publishPackageDrafts` — the real entry point behind Studio's
"publish everything" (ADR-0033 / ADR-0067 D2) — had exactly one type-aware
pre-flight: the object namespace-prefix rule, gated on `d.type === 'object'`.
An `api` draft was therefore promoted draft→active having met no gate at all,
the same shape #5189 closed on `MetadataManager.publishPackage`, one path over.

The gate reuses `validateApiEndpointDeclarations` (public from
`@objectstack/spec/api` since #5203) verbatim — the same function the stack
schema runs, the same function `publishPackage` runs, and the same
`firstFailure` the endpoint matcher's load-time backstop runs. Refusal messages
are the gate's own; nothing here restates a criterion for what is servable.

Unlike `publishPackage`, this path already resolves the package's declared
`manifest.namespace` for the object-prefix rule, so the FULL gate runs,
namespace included (ADR-0121 D1/D2).

Failure posture mirrors the pre-existing namespace-prefix rule exactly: a
pre-flight refusal found before anything is promoted, whole batch withheld
(`success: false`, `publishedCount: 0`, `published: []`), one entry per
offending item in `failed[]`. Both violation classes now come back in one
report instead of one class per publish attempt.

The #5203 load-time backstop is untouched — this is the earlier door, not a
replacement for the last one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pbu27iNUfQCHeuS551Rqo7
@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 3:54pm

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-protocol.

3 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-protocol)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/metadata-protocol)
  • content/docs/releases/v9.mdx (via @objectstack/metadata-protocol)

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.

Copy link
Copy Markdown
Contributor Author

check:engine-double-contract 红了 —— 处方路线已实测阻塞,等一个决定

CI 的 ESLint job 里 pnpm check:engine-double-contract 报本 PR 新增的测试文件:

x PINNED: packages/metadata-protocol/src/protocol-publish-drafts-endpoint-gate.test.ts
  declares 1 engine double(s) whose delete() does not route through
  assertEngineDeleteDispatch (line 122).

处方是「用 assertEngineDeleteDispatch(options) 打开 fake 的 delete,@objectstack/objectql 不在 devDependencies 就加上」。我把这条路跑到底了,它是死路,而且是这个台账自己早就记过的死路。

实测 1:devDependency 会让 turbo 整个图作废

@objectstack/objectqldependencies 里就有 @objectstack/metadata-protocol(workspace:*,本分支静态复核过),所以反向加边必然成环。我按处方加了边、pnpm install,然后:

$ pnpm exec turbo run build --filter=@objectstack/metadata-protocol --dry
• turbo 2.10.7
 WARNING  Circular package dependency detected: @objectstack/objectql, @objectstack/metadata-protocol
  x Cyclic dependency detected:
  |     @objectstack/objectql#build, @objectstack/metadata-protocol#build

turbo 直接拒绝构建图 —— 不是「构建变慢」而是 pnpm build 整仓拿不到图。已把 package.json 与 lockfile 还原,当前工作树干净。

这与 scripts/engine-double-contract.baseline.json 里两条 MEASURED 条目(#4867#4981)记录的完全一致,它们的 closes 也早写明 devDependency 路线对本包已关闭,真正的出路是把判定函数下沉到双方共同依赖的包(@objectstack/metadata-core,或 @objectstack/spec/contracts),tracked as #4987。我复核了 #4987 尚未落地(metadata-core / spec 里都还没有 EngineDeleteDispatch)。

实测 2:这个 fake 的 delete 到底松在哪 —— 它其实一点没松

先确认 delete 是否真被调用(把 fake 的 delete 换成抛错跑一遍):

Error: PROBE: delete WAS called with {"where":{"id":"r_1"}}

结论两点:

  1. delete 确实被走到(promote 落 draft 行),所以「删掉这个方法绕开扫描」不成立 —— 那也是在糊弄门禁,不做;
  2. 调用形态是标量 where.id,正是真引擎 ObjectQL.delete 接受的 by-id 分支。也就是说装上 guard 会直接通过,这个 double 在本路径上并没有掩盖任何 sharing: DELETE /sharing/rules/:idOrName answers 500 for both address forms — rules cannot be deleted over REST #4434 型缺陷 —— 与那两条 MEASURED 条目对 SysMetadataRepository.delete 的描述一致。结构上仍比契约松,但松出来的那部分今天没有被踩到。

我没有做的事

其余一切仍是绿的

$ pnpm --filter @objectstack/metadata-protocol test -- --maxWorkers=2
 Test Files  42 passed (42)
      Tests  388 passed (388)

eslint 规则本身、build、typecheck delta 0 均未变(见 PR 正文)。红的只有这一道 ratchet。

请裁决(我的建议:选 1)

  1. 给本文件加一条 MEASURED baseline 条目,理由用上面两条实测(环 + by-id 实证),closes 指向 [engine-double-contract] 四条 metadata-protocol 基线条目的 closes 指向一个不可能的动作:加 @objectstack/objectql devDependency 会让 turbo 直接判环 #4987代价:台账多一条、我的 double 仍未 pin。理由:这正是本包既有 6 条同类条目的处置方式,其中两条的环形理由与我的一字不差;台账存在的意义就是承接「结构上松但路线被封」的情形,且我还额外带来了 by-id 的实证,比现存条目更硬。
  2. 先做 [engine-double-contract] 四条 metadata-protocol 基线条目的 closes 指向一个不可能的动作:加 @objectstack/objectql devDependency 会让 turbo 直接判环 #4987(下沉判定函数到 metadata-core)再 pin。代价:跨包契约决定 + 33 条条目的连带,应当是独立 PR;把它挂在本 PR 上会让一个门禁修复变成一次架构改动。
  3. 维持红着不动,等 [engine-double-contract] 四条 metadata-protocol 基线条目的 closes 指向一个不可能的动作:加 @objectstack/objectql devDependency 会让 turbo 直接判环 #4987代价:本 PR 无法合入。

倾向 1:它让本 PR 按自己的范围收口,同时把真正的修复留在它该在的地方(#4987),不拿一个「端点发布门」去偷偷改全仓的测试替身契约。


Generated by Claude Code

…s measured DEBT

`check:engine-double-contract` flagged the fake engine in
protocol-publish-drafts-endpoint-gate.test.ts as unpinned. The prescribed
fix — a devDependency on @objectstack/objectql so the fake's delete can route
through assertEngineDeleteDispatch — was run to completion rather than assumed:
@objectstack/objectql depends on @objectstack/metadata-protocol in
`dependencies`, so the reverse edge makes turbo refuse the build graph
(`Cyclic dependency detected: @objectstack/objectql#build,
@objectstack/metadata-protocol#build`). The edge and lockfile were reverted.

That is the same cycle, measured the same way, as the #4867 and #4981 entries
already in this ledger, whose `closes` both point at #4987 (sink the predicate
into a package both sides already depend on). This is the sixth
metadata-protocol file to hit a route those entries record as closed.

The entry is DEBT, not EXEMPT. It records that the double is structurally
looser than the contract; it does not claim the looseness is currently
harmless. The probe finding — delete is exercised, and only as a scalar
`{ where: { id } }` — is recorded in `why` as a fact about this file, which is
explicitly not an argument about the contract, per this ledger's own rule.

Purely additive: one entry, no existing entry reordered, reworded or lowered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pbu27iNUfQCHeuS551Rqo7
@os-zhuang
os-zhuang marked this pull request as ready for review August 4, 2026 16:06
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 4, 2026
Merged via the queue into main with commit 15e61fb Aug 4, 2026
25 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-5206-publish-drafts-endpoint-gate branch August 4, 2026 16:16
os-zhuang pushed a commit that referenced this pull request Aug 4, 2026
`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
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 6, 2026
…A_TYPE_SCHEMAS (objectstack-ai#5271) (objectstack-ai#5312)

* feat(spec): `api` 补进 DEFAULT_METADATA_TYPE_REGISTRY 与 BUILTIN_METADATA_TYPE_SCHEMAS (objectstack-ai#5271)

Part of objectstack-ai#5206 (step 1, spec 车道)。

`api` 条目一直被产出(artifact ingest 把 `defineStack({ apis })` 映射为 `api`)、
被索引(`buildEndpointIndex`)、被执行(objectstack-ai#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,且 objectstack-ai#5086 在落库前对 draft 一视同仁地拒绝,objectstack-ai#5206 第 2 步
(PR objectstack-ai#5279)将无 draft 可门。

`ApiEndpointSchema` 的收紧被实测否掉:同一个 schema 也解析存量行,而存量行带
`packageId` / `state`,`strictObject` 让 packages/metadata 10 条测试转红。`api` 因此
与 `view` 同列 STILL_STRIP,实测写进该列表注释,真正的修法(信封/正文分离)另立
objectstack-ai#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 基线;补 objectstack-ai#5279 fixture 的落地路径

`git merge origin/main`(至 5aae790,无冲突),生成物按 os-regen 四步互保:
generated 文件整体 checkout 回合并基线,再 wholesale 重生成,最后断言兄弟 PR
的条目仍在。

两处过程中发现并纠正的坑,记下来免得下一个人重踩:

1. `gen:api-surface` 读的是**构建产物**,不是源码。合并后没重建就重生成,会把
   objectstack-ai#5021(PR objectstack-ai#5289)刚退役的 `AnimationSchema` / `ZIndexSchema` 四行**重新加
   回去** —— 正是 AGENTS.md §9 的陈旧产物陷阱。重建 spec 后重生成才对
   (4422 → 4418 exports)。
2. 第 2 步的 `git checkout origin/main -- <generated>` 必须用**你实际合并的那个
   tip**,不是 `origin/main` 的当前值。main 在我合并与 checkout 之间又前进了,
   于是把 objectstack-ai#4938 的 http-server 生成文档拉了进来 —— 而我的源码里没有那个改动,
   等于提交了一份源码产不出的生成物。改用合并基线 5aae790 后归零。

最终生成物 delta 相对合并基线只有 7 行,纯增量(ApiEndpoint 的保护信封键);
objectstack-ai#5021 的退役完好(Animation/ZIndex 确认缺席)。

`protocol-publish-drafts-endpoint-gate.test.ts`(objectstack-ai#5279,已合入 main)的
ENDPOINT_SCHEMA 用例改了**落地路径**而非断言:该 draft 现在过不了 saveMetaItem
的 422(这正是 objectstack-ai#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 治理并播种台账;重生成三份参考文档 (objectstack-ai#5271)

PM 返工第 1 轮。把 `api` 注册成 metadata type 就落入「每个 REGISTERED 类型
必须被治理」不变量,而 `check:liveness` 不在我上一轮跑过的门清单里:

    ✗ 1 REGISTERED metadata type(s) governed by nothing:  api

走的是第一条路线(纳入 GOVERNED + 播种台账),没有用 PENDING_GOVERNANCE ——
这是治理成本最低的时点:objectstack-ai#5040 的 E 系列执行器全部已合 main,每个键**今天**
都有真实证据路径,而不是一句承诺。datasource 的教训(objectstack-ai#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 裁决
(objectstack-ai#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 (objectstack-ai#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 漂移
(objectstack-ai#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>
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.

2 participants