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
Structs
Section titled “Structs”BuildTaskFileInput
Section titled “BuildTaskFileInput”pub struct BuildTaskFileInputInput 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’scalternal-id(#27 T6, spec A §C.2) — leads the canonical frontmatter field order when present.Option, NOT a bare requiredString, 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)buildTaskFilecall 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 —Nonehere 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: Stringpub priority: Option<TaskPriority>pub due: Option<CivilDate>pub scheduled: Option<CivilDate>pub start: Option<CivilDate>: Import-only date. The writer folds this intoscheduledwhen absent and never writes astart:key (#430, issue decision T12b).pub due_time: Option<String>: Time-of-day companion fordue(#27 T8, spec A §D.2),"HH:MM". When present alongsidedue,build_task_fileemitsdue: YYYY-MM-DD HH:mm:00instead 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 whendueisNone. WIRED (#27 live-verify follow-up):createTask’s file branch (repo.ts) now passesparsed.dueTime/scheduledTime/startTimestraight 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_timeis 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 writesstart:.pub recurrence: Option<String>: Canonical repeat display string (e.g. “every Monday”). The TS caller canonicalises theTaskRecurrencestruct before passing it here so the file writer can emit the readablerepeat: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 (ParsedTaskInputhas 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\nbefore the checkbox. Never re-parsed by this builder — seebuild_task_file’s doc comment.
Implements: Clone, PartialEq, Eq, Debug, Serialize, Deserialize
Source: crates/calternal-notes-core/src/tasks/model.rs:249
TaskIndexEntry
Section titled “TaskIndexEntry”pub struct TaskIndexEntryOne 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_idis its stable link ID.
- Inline:
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 equalstask_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.Nonefor file rows and inline lines that lack an anchor. Used to constructtask_idand to locate the exact line forsetTaskLineStatus.pub parent_id: Option<String>: Parent Task’stask_idfor child Checkbox lines inside its Note.Nonefor 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 byrollupStatusover child task lines unlessstatus_overridden = true, in which case the frontmatter value wins. Plans 2/3 surface this value; they do NOT recompute it.pub status_overridden: bool:truewhen the file frontmatter carries an explicitstatus: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:truewhen 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 fromdue:. Calendar day membership usesscheduledfirst; a deadline alone does not make a Task appear in Today early.pub scheduled: Option<CivilDate>: When date. Importedstart:supplies this only whenscheduled: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-onlycreatedvalues stay valid and do not gain an invented time (#655, DESIGN §30 C12a).pub due_time: Option<String>: Time-of-day companion fordue(#27 T7, spec A §D.1/§D.3):Some("HH:mm:ss")when the frontmatterdue:value carried a time (YYYY-MM-DD HH:mm:ss),Nonefor a date-only value or whendueitself isNone. The DATE half always stays indueabove — 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 comparedduesurvives untouched.NoneforInlinerows (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 importedstart:when the Note has noscheduled:field (#430).pub start_time: Option<String>: Time-of-day companion for an importedstart:value. New Notes do not write this field (#430, issue decision T12b).pub recurrence: Option<String>: Canonical display string for therepeat:rule (e.g. “every 2 weeks”, “every Monday”). The API retains itsrecurrencefield name for wire compatibility; the Markdown writer usesrepeat:(#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## Linkssection from this projection; it is never written back into the Task file.pub area: Option<String>: Primary area tag value (e.g. “work” forarea/work), extracted fromtagsby the projector. Used for color/label in the UI.Nonewhen noarea/tag is present.pub untagged: bool:truewhen the task has no area tag. Drives Inbox membership (v1: filed tasks store onlyarea:, 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/…)).Nonefor plain inline lines and for file rows. The API ticks the referenced Task instead of this Checkbox:setTaskStatuson thereference_targetfile, not on the line’s own file (contract §7). The toggle path branches on whether this isSome.pub calternal_id: Option<String>: Thecalternal-idfrontmatter value (#27 T6, spec A §C.2) — the task file’s authoritative identity, same scheme as notes (R4).NoneforInlinerows (inline checkbox lines have no file-level identity of their own) and forFilerows whose frontmatter has nocalternal-idyet (a legacy, pre-migration task file — seecalternal 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.NoneforInlinerows (a body block is aFile-row-only concept) and forFilerows 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
TaskLine
Section titled “TaskLine”pub struct TaskLineOne 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: TaskStatuspub title: Stringpub 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 callrecurrence_to_stringto produce the canonical display form forTaskIndexEntry::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.Nonefor 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
TaskKind
Section titled “TaskKind”pub enum TaskKindWhether 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
TaskStatus
Section titled “TaskStatus”pub enum TaskStatusTask 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
TodoDoingBlockedWaitingDoneCancelled
Implements: Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize
TaskStatus::as_word
Section titled “TaskStatus::as_word”pub fn as_word(self) -> &'static strCanonical lowercase keyword. Matches the serde wire form and the
day-file/frontmatter status field values. Round-trips with from_word.
TaskStatus::from_word
Section titled “TaskStatus::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.