fix(metadata-protocol): publishPackageDrafts 对 api draft 跑 ADR-0121 端点发布门 (#5206 step 2) - #5279
Conversation
…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
…lish-drafts-endpoint-gate
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
|
…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
Part of #5206 (step 2, engine half; step 1 tracked in #5271)
问题
protocol.publishPackageDrafts是 Studio「全部发布」按钮的真实入径(ADR-0033 / ADR-0067 D2)。在此之前,它唯一的按类型前置检查是对象命名空间前缀规则(validateObjectNamespacePrefix,d.type === 'object'才跑),于是一条apidraft 不经任何一道门就被提升为active—— 与 #5189 在MetadataManager.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 失败姿势(即命名空间前缀违规所用的那种),不要发明新的批次语义」。我找到的既有姿势是:
success: false、publishedCount: 0、published: [],同批的健康 draft 保持 draft 态;failed[]里各占一项,带type/name/error/code,点名到具体条目。也就是说「per-item」说的是报告粒度,「整批拒绝」说的是落地语义,两者并存。端点门原样加入同一个 pre-flight,行为完全一致 —— 这既是 ADR-0067 D2 的「一次 commit 不能落一半」,也正是 #5189 在另一条路上的同一姿势(一份
validationErrors、itemsPublished: 0)。唯一的组织性变化:原先命名空间违规是在
if (pkgNamespace)块里直接 return,现在两类违规先汇入同一个preflightViolations再统一 return。于是作者一次往返就能同时看到对象命名违规和端点违规,而不是修完一类再撞下一类。gateApiDraftsForPublish的实现是 #5189gateApiItemsForPublish的逐段镜像,包括「命名空间门只报一次、不归属到任何单条端点(name: '')、并追加本入径专属的 remedy 文案」这一处理 —— 后者对应 metadata-manager 里的PUBLISH_NAMESPACE_REMEDY常量,是既有模式而非本 PR 的发明。remedy 只回答「那我该去哪儿设」,不改变判据(拼接形式为${issue.message} ${REMEDY},门的原文一字不动)。边界(写明而非默认)
active的端点撞车不在此拦截 —— 匹配器对全库重复声明有确定性裁决并error点名(buildEndpointIndex);把范围扩到整包 active 集合意味着「因为你没在发布的东西而拒绝这次发布」,那是另一份契约,不是一个 bug 修复。listDrafts之后消失的 draft 行跳过而不是伪造成门失败,由提升循环报它真正的no_draft。ApiEndpointSchema都不满足的,code为ENDPOINT_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/objectql的dependencies里就有@objectstack/metadata-protocol(workspace:*),反向加边必然成环。按处方加边、pnpm install,然后:turbo 直接拒绝构建图。边与 lockfile 已还原,本 PR 未新增任何 devDependency。
这与
scripts/engine-double-contract.baseline.json中sys-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。范围
packages/spec/**零改动 —— 第 1 步(api进DEFAULT_METADATA_TYPE_REGISTRY/BUILTIN_METADATA_TYPE_SCHEMAS,即 Studio 直写路径的 422)拆在子单 [spec]api补进 DEFAULT_METADATA_TYPE_REGISTRY 与 BUILTIN_METADATA_TYPE_SCHEMAS(#5206 第 1 步,拆单) #5271(spec 车道)。本改动不依赖它落地。saveMetaItem的 metadata: allowRuntimeCreate:false is not enforced — PUT /meta creates job and agent items the registry declares code-only #5086 授权块、seed-loader.ts、deleteMetaItem。.changeset未因台账条目改动(不是发布面变化)。验证
代码部分在
git merge origin/main(合并至0f2fdcde1,无冲突)之后、并重建依赖链(AGENTS.md §9)后复跑。本包没有
typecheckscript(scripts/check-type-check-coverage.mjs里的 DEBT 台账条目),所以按tsc --noEmit -p packages/metadata-protocol与origin/main基线对比:顺带记录一条与本 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 例):apidraft 违反自由身份门(D6:authRequired: false无 armed 预算)→ publish 拒绝并点名越界的 key;rateLimit存在但enabled非true仍拒绝(D6 不是存在性检查);apidraft 照常发布;anonymous + armed 预算也照常发布(D6 可满足,不是禁令);ApiEndpointSchema→ENDPOINT_SCHEMA;apidraft 的健康同批兄弟保持 draft 态;apidraft 完全不受影响;🤖 Generated with Claude Code
https://claude.ai/code/session_01Pbu27iNUfQCHeuS551Rqo7