You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
publishPackageDrafts answers success:false for a publish that refused nothing, and leaves no trace of that exit #10462
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.
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.
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.
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.
Activity
os-zhuang commentedon Aug 21, 2026
分诊落卡(
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仍在。failed.length > 0区分,该不变式("凡失败必有 failed[] 条目")今天无测试钉住。选项 × 真实代价
noop或reason字段)+ no-op 路径补一行日志failed.length判别success:true(成功发布了 0 项)四轴(业务立场)
<!-- os-decision-facets -->推荐 A;回退 B。
置信缺口:本分析看不见 repo 之外是否还有依赖现状
success语义的消费者(未测)。裁后执行(你不用管)
回一个字母即可。A → 入队
domain:engine,按条款②契约复审档派发(响应契约变化),cloud 消费端补丁随后收敛到新字段;B → 降为 test-only pin 卡直接入队;C → 需另评估现有调用方,默认不推荐。Generated by Claude Code
os-zhuang commentedon Aug 21, 2026
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
os-zhuang commentedon Aug 21, 2026
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 thepublishPackageDraftsresponse, 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 onefailed[]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 > 0discrimination (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
[RESCUED — evidence complete] feat(metadata-protocol,spec): declare `…