# Task date marks

This guide is the authoritative display contract for task date-family marks (canonical task dates plus recurrence rules), in the style of `docs/freshness.md` §11 and the `docs/projects.md` "Priority marks" section. The JavaScript mirror is `api.dateMarks` (namespace v1, top-level api v3) in bob-ledger-tools, which stays dates-only; its tests run the conformance vectors below verbatim.

## What you see

Every canonical `created`, `scheduled`, `completion`, and `cancelled` task date renders as a small monochrome icon plus a calendar label instead of a `KEY | YYYY-MM-DD` Dataview pill, and every readable `[repeat:: …]` rule renders as a quiet loop plus a spoken cadence:

```
before: - [ ] Add ability to install agent CLIs  CREATED│2026-08-31  SCHEDULED│2026-09-04
after:  - [ ] Add ability to install agent CLIs  + Aug 31  ⧗ Sep 4
```

More examples (glyphs approximated in plain text):

```
- [?] Ship the importer  + yesterday  ⧗ Fri
- [ ] Rename the queue input  + Sep 29  ⧗ today          ← scheduled today reads a touch stronger
- [x] Review the /sase_memory_write skill  + Aug 29  ✓ Sep 3   ← closed tasks rest (faint)
- [-] Old idea  + Mar 2  ⊘ today
- [?] Brush teeth  ⟲ daily  + today  ⧗ tomorrow
- [ ] Pay Kelly salary!  ⟲ monthly · 1st  + Oct 8  ⧗ Nov 1
```

Hovering any mark shows the exact date and its distance, for example `Scheduled Fri, Oct 9 · in 2 days` followed by `Ctrl+Shift+P to reschedule`. Moving the cursor into a mark, or clicking it, reveals `[scheduled:: 2026-10-09]` for editing.

## Principles

Display-only: `[key:: YYYY-MM-DD]` stays the only stored form; nothing writes a date mark, and the Rust, Tasks, and Dataview semantics are unchanged. Store absolute, show relative: near dates read as words, and the tooltip always has the exact date. Shape carries meaning; color whispers: each field has its own silhouette, ink is monochrome, and emphasis comes from weight and opacity, never hue. Quiet by default, louder only when it matters: `created` is the quietest mark, and a scheduled date arriving today is the only emphasized state. Reversible: the cursor or a click reveals the raw field, source mode shows raw text, a session toggle restores the pills, and without bob-ledger-tools the vault looks exactly as it does today. One glyph definition (CSS masks) shared by every surface.

## Fields and glyphs

The four glyphs are refined, monochrome versions of the Tasks emoji already seen in query results (`➕ ⏳ ✅ ❌`). The one deliberate change is cancelled: `⊘` replaces `×`, because a bare `×` beside the created `+` reads as a pair of math operators (and as a close button).

| Stored key | Tooltip verb | Glyph | Tasks emoji it replaces |
| --- | --- | --- | --- |
| `created` | `Created` | plus | ➕ |
| `scheduled` | `Scheduled` | hourglass | ⏳ |
| `completion` | `Done` | check | ✅ |
| `cancelled` | `Cancelled` | circle-slash (⊘) | ❌ |
| `repeat` | — (not a date; see below) | loop (⟲) | 🔁 (Tasks recurrence) |

`due` and `start` are out of scope: the vault uses `start` for clock times and `due` only 3 times. Adding one later is a new row here, a new glyph, and a new key in the repair-flag selector.

Final geometry (viewBox `0 0 16 16`, black stroke, `stroke-width 1.9`, `stroke-linecap`/`stroke-linejoin` `round`, `fill none`; used as CSS masks): plus `M8 3.25v9.5 M3.25 8h9.5`; hourglass `M4 1.9h8 M4 14.1h8` with bulbs `M5.1 1.9v1.6c0 1.9 1.2 3.1 2.9 4.5c1.7-1.4 2.9-2.6 2.9-4.5V1.9` and `M5.1 14.1v-1.6c0-1.9 1.2-3.1 2.9-4.5c1.7 1.4 2.9 2.6 2.9 4.5v1.6`; check `M3.1 8.6l3.1 3.1l6.7-6.9`; circle-slash `<circle cx="8" cy="8" r="5.9"/>` plus `M3.83 3.83l8.34 8.34`.

## Label grammar

Let `Δ` be whole days from today (local date) to the field's date.

| Δ | Label |
| --- | --- |
| 0 | `today` |
| \+1 | `tomorrow` |
| −1 | `yesterday` |
| \+2 … +6 | short weekday of the date: `Mon` … `Sun` |
| any other Δ, same year | `Oct 22` (short month, day without padding) |
| any other Δ, different year | `Oct 22, 2027` |

Weekday names are used only for the coming six days: a past weekday would make `⧗ Mon` ambiguous, so past dates beyond yesterday use month and day. The weekday rule wins over the year rule (on Dec 30, a Jan 1 date reads `Fri`). Words are lowercase; month and weekday names are capitalized fixed English, matching the freshness mark. One grammar for all four fields makes a closed task read as a small timeline (`+ Aug 29  ✓ Sep 3`); the task's age stays one hover away.

## Tooltip

The mark carries `role="img"`, `aria-label`, and `data-tooltip-position="top"`, lines joined with `\n`, never containing `::`. Line 1 is `{Verb} {short date} · {relative}`, where `{short date}` is the freshness short date (`Fri, Oct 9`, year appended across years) and `{relative}` is `today`, `tomorrow`, `yesterday`, `in N days`, or `N days ago` in exact days. Line 2, scheduled only, is `Ctrl+Shift+P to reschedule`. Examples: `Created Tue, Sep 29 · 8 days ago`; `Scheduled Fri, Oct 9 · in 2 days` + reschedule hint; `Done Mon, Oct 5 · 2 days ago`; `Cancelled Tue, Dec 30, 2025 · 281 days ago`.

## Anatomy and tones

A mark is `span.bob-date-mark` with `span.bob-date-mark-glyph` (the mask) and `span.bob-date-mark-label` (the text), carrying `data-field`, `data-date`, `data-when` (`past` | `today` | `future`), and `data-fold-space` (`data-rendered="true"` on rendered-view marks). Metrics match `.bob-fresh-mark` exactly (interface font at 0.8em, `tabular-nums`, weight 550, inline-flex with a 0.22em gap, pill padding/margin, `vertical-align: 0.05em`); the glyph box is 1.08em square; hover is opacity 1 plus a 10% `currentColor` capsule; transitions off under `prefers-reduced-motion`.

| Situation | Ink | Opacity / weight |
| --- | --- | --- |
| default | `--bob-date-color-{field}`, else `--text-muted` | 0.85 |
| quiet: any `created`; past `scheduled` | same ink | 0.7 |
| emphasis: `scheduled` with `data-when="today"` | `--bob-date-color-today`, else `--text-normal` | 1, weight 650 |
| resting: inside a closed task (`x`, `X`, `-`) | `--text-faint` | 0.75; wins over every other row |

Resting is derived from the nearest `li.task-list-item[data-task]` or `.HyperMD-task-line[data-task]` ancestor, exactly like the priority mark. Theme hooks `--bob-date-color-{created,scheduled,completion,cancelled,today}` are unset by default. There is no red and no "overdue" state: a past scheduled date means the task already resurfaced, not that it is late.

## Eligibility and folding

For each key: the occurrence gate counts `/(^|[^A-Za-z0-9_-])key\s*::/gi` and requires exactly one (so `[rescheduled:: …]` never counts, and duplicates get no mark). The canonical form is `[key:: YYYY-MM-DD]` or `(key:: YYYY-MM-DD)` with a matching bracket pair, a lowercase key touching `::`, padding spaces only, and a strict calendar date. Each key is judged on its own. Any canonical field outside code gets a mark: task lines, plain bullets, quotes, continuation lines, paragraphs. Excluded: source mode and code positions in Live Preview; text under `code`, `pre`, `.dataview.inline-field`, `.bob-date-mark`, `.bob-priority-mark`, or `.bob-fresh-mark` in rendered views.

Whitespace-run folding (Live Preview): the decoration starts at the first space of the U+0020 run directly before the field, and the widget restores one uniform gap — Tasks-written double-spaced `completion`/`cancelled` fields sit with the same gap as single-spaced ones. A field beginning the line's content never folds. A selection endpoint never sits inside a fold. While a cursor or selection end lies strictly inside the space run, that field's mark folds nothing and the spaces show as text; the fold returns once the cursor leaves the run. Folded spaces still never reveal the field.

## Repair flag

While marks are on, any leftover `created`/`scheduled`/`completion`/`cancelled`/`repeat` Dataview pill in Live Preview is a field Tasks cannot read (or a guarded repeat): a bad date, a word, a datetime, a duplicate, mismatched brackets, an uppercase key, a space before `::`, or the checklist guard. CSS flags it with full opacity and a dashed orange border, matching the freshness and priority flags. Template placeholders in `_templates` are also flagged: the flag truthfully says Tasks cannot read the value as a date.

## Recurrence (not a date)

A recurrence mark is `span.bob-date-mark[data-field="repeat"]` with `data-rule` (the rule as written, whitespace-collapsed), `data-when-done` (`true`/`false`), and `data-fold-space` (`data-rendered="true"` on rendered views). It contains the usual `span.bob-date-mark-glyph` + `span.bob-date-mark-label`. It has no `data-date` and no `data-when`, and keeps `role="img"`, `aria-label`, and `data-tooltip-position="top"`.

Glyph: a rectangular two-arrow loop, a refined monochrome 🔁. It deliberately avoids the circular arrow, which already means `⟳ Review` and due freshness. Geometry (viewBox `0 0 16 16`, black stroke, `stroke-width 1.8`, round caps and joins): `M2.6 9.4V6.6A2.6 2.6 0 0 1 5.2 4h8.2M11 1.6 13.4 4 11 6.4M13.4 6.6v2.8A2.6 2.6 0 0 1 10.8 12H2.6M5 9.6 2.6 12 5 14.4`. Defined as `--bob-date-glyph-repeat`.

Ink is always the quiet tone: `--bob-date-color-repeat`, else `--text-muted`, at opacity 0.7 (like `created`). There is no emphasis state and no hue. Closed rows rest faint via explicit `li.task-list-item[data-task=x|X|-] .bob-date-mark[data-field="repeat"]` and `.HyperMD-task-line[...]` rules, because the existing resting selector requires `[data-when]`.

Validity gate (all must hold, else the native pill stays): exactly one `/(^|[^A-Za-z0-9_-])repeat\s*::/gi` on the line or text node; `[repeat:: …]` or `(repeat:: …)` with matching brackets, a lowercase key touching `::`, and ASCII space padding only; the raw value matches Tasks' charset `^[a-zA-Z0-9, !]+$` before repeated spaces are collapsed, and starts with `every` plus a space and more text (case-insensitive); tabs, non-breaking spaces, and other whitespace are invalid rather than normalized; code positions are excluded exactly as for dates; and the checklist guard below does not fire. Known limit: the charset check is necessary but not sufficient — `every blursday` gets a verbatim label, which is still truthful text.

Checklist guard: no mark when the line is a checklist member (`checklistKindForText` non-null), the task is open (status not `x`, `X`, `-`), and the rule lacks `when done`. This mirrors the `checklist_repeat_not_when_done` lint scope (open members), so the display never hides what the lint flags. Live Preview reads the whole line plus `freshnessTaskStatus`; rendered views read the nearest `li.task-list-item` ancestor's own `a.tag` anchors (skipping nested lists and code) and its `data-task`, lazily and only for text that contains a repeat source.

