The Circle owns its Items, and deletion is reversible¶
Supersedes ADR-0003.
An Item belongs to the Circle it was created in, permanently, and every Member of that
Circle may reword, delete, schedule and finish it. created_by survives as provenance —
whose idea this was — and governs nothing.
The Circle is stamped, not derived¶
Item gets a non-null circle FK, set in create_item and by nothing else. The
alternative, reaching the Circle through created_by, fails on more than the extra join:
the join has no answer once a Membership row is gone. When a Member leaves, every Item
they ever created drops silently out of created_by__membership__circle — off the
wishlist, off the calendar, including the ones the remaining person scheduled. The data is
still there and simply unreachable. A tenancy boundary that loses Items as a side effect of
somebody leaving is the worst available failure for the thing whose only job is scoping.
The disagreement we accept instead: item.circle can differ from where created_by is a
Member now. That is not drift to repair. The Circle is a property of the Item, stamped at
creation, not a property of whoever added it — and the "orphaned" Item is exactly the one
the remaining person still wants to see. There is no operation that moves an Item between
Circles.
Why ADR-0003 inverts rather than narrows¶
ADR-0003 protected an Item's words and not its progress: only the author could reword or
delete, either person could schedule or finish. That rule was never coherent.
mark_item_done already drops an Item off both views for both people — so either
person could make the other's Item disappear, but not fix a typo in its title. The guarded
act was strictly less destructive than the permitted one. ADR-0003 says as much about its
own origin: the rule "existed only by accident — two of the four mutating views checked
ownership and two didn't".
Its foundation also goes away here. It reasons over "the app is shared by two people", a global fact that a Circle replaces with a boundary. Once the boundary protects Items, a second boundary inside it, between two people who chose each other, defends against an adversary who is not present. The asymmetry was politeness, and politeness is a social norm, not a permission check.
So all four operations ask one question: is the actor a Member of this Item's Circle?
Deletion is a lid, not a Status¶
Symmetric delete is only safe because delete is reversible. Item gets a nullable
deleted_at; a deleted Item is hidden everywhere and can be restored.
deleted_at is orthogonal to Status, not a fourth Status value. An Item has exactly one
Status at a time, so a Deleted status would overwrite the fact that the Item was Scheduled,
and undo would have nothing to restore it to — recoverable only by adding a
previous_status column, which is deleted_at in a worse costume. Status answers where
has this got to; deletion says the Item should not be in the app at all. Different
questions, different columns. A timestamp rather than a boolean also gives a retention
window for free, if one is ever wanted.
Consequences¶
- Every Item read carries two filters:
circle=…anddeleted_at__isnull=True. Forgetting the first leaks across Circles; forgetting the second leaks deleted Items into a list. Both belong in one place — a manager or queryset every read goes through — rather than as two things each caller must remember. - Expiry skips deleted Items. ADR-0004's sweep moves past-dated Scheduled Items to Done. Left alone it would rewrite the Status under the lid, so delete-then-undo across a date boundary would hand back a Done Item instead of the Scheduled one that was deleted.
item_deletestops narrowing its queryset withcreated_by=request.user. Left in place, the 404 it produces would keep masking the new rule.- Undo is a service operation like any other, so it broadcasts and both devices refresh.
- How undo surfaces — a toast after the delete, or a browsable trash — and whether deleted Items are ever purged are open. The timestamp supports either.