ADR-0015: Item lists and points auto-claim on completion
Metadata
- Status: Accepted
- Date: 2026-07-25
- Deciders: EagraΓ Clainne Team (decisions taken in a design interview, 2026-07-25)
- Related: ADR-0003 (reward costs and the points ledger β amended alongside
this ADR to redefine balance as Ξ£ claimed POINTS β Ξ£ SPEND); ADR-0009
(service-layer authorization β the empty-owner pass through
RequireOwnerthat already opens unowned entries to everyone); ADR-0014 (job ownership and editing β its manager/taker rules keep governing item editing inside lists)
Context
Items are jobs: standalone chores with an owner, a deadline, and a place on the Jobs board. Families also keep lists β shopping lists, packing lists, "before Granny arrives" lists β and today the app has nowhere to put them. A list entry is item-shaped (a title you tick off) but list-flavoured: quick to add, usually deadline-less, and meaningful only inside its list.
Points have a related gap. ADR-0003 gave rewards a cost and a spend ledger, but earning is still manual: a parent grants a POINTS reward after the fact. There is no way to say "this job is worth 10 points" and have the points land when the job is done. And under the balance-as-granted definition, points counted the moment they were granted, before anything was earned β which is why the balance is being redefined (in a separate ADR-0003 amendment) as Ξ£ claimed POINTS β Ξ£ SPEND: claiming is banking.
The questions this ADR answers: how do lists enter the item model without forking it, and how does completing a job pay out its points?
Decision
Lists
A list entry is a full Item with a list reference
A list entry is an ordinary Item carrying an optional ListRef. ItemList
is its own record holding uid and name only β no kind enum, no stored
complete/archived state; a list's progress is derived from its items at read
time, and ItemList.items is a read-time projection, never stored.
Jobs remain list-less items, and the Jobs board keeps meaning standalone chores: boards (the Jobs page and Today) exclude items with a list ref. The Calendar keeps showing any item with a deadline, wherever it lives.
RBAC: managers run lists, everyone adds to them
- ADMIN and MEMBER create, rename and delete lists, and run
UncheckAll. - Any signed-in member β including CHILD β may create an item into an existing list. This is a deliberate loosening of the creation rules, scoped strictly to in-list creation: a child adding "crisps" to the shopping list is the point of a shared list.
- Item editing inside lists keeps the ADR-0014 manager/taker rules unchanged.
- Ticking unowned entries is already open to all roles via
RequireOwner's empty-owner pass (ADR-0009) β no new rule needed.
Deleting a list cascades
Deleting a list deletes its items; the UI confirms with a count of what goes. Banked points survive the cascade because rewards are separate rows β the ledger never lived on the item.
List entries are title-only; deadlines mean "job"
Quick-add in a list takes a title and nothing else, and the list UI offers no deadline field: a deadlined task is a job, and the sheet offers "Convert to a job" for exactly that. There is no server rule against a deadline on a list entry β it is not a domain invariant, just not something the UI invites.
SetList is the membership move
SetList(item_uid, list_uid) is the generic membership operation; an empty
list_uid means standalone. The UI exposes it only as "Convert to a job".
The reverse direction β moving a job into a list β is not offered in the UI,
and is server-rejected when the item has a linked reward, so a points-
bearing job can never slip into a list (see the intake rule below).
UI placement
Lists get a sixth "Lists" tab for now. The tab bar is getting crowded; the menu rethink (an admin page absorbing the long tail) is deferred to its own decision.
Points auto-claim
Items tie to points through the existing Reward.item linkage
There is no Item.points field. A job is worth points when a
pre-existing, unclaimed POINTS reward is linked to it via the existing
Reward.item linkage. The job form lets a manager select an existing
eligible reward or create one inline.
Eligibility is strict: unclaimed POINTS rewards, not already linked to another item, granted to the job's owner or unassigned. For an unowned (pool) job, only unassigned rewards qualify β a linked reward on a pool job is a bounty for whoever takes and completes it.
Completing a job banks its points
CompleteItem(complete=true) auto-claims every unclaimed POINTS reward
linked to that item. The beneficiary is the item's owner at completion,
via Claim's existing owner-reassignment of unassigned rewards. POINTS
rewards cannot carry a cost (ADR-0003), so an auto-claim can never trigger a
SPEND β banking is always a pure credit.
No revocation path
Un-completing an item never un-claims; re-completing finds nothing unclaimed and does nothing. The flow is idempotent with no revocation code at all. Corrections β a falsely ticked chore β go through a manager deleting the reward (a web UI for reward deletion is added alongside), which is the same honest correction path ADR-0003 established.
List entries never carry points
RewardService.Create rejects an item_uid that points at an item belonging
to a list β the same intake-rule pattern as ADR-0003's rejection of costed
POINTS rewards. Together with the SetList rejection above, points and lists
cannot meet from either direction.
No migration
The balance redefinition ships without a migration: the system is not live anywhere, so there are no historical balances to reconcile.
Consequences
Positive
- Lists reuse the entire item machinery β one record type, one set of RPCs, one authorization story β instead of a parallel checklist model.
- The list/job boundary is one sentence: in a list means list entry, deadlined means job, and "Convert to a job" is the only door between them.
- Points become earnable end-to-end: link a reward to a job, do the job, the points bank themselves. Balance finally means "what you have earned and not spent".
- No revocation machinery: idempotent completion plus manager delete covers every correction case with code that already exists.
- The two intake rules (SetList rejection, RewardService.Create rejection) keep "list entries never pay" enforced at the server, in the same style as ADR-0003's cost rules.
Negative / trade-offs
- Bounty semantics need watching. A reward linked to a pool job pays whoever takes and completes it β deliberate, but a child racing a sibling to a tick is a new dynamic; if it sours, per-family configuration is a future amendment.
- The balance redefinition creates a one-time "claim your old points" moment: previously granted-but-unclaimed POINTS stop counting until claimed. Fine while nothing is live, but the amendment must be understood by anyone seeding data today.
- CHILD gaining in-list creation is the first crack in the create matrix; it is scoped to existing lists, but it is a precedent to cite carefully.
- Deriving list progress at read time means no cheap "completed lists" archive; if families want history, storage comes back in a later ADR.
- A sixth tab strains the navigation; the deferred menu rethink becomes more urgent with every tab added.
Alternatives considered
- A separate checklist model (list entries as their own record type) β rejected: forks the item machinery, its RBAC, and both frontends for a thing that is item-shaped already.
- A kind enum or stored completion state on ItemList β rejected: kind is presentation, and stored progress drifts from the items it summarises; derive it.
- An
Item.pointsfield β rejected: the Reward.item linkage already exists, keeps the ledger the single source of points, and gives the manager form real rewards to pick from. - Un-claim on un-complete β rejected: revocation code, a mutable ledger, and a child watching banked points vanish; manager delete is the honest correction and already refunds correctly per ADR-0003.
- A server rule forbidding deadlines on list entries β rejected: not a domain invariant; the UI simply doesn't offer it, and "Convert to a job" handles the real case.
- Offering job β list in the UI β rejected: no family story asks for demoting a job, and the reward-linkage rejection would make the button fail confusingly half the time.