Key tables
Tasks: task definitions (relativeCost, duration, body/mind weights, timing fields, essential flag, and default details copy).
TaskOccurrences: canonical per-instance schedule and state rows (dueDatetime, completion/cancellation/deferral, per-instance edits).
Every task is seeded with at least one persisted occurrence row at creation time (including timingType=none) so ingest paths are uniform. For recurring tasks, due slots are generated in the active horizon window at read time and inserted idempotently using deterministic occurrence keys before compute runs.
CheckIns: daily body/mind/mood ratings, check-in tier, day-type classification.
Events: append-only audit log; source of truth for rebuilding derived state.
ManaCalibrationSnapshots: per-day derived calibration state (algorithmVersion, pool/coefficient EWMA, confidence, alpha/bias, daily spend ratio inputs) used for deterministic fast reads and migration checkpoints.
ManaCalibrationMigrations: append-only algorithm migration/rollback records (fromVersion, toVersion, migration type, timestamp, optional rollback checkpoint reference).
manaCost is not stored as source-of-truth. It is computed at read time from task properties and today’s axis factor.
Key fields
| Table | Field | Type | Notes | |
|---|
| Task | details | string? | default details/instructions for this task | |
| Task | relativeCost | double | effort relative to baseline | |
| Task | isEssential | bool | user-controlled | |
| Task | timingType | enum | none, deadline, scheduled, day_quality | |
| Task | recurrenceRule | string? | RFC 5545 RRule, optionally with DTSTART and EXDATE lines | |
| Task | dayQualityLevel | enum? | required only for timingType = day_quality | |
| Task | dayQualityMode | enum? | required only for timingType = day_quality | |
| TaskOccurrence | id | int | auto-increment local surrogate ID | |
| TaskOccurrence | taskId | string | FK to task definition (Tasks.id) | |
| TaskOccurrence | dueDatetime | datetime | canonical due timestamp for this instance | |
| TaskOccurrence | occurrenceKey | string | deterministic identity key: `taskId | dueDatetimeUtcMillis` |
| TaskOccurrence | state | enum | active, completed, cancelled; missed is derived, not stored | |
| TaskOccurrence | details | string? | per-instance override; if null, UI can use Task.details | |
| TaskOccurrence | deferredUntil | datetime? | shifts the effective due time without changing the canonical key | |
| TaskOccurrence | completedAt | datetime? | required when state = completed | |
| TaskOccurrence | dueDatetimeOverride | datetime? | edit-this-only recurrence exception; takes precedence over dueDatetime | |
| TaskOccurrence | partialCompletionFraction | double? | 0.0-1.0 fraction already completed; null means no partial completion | |
| CheckIn | checkInDate | datetime | local calendar day identity, normalized to local midnight | |
| CheckIn | checkInTier | enum | full, quick, momentum, none with confidence weights 1.0, 0.7, 0.4, 0.1 | |
| CheckIn | dayType | enum | normal, packed, blocked, unusual; carries opportunity weights 1.0, 0.25, 0.0, 0.0 | |
| ManaCalibrationSnapshot | day | datetime | normalized local day identity for the snapshot | |
| ManaCalibrationSnapshot | algorithmVersion | int | explicit algorithm state version tag | |
| ManaCalibrationSnapshot | poolEwma | double | persisted pool EWMA state | |
| ManaCalibrationSnapshot | coefficientEwma | double | persisted coefficient EWMA state | |
| ManaCalibrationSnapshot | confidence | double | persisted confidence value used for bias | |
| ManaCalibrationSnapshot | baselineSpend | double | day-level baseline spend input for EWMA updates | |
| ManaCalibrationSnapshot | dailyRatio | double | baselineSpend / allocatedMana ratio used for coefficient learning | |
| ManaCalibrationMigration | fromVersion / toVersion | int | version transition boundary | |
| ManaCalibrationMigration | type | enum-like string | parameter_only, state_transform, recompute, rollback | |
| UserProfile | lastCheckInDate | datetime | absence detection | |
| UserProfile | returnRecoveryDaysRemaining | int | 3-step return recovery | |
| PemEvent | attributionConfidence | double | 0.4-1.0 window-based | |
| PemEvent | userAttributedTrigger | string? | optional user attribution | |
| PersonalTaskEntry | observedHRResponse | JSON? | reserved for wearable integration | |
Task schema v1
Canthus uses product names in planning and persisted names in code. Keep both in sync when writing specs or implementation tasks.
| Product term | Persisted representation | User-facing meaning |
|---|
| Flexible task | timingType = none | Can be done when it fits today’s capacity. A seeded occurrence keeps ingest and completion paths uniform. |
| Deadline-window task | timingType = deadline | Can be completed before a due date/time. The due timestamp defines the latest useful completion point, not a shame state. |
| Appointment task | timingType = scheduled | Happens on a specific date/time. It may appear early for planning, but it is only completable on its due day. |
| Day-quality task | timingType = day_quality | Implementation-specific flexible task gated by a day-quality bucket. It is part of the persisted enum and must be documented when schema code references it. |
Task definitions describe recurring intent and cost inputs. Task occurrences describe concrete instances, state, and per-instance edits. A task can be recurring or non-recurring in any temporal type that has a due timestamp.
Schema examples by task type
Flexible task (timingType = none)
Task:
id: "task-flexible-uuid"
timingType: "none"
recurrenceRule: null
relativeCost: 1.5
durationMinutes: 30
TaskOccurrence (seeded at creation, due = createdAt):
occurrenceKey: "task-flexible-uuid|1762214400000"
dueDatetime: 2026-05-04T00:00:00
state: "active"
Deadline-window task (timingType = deadline, non-recurring)
Task:
timingType: "deadline"
recurrenceRule: null
TaskOccurrence (seeded at creation):
dueDatetime: 2026-05-10T17:00:00 # latest useful completion point
state: "active"
Missed state is derived at read time: if dueDatetime is in the past and state = active, surfacing derives missed.
Appointment task (timingType = scheduled, recurring)
Task:
timingType: "scheduled"
recurrenceRule: "DTSTART:20260505T090000\nRRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR"
Generated due slots (horizon window): 2026-05-05, 2026-05-07, 2026-05-09…
TaskOccurrence (persisted when state changes):
occurrenceKey: "task-id|1746435600000"
dueDatetime: 2026-05-05T09:00:00
state: "completed"
completedAt: 2026-05-05T09:45:00
partialCompletionFraction: null
Appointment previews can appear before the due day for planning. They are not completable until the due day.
Recurrence exceptions
Recurring series use deterministic occurrence keys so each generated slot can be matched to persisted state.
| User action | Stored representation | Notes |
|---|
| Skip this occurrence | Persist the occurrence with state = cancelled | Future generated slots are unaffected. |
| Pause series | Store a bounded pause as cancelled occurrences or equivalent EXDATE lines for the paused slots | Pause does not delete history. |
| Edit this occurrence only | Set dueDatetimeOverride on the occurrence | The override changes effective scheduling but not occurrenceKey. |
| Edit this and all future | Add EXDATE for the old slot on the original task and create a sibling task from the new DTSTART | This preserves old history and gives future slots a new rule. |
Edit this occurrence only
TaskOccurrence:
dueDatetime: 2026-05-07T09:00:00 # original slot
dueDatetimeOverride: 2026-05-07T14:00:00 # moved to afternoon
state: "active"
Edit this and all future
Original Task:
recurrenceRule: "DTSTART:20260505T090000\nRRULE:FREQ=WEEKLY;BYDAY=MO\nEXDATE:20260512T090000"
Sibling Task:
timingType: "scheduled"
recurrenceRule: "DTSTART:20260512T140000\nRRULE:FREQ=WEEKLY;BYDAY=MO"
Splitting and partial completion
Partial completion records that some work happened while the occurrence remains open.
TaskOccurrence:
state: "active" # still open
partialCompletionFraction: 0.5 # half done
completedAt: null
Mana cost displayed = full cost × (1 - 0.5) = 50% of base cost.
Spend accounting happens when an occurrence is completed. A partial completion alone does not write completed spend for the day. If the user defers the remaining work, deferredUntil shifts the same occurrence’s effective due time and the visible remaining cost is still based on 1 - partialCompletionFraction.
Splitting a task into separate future work should create separate occurrences or sibling tasks with their own due timestamps. Do not duplicate completed spend from the original occurrence into the split work.
The schema and costing rules support partial completion and recurrence exceptions. Some app-level editing flows are still implementation follow-ups; the contract above is the source of truth for future work.
Storage model
- Encrypted local SQLite via Drift + SQLCipher
- No remote DB, no cloud sync
- Offline-first behavior
- Encryption key in platform keystore (not cloud backed up)
Settings and feature flags are in shared_preferences and persist across health-data reset.
Runtime recurrence query
dueSlots = generateFromRRule(task.recurrenceRule, horizonStart, horizonEnd)
TaskOccurrences.insertMissing(dueSlots, key = taskId|dueDatetimeUtcMillis)
occurrences = merge(dueSlots, TaskOccurrences.persistedRows)
activeOccurrence = nearest unresolved future dueDatetime
Recurrence parsing contract
- Stored recurrence input may be either:
- a full content line (
RRULE:FREQ=...)
- or a bare rule value (
FREQ=...)
- If
DTSTART exists in the stored string, it is used as the recurrence anchor.
- If
DTSTART is absent, the anchor defaults to Task.createdAt.
- Line parsing splits on both Unix and Windows line endings (
\\n, \\r\\n).
- Implementation detail: the parser uses regex pattern
[\\r\\n]+ to split lines.
\\r matches carriage return, \\n matches newline, + collapses one-or-more line break chars into one separator.
- Example:
DTSTART:...\\r\\nRRULE:... and DTSTART:...\\n\\nRRULE:... both split into the same two logical lines.
- Invalid recurrence strings fail soft:
- no generated slots are returned
- a typed warning is emitted by the recurrence service
Example:
DTSTART:20260310T090000
RRULE:FREQ=DAILY;COUNT=2
Generates:
2026-03-10 09:00
2026-03-11 09:00
Derived status contract
missed is derived at read time in domain scheduling logic.
missed is not persisted in TaskOccurrences.
deferredUntil shifts an occurrence’s effective due time used by scheduling.
Read next