Skip to content

Repeat rules stay readable by 1.0 devices where an equivalent exists - #1022

Merged
pixari merged 1 commit into
mainfrom
fix/recurrence-1-0-compatible
Nov 7, 2026
Merged

pixari merged 1 commit into
mainfrom
fix/recurrence-1-0-compatible

Conversation

@pixari

@pixari pixari commented Nov 7, 2026

Copy link
Copy Markdown
Collaborator

Follow-up to #1019 (#1016).

The compatibility problem

#1019 added every N weeks on <weekdays> and made quick add, CalDAV, the importer and the vault write it, even for "every other Friday" due on a Friday, where 1.0's every 2 weeks means exactly the same. A 1.0.0 client cannot parse the new form: in v1.0.0's completeTask (web/src/lib/ops.ts), seriesRule() returns null for an unparseable rule, the task takes completePlain, and the series ends for good. docs/stability.md promises that every 1.x client (including a web app still cached on a phone) keeps working against a newer server, so this was data loss on a 1.0 device.

The fix

  • every N weeks on <day> on a task whose due date (at write time) falls on that day is stored as every N weeks, which the due date anchors. The until / for N times end is kept. The two give the same dates, including catch-up.
  • Several weekdays with N > 1, a due date on another weekday, or no due date keep the new form, which is the only correct one.
  • One function per language: recurrence.Compatible (Go) and compatibleRule (TS), pinned by the new compatible vectors in recurrence.vectors.json. Every writer goes through it: quick add (both parsers), the repeat field (setRecurrence) and planTask, REST (canonicalizeRecurrence, using the body's due or the task's), the agent tools (create and update), CalDAV (FromRRULE, which also covers the importer's and the vault notes' RRULEs), the importer planner and own-export import, the vault's 🔁 lines, and a series split (Go and web).
  • Compatibility test: internal/recurrence/recurrence10 and web/src/lib/testdata/recurrence-1.0.ts are frozen copies of the v1.0.0 grammar. Check / check10 fail a stored rule that 1.0 can't read when a 1.0 rule gives the same dates over 60 occurrences. Every writer's test runs its output through it, and so does every quick-add vector, in both languages. TestNotLinked keeps the Go copy out of both binaries.
  • The three inputs from Quick add: "today … every other Saturday" loses the weekday; "and repeat" and "starting" stay in the title #1016 are now vectors with today = Wednesday 2026-11-04 and today = Saturday 2026-11-07.
  • What remains (a rule only 1.1 can express shows as text on a 1.0 device, and completing it there ends the series) is documented in docs/stability.md under what may change within 1.x, with the advice to update devices. I left resume-detection out: the op doesn't record which client version completed the task, and "completed with a rule 1.0 can't read" would also catch series someone ended on purpose.

What each writer stores now

Input Due Stored
"every other Saturday" (quick add, import, repeat field, REST, agent, vault) Saturday every 2 weeks
"take the bins out today and repeat every other Saturday" typed on a Wednesday Wednesday (today) every 2 weeks on saturday
same line typed on a Saturday Saturday every 2 weeks
CalDAV FREQ=WEEKLY;INTERVAL=2;BYDAY=SA, Saturday DTSTART/DUE Saturday every 2 weeks
CalDAV same RRULE, Wednesday DUE Wednesday every 2 weeks on saturday

OpenSpec change recurrence-1-0-compatible (archived): it adds "Rules a 1.0 client reads" to recurring-tasks and updates the "every other" scenarios in natural-language-capture. Docs: stability.md, recurrence.md (new "Rules older devices read"), quick-add.md, repeat-rules.md, caldav.md, import.md, plus AGENTS.md and architecture.md for the new package. The changelog fragment from #1019 now describes the final behaviour.

Checks

go test ./..., go vet ./..., staticcheck on the touched packages; npx vitest run src/lib/nlp src/lib/recurrence src/lib/recurring src/lib/recurrenceCompatible src/lib/seriesSplit src/lib/habits src/app/task-detail (1089 tests), npm run typecheck, make web-lint, npm run build, make bundle-budget (687.0 of 687 KB), make docs-check, make openspec-validate, make repo-check, make i18n-check.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Bkw4TZLFDGeMdZR3vWZF13

`every N weeks on <day>` on a task due on that day is `every N weeks`
anchored on the due date. 1.0 cannot parse the new form and ends the
series when it completes the task, so every writer now stores the old
form where it is equivalent: quick add (web and Go), the repeat field,
REST, the agent tools, CalDAV (FromRRULE), the importer, the vault and a
series split, through recurrence.Compatible and compatibleRule, pinned by
the "compatible" vectors. A frozen copy of the 1.0.0 grammar checks every
writer's output and every quick-add vector. docs/stability.md says what
remains for rules only 1.1 can express. OpenSpec change
recurrence-1-0-compatible (archived).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bkw4TZLFDGeMdZR3vWZF13
@pixari
pixari merged commit 87e485e into main Nov 7, 2026
34 checks passed
@pixari
pixari deleted the fix/recurrence-1-0-compatible branch November 7, 2026 13:11
@pixari pixari mentioned this pull request Nov 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant