Skip to content

publishPackageDrafts answers success:false for a publish that refused nothing, and leaves no trace of that exit #10462

Description

@os-zhuang

What

publishPackageDrafts (packages/metadata-protocol/src/protocol.ts) ends with

return {
    success: failed.length === 0 && published.length > 0,
    publishedCount: published.length,
    ...
};

so a publish that had nothing to promote — no pending drafts for the requested packageId — resolves

{ "success": false, "publishedCount": 0, "failedCount": 0, "published": [], "failed": [] }

That is the same success:false a genuine ADR-0067 D2 refusal answers with. One boolean is carrying two facts ("nothing was refused" and "nothing landed") and the caller cannot tell them apart from success alone.

Why it matters

The no-op path is also the only publishPackageDrafts exit that leaves no trace at all. Both refusal returns (the pre-flight block and the Phase-1 unwind) write recordMetadataAudit refusal rows; this one writes nothing. So a caller that reads success:false as a refusal reports a rollback, and an operator who then goes looking for the rollback in the audit trail and the server log finds neither.

Measured on the cloud side (objectstack-ai/cloud#1488, fixed consumer-side in objectstack-ai/cloud#1492): the AI Studio publish tools graded that shape as a rollback and told users "发布被拒绝并整体回滚" over turns whose metadata was entirely state='active', then burned two automatic repair rounds re-authoring artifacts that were already correct. The cloud fix discriminates on failed.length > 0 — which works only because today both refusal returns happen to build a non-empty failed[]. That is an invariant no test states and nothing pins.

Suggested shape (not a ruling)

Make the answer say which of the two it is, rather than making every caller infer it from failed[]:

  • a distinct field on the response (e.g. publishedCount === 0 && failedCount === 0 given a first-class name), or
  • a reason / discriminant on the envelope, or
  • pin the current invariant explicitly ("every non-success return carries at least one failed[] entry") with a test, so consumers may rely on it.

Any of the three is fine; the thing worth deciding is that the distinction becomes part of the contract instead of a shape consumers reverse-engineer. Worth pairing with a log line on the no-op path so the exit is not invisible.

Filed from cloud#1488 by the dev agent that fixed the consumer half. No assignee.

Activity

  1. added theissue type on Aug 21, 2026
  2. os-zhuang commented on Aug 21, 2026

    @os-zhuang
    ContributorAuthor

    分诊落卡(needs-user-decision + domain:engine):契约形状提案,按人工地板交维护者。以下分析按业务视角写。

    一句话问题

    管理员对一个套件点"发布"时,如果本来就没有待发布的草稿,系统回答"发布失败"——和真正被安全闸拒绝并整体回滚时的回答一模一样,且不留任何日志。AI 工具据此向用户报"发布被拒绝并整体回滚",再花两轮去修本来就正确的数据(cloud#1488 实测,消费端已在 cloud#1492 打了补丁)。

    前提(re-check)

    • git log --oneline -5 -- packages/metadata-protocol/src/protocol.ts — 确认 publishPackageDrafts 结尾的 success: failed.length === 0 && published.length > 0 仍在。
    • cloud 侧补丁靠 failed.length > 0 区分,该不变式("凡失败必有 failed[] 条目")今天无测试钉住。

    选项 × 真实代价

    选项 做什么 客户可感知代价
    A 响应加第一类区分(如 noop 或 reason 字段)+ no-op 路径补一行日志 公开契约加一个键(一次性);老消费者不受影响。业务上 = 收银台明确告诉你"你没买东西",而不是"交易被拒"
    B 只用测试钉住现状不变式("凡失败必有 failed[]"),消费者靠 failed.length 判别 契约面不动,但规则是隐性的——每个新消费者(多半是 AI 写的)都要重新发现它,cloud#1488 的误报剧本会重演。业务上 = 规则只在老店员脑子里
    C no-op 改回 success:true(成功发布了 0 项) 语义翻转,现有把 success 读作"确实发布了东西"的调用方转为误报另一个方向

    四轴(业务立场)

    <!-- os-decision-facets -->

    • 长远合理性:A 缩小歧义面——一个响应一个含义,契约收紧方向;B 让隐性契约继续增生。
    • 实际业务拉动:已有真实事故(cloud#1488:用户看到假"回滚"警报 + 两轮无效自动修复),非投机能力面。
    • 防 AI 犯错:A 响亮直白,AI 消费端无需逆向推断;B 下一个 AI 消费者大概率复刻同一误读(已实测发生一次)。
    • 创业不扩散:加一个判别字段是一次性小义务,换掉的是每个消费端各自的推断逻辑;不是能力扩张。

    推荐 A;回退 B。
    置信缺口:本分析看不见 repo 之外是否还有依赖现状 success 语义的消费者(未测)。

    裁后执行(你不用管)

    回一个字母即可。A → 入队 domain:engine,按条款②契约复审档派发(响应契约变化),cloud 消费端补丁随后收敛到新字段;B → 降为 test-only pin 卡直接入队;C → 需另评估现有调用方,默认不推荐。


    Generated by Claude Code

  3. os-zhuang commented on Aug 21, 2026

    @os-zhuang
    ContributorAuthor

    Marker re-post (sanitizer ate the code-span spelling in the analysis comment above — read-back caught it): the machine-findable facet-block marker for this card, in surviving spelling —

    <!-- os-decision-facets -->

    The four-facet block itself is in the analysis comment above (四轴(业务立场) section). Recommendation A, fallback B, one-letter reply suffices.


    Generated by Claude Code

  4. os-zhuang commented on Aug 21, 2026

    @os-zhuang
    ContributorAuthor

    Maintainer ruling (2026-08-21, live PM chat — recorded by the triage seat)

    Verbatim (untranslated): 「其他接受」 — accepting the triage recommendation presented for this card in the decision discussion: A + pin (the "C-lite" composite): add a first-class discriminant (reason / no-op field) to the publishPackageDrafts response, add the missing no-op audit/log line so that exit is no longer traceless, and pin the invariant "every non-success return carries at least one failed[] entry" with a test so existing consumers' discrimination stays reliable during the transition.

    State: needs-user-decision → pm:queue (domain:engine), dispatchable.

    Dispatch notes: response-envelope widening ⇒ Clause-②: yes, contract-review tier applies. Follow-up owed to the consumer side once merged: cloud's failed.length > 0 discrimination (cloud#1492) should converge on the new discriminant — the accepting seat files that card in cloud per the cross-repo rule.


    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

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions