Capture tasks, with one intake seam mail can call later (#130)

A task is not a fact and not a note. A fact is a claim about the world that a
correction supersedes; a note is something to recall by meaning. A task is work
with a lifecycle, and the read that matters is "everything outstanding right
now" — which over an append-only log would mean replaying history on every
question. So: a tasks table, migration #14, statuses candidate/open/done/dropped
that each move forward exactly once.

Dedupe is on normalised text among LIVE rows only, via a partial unique index.
That is the property the mail side needs: an extractor may call CaptureTask for
every message it reads, as often as it likes, without growing the list — while a
weekly errand is still capturable again once the last one is done.

Three ways in, one seam. ipc.CaptureTaskReq is it: the voice path
(router.ParseTaskCapture on an explicit marker — "добавь в задачи …", never
"надо бы поспать"), the /tasks form, and the email reader from #246 when it
exists. Mail-derived items set Source "email:<account>", Status "candidate" and
Evidence to whatever makes the row reviewable; a candidate is inert until he
confirms it on /tasks, and Maven names it as unconfirmed when she recites the
list rather than putting words in his mouth.

No new intent — the router enum is a contract with the relabelling prompt, so
capture rides the note intent and the list rides a query source, both matched
deterministically like the calendar and plan matchers already are.

Nothing here speaks. No tick rule reads tasks; the list is answered when asked
about, which is why /tasks POST is not step-up gated the way /tools and
/routines are — a task write moves no boundary.

Vikunja #130
This commit is contained in:
kami
2026-08-01 02:33:47 +04:00
parent c8444813e2
commit 7b2b96b957
18 changed files with 1584 additions and 1 deletions
+29
View File
@@ -131,6 +131,35 @@ ALTER TABLE reminders ADD COLUMN next_fire_ts INTEGER;`, // #2
expires_ts INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_digest_entries_status ON digest_entries (status);`,
// #14 — the task capture store (Vikunja #130). Deliberately NOT facts:
// a fact is a claim about the world that gets superseded, a task is a
// piece of work with a lifecycle (captured → open → done), and the
// prioritiser needs to read the live set cheaply.
//
// status: 'candidate' is a task Maven derived from something she read
// (mail, later) and has NOT been confirmed by the owner; 'open' is a task
// he actually stated (or confirmed). Nothing schedules or announces off
// this table — capture is not a nag.
//
// norm is the normalised dedupe key. The unique index is PARTIAL, over
// live rows only: re-capturing "купить молоко" after last week's one is
// done must work, while the same mail arriving twice must not produce two
// rows.
`CREATE TABLE IF NOT EXISTS tasks (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_ts INTEGER NOT NULL,
text TEXT NOT NULL,
norm TEXT NOT NULL,
source TEXT NOT NULL,
evidence TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'open' CHECK (status IN ('candidate','open','done','dropped')),
due_ts INTEGER,
weight INTEGER NOT NULL DEFAULT 0,
resolved_ts INTEGER
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_tasks_live_norm ON tasks (norm) WHERE status IN ('candidate','open');
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks (status, created_ts DESC);`,
}
// migrate applies every migration with a number greater than the DB's current