* refactor(store): convert singleton to Store.new() factory
Problem: store.lua used module-level _data singleton, making
project-local stores impossible and creating hidden global state.
Solution: introduce Store metatable with all operations as instance
methods. M.new(path) constructs an instance; M.resolve_path()
searches upward for .pending.json and falls back to
config.get().data_path. Singleton module API is removed.
* refactor(diff): accept store instance as parameter
Problem: diff.apply called store singleton methods directly, coupling
it to global state and preventing use with project-local stores.
Solution: change signature to apply(lines, s, hidden_ids?) where s is
a pending.Store instance. All store operations now go through s.
* refactor(buffer): add set_store/store accessors, drop singleton dep
Problem: buffer.lua imported store directly and called singleton
methods, preventing it from working with per-project store instances.
Solution: add module-level _store, M.set_store(s), and M.store()
accessors. open() and render() use _store instead of the singleton.
init.lua will call buffer.set_store(s) before buffer.open().
* refactor(complete,health,sync,plugin): update callers to store instance API
Problem: complete.lua, health.lua, sync/gcal.lua, and plugin/pending.lua
all called singleton store methods directly.
Solution: complete.lua uses buffer.store() for category lookups;
health.lua uses store.new(store.resolve_path()) and reports the
resolved path; gcal.lua calls require('pending').store() for task
access; plugin tab-completion creates ephemeral store instances via
store.new(store.resolve_path()). Add 'init' to the subcommands list.
* feat(init): thread Store instance through init, add :Pending init
Problem: init.lua called singleton store methods throughout, and there
was no way to create a project-local .pending.json file.
Solution: add module-level _store and private get_store() that
lazy-constructs via store.new(store.resolve_path()). Add public
M.store() accessor used by specs and sync backends. M.open() calls
buffer.set_store(get_store()) before buffer.open(). All store
callsites converted to get_store():method(). goto_file() and
add_here() derive the data directory from get_store().path.
Add M.init() which creates .pending.json in cwd and dispatches from
M.command() as ':Pending init'.
* test: update all specs for Store instance API
Problem: every spec used the old singleton API (store.unload(),
store.load(), store.add(), etc.) and diff.apply(lines, hidden).
Solution: lower-level specs (store, diff, views, complete, file) use
s = store.new(path); s:load() directly. Higher-level specs (archive,
edit, filter, status, sync) reset package.loaded['pending'] in
before_each and use pending.store() to access the live instance.
diff.apply calls updated to diff.apply(lines, s, hidden_ids).
* docs(pending): document :Pending init and store resolution
Add *pending-store-resolution* section explaining upward .pending.json
discovery and fallback to the global data_path. Document :Pending init
under COMMANDS. Add a cross-reference from the data_path config field.
* ci: format
* ci: remove unused variable
* refactor(sync): extract backend interface, adapt gcal module
Problem: :Pending sync hardcodes Google Calendar — M.sync() does
pcall(require, 'pending.sync.gcal') and calls gcal.sync() directly.
The config has a flat gcal field. This prevents adding new sync backends
without modifying init.lua.
Solution: Define a backend interface contract (name, auth, sync, health
fields), refactor :Pending sync to dispatch via require('pending.sync.'
.. backend_name), add sync table to config with legacy gcal migration,
rename gcal.authorize to gcal.auth, add gcal.health for checkhealth,
and add tab completion for backend names and actions.
* docs(sync): update vimdoc for backend interface
Problem: Vimdoc documents :Pending sync as a bare command that pushes
to Google Calendar, with no mention of backends or the sync table config.
Solution: Update :Pending sync section to show {backend} [{action}]
syntax with examples, add SYNC BACKENDS section documenting the interface
contract, update config example to use sync.gcal, document legacy gcal
migration, and update health check description.
* test(sync): add backend dispatch tests
Problem: No test coverage for sync dispatch logic, config migration,
or gcal module interface conformance.
Solution: Add spec/sync_spec.lua with tests for: bare sync errors,
empty backend errors, unknown backend errors, unknown action errors,
default-to-sync routing, explicit sync/auth routing, legacy gcal config
migration, explicit sync.gcal precedence, and gcal module interface
fields (name, auth, sync, health).
Problem: no way to know about overdue or due-today tasks without
opening :Pending. No ambient awareness for statusline plugins.
Solution: add counts(), statusline(), and has_due() public API
functions backed by a module-local cache that recomputes after every
store.save() and store.load(). Fire a User PendingStatusChanged event
on every recompute. Extract is_overdue() and is_today() from duplicate
locals into parse.lua as public functions. Refactor views.lua and
init.lua to use the shared date logic. Add vimdoc API section and
integration recipes for lualine, heirline, manual statusline, startup
notification, and event-driven refresh.
Problem: CI lua-typecheck-action reported three categories of errors:
1. parse.lua - multi-assignment of tonumber() results left y/m/d typed
as number? rather than integer, failing os.time()'s field types
2. gcal.lua - url_encode returned str:gsub() which yields string+integer
but the annotation declared @return string (redundant-return-value)
3. gcal.lua - calendar_id typed string? from find_or_create_calendar was
passed to functions expecting string; the existing `if err` guard did
not narrow the type for LuaLS
Solution: replace the y/m/d multi-assignment with yn/mn/dn locals whose
types resolve cleanly to integer; wrap the gsub return in parentheses to
discard the count; add `or not calendar_id` to the error guard so LuaLS
narrows calendar_id to string for the rest of the scope.
Problem: gcal.lua had ~10 LuaLS errors from untyped credential and
token tables, string|osdate casts, and untyped _gcal_event_id
field access.
Solution: add pending.GcalCredentials and pending.GcalTokens class
definitions, annotate all local functions with @param/@return, add
--[[@as string]] casts on os.date returns, and fix _gcal_event_id
access to use bracket notation with casts.
* feat(config): add category_order field
Problem: category display order was always insertion order with no way
to configure it.
Solution: add category_order to config defaults so users can declare a
preferred category ordering; unspecified categories append after.
* feat(parse): add relative date resolution
Problem: due dates required full YYYY-MM-DD input, adding friction for
common cases like "today" or "next monday".
Solution: add resolve_date() supporting today, tomorrow, +Nd, and
weekday abbreviations; extend inline token parsing to resolve relative
values before falling back to strict date validation.
* feat(views): overdue flag, category in priority view, category ordering
Problem: overdue tasks were visually indistinct from upcoming ones;
priority view had no category context; category display order was not
configurable.
Solution: compute overdue meta flag for pending tasks past their due
date; set show_category on priority view task meta; reorder categories
according to config.category_order when present.
* feat(buffer): overdue highlight, category virt text in priority view
Problem: overdue tasks had no visual distinction; priority view showed
no category context alongside due dates.
Solution: add PendingOverdue highlight group; render category name as
right-aligned virtual text in priority view, composited with the due
date when both are present.
* feat(init): undo write and buffer-local default mappings
Problem: _undo_state was captured on every save but never consumed;
toggle_priority and prompt_date had no buffer-local defaults, requiring
manual <Plug> configuration.
Solution: implement undo_write() to restore pre-save task state; add !,
d, and U as buffer-local defaults following fugitive's philosophy of
owning the buffer; expose :Pending undo as a command alias.
* test(views): add views spec
Problem: views.lua had no test coverage.
Solution: add 26 tests covering category_view and priority_view
including sort order, line format, overdue detection, show_category
meta, and category_order config behavior.
* test(archive): add archive spec
Problem: archive had no test coverage.
Solution: add 9 tests covering cutoff logic, custom day counts, pending
task preservation, deleted task cleanup, and notify output.
* docs: add vimdoc
Problem: no :help documentation existed.
Solution: add doc/pending.txt covering all features — commands,
mappings, views, configuration, Google Calendar sync, highlight groups,
data format, and health check — following standard vimdoc conventions.
* ci: format
* fix: resolve lint and type check errors
Problem: selene flagged unused variables in new spec files; LuaLS
flagged os.date/os.time return type mismatches, integer? assignments,
and stale task.Task/task.GcalConfig type references.
Solution: prefix unused spec variables with _ or drop unnecessary
assignments; add --[[@as string/integer]] casts for os.date and
os.time calls; add category_order field to pending.Config annotation;
fix task.GcalConfig -> pending.GcalConfig and task.Task[] ->
pending.Task[]; add nil guards on meta[row].id before store calls;
cast store.data() return to non-optional.
* ci: format
* fix: sync
* ci: format