fix: honor documented repeater cookie semantics (#12523)

* fix: honor the three documented repeater cookie semantics

Logseq's documented repeater semantics (per docs.logseq.com and
`logseq/docs` `Tasks.md`) define three org-mode-style cookies for
recurring tasks:

  `.+`: repeats from the last completion date
  `++`: advances from scheduled, skipping in whole intervals to future
  `+`:  advances from scheduled by the declared interval (can stack)

The scheduler in `worker/commands.cljs` has been ignoring the cookie
entirely and applying a single, `++`-like semantic for every
recurring task. A user who wrote `.+1w` in markdown — expecting "a
week from when I actually finished" — silently got `++1w` behavior
("a week from the original scheduled date, skipped to future"),
which for a weekly task scheduled 2026-04-01 and completed on
2026-04-05 returns the next occurrence on 2026-04-08 rather than
the documented 2026-04-12.

This change:

  * Adds a closed-values `:logseq.property.repeat/repeat-type` property
    with values `:dotted-plus` / `:plus` / `:double-plus`. Default is
    `:double-plus` so existing recurring tasks see no behavior change
    on upgrade.
  * Rewrites the scheduler to branch on repeat-type and implement each
    semantic: `.+` anchors on now; `+` advances from original once (can
    stack overdue, per org-mode); `++` iterates in whole intervals
    until strictly after now. The `++` path is mathematically
    equivalent to the previous scheduler, so default-path behavior is
    preserved.
  * Guards against frequency <= 0 — the old code would silently produce
    nonsense and, under the new `++` loop, would spin forever. The
    guard short-circuits to `nil`.
  * Extracts `resolve-recur-frequency` and fixes the previous
    `(or [A B] [C D])` pattern in `compute-reschedule-property-tx` —
    any 2-vector is truthy in Clojure, so the default-value branch
    was unreachable and entities without an explicit
    `:recur-frequency` silently fell through to `frequency = nil`.
    `if-let` makes both branches reachable so default-to-1 actually
    works at migration time.
  * Restores the cookie-type selector that was removed from the
    date-time popover in `0a5b88467` (Nov 2020) — in-code support for
    all three cookies has been present but not user-pickable for the
    last ~5.5 years.
  * Adds `docs/recurring-tasks.md` — a technical spec for contributors
    and users that restates and augments the upstream `Tasks.md` text,
    adds decision guidance, and documents the implementation surface.
  * Extends the file-graph → DB-graph migration (built on top of
    `44d6bd49c4` "fix: preserve repeated schedule import") to also
    carry the cookie kind via a new `repeat-types` map in
    `graph-parser/exporter.cljs`, so an imported `.+1w` task lands in
    the DB-graph with `:repeat-type.dotted-plus` rather than picking
    up the `:double-plus` default. Test updated to assert this.
  * Adds deftests covering each cookie's distinctive behavior plus
    boundary cases (non-positive frequency, unknown unit, frequency > 1
    variants, `++` at month/year units, and both branches of
    `resolve-recur-frequency`).

The preexisting `get-next-time-test` passes unchanged under the
`:double-plus` default, preserving the existing regression contract.
Tests pin `t/now` via `with-redefs` for determinism.

Refs #7731, #11260, #6715, #8531. Folds in the small remaining delta
from #12532 (now closed as superseded by `44d6bd49c4`).

* fix: harden recurring task repeat type

* fix: contain repeat type selector

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix: handle clamped monthly repeats

---------

Co-authored-by: Tienson Qin <tiensonqin@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Cory Donnelly
2026-05-14 05:53:11 -04:00
committed by GitHub
parent f6e2ab9026
commit 2169a86f03
13 changed files with 588 additions and 130 deletions

View File

@@ -1434,6 +1434,7 @@
:property.built-in/repeat-checked-property "Repeating Checked Property"
:property.built-in/repeat-recur-frequency "Repeating recur frequency"
:property.built-in/repeat-recur-unit "Repeating recur unit"
:property.built-in/repeat-repeat-type "Repeating type"
:property.built-in/repeat-repeated "Node Repeats?"
:property.built-in/repeat-temporal-property "Repeating Temporal Property"
:property.built-in/scalar-default-value "Non ref type default value"
@@ -1484,6 +1485,7 @@
:property.repeat/datetime "Repeat datetime"
:property.repeat/every "Every"
:property.repeat/is-label "is:"
:property.repeat/next-date "Next date"
:property.repeat/task "Repeat task"
:property.repeat/when "When"
@@ -1494,6 +1496,10 @@
:property.repeat-recur-unit/week "Week"
:property.repeat-recur-unit/year "Year"
:property.repeat-repeat-type/dotted-plus "Advance from completion"
:property.repeat-repeat-type/double-plus "Advance from scheduled, skip to future"
:property.repeat-repeat-type/plus "Advance from scheduled"
:property.status/backlog "Backlog"
:property.status/canceled "Canceled"
:property.status/doing "Doing"

View File

@@ -1423,6 +1423,7 @@
:property.built-in/repeat-checked-property "重复的选中属性"
:property.built-in/repeat-recur-frequency "重复频率"
:property.built-in/repeat-recur-unit "重复单位"
:property.built-in/repeat-repeat-type "重复类型"
:property.built-in/repeat-repeated "节点是否重复?"
:property.built-in/repeat-temporal-property "重复的时间属性"
:property.built-in/scalar-default-value "非引用类型默认值"
@@ -1473,6 +1474,7 @@
:property.repeat/datetime "重复日期时间"
:property.repeat/every "每"
:property.repeat/is-label "是:"
:property.repeat/next-date "下次日期"
:property.repeat/task "重复任务"
:property.repeat/when "当"
@@ -1483,6 +1485,10 @@
:property.repeat-recur-unit/week "周"
:property.repeat-recur-unit/year "年"
:property.repeat-repeat-type/dotted-plus "从完成时间推进"
:property.repeat-repeat-type/double-plus "从计划时间推进,跳到未来"
:property.repeat-repeat-type/plus "从计划时间推进"
:property.status/backlog "待办列表"
:property.status/canceled "已取消"
:property.status/doing "进行中"