Skip to content

Paper lifecycle

Every paper in ALMa has two independent state dimensions. They are stored as two columns on the papers table and they never overload each other.

Membership axis

The papers.status column holds one of five mutually-exclusive values:

Value Meaning UI surfaces
tracked ALMa knows about this paper but you haven't curated it. Default state for anything pulled from a monitor or a backfill. Feed, Discovery candidates, Corpus Explorer
inbox You sent this to yourself from another channel and haven't triaged it. A buffer between tracked and library — see Inbox. Home Inbox section
library You have explicitly saved this paper. It is part of your curated collection. Library tabs
dismissed The global hide: don't surface this anywhere. Visibility only — it carries no opinion (see below). Hidden everywhere by default; visible in Corpus Explorer
removed You used to have this in your Library and chose to remove it. The row is preserved for provenance and as a negative signal. Hidden by default; visible in Corpus Explorer

Removal is a soft transition, not a hard delete ( why). The row stays so that Discovery knows you've explicitly rejected it and so that Insights counts stay coherent.

Colour is valence, membership is not (D6, amended 2026-07-26)

Four verbs move a paper, and they answer different questions. The word "dismiss" used to answer all of them at once — it set status='dismissed', stamped rating=1 and wrote a negative feedback event — so "I've dealt with this" was indistinguishable from "this is bad".

Verb Axis Scope Writes
save / like / love / dislike valence global — one opinion per paper rating + a feedback_events row
dismiss resolution the surface that raised it that surface's row only. No rating, no event, no lens signal.
defer membership the Inbox inbox → tracked. Nothing else.
remove membership global status='removed' — still a negative (D3)

Consequences worth knowing:

  • Dismissing in one Discovery lens does not mute the paper in any other. The cooldown is scoped WHERE lens_id = ?; previously one dismiss suppressed the paper everywhere.
  • status='dismissed' carries no valence. It is the global hide — a visibility choice. It is not in signal_valence.NEGATIVE_STATUSES, so tidying your surfaces cannot poison a paper's map colour.
  • Negative opinion is dislike. "Bad and gone" is dislike plus dismiss: two verbs, composed deliberately.
  • defer is not a verdict, it is the absence of one — which is why the Inbox X button cannot reuse dismiss.

Reading axis

The papers.reading_status column holds one of four values (NULL = none):

Value Meaning
(none) Default. Reading workflow has nothing to say about this paper.
reading You're actively reading it.
done You've finished reading it.
excluded You evaluated and decided not to read it. Distinct from dismissed (membership), which means "don't suggest".

The reading axis is independent of membership. A paper in your Reading list does not have to be in your Library — you can queue something for reading while still deciding whether to keep it.

Every surface that changes it — Library's reading select, the Reading list, Feed and Discovery's Queue button, undo, and a capture "to the reading list" — goes through one writer, paper_actions.set_reading_status. It validates the value (the retired queued is accepted as reading), writes it on the paper-group root, and bumps updated_at.

Why two axes

The most common UI failure in literature tools is conflating "I keep this paper" with "I have read this paper" with "I rate this paper highly". The three are independent:

  • You can have an unread paper in your Library that you haven't rated yet.
  • You can finish reading a paper without saving it.
  • You can rate a paper without reading it (you've decided it looks promising).

Splitting the axes means each control in the UI has exactly one job. The Reading status select sets reading_status. The Save button sets status='library'. The star rating sets rating. None of them touch the others.

Rating

The third small piece of state is papers.rating — an integer 0–5 that ALMa derives from the rating verb you used when saving:

Verb Resulting rating
Save (or Add) 3
Like 4
Love 5
Dislike 1
  • Ratings 1–2 are negative signals for the recommender.
  • Rating 3 is neutral / minimally positive.
  • Rating 4 is a +1 positive signal.
  • Rating 5 is a +2 positive signal.

The rating is monotonic when re-saving: re-saving a Loved (5) paper with a plain Save will not downgrade it to 3. Only an explicit Dislike or a manual rating change can lower it.

How states transition

stateDiagram-v2
    direction LR
    [*] --> tracked: monitor / backfill
    [*] --> inbox: capture (Slack / any channel)
    tracked --> inbox: capture
    inbox --> library: Save · Like · Love
    inbox --> tracked: Defer (the X button)
    tracked --> library: Save · Like · Love
    tracked --> dismissed: Hide
    library --> removed: Remove from Library
    removed --> library: Re-save
    removed --> inbox: capture (reconsidering)
    dismissed --> library: Save (overrides)
    dismissed --> inbox: capture (reconsidering)
    library --> library: Like / Love / rating change

A capture never demotes a library paper — re-sending something you already saved is reported as a duplicate and the row is left untouched.

Reading transitions are completely orthogonal to the diagram above.

What this means for the UI

  • Feed shows tracked papers from your monitors. Saving a Feed paper transitions it to library. Disliking it writes a negative signal but keeps it visible (Feed is chronological — it does not hide things).
  • Discovery shows tracked candidates the recommender thinks you haven't seen. Dismissing hides that recommendation in its lens without changing preference. Saving transitions to library.
  • Home shows the Inbox — papers you sent yourself from another channel. Triage moves them to library, or the X button (defer) drops them back to tracked writing no signal at all.
  • Library shows library papers. Removing transitions to removed.
  • Reading list shows papers with reading_status='reading' — regardless of membership.
  • Corpus Explorer (Settings → Data & system) shows everything including dismissed and removed, with the full state visible.

Where to read more

Repairing versions and components

Health and Settings share Reconcile paper groups. A published paper receives its preprint's saved state, reading state, ratings and notes; the preprint remains as a version pointer. Components donate no user state. Child records cannot independently affect Library counts, calibration or recommendation evaluation.

Title/year matches merge automatically only when each side has one plausible partner and both carry a DOI — a shared title and year alone is how two different papers become one. Ambiguous matches remain separate and are listed in Activity with the relevant paper IDs.

Preview before you repair. Health and Settings both offer a preview: it reads exactly what a pass would act on — relationships to repair, components to classify, merges ready, what the run limit would defer, and what is left for a person to judge — and writes nothing. Unattended auto-repair is unaffected.

The schema holds the rule too. papers.canonical_paper_id and parent_paper_id cannot name their own row or a paper that is not in the corpus; SQLite refuses the write. A database that already contains such a row stays editable until Reconcile clears it, and Health counts it meanwhile.

Registered datasets know their parent. A DOI Crossref does not have is looked up in DataCite, whose IsSupplementTo relation names the article a deposited dataset, software release or supplement belongs to. It then becomes a component of that paper like any other. The repair reports actual merges, failures and remaining title matches. Its limit applies to new title matches; existing relationship repair scans the corpus. Each group commits independently, so Library actions remain available and a failed group does not undo successful repairs.