Label grammar ("say it the way a person would"): normalize by trimming, collapsing whitespace, and stripping a trailing `when done` (case-insensitive). Labels are lowercase except weekday and month names, use `·` (U+00B7 with spaces) between cadence and detail, and are never truncated. `day`/`weekday` → `daily`/`weekdays`; `week`/`month`/`year` (or `1 week` etc.) → `weekly`/`monthly`/`yearly`; `N days`/`weeks`/`months`/`years` (N ≥ 2) → `every N days` (and so on); `<Weekday>` or `week on <Weekday>` → `Mondays`; `week on <Weekday list>` → `weekly · Tue, Fri`; `N weeks on <Weekday list>` (N ≥ 2) → `every N weeks · Fri`; `month on the <ordinal>`/`last`/`last <Weekday>`/`<ordinal> <Weekday>` → `monthly · 1st`/`monthly · last`/`monthly · last Fri`/`monthly · 2nd Wed`; `N months on the <ordinal>[ <Weekday>]` (N ≥ 2) → `every N months · 1st`; `<Month> on the <ordinal>` → `yearly · Jan 15`; anything else stays verbatim (collapsed, minus `when done`). Ordinals keep their written token, lowercased. The grammar covers every rule in the vault today.

Tooltip: two lines, never containing `::`. Line 1 is `Repeats <rule as written, collapsed>`; line 2 is `Next date counts from completion` when `whenDone`, else `Next date follows the schedule`. `relabelRenderedDateMarks` already skips repeat marks because `dateMarkModel` returns `null` for `repeat`. `api.dateMarks` v1 stays dates-only: `fields` is unchanged and `model("repeat", …)` is `null`.

## Surfaces and interaction

Live Preview uses a `Prec.highest` ViewPlugin emitting one `Decoration.replace` per eligible field, in line order. The mark hides while any selection overlaps its own field span, so editing `created` never reveals `scheduled`. A selection endpoint never sits inside a fold: a cursor resting in the folded spaces unfolds that field's mark to fold 0. Mousedown places the cursor at the field start and focuses the editor; it never writes. Widget equality includes the label, `when`, and tooltip, so a new day always re-renders. Rebuilds happen on doc, viewport, or selection changes, Live Preview or file switches, and the `dateMarksRefresh` effect. The rendered-view post-processor (sort order 50, before Dataview's pass at 100) splits each matching text node into text / mark / text / mark / … for every eligible field, covering reading view, embeds, hover previews, Dataview `TASK` views, and Tasks descriptions with a non-trailing field. A duplicate split across separate text nodes is not detected (the accepted priority limit).

## Tasks query results

Tasks 8.4.0 always renders query-result date components with its emoji serializer: `span.task-created|task-scheduled|task-done|task-cancelled` wrapping an inner `span` whose text is `" ⏳ 2026-10-09"` (full mode) or `" ⏳"` (short mode). The DOM contract, confirmed against the installed `obsidian-tasks-plugin/main.js` (`renderTaskLine`, `taskToHtml`, `tasks-layout-short-mode`):

- Each date component is a direct child span of `li.plugin-tasks-list-item > .tasks-list-text`, with its text in a single inner span. `done` maps to `task-done`.
- The li receives `data-task` only at the end of `renderTaskLine`, so it is the completion signal.
- Short-mode containers carry `tasks-layout-short-mode`.
- Date spans keep Tasks' click (date editor) and contextmenu (postpone) listeners.

CSS cannot split the emoji from the date, so unlike the priority mark (whose Tasks host is CSS-only), full mode needs a small, narrow JS frame pass — the narrowest JS that degrades to Tasks' native rendering. `scheduleTasksResultDateMarks(el)` runs at the end of `renderDateMarksIn`: Tasks renders every description through `MarkdownRenderer.render`, so it fires once per result row. It queues `el` when `el` has a result-row ancestor or is still detached, keeps at most one pending frame (`requestAnimationFrame`, falling back to a 16ms timeout), and schedules nothing when marks are off or for a connected non-Tasks element. `runTasksResultDateMarkFrame` resolves each entry to its row — a still-detached element is retried for up to 3 frames, a row without `data-task` for up to 60 — deduplicates rows with a WeakSet, and decorates complete rows with `decorateTasksResultDates(li, today)`. Exhausted entries are dropped silently, leaving Tasks' native emoji dates; in the common case the pass lands before the next paint, so the emoji dates never flash. Each `task-created`, `task-scheduled`, `task-done`, and `task-cancelled` span without `data-bob-date-mark` whose inner text ends in a canonical ISO date gets the same mark (`foldSpace: true`, `rendered: true`, `data-host="tasks"`) appended inside the span, which is then flagged `data-bob-date-mark="true"`; short-mode spans (no date), invalid dates, `task-due`, and `task-start` are untouched. The pass is idempotent, clicking a mark still opens Tasks' own date editor (the click bubbles to Tasks' span), and right-click postpone still works. `relabelRenderedDateMarks` already covers Tasks-host marks through `data-rendered="true"`, so they roll over at midnight like every other rendered mark. In short mode (`rotten.md`, `crowded.md`), CSS alone turns each date span into a 1.08em glyph box drawing the shared field mask at the default ink, resting from the row's `data-task`.

