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.
What
publishPackageDrafts(packages/metadata-protocol/src/protocol.ts) ends withso 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:falsea 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 fromsuccessalone.Why it matters
The no-op path is also the only
publishPackageDraftsexit that leaves no trace at all. Both refusal returns (the pre-flight block and the Phase-1 unwind) writerecordMetadataAuditrefusal rows; this one writes nothing. So a caller that readssuccess:falseas 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 onfailed.length > 0— which works only because today both refusal returns happen to build a non-emptyfailed[]. 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[]:publishedCount === 0 && failedCount === 0given a first-class name), orreason/ discriminant on the envelope, orfailed[]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.