- notescraft-lint (rules, safe fixes, learned style) and notescraft-blocks (block model and edits); engine lint.* and block.* commands with CLI and MCP parity - Lint tab, underlines, slash menu with preview in Source and Visual, block grip and menu, Settings > Editor and lint, Vim block keys - Visual view: tables, note embeds, %% comments, wrapping property chips - Vault knowledge (Related tab, assistant sources, Settings) and calendar plugin - Sidebar top padding fixed, dot grid follows PaperCraft spacing, right-card tabs no longer cover the collapse button Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
9.5 KiB
Plan: Calendar plugin redesign
Approach
Build bottom-up in the order the spec fixed: pure logic crate, then engine commands, then CLI/MCP exposure (free, both read the registry), then egui views, then ICS/Google sync and notifications. Events are one Markdown file each (spec Q1), so the vault stays Obsidian-readable and every write reuses Session note APIs and craft-atomic. A new L3 crate notescraft-calendar holds the model, recurrence subset, quick-add parser, filter/sort and ICS parser with no IO; the engine only wires it to the vault. Alternative rejected: a WASM plugin. The Calendar is a bundled core plugin and needs vault index access that the sandbox deliberately withholds.
Delegation: phases are split into small tasks (see tasks.md, next step). Mechanical tasks (command registration boilerplate, i18n keys, serde types, unit tests for pure functions, doc updates) go to many parallel Haiku 5.5 subagents, one task and one file set each, so they never touch the same file. Judgment-heavy tasks (recurrence, quick-add parser, sync conflict policy, keychain/OAuth, keyboard model, anything security-relevant) stay with the main model. Every subagent result is checked by cargo xtask ci plus a review pass before the next phase starts.
Architecture
- Data flow: UI / CLI
run/ MCP tool / assistant ->Registrycommandcalendar.*->Sessioncalendar methods ->notescraft-calendar(pure) ->Vault::write(event files) andIndex(reads). Sync:calendar-syncfetches off the UI thread -> cache under.notescraft/viawrite_private-> same read path as local events. - New modules:
crates/calendar(L3,notescraft-calendar):model(Event, Reminder, Calendar),recur(daily/weekly/monthly/yearly + interval, weekdays, until/count, exceptions),quickadd(English phrase to Event + error naming the unparsed part),query(filter, sort, group, saved filters),ics(read-only parser, caps on size and depth),free(free-time and conflict search),review(week summary).crates/calendar-sync(L3, later phase): feed refresh, Google read-only client, offline cache, sync log.crates/engine/src/calendar/: command specs andSessionglue, grouped by concern (events, query, accounts, reminders).crates/ui-egui/src/views/calendar_view/: month, week, day, agenda, year, quick-add popover, filter popover, bulk bar, reminders inbox, keymap.
- Modified modules:
crates/engine/src/vault/daily.rs:34— keepcalendar.monthanddaily.openunchanged.crates/engine/src/ai/assistant_gate.rs:55andcrates/mcp/src/calls/gate.rs:23— add read-only calendar ids to the read lists; writes stay default-gated so assistant changes become pending reviews.crates/engine/src/prefs/config.rs:54-71— add calendar settings (events folder, default view, default reminders, quiet hours, refresh interval) next toweek_start.crates/ui-egui/src/panels/sidebar.rs:318-386— mini month stays; add a link that opens the full Calendar view.crates/ui-egui/src/views/settings_view/categories.rs:43-47— Calendar settings pages.crates/ui-egui/src/views/center.rs— route tocalendar_view.xtask/src/layers.rsTABLE — register both crates at L3.crates/i18n/locales/*.toml— keys for every new string.
Files
Paths marked new do not exist yet.
- Create:
crates/calendar/{Cargo.toml,src/lib.rs,model.rs,recur.rs,quickadd.rs,query.rs,ics.rs,free.rs,review.rs,error.rs}— new, pure logic, oneErrorenum,#![deny(...)]header like sibling crates.crates/calendar/tests/— recurrence, parser and hostile-input suites (new).crates/engine/src/calendar/{mod.rs,events.rs,query.rs,reminders.rs,accounts.rs}— new commands.crates/engine/tests/calendar_commands.rs— new; command parity and panic-hunt entries.crates/ui-egui/src/views/calendar_view/{mod.rs,month.rs,week.rs,day.rs,agenda.rs,year.rs,quickadd.rs,filters.rs,bulk.rs,inbox.rs,keymap.rs}— new, each under 400 lines.crates/calendar-sync/— new, phase 5.apps/notescraft-cli/src/— no new file expected;run <id> [json]already reaches every command (args.rs:387). Add friendlycalendarsubcommands inargs.rs/commands.rsonly ifrunproves too raw (to decide at task time).
- Modify: files listed under Modified modules above, plus root
Cargo.toml[workspace.dependencies](add the two crates),ROADMAP.md,docs/ARCHITECTURE.md,SPEC.mditem 26 (rewrite with the real command list). - Delete: nothing.
readof each Modify file is repeated at task start; line ranges above come from this session's reads and may shift.
Data
- Schema changes: none to a database. Event file: Markdown with frontmatter keys (title, start, end, all_day, tz, repeat, exceptions, calendar, reminders, tags, priority, status, link, note) in a vault calendar folder. Frontmatter edits reuse
notescraft-markdownproperty functions. Unknown keys are preserved on rewrite. No migration; absence of the folder means no events. - API shape (engine commands, JSON in/out, same schema for CLI
--json, MCP, assistant; all namescalendar.*):- read:
agenda{from,to,filter,sort,group},next,free{day,min_minutes},conflicts{from,to},year{year},review{week},parse{text}(quick-add preview, no write). - write:
add{event|text},edit{id,fields,scope},move{id,start,scope},delete{id,scope},note{id},bulk{ids,action},filter_save,filter_delete. scopeis required for repeating events:this|following|all; missing givesinvalid_paramsnaming the options.- Errors are the existing
CommandError{code,message}. - MCP naming (decided, task 1):
calendar.agendais the engine id; its tool iscalendar_agenda(SPEC 26 names). The assistant list (--via-control) keeps only aliases,searchandplugin_*(crates/mcp/src/calls/tools.rs:77), so task 9 adds alias entriescalendar_*incrates/mcp/src/calls/alias.rs. Noplugin_calendar_*rename.
- read:
Dependencies
- Phases 1-4: none. ICS parsing, the recurrence subset and the quick-add parser are written in-house (spec Q7).
notescraft-templatesDate/DateTimeis reused. - Phase 5 (each needs owner approval before use, one written reason each, nothing added yet): HTTPS client for feeds and Google; OAuth loopback flow helper or hand-written; system keychain access; desktop notifications. Candidates and alternatives are listed at that phase's start.
- No breaking upgrades. craft-libs rev stays pinned.
Risks
- Recurrence edge cases (month end, DST, zones, "following" splits) produce wrong or duplicated occurrences -> table-driven tests plus a property test that expansion never loops past a cap and every occurrence is inside the window.
- Hostile or huge input (ICS, event files, remote payloads) causes panic or memory blowup -> caps on bytes, events, depth and expansion count; add the calendar commands to
panic_hunt; no indexing with[i]. - 10,000-event target missed because agenda scans the whole vault -> in-memory event index rebuilt from
VaultEvents, not re-parsed per frame; benchmark#[ignore]test with the budget in the spec. - Vim keys clash with editor normal mode -> keymap is scoped to Calendar focus (spec Q11); keymap is data with a unit test listing every binding to catch duplicates.
- Credentials leak into logs or vault -> tokens only through a keychain trait, never serialised; a test greps sync log and settings output for a canary token.
- Google release blocked by OAuth client registration (spec Q9) -> ICS ships first; Google stays behind the client id build config.
- Weak subagent output introduces bugs (reported with the previous model) -> small single-file tasks, failing test first,
clippy -D warningsandcargo xtask ciper task, main model reviews the diff before merge; no subagent edits shared files such asCargo.toml. ui-egui/src/viewsalready holds many loose files (xtask treelimit 12) -> calendar goes in its own sub-folder.- Reminders only fire while the app runs (Q5) -> inbox shows missed ones on start; documented, not hidden.
Out of Scope (Defer)
- Two-way sync, Apple EventKit and CalDAV, Outlook, generic CalDAV (spec Q3/Q4).
- Full RFC 5545 recurrence; imported rules outside the subset show read-only.
- Background reminder agent.
- Non-English quick add.
- Light-theme and 4K variants, colour-blind-safe palette.
- Invitations, RSVP, other people's free/busy.
- Drag with the mouse can follow the keyboard move; polish deferred.
Test Strategy
- Unit: recurrence expansion tables, quick-add phrases and failures, filter/sort/group, ICS parser on valid and hostile files, free-time and conflict search, keymap uniqueness, week review.
- Integration: engine tests per command (round trip file on disk, scope handling, read-only session refusal); one test asserting engine command, CLI
run --jsonand MCP tool return identical JSON; assistant gate test (writes become pending, toggle off hides tools);panic_huntwith the 22 hostile inputs; atomic-write check; sync with a local fixture feed and offline cache; canary-token leak test. - UI:
egui_kittestrender tests per view and per keyboard flow; snapshot PNGs compared to the PaperCraft designs with--open; truncation test (ellipsis plus hover text). - Perf:
#[ignore]benchmarks for month render and agenda filter at 10,000 events. - Manual: connect a real ICS feed and Google account, fire a reminder with a real system notification, confirm token meter and assistant calendar tool call in the running app.
- Gate per task:
cargo test -p,clippy -D warnings,cargo fmt; per phase:cargo xtask ci,cargo xtask i18n,cargo xtask tree.