Recurring Tasks results use `span.task-recurring` as a fifth Tasks host. In full mode the frame pass strips the leading `🔁` (with an optional variation selector), validates the remaining rule with the shared recurrence grammar, and appends the same loop mark used elsewhere. The mark carries `data-host="tasks"`; the native inner span is clipped while Tasks keeps its event listeners. Unknown shapes, invalid rules, and open PRE/POST checklist members whose rule lacks `when done` keep the native rule visible. Checklist membership comes only from the row's `.task-description` tag pills, and the guard applies only to open rows. In short mode, CSS replaces the recurring component with the shared loop mask and rests it faintly for closed rows. With date marks off, the existing Tasks-host cover hides stale marks until Tasks redraws.

## Rollover and toggle

The existing minute interval calls `refreshDateMarksForRollover(now)`: when the local date changed, it dispatches `dateMarksRefresh` to every markdown editor and relabels every `.bob-date-mark[data-rendered="true"]` in place (label, `aria-label`, `data-when`). Live Preview widget DOM is never edited in place. The command "Toggle task date marks" (id `toggle-date-marks`) is session-only and on by default: it flips `body.bob-date-marks` (which also gates the repair flag), dispatches the refresh effect, triggers the Tasks re-render event, and shows `Date marks on` / `Date marks off`. When off, JS creates no marks: a Tasks-result frame that runs after toggle-off drops its queue without writing, and `onunload` disables marks and clears the Tasks queue/pending/dedup state so late callbacks cannot decorate after unload. Re-enabling decorates new rows normally.

## `api.dateMarks` v1

Frozen and additive (top-level api stays v3): `{ version: 1, fields: ["created", "scheduled", "completion", "cancelled"], model(field, dateText), render(host, field, dateText, options) }`. Both are synchronous and never throw. `render` appends to `host` and returns the element, or `null` for an unknown field or a non-canonical date. `options.decorative` renders `aria-hidden` with no tooltip; `options.inheritColor` makes the ink `currentColor`. Nothing consumes the namespace yet; it exists so a later Task Card or notice can show `⧗ Fri` with the same glyph.

## Rejected alternatives

Changing storage to Tasks emoji or relative text breaks the Tasks Dataview format and Dataview's field index, rewrites thousands of vault fields plus the bob-cli parsers and writers, and relative text rots overnight. A CSS-only restyle of the Dataview pill cannot read the value, so the ISO date would stay with no `today`/`Fri` and no repair semantics. A CSS-only restyle of Tasks query results was rejected for the same reason in full mode — CSS cannot split Tasks' emoji from its date — while a global MutationObserver over Tasks' DOM was rejected as wider than needed: the description post-processor already fires once per result row, so one bounded frame pass that degrades to Tasks' native emoji dates is enough, and short mode stays CSS-only. An age voice for created (`5w`) adds a second grammar and breaks the closed-task timeline. Weekday names for past dates are ambiguous for scheduled. `×` for cancelled reads as math next to `+` and as a close button. Per-field hues clash with the status colors, and a past scheduled date is not overdue. Hiding created on closed tasks drops data; the resting tone quiets it instead. One lifecycle chip per task loses per-field reveal. Click never opens a card or picker: display surfaces never act. `due` and `start` are out of scope (see above).

Non-goals: storage, parser, Rust CLI/TUI, `bob query`, Bob Mac Capture output; Task Card, notice, and picker rows (the api enables them later); italic dates in Schedule/Work/Cancel Log entries; Tasks group headings; project-note frontmatter properties; memory notes.

