Skip to content

calternal_notes_core::tasks::model

Task projection and write-input types for #430.

Markdown Note frontmatter is authoritative. The Index projection keeps the date and status values needed by API and UI queries. start remains in the read model for Obsidian imports, but new Task files write it as scheduled only when no scheduled value exists (issue #430, DESIGN §40).

Wire shapes match the TS contract exactly (contract §3/§5):

  • Enums use serde(rename_all = "lowercase") → “todo”, “file”, etc.
  • Structs use serde(rename_all = "camelCase") → “taskId”, “blockId”, etc.

Reuse policy: CivilDate, TaskPriority, and TaskRecurrence are deliberately NOT redefined here — they live in crate::nlp and are shared across the NLP, file-model, and index layers. Re-defining them would create two incompatible date/priority types and break any code that crosses module boundaries.

Source: crates/calternal-notes-core/src/tasks/model.rs

pub struct BuildTaskFileInput

Input shape for buildTaskFile — the fields a caller must supply to create a new Task Note (DESIGN §40, issue #430).

rename_all = "camelCase" keeps the wire form consistent with TaskIndexEntry and the TS buildTaskFile signature (contract §5).

created is required (not Option) because every new task file must record its creation date — callers pass the current civil date. Making it optional would allow omitting it and silently creating undated tasks.

Fields

  • pub calternal_id: Option<String>: The task file’s calternal-id (#27 T6, spec A §C.2) — leads the canonical frontmatter field order when present. Option, NOT a bare required String, DESPITE the spec prose calling it “required for file builds”: that phrase describes the composer’s OWN discipline (every NEW composer-driven file build always mints and passes one) — it is not a hard type-level requirement, because every PRE-EXISTING (non-composer) buildTaskFile call site in the codebase does not pass one yet (those callers land it in a later task, T13/T14). Making this a required field would immediately break those call sites’ JS-object literals (a missing required field is a deserialize ERROR, not a silent default) the moment this struct changed — None here instead reproduces the exact PRE-T6 (legacy, no-id) frontmatter shape byte-for-byte, matching the spec’s own explicit callout: “byte-identity holds … only for builds with ALL new inputs absent”.
  • pub title: String
  • pub priority: Option<TaskPriority>
  • pub due: Option<CivilDate>
  • pub scheduled: Option<CivilDate>
  • pub start: Option<CivilDate>: Import-only date. The writer folds this into scheduled when absent and never writes a start: key (#430, issue decision T12b).
  • pub due_time: Option<String>: Time-of-day companion for due (#27 T8, spec A §D.2), "HH:MM". When present alongside due, build_task_file emits due: YYYY-MM-DD HH:mm:00 instead of the date-only form — the ONE place a date+time combination is assembled (compute-in-core: no TS caller ever hand-builds this string). Ignored when due is None. WIRED (#27 live-verify follow-up): createTask’s file branch (repo.ts) now passes parsed.dueTime/scheduledTime/startTime straight through on every file-routed task — before this fix these fields were prepared on the struct but no writer ever set them, so a time-bearing task (e.g. “buy milk tomorrow 5pm”) silently lost the time-of-day: only the bare date reached the frontmatter.
  • pub scheduled_time: Option<String>: Time-of-day companion for When. If only an imported start value exists, start_time is used with that date when the file is built (#430).
  • pub start_time: Option<String>: Time-of-day companion for an imported start date; never writes start:.
  • pub recurrence: Option<String>: Canonical repeat display string (e.g. “every Monday”). The TS caller canonicalises the TaskRecurrence struct before passing it here so the file writer can emit the readable repeat: property (#430).
  • pub estimate: Option<String>: Human-readable duration scalar (#27 T6, spec A §C.2 field table) — e.g. "2h 15m", "45m", "1h". RESERVED + round-tripped only in v1: no NLP capture writes this yet (ParsedTaskInput has no estimate field), so in practice this parameter is only exercised by tests today — the FUTURE writer that sets it must always emit the SPACED form (the canonical/emitted shape per the field table), which is why this builder writes whatever string it is given verbatim rather than reformatting: the spacing rule binds the writer that actually chooses the value, not this pass-through parameter.
  • pub area: Option<String>
  • pub created: CivilDate: Creation date — always required (no default). Pass the current civil date.
  • pub body: Option<String>: Free-form prose emitted between the frontmatter block and the root checkbox line (#27 T6, spec A §C.2) — the composer multi-line TASK commit’s body. None/absent reproduces the exact pre-T6 skeleton (frontmatter → blank line → checkbox, zero extra bytes); Some(body) with a non-empty string appends <body>\n\n before the checkbox. Never re-parsed by this builder — see build_task_file’s doc comment.

Implements: Clone, PartialEq, Eq, Debug, Serialize, Deserialize

Source: crates/calternal-notes-core/src/tasks/model.rs:249

pub struct TaskIndexEntry

One indexable row produced by projecting a single task out of a file.

Mirrors TaskRecord’s storable fields (contract §3/§5) minus role — role is day-relative (due / scheduled / overdue / …) and computed by the query layer (plan 2), never stored in the index. Adding role here would couple the index to day-query semantics and force re-projection on every date change.

rename_all = "camelCase" produces the camelCase wire form that matches the TS TaskRecord interface and the web SQLite column names. Without this serde emits snake_case, silently breaking every consumer that reads taskId, blockId, statusOverridden, referenceTarget, etc.

Fields

  • pub task_id: String: Stable, unique task id.
    • Inline: ${source}#${blockId} when a block-id anchor is present, ${source}#L${ordinal} as a fallback when no anchor exists.
    • File: its source path internally; calternal_id is its stable link ID.
  • pub source: String: Vault path of the file the task lives in — the write-back target for status flips. For inline tasks this is the day/body file; for file tasks it equals task_id.
  • pub kind: TaskKind: Whether this is a Task Note or an inline Checkbox row.
  • pub block_id: Option<String>: Markdown block-id anchor (^abc123) on the line, if present. None for file rows and inline lines that lack an anchor. Used to construct task_id and to locate the exact line for setTaskLineStatus.
  • pub parent_id: Option<String>: Parent Task’s task_id for child Checkbox lines inside its Note. None for top-level and standalone tasks. Plan 2 uses this to scope child queries and plan 3 uses it to render nested task lists.
  • pub title: String: Display title — everything after the checkbox marker, stripped of all recognised sigil tokens (dates, priority, tags, recurrence, reference).
  • pub status: TaskStatus: Rolled-up lifecycle status. For project files (§6) this is derived by rollupStatus over child task lines unless status_overridden = true, in which case the frontmatter value wins. Plans 2/3 surface this value; they do NOT recompute it.
  • pub status_overridden: bool: true when the file frontmatter carries an explicit status: field that overrides the rollup (spec §6). Plan 3 shows a “resume auto” affordance when this is set, allowing the user to clear the override.
  • pub is_project: bool: true when a Task Note has multiple child Checkbox lines. Computed by the projector so views do not need to count the body on every render.
  • pub priority: Option<TaskPriority>
  • pub due: Option<CivilDate>: Deadline date from due:. Calendar day membership uses scheduled first; a deadline alone does not make a Task appear in Today early.
  • pub scheduled: Option<CivilDate>: When date. Imported start: supplies this only when scheduled: is absent, so query code has one effective When value (#430).
  • pub start: Option<CivilDate>: Obsidian import value. Retained for read compatibility and never emitted by the Task writer (#430, issue decision T12b).
  • pub created: Option<CivilDate>
  • pub created_at: Option<String>: RFC 3339 creation instant for new file Tasks. Its explicit offset keeps the User’s creation clock intact; legacy date-only created values stay valid and do not gain an invented time (#655, DESIGN §30 C12a).
  • pub due_time: Option<String>: Time-of-day companion for due (#27 T7, spec A §D.1/§D.3): Some("HH:mm:ss") when the frontmatter due: value carried a time (YYYY-MM-DD HH:mm:ss), None for a date-only value or when due itself is None. The DATE half always stays in due above — this is index-internal storage (§D.3: “date columns stay date-only, time rides in a nullable companion”) so every existing day-membership/overdue predicate that only ever compared due survives untouched. None for Inline rows (inline checkbox date tokens are the separate, date-only emoji shorthand — out of this amendment’s scope).
  • pub scheduled_time: Option<String>: Time-of-day companion for effective When, including imported start: when the Note has no scheduled: field (#430).
  • pub start_time: Option<String>: Time-of-day companion for an imported start: value. New Notes do not write this field (#430, issue decision T12b).
  • pub recurrence: Option<String>: Canonical display string for the repeat: rule (e.g. “every 2 weeks”, “every Monday”). The API retains its recurrence field name for wire compatibility; the Markdown writer uses repeat: (#430).
  • pub tags: Vec<String>: All tags on the task including namespaced forms (area/…, context/…, project/…). Full tag bodies, not stripped prefixes.
  • pub links: Vec<String>: Outgoing Markdown links. The UI derives its ## Links section from this projection; it is never written back into the Task file.
  • pub area: Option<String>: Primary area tag value (e.g. “work” for area/work), extracted from tags by the projector. Used for color/label in the UI. None when no area/ tag is present.
  • pub untagged: bool: true when the task has no area tag. Drives Inbox membership (v1: filed tasks store only area:, so “untagged” = no area tag). Inline tasks still carry all tags from the line.
  • pub reference_target: Option<String>: Vault path of the linked Task Note for promoted reference lines (- [ ] title → [Title](Notes/…)). None for plain inline lines and for file rows. The API ticks the referenced Task instead of this Checkbox: setTaskStatus on the reference_target file, not on the line’s own file (contract §7). The toggle path branches on whether this is Some.
  • pub calternal_id: Option<String>: The calternal-id frontmatter value (#27 T6, spec A §C.2) — the task file’s authoritative identity, same scheme as notes (R4). None for Inline rows (inline checkbox lines have no file-level identity of their own) and for File rows whose frontmatter has no calternal-id yet (a legacy, pre-migration task file — see calternal migrate-tasks, spec A §C.4).
  • pub body: Option<String>: Free-form prose between the frontmatter’s closing fence and the file’s first checkbox line (#27 T6, spec A §C.2) — the composer multi-line TASK commit’s body. None for Inline rows (a body block is a File-row-only concept) and for File rows with no such prose (the common case: a task file with only checkbox lines after its frontmatter). NEVER re-parsed — this is inert display text, exactly like a note’s body.

Implements: Clone, PartialEq, Eq, Debug, Serialize, Deserialize

Source: crates/calternal-notes-core/src/tasks/model.rs:125

pub struct TaskLine

One parsed Checkbox from a Task Note or another Note’s body.

Internal only — not serialized across the WASM boundary. Plan 4 (the projector, extractTaskIndex) fills TaskLine values from markdown text and assembles TaskIndexEntry rows from them. Keeping the parsed form separate from the index row lets the projector accumulate all child lines before computing rollup / isProject.

recurrence carries the structured TaskRecurrence here (not a canonical string) because the projector canonicalises it via recurrence_to_string when building TaskIndexEntry. All other date/priority/tag fields are already resolved by the NLP layer.

No serde derives — this type is not part of the WASM wire surface. If it were serialised its TaskRecurrence shape would diverge from the display string in TaskIndexEntry, creating two representations for the same concept.

Fields

  • pub status: TaskStatus
  • pub title: String
  • pub block_id: Option<String>
  • pub due: Option<CivilDate>
  • pub scheduled: Option<CivilDate>
  • pub start: Option<CivilDate>
  • pub created: Option<CivilDate>
  • pub completed: Option<CivilDate>
  • pub recurrence: Option<TaskRecurrence>: Structured recurrence rule as parsed by the NLP layer. The projector will call recurrence_to_string to produce the canonical display form for TaskIndexEntry::recurrence.
  • pub priority: Option<TaskPriority>
  • pub tags: Vec<String>: All tags on the line (verbatim tag bodies, as extracted by the sigil parser).
  • pub reference: Option<String>: The Home-relative Note target from a promoted → [Title](Notes/…) reference. Set by the inline-task extractor and used to tick the linked Task Note. None for a plain Checkbox (#430).
  • pub indent: usize: Number of leading spaces / tab-stops before the - [ ] marker. The projector uses this to detect child task lines inside a Task Note (indent > 0 → child of the previous task with lower indent).

Source: crates/calternal-notes-core/src/tasks/model.rs:335

pub enum TaskKind

Whether a Task is a Note with task frontmatter or a Checkbox in another Note. Task Notes may live anywhere in Home (DESIGN §40, issue #430).

rename_all = "lowercase" → “file” / “inline” on the wire, matching the TS kind: 'file' | 'inline' field on TaskRecord (contract §3). The distinction matters for write-back: file rows flip status via frontmatter; inline rows use setTaskLineStatus on the line’s own file (or via referenceTarget if promoted).

Variants

  • File: A Note marked as a Task by its frontmatter.
  • Inline: An inline checkbox line inside a day/body file (incl. preserved date emoji lines, which are read-only per spec §4.3).

Implements: Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize

Source: crates/calternal-notes-core/src/tasks/model.rs:99

pub enum TaskStatus

Task lifecycle status. Maps to the TS TaskStatus literal union type in protocol.ts (‘todo’ | ‘doing’ | ‘blocked’ | ‘waiting’ | ‘done’ | ‘cancelled’).

rename_all = "lowercase" ensures the wire form is “todo” / “doing” / etc., matching the TS literal union and the day-file sigil table (spec §4). Without this serde would emit PascalCase (“Todo”), breaking every TS consumer.

as_word / from_word provide a round-trip through the same lowercase string so the line writer and frontmatter writer can produce status: done without going through serde — and callers can parse a raw frontmatter string back.

Variants

  • Todo
  • Doing
  • Blocked
  • Waiting
  • Done
  • Cancelled

Implements: Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize

pub fn as_word(self) -> &'static str

Canonical lowercase keyword. Matches the serde wire form and the day-file/frontmatter status field values. Round-trips with from_word.

pub fn from_word(word: &str) -> Option<Self>

Parse a lowercase (or mixed-case) keyword into a TaskStatus. Returns None for unrecognised words so callers can fall through to a default gracefully (e.g., treat unknown frontmatter values as Todo).

Uses eq_ignore_ascii_case instead of to_lowercase() to avoid a heap allocation per call — these are called in the projector loop over every task entry, so zero-alloc comparison matters.

Source: crates/calternal-notes-core/src/tasks/model.rs:36