domain: deleting the published property on a live post leaves it Published but invisible #87
Labels
No labels
kind
bug
kind
enhancement
wayfinder
grilling
wayfinder
map
wayfinder
prototype
wayfinder
research
wayfinder
task
workflow
needs-info
workflow
needs-triage
workflow
ready-for-agent
workflow
ready-for-human
workflow
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
rosa/vernier#87
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found during triage of #53.
Location:
crates/domain/src/models/micropub.rs(MicropubUpdate::apply_to, thepublishedbranch)Severity: Correctness — silent invisibility
Problem
A Micropub update with
delete: ["published"]on a Published post resolvespublished_attoNonewhile the status staysPublished.Post::is_liverequirespublished_at.is_some_and(|p| p <= now), so the post silently drops out of public view, listings, and webmention dispatch while still reporting itself as Published. The current resolution behavior is codified by theclear_published_unsets_itunit test.Why #53 doesn't cover this
The #53 fix is deliberately transition-based: it defaults
published_atonly when a status change to Published leaves it unset, and never counteracts adeleteoperation the client explicitly specified (Micropub update semantics define what the specified operations do; re-adding a property the client just deleted would contradict the request).So this case needs its own semantics decision. Options:
deleteofpublishedwithout also leaving the published state is an invalid request.Failure scenario
Publish a post via Micropub (
published_atset), then send an update deleting thepublishedproperty with no other changes. The post remainsPublishedbutis_liveis false forever; only the author sees it, and no un-publish webmentions go out.Triage
Confirmed against the code and the domain docs. This is the exact hole #53's
brief deferred ("the adjacent hole where an explicit delete of
publishedon alive post leaves it Published-but-invisible — separate semantics question,
reject vs. honor"), so the reproduction and location hold.
Labeling ready-for-human rather than ready-for-agent: the problem is fully
specified, but the fix is an undecided semantics decision among three options
with materially different behavior. An agent shouldn't pick between reject /
honor / reinterpret on its own — that's a maintainer call. Once the option is
chosen, the implementation itself is small and agent-sized.
Signal from the domain docs (toward Option 1, Reject)
The glossary defines Published as "a Post that has been released, carrying a
publication instant." A Published post with no
published_atis therefore astate the ubiquitous language says cannot exist. ADR-0004 reinforces this: every
derived state that makes a Published post observable (live, Scheduled, the
viewability and listing rules) is defined in terms of
published_at, so aPublished post without one falls through every rule to permanent author-only
invisibility.
That makes Option 2 (honor literally) an out-of-glossary state and Option 3
(reinterpret as draft) a silent contradiction of the client's explicit
operation. Option 1 (Reject) — a
deleteofpublishedthat would leave thepost Published is an invalid request — is the only option consistent with the
domain model. A new
ApplyUpdateErrorvariant alongside the existingContentRequired/NonPublicVisibilitywould be the natural shape, andclear_published_unsets_itwould be re-scoped to the draft case (where clearingis harmless).
Note this is a recommendation, not a decision — flagging it for the maintainer.
Decision: Option 1 (Reject)
A
deleteofpublishedthat would leave the post Published is an invalidrequest. Chosen because the glossary defines Published as "carrying a
publication instant," so Published-without-
published_atis a state the domainlanguage says cannot exist (see the triage comment above).
Agent Brief
Category: bug
Location:
crates/domain/src/models/micropub.rs(MicropubUpdate::apply_to)Summary: Reject any Micropub update whose resolved edit would be
status = Publishedwithpublished_at = None, with a dedicated error.Current behavior:
apply_toresolvesstatusandpublished_atindependently and can produce anedit with
status = Published, published_at = None— e.g. aClearofpublishedon a live post. The resulting post stays Published butis_liveisfalse forever (silently invisible; no un-publish webmentions).
Desired behavior:
Make "a Published post carries a publication instant" an enforced invariant of
resolution. After resolving
statusandpublished_at, ifstatus.is_published() && published_at.is_none(), return a new error variantinstead of an
Ok(PostEdit). The check is resulting-state-based, so itcovers every path into that state, not just the
delete-on-live case.Do the check after the existing #53 draft→publish defaulting so a plain
draft→publish (no
publishedoperation) still defaults tonowand passes.It only fires when the client's own operations resolve to Published + no date.
Key interfaces:
MicropubUpdate::apply_to(crates/domain/src/models/micropub.rs:67) — addthe guard just before constructing
PostEdit.ApplyUpdateError(crates/domain/src/errors.rs:179) — add a variant,e.g.
PublishedRequiresDate("a published post requires a publication date"),parallel to
ContentRequired/NonPublicVisibility.MicropubError(crates/domain/src/errors.rs:91) — add the matching wirevariant, and map it at the boundary in
crates/domain/src/services.rs:896(theapply_to(...).map_err(...)arm),alongside the existing three arms.
Behavior changes to existing tests (intended — call out, don't silently keep):
clear_published_unsets_it(micropub.rs:281) runs on a Published post,so clearing
publishednow rejects. Re-scope it to a draft post (clearingpublishedon a draft is harmless: stays Draft +None) so it stilldemonstrates that a bare
Clearunsets the date where that is legal.publishing_a_draft_that_also_clears_published_stays_unset(micropub.rs:324)currently asserts
(Published, None). That is exactly the now-invalid state —flip it to assert the new
PublishedRequiresDateerror.Acceptance criteria:
Clearofpublished(no other changes)resolves to
Err(PublishedRequiresDate).Clearofpublishedresolves to the sameerror (resulting-state rule, not path-specific).
publishedoperation stilldefaults
published_attonowand succeeds (#53 behavior preserved).publishedon a post that stays a Draft still unsets it andsucceeds.
micropub_updateas the newMicropubErrorvariant.
format,ci, andclippymise tasks pass.Out of scope:
is_live/ Scheduled semantics — unchanged; they are the observers, not thebug.