## Conformance vectors

Today is `2026-10-07` (Wednesday) unless noted. ⏎ separates tooltip lines. `fold N` is `foldLength` in Live Preview.

| \# | Input | Expected |
| --- | --- | --- |
| DM1 | `- [ ] #task Buy milk [created:: 2026-10-07]` | created · `today` · when `today` · `Created Wed, Oct 7 · today` · fold 1 · quiet tone |
| DM2 | `- [ ] #task Call mom [created::2026-10-06] ^call` | created · `yesterday` · `Created Tue, Oct 6 · yesterday` |
| DM3 | `- [?] #task Ship [scheduled:: 2026-10-08]` | scheduled · `tomorrow` · `Scheduled Thu, Oct 8 · tomorrow⏎Ctrl+Shift+P to reschedule` |
| DM4 | `[scheduled:: 2026-10-09]` | `Fri` · `Scheduled Fri, Oct 9 · in 2 days⏎Ctrl+Shift+P to reschedule` |
| DM5 | `[scheduled:: 2026-10-13]` | `Tue` (Δ 6) · `Scheduled Tue, Oct 13 · in 6 days⏎…` |
| DM6 | `[scheduled:: 2026-10-14]` | `Oct 14` (Δ 7) · `Scheduled Wed, Oct 14 · in 7 days⏎…` |
| DM7 | `- [ ] #task Rename [scheduled:: 2026-10-07]` | `today` · when `today` (emphasis tone) · `Scheduled Wed, Oct 7 · today⏎…` |
| DM8 | `- [ ] #task Install [scheduled:: 2026-09-04]` | `Sep 4` · when `past` (quiet tone) · `Scheduled Fri, Sep 4 · 33 days ago⏎…` |
| DM9 | `- [x] #task Report [completion:: 2026-10-05]` | completion · `Oct 5` (never a past weekday) · `Done Mon, Oct 5 · 2 days ago` · resting (CSS) |
| DM10 | `- [-] #task Old [cancelled:: 2025-12-30]` | cancelled · `Dec 30, 2025` · `Cancelled Tue, Dec 30, 2025 · 281 days ago` |
| DM11 | `[scheduled:: 2027-01-02]` | `Jan 2, 2027` · `Scheduled Sat, Jan 2, 2027 · in 87 days⏎…` |
| DM12 | today `2026-12-30`, `[scheduled:: 2027-01-01]` | `Fri` (Δ 2 across the year) · `Scheduled Fri, Jan 1, 2027 · in 2 days⏎…` |
| DM13 | `- [x] #task Review skill [created:: 2026-08-29]  [completion:: 2026-09-03] ^review` | two marks in order: created `Aug 29` fold 1; completion `Sep 3` fold 2 |
| DM14 | `- [ ] #task A [fresh:: 2026-10-05] [created::2026-09-29] [scheduled:: 2026-10-09]` | created `Sep 29`, scheduled `Fri`; both ranges disjoint from the fresh mark's range |
| DM15 | `- [ ] #task B (scheduled:: 2026-10-09)` | `Fri` |
| DM16 | `- [x] #task C [ completion:: 2026-10-07 ]` | `today` |
| DM17 | `- Idea for later [created::2026-07-03]` | created `Jul 3` (plain bullet) |
| DM18 | `> - [ ] #task Quoted [created:: 2026-10-01]` | `Oct 1` · fold 1 |
| DM19 | `- [ ] #task D x[created:: 2026-10-01]` | `Oct 1` · fold 0 |
| DM20 | `- [created:: 2026-10-01] starts the bullet` | `Oct 1` · fold 0 (the field begins the content) |
| DM21 | `Paragraph text [scheduled:: 2026-10-09]` | `Fri` (non-list line) |
| DM22 | `` - [ ] #task E `[created:: 2026-10-01]` `` | untouched (code) |
| DM23 | `- [ ] #task G [rescheduled:: 2026-10-09] [scheduled:: 2026-10-10]` | scheduled `Sat`; `rescheduled` untouched and not counted as a duplicate |
| DM24 | `- [ ] #task  [created:: 2026-10-07]`, cursor 12 (between the two spaces) | created `today`; cursor in the run unfolds to fold 0 (`[13, 35)`); cursor 11 keeps fold 2; after typing `b` (cursor 13) fold 1 |
| DM25 | `- [x] #task Review skill [created:: 2026-08-29]  [completion:: 2026-09-03] ^review`, cursor 48 | completion fold 0 (`[49, 74)`); created stays fold 1 (`[24, 47)`) |
| DN1 | `[scheduled:: 2026-13-01]` | no mark (repair pill) |
| DN2 | `[scheduled:: tomorrow]` | no mark (repair pill) |
| DN3 | `- [ ] #task F [scheduled:: 2026-09-10] (scheduled:: 2026-09-11)` | no mark on either (repair pills) |
| DN4 | `[Created:: 2026-10-01]` | no mark (key case) |
| DN5 | `[created:: 2026-10-01)` | no mark (mismatched brackets) |
| DN6 | `[created :: 2026-10-01]` | no mark (key must touch `::`) |
| DN7 | `[created:: 2026-10-01T09:30]` | no mark |
| DN8 | `[due:: 2026-10-09]` / `[start::1700]` | untouched, and never repair-flagged |
| DN9 | `- [ ] #task H [scheduled:: 2026-13-01] [created:: 2026-10-01]` | created `Oct 1` still marks; scheduled is a repair pill |
| RO1 | a rendered `Fri` mark (date `2026-10-09`) when the day becomes `2026-10-08`, then `2026-10-09` | relabels in place to `tomorrow`, then `today` (`data-when="today"`); Live Preview editors get the refresh effect |
| T1 | a complete li (`data-task=""`) with `span.task-scheduled > span` text `" ⏳ 2026-10-09"` | a `Fri` mark appended inside the span, which gets `data-bob-date-mark="true"` |
| T2 | `span.task-done > span` text `" ✅ 2026-10-05"` | a completion mark `Oct 5` |
| T3 | short-mode `span.task-created > span` text `" ➕"` | JS leaves it untouched |
| T4 | an li without `data-task` | retried each frame, decorated once the attribute appears, left untouched after 60 frames |
| T5 | a second pass | adds nothing |
| T6 | `span.task-due` and `span.task-start` | untouched |
| T7 | `" ⏳ 2026-13-01"` | untouched |
| T8 | marks off; a non-Tasks connected `el` | nothing is scheduled |
| T9 | a Tasks-host `Fri` mark on rollover | relabels to `tomorrow` |
| RT1 | complete li with `span.task-recurring > span` text `" 🔁 every day when done"` | a `daily` mark appended inside the span; span flagged; second pass adds nothing |
| RT2 | `" 🔁 every week on Monday when done"` / `" 🔁 every month on the 1st"` | `Mondays` / `monthly · 1st` |
| RT3 | short-mode `span.task-recurring > span` text `" 🔁"` | JS leaves it untouched; CSS draws the loop |
| RT4 | open li whose description has `#gtd` + `#pre` pills, rule `" 🔁 every day"` | untouched (checklist guard) |
| RT5 | `" 🔁 every day; x"` / an li without `data-task` | untouched / retried, then decorated or dropped as for dates |
| RT6 | marks off | nothing scheduled; a stale Tasks-host repeat mark is hidden by the cover rule |
| RM1 | `- [?] #task #gtd #pre Brush teeth  [repeat:: every day when done]  [created:: 2026-10-21]` | `daily`, fold 2, quiet; `Repeats every day when done⏎Next date counts from completion`; created mark unaffected and disjoint |
| RM2 | `- [ ] #task Water plants [repeat:: every day]` | `daily`; `…⏎Next date follows the schedule` (not a checklist row, so no guard) |
| RM3 | `every week on Monday when done` / `every week on Sunday` / `every Friday` | `Mondays` / `Sundays` / `Fridays` |
| RM4 | `every month on the 1st` / `every month on the last Friday` / `every month on the 2nd Wednesday` | `monthly · 1st` / `monthly · last Fri` / `monthly · 2nd Wed` |
| RM5 | `every 30 days when done` / `every 28 days` | `every 30 days` / `every 28 days` |
| RM6 | `every 1 week` / `every 3 weeks on Friday` / `every January on the 15th` | `weekly` / `every 3 weeks · Fri` / `yearly · Jan 15` |
| RM7 | `every week on Tuesday, Friday` / `every week on Tuesday and Friday` / `every weekday` | `weekly · Tue, Fri` (both) / `weekdays` |
| RM8 | `every April and December on the 1st and 24th`; `every constructor`; `every week on constructor`; `every constructor on the 1st` | each unknown valid-charset rule is verbatim; `when done` is omitted from the label and retained in the tooltip |
| RM9 | `(repeat:: every week)` / `[ repeat:: every  day ]` | `weekly` / `daily` |
| RM10 | `Every Day When Done` | `daily`; tooltip `Repeats Every Day When Done⏎Next date counts from completion` |
| RM11 | cursor inside the field / in its folded run | reveals that field only / unfolds to fold 0 |
| RM12 | `- [x] #task #gtd #pre Old [repeat:: every day]` (closed member) | mark shown (guard covers open rows only), resting tone |
| RN1 | `[repeat:: weekly]`, `[Repeat:: every day]`, `[repeat :: every day]`, `[repeat:: every day; when done]`, `[repeat:: every\tday]`, `[repeat:: every day]`, tab padding, two `repeat::` fields, `[repeat:: every day)` | no mark (pill; repair-flag selector covers `repeat`); repeated ASCII spaces and uppercase cadence remain valid |
| RN2 | `- [?] #task #gtd #pre Brush [repeat:: every day]` / `- [ ] #task Brush #gtd #post [repeat:: every day]` | no mark on either (the guard follows membership, not the glyph slot); the pill keeps the dashed orange flag |
| RN3 | `` - [ ] `[repeat:: every day]` `` | untouched (code) |
| RR1 | reading view postprocessor invoked on a PRE/POST li root or containing wrapper; open li text holds `[repeat:: every day]` / `… when done`; closed li holds `[repeat:: every day]` | no mark / `daily` mark for permitted `when done` and closed rows, regardless of root shape |
| RO2 | rollover over a rendered repeat mark | untouched; no error |
| RA1 | `api.dateMarks.fields` / `api.dateMarks.model("repeat", "every day")` | unchanged four fields / `null` |

Today for RM/RN/RR/RO vectors is `2026-10-21` unless noted; ⏎ separates tooltip lines.

## Live verification (Bryan, in Obsidian)

- \[ \] All four glyphs and every label form look right in light and dark themes
- \[ \] The cursor or a click reveals only that field's raw text
- \[ \] Vim `cw` on the word before a mark (and Backspacing that word without vim) types in order, before the mark
- \[ \] Tasks-written double-spaced `[completion:: …]` fields sit with the same gap as single-spaced ones
- \[ \] Created reads quiet, scheduled-today reads stronger, and closed tasks rest
- \[ \] A hand-broken `[scheduled:: 2026-13-01]` shows the dashed repair pill
- \[ \] The toggle restores the pills and then turns the marks back on
- \[ \] Reading view, embeds, and hover previews show the marks
- \[ \] Labels roll over after midnight without a reload
- \[ \] Fresh, priority, and date marks sit together cleanly on one line
- \[ \] Metadata Menu does not double-decorate
- \[ \] Mobile (iOS) renders the glyphs
- \[ \] `dash.md` and `blocked.md` Tasks results show date marks
- \[ \] Clicking one opens Tasks' date editor, and right-click postpone works
- \[ \] `rotten.md` and `crowded.md` (short mode) show glyphs instead of emoji
- \[ \] Toggling off restores Tasks' emoji dates
- \[ \] `gtd_daily.md`, `recur.md`, and `cash.md` rows show `daily`, `Mondays`, `monthly · 1st` in Live Preview and reading view
- \[ \] A hand-typed `#task #gtd #pre … [repeat:: every day]` without `when done` keeps the dashed orange repair pill
