Add notebook exclusions for v2.1.0

This commit is contained in:
Victor Wiebe 2026-08-08 14:04:55 -04:00
parent 83ed034cee
commit 1f0679568b
41 changed files with 457 additions and 4155 deletions

4
.gitignore vendored
View File

@ -3,3 +3,7 @@ dist/
node_modules/
publish/
build-archive/
scripts/
SLICE*.md
TASKS.md
SPEC.md

View File

@ -2,6 +2,22 @@
All notable changes to GTD Calendar are documented here. Versions follow the plugin's `manifest.json` / `package.json` version, which also matches the published npm releases.
## 2.1.0 — Unreleased
### Notebook-tree exclusions
- Added `exclude-notebook:` to `gtd-calendar`, `gtd-kanban`, and `gtd-matrix`.
It accepts one notebook reference or a YAML list and removes every resolved
notebook plus all descendants from the otherwise normal scope.
- Exclusions support the same folder ID, unique bare-title, and full-path forms
as `notebook:`. Unknown or ambiguous entries warn without affecting other
valid exclusions.
- Exclusions apply after every scope form, including `scope: all`, and happen
before note bodies or tags are fetched. Statistics and global-scope warnings
reflect the post-exclusion scan.
- Notebook-grouped boards and matrices omit excluded trees. Editable kanban and
Eisenhower moves revalidate the exclusion boundary against fresh state.
## 2.0.0 — Unreleased
### Opt-in editable Kanban

View File

@ -1,7 +1,7 @@
# Drag-and-drop v2.0.0 — next-session handoff
Use this document as the opening prompt and state record for the next development
session. The planning source of truth is `SLICE14.md`, followed by `SLIDE15.md`.
session. The planning source of truth is `SLICE14.md`, followed by `SLICE15.md`.
## Current repository state
@ -31,7 +31,7 @@ session. The planning source of truth is `SLICE14.md`, followed by `SLIDE15.md`.
- Kanban moves persist note workflow tags or native to-do completion according
to `SLICE14.md`.
- Eisenhower moves persist urgent/important tag combinations according to
`SLIDE15.md`.
`SLICE15.md`.
- Completion wins over in-progress in normal collection. An intentional kanban
move canonicalizes conflicting workflow tags.
- Eisenhower keeps urgent/important axes but adopts Skeleton quadrant names:
@ -43,7 +43,7 @@ session. The planning source of truth is `SLICE14.md`, followed by `SLIDE15.md`.
1. Read every repository `*.md` file before implementation, with special
attention to `SPEC.md`, `TASKS.md`, `SLICE11.md`, `SLICE12.md`, `SLICE13.md`,
`SLICE14.md`, and `SLIDE15.md`.
`SLICE14.md`, and `SLICE15.md`.
2. Inspect `git status --short`, the latest commit, all package/manifest versions,
and the current test scripts. Preserve unrelated user changes.
3. Execute `SLICE14.md` phase by phase. Update its checkboxes and Status after
@ -53,7 +53,7 @@ session. The planning source of truth is `SLICE14.md`, followed by `SLIDE15.md`.
5. Stop for the user's explicit SLICE 14 manual Joplin sign-off before marking
its manual phase complete or beginning SLICE 15, unless the user explicitly
asks to continue without that checkpoint.
6. Execute `SLIDE15.md` only after the SLICE 14 foundation is accepted. Update it
6. Execute `SLICE15.md` only after the SLICE 14 foundation is accepted. Update it
with the same state discipline.
7. After both slices pass automation and explicit manual acceptance, complete
consolidated v2.0.0 documentation/version/release preparation. Do not publish
@ -67,7 +67,7 @@ Copy the following request into the next session:
> the handoff and begin `SLICE14.md` Phase 1. Work phase by phase, update the
> slice file after every completed phase and whenever you pause, and keep
> implementation, automated validation, packaging, and manual Joplin acceptance
> separate. Do not begin `SLIDE15.md` until SLICE 14 has passed its required
> separate. Do not begin `SLICE15.md` until SLICE 14 has passed its required
> acceptance boundary.
## Baseline files to inspect before editing

View File

@ -48,6 +48,7 @@ Goes in the note that should display the calendar. All options are optional.
| `title` | text | — | Heading above the calendar. |
| `scope` | `this-folder`, `children`, a number, or `all` | `this-folder` | Which folders to scan: just this one, all descendants, or *n* levels down (`scope: 2`). Broad scopes scan every note body in the tree — fine for normal folders, worth knowing for huge ones. **`scope: all` scans every notebook in your entire profile** — powerful for a global dashboard, but potentially slow on large profiles; a footnote reports how much was scanned, and a ⚠ appears past ~2000 notes. `notebook:` is ignored under `scope: all`. |
| `notebook` | notebook name, `Parent/Child` path, or folder id | this note's folder | Root the scan at a specific notebook instead of the one holding this note — even one in a different tree. `scope` then applies relative to it. A bare name must be unique; if several notebooks share it, qualify with a path (`Work/Archive`). Unknown/ambiguous → ⚠ warning + falls back to this note's folder. |
| `exclude-notebook` | one notebook or a YAML list | — | Remove each named notebook and its complete descendant tree from the resolved scope. Uses the same unique name, full path, and folder-id forms as `notebook`. Unknown or ambiguous entries warn and exclude nothing. This still applies under `scope: all`. |
| `notes` | `gtd-only`, `all`, `none` | `gtd-only` | Which plain notes appear: only those with a `gtd` block, every note in scope, or none. |
| `todos` | `gtd-only`, `all`, `none` | `gtd-only` | Same, for to-dos. `todos: all` is handy — every to-do with a due date lands on the calendar with zero markup. |
| `sort` | `asc`, `desc` | `asc` | Order of events *within* each day/week/month and in Unscheduled. The timeline itself is always chronological. |
@ -55,6 +56,20 @@ Goes in the note that should display the calendar. All options are optional.
Invalid values fall back to defaults and show a ⚠ warning above the calendar.
For a profile-wide dashboard that omits private or expensive notebook trees,
use a scalar for one exclusion or a YAML list for several:
```yaml
scope: all
exclude-notebook:
- IBM
- Archive/Old Projects
```
Exclusions are resolved independently and removed before notes are fetched.
Excluding a notebook also excludes every notebook beneath it. With a narrower
scope, an exclusion outside that scope is simply a no-op.
## The `gtd` block
Goes in any note or to-do that should participate in calendar, kanban, or matrix views. **An empty block is valid**—it is the minimal opt-in. All properties are optional:
@ -132,6 +147,7 @@ not filtered by `done-window` because they have no completion timestamp. The
| `title` | text | — | Heading above the board. |
| `scope` | `this-folder`, `children`, integer, `all` | `this-folder` | Same folder-scanning rules as the calendar, including `all` (every notebook — see the calendar table's caution). |
| `notebook` | notebook name, `Parent/Child` path, or folder id | this note's folder | Root the scan at a specific notebook (see the calendar table for the full resolution rules). `scope` applies relative to it. |
| `exclude-notebook` | one notebook or a YAML list | — | Remove each resolved notebook and all descendants after applying `scope`; see the calendar table for resolution and warning rules. |
| `group` | `notebook` | — | With the literal `scope: children`, render a separate board for the root notebook and every non-empty descendant instead of one aggregated board. Other scope forms warn and remain aggregated. |
| `notes` | `all`, `none` | `all` | Which project notes appear. `all` means every ordinary note in scope that explicitly opts in with a `gtd` block; it never includes plain notes without that block. |
| `todos` | `gtd-only`, `all`, `none` | `gtd-only` | Which to-dos appear. `gtd-only` requires a `gtd` block; `all` includes every to-do in scope; `none` excludes all to-dos. This option does not affect opted-in ordinary notes. |
@ -290,6 +306,7 @@ On Deck (urgent), and Backlog (neither).
| `title` | text | — | Heading above the matrix. |
| `scope` | `this-folder`, `children`, integer, `all` | `this-folder` | Same folder-scanning rules as the calendar and kanban, including `all` (every notebook — see the calendar table's caution). |
| `notebook` | notebook name, `Parent/Child` path, or folder id | this note's folder | Root the scan at a specific notebook (see the calendar table for the full resolution rules). `scope` applies relative to it. |
| `exclude-notebook` | one notebook or a YAML list | — | Remove each resolved notebook and all descendants after applying `scope`; see the calendar table for resolution and warning rules. |
| `group` | `notebook` | — | With the literal `scope: children`, render a separate complete matrix for the root notebook and every non-empty descendant. Other scope forms warn and remain aggregated. |
| `notes` | `all`, `none` | `all` | Which project notes appear. `all` includes only ordinary notes explicitly opted in with a `gtd` block; `none` produces a task-only matrix. |
| `todos` | `gtd-only`, `all`, `none` | `gtd-only` | Which to-dos appear. This option does not affect ordinary notes, which appear only when they contain a `gtd` block. |

View File

@ -1,76 +0,0 @@
# SLICE 1 — Groundwork: process rules + body-fetch optimization
> **State-saving rule:** update this file immediately after EVERY completed task (tick the checkbox, update Status and Resume notes) and whenever pausing for input — automatically, without being asked. State on disk must always match reality.
## Status
**COMPLETE** (2026-07-15). All six tasks done; 97/97 tests pass. Ready to
proceed to SLICE2.
Decision recorded 2026-07-15: body-fetch optimization (Task 4) is scoped
**plumbing-only, behaviour-preserving** — add the fields-hint adapter param and
drop `body` only where `extractGtdBlock` is provably never called (both-`none`
paths). No change to card styling/title for `todos: all`. This lands the adapter
plumbing SLICE3's `scope: all` needs without altering render output.
Pending user input (non-blocking, defaults recorded in SLICE4):
1. gtd-gantt fence disambiguation: `project:` key present = item block,
absent = chart block (chart filters via `filter-project:`).
2. Dateless gantt items: excluded with a warning (no Unscheduled strip).
## Goal
Set up the working rules for the whole effort and land the performance
optimization that `scope: all` (SLICE3) depends on.
## Tasks
- [x] Verify `.git/info/exclude` covers local guidance, planning files, and`n other workspace-only metadata. Confirmed 2026-07-15; no change needed.
- [x] Establish the .jpl archive rule: `scripts/archive-jpl.sh` moves existing
`publish/*.jpl` to `build-archive/<name>-<version>-<yyyymmdd-HHMMSS>.jpl`.
Wired as the `predist` npm script so it runs automatically before every
`npm run dist`. Validated 2026-07-15 (archives + no-op-on-empty both work).
CORRECTION (2026-07-15, during first real `npm run dist`): the archive was
originally written to `publish/archive/`, but webpack's `buildMain` step
runs `fs.removeSync(publishDir)` (wipes it) and `package.json`
`"files": ["publish"]` would ship it to npm. Moved to repo-root
`build-archive/` (git-ignored, outside publish/). local guidance + .gitignore
updated. Re-validated with a full `npm run dist`: archive survives, fresh
jpl written to publish/.
- [x] Fix `jest.config.js` ESM/CJS mismatch (`export default``module.exports`).
Done 2026-07-15: `npx jest` runs, all 88 tests pass (8 suites).
- [x] Body-fetch optimization (plumbing-only, behaviour-preserving). Done 2026-07-15:
- `DataAdapter.getNotesInFolder` gains an `includeBody: boolean` fetch hint;
`RawNote.body` is now optional. Real adapter (`index.ts`) requests
`NOTE_FIELDS_WITH_BODY` vs `NOTE_FIELDS` accordingly.
- Each collector computes `needsBody` before the fetch:
events = `notes !== "none" || todos !== "none"`;
kanban/matrix = `todos !== "none"`.
These are exactly the paths where `extractGtdBlock` is reached, so render
output is identical — body is only dropped for degenerate empty views.
- `extractGtdBlock` param widened to `string | undefined` (already guarded
by `body || ""`).
Investigation confirmed the slice's stated "todos: all" win is NOT
behaviour-preserving (block is read for card styling even in `all` mode),
so it was deliberately excluded per the recorded decision. The value here is
the adapter plumbing that SLICE3's `scope: all` needs.
- [x] Unit tests for the fields-hint logic. Done 2026-07-15:
`src/tests/Gtd/fieldsHint.test.ts` — a recording adapter asserts each
collector passes the correct `includeBody` flag per mode (9 cases).
- [x] Run full test suite; record results here. 2026-07-15: **97 passed, 97 total**
(9 suites) — 88 pre-existing + 9 new fields-hint cases.
## Decisions / notes
- Node on this machine: 18.19.1 (WSL Ubuntu). Jest 30.
- Run all commands via `wsl -d Ubuntu -- bash -c "cd ~/projects/joplin-plugin-gtd-calendar && ..."`.
## Resume notes
SLICE1 is complete. Files touched:
- `jest.config.js` (`export default``module.exports`)
- `scripts/archive-jpl.sh` (new) + `predist` npm script in `package.json`
- `src/Gtd/types.ts` (`RawNote.body` optional; `getNotesInFolder` gains `includeBody`)
- `src/index.ts` (`NOTE_FIELDS` / `NOTE_FIELDS_WITH_BODY`, conditional fetch)
- `src/Gtd/collectEvents.ts`, `collectKanban.ts`, `collectMatrix.ts` (`needsBody`)
- `src/Gtd/gtdBlock.ts` (param `string | undefined`)
- `src/tests/Gtd/fieldsHint.test.ts` (new)
Nothing left open. `npx tsc --noEmit` shows only pre-existing `api/` typing
errors (generated Joplin typings referencing absent modules); no new errors in
`src/`. Next: SLICE2.

View File

@ -1,478 +0,0 @@
# SLICE 10 — Separate child-notebook kanban and matrix views
> **State-saving rule:** update this file and `TASKS.md` after every completed
> task and whenever work pauses. Automated checks and manual Joplin acceptance
> must be recorded separately.
## Status
**COMPLETE.** Phases 18 are implemented and validated. Automated checks and
manual Joplin acceptance passed; README, SPEC, and v1.0.0 CHANGELOG coverage are
complete.
## Goal
When a kanban or matrix block uses `scope: children` with `group: notebook`,
render a separate complete view for the root notebook and each descendant notebook that owns eligible
cards. This keeps large notebook trees browsable without nesting subsections
inside one board or matrix.
## Confirmed behavior
- Multi-view rendering requires both the literal `scope: children` keyword and
`group: notebook`.
- `scope: children` without `group: notebook` retains the current aggregated
single view.
- `this-folder`, numeric depths (including very large values), and `scope: all`
keep their current single aggregated view.
- The root notebook gets its own view for cards it owns directly.
- Every descendant notebook gets its own view for cards it owns directly.
- Cards are not rolled up into ancestor views; each card appears exactly once.
- Empty notebook views are omitted.
- If every notebook is empty, show the existing overall empty board/matrix once.
- Each view heading uses a full notebook path so duplicate titles are clear.
- Views use deterministic depth-first tree order: root first, then descendants;
siblings sort case-insensitively by title with folder ID as a stable tie-breaker.
- Each generated view contains the normal three kanban columns or four matrix
quadrants, rather than notebook subsections inside those buckets.
- Each bucket receives independent SLICE8 visible-count state and the configured
`page-size`.
- SLICE9 normal-note cards group by `parent_id` exactly like to-do cards.
- Overall scan statistics count folders and notes once. Card totals equal the sum
of all non-empty notebook groups.
## Example
```text
Projects <- root view (direct cards only)
├── Home <- separate Home view
│ ├── Repairs <- separate Repairs view
│ └── Garden <- separate Garden view
└── Work <- separate Work view
└── Release <- separate Release view
```
If `Home` has no eligible cards, its view is omitted; `Repairs` and `Garden`
still appear if they have cards.
## Architecture and data flow
```text
parse scope
children + group: notebook -> scopeDepth: Infinity + groupByNotebook: true
all other combinations -> existing depth + groupByNotebook: false
|
v
resolve root notebook + ordered scoped folder metadata
|
v
collector scans each folder once
single mode -> existing aggregate board/matrix
grouped mode -> one exact-owner board/matrix per folder
|
v
plugin payload: discriminated single or notebook-grouped result
|
v
webview
single -> current component
grouped -> repeat complete component per non-empty notebook group
(each component owns independent SLICE8 state)
```
## Data-contract decision
Use a discriminated view layout instead of returning both aggregate and grouped
copies of every card:
```ts
type KanbanLayout =
| { kind: "single"; board: KanbanBoard }
| { kind: "notebooks"; groups: NotebookKanbanGroup[] };
type MatrixLayout =
| { kind: "single"; board: MatrixBoard }
| { kind: "notebooks"; groups: NotebookMatrixGroup[] };
```
Each group carries `folderId`, `notebookPath`, its board/matrix, `cardCount`, and
item-level warnings for that notebook. Config warnings, notebook-resolution
warnings, and profile-wide soft-cap warnings remain at the overall block level.
The empty grouped case carries `groups: []` and renders one existing empty state.
## Implementation plan
### Phase 1 — Preserve the explicit grouping intent
- [x] Add `groupByNotebook: boolean` to `KanbanConfig` and `MatrixConfig`
in `src/Gtd/types.ts`.
- [x] Recognize `group: notebook` in both parsers.
- [x] Set `scopeDepth = Infinity` for literal `scope: children`, preserving
its existing recursion behavior.
- [x] Set `groupByNotebook = true` only when literal `scope: children` and
`group: notebook` appear together.
- [x] Default `groupByNotebook` to false, including `scope: children` without
the grouping option.
- [x] Keep `this-folder`, numeric depth, invalid-value fallback, and
`scope: all` parsing unchanged.
- [x] Warn and remain non-grouped when `group: notebook` is combined with
another scope, or when `group:` has an unsupported value.
- [x] Add parser tests covering the grouped combination and every non-grouped
scope form.
A boolean is deliberately preferred over a broad scope-enum refactor: existing
collectors still need `scopeDepth` and `scopeAll`, while this slice needs only
one additional rendering decision.
### Phase 1 completion record
- Kanban and matrix configs now preserve explicit notebook-grouping intent
independently of recursive scope depth.
- Existing `scope: children` dashboards remain aggregated unless they opt in
with `group: notebook`.
- Focused validation: 4 suites and 104 tests passed.
- Full validation: 14 suites and 205 tests passed.
- TypeScript, whitespace, and prohibited-reference checks passed.
- No manual Joplin test is required for this parser-only phase.
### Phase 2 — Ordered notebook metadata helper
- [x] Extend `folderScope.ts` or add a focused `folderTree.ts` helper that returns
scoped folder metadata rather than only IDs when grouping is requested.
- [x] Include `id`, `parentId`, display title, and full path for the resolved root
and every descendant.
- [x] Build paths from the actual notebook tree, including ancestors above a
`notebook:`-selected root so headings remain unambiguous.
- [x] Order root first, then perform depth-first traversal with siblings sorted
by case-insensitive title and folder ID tie-breaker.
- [x] Handle missing/empty titles with a stable documented fallback such as
`Untitled notebook` while retaining the folder ID tie-breaker.
- [x] Defend against malformed folder data (orphan parent references and cycles)
without hanging; keep every reachable scoped folder at most once.
- [x] Leave `resolveScopedFolderIds` behavior intact for calendars, Gantt, and
non-grouped views.
### Phase 2 completion record
- Added `folderTree.ts` with typed scoped-folder metadata: folder ID, parent ID,
display title, and full notebook path.
- Ordering is deterministic root-first depth-first; siblings sort by
case-insensitive display title and then folder ID.
- Notebook-selected roots retain real ancestor paths above the selected root.
- Blank titles use `Untitled notebook`.
- Missing roots return no metadata; orphan roots remain usable; visited guards
prevent cycles and duplicate traversal.
- The legacy `resolveScopedFolderIds` implementation and output remain unchanged.
- Focused validation: 4 suites and 52 tests passed.
- Full validation: 15 suites and 214 tests passed.
- TypeScript, whitespace, and prohibited-reference checks passed.
- No manual Joplin test is required for this pure metadata phase.
### Phase 3 — Grouped collector contracts
- [x] Add shared notebook-view metadata types plus `KanbanLayout` and
`MatrixLayout` discriminated unions in `src/Gtd/types.ts` or collector-local
result types where appropriate.
- [x] Refactor kanban collection so each eligible card is appended to the board
associated with the current scanned `folderId` when grouping is enabled.
- [x] Apply done-window filtering and sort each kanban group's columns using the
existing logic.
- [x] Refactor matrix collection equivalently and sort each group's quadrants.
- [x] Attach malformed-block/item warnings to the owning notebook group.
- [x] Omit groups whose final `cardCount` is zero, including groups emptied by
done-window or eligibility filtering.
- [x] Retain the current aggregate path and result behavior when grouping is off.
- [x] Compute overall `scannedFolders`, `scannedNotes`, and `cardCount` once,
without summing repeated scans or duplicating cards.
- [x] Ensure the host view note is excluded before group counts are finalized.
### Phase 3 completion record
- Added shared notebook-view metadata plus discriminated single/notebook layouts
for kanban and matrix results. The existing top-level board remains a temporary
empty alias in grouped mode so later payload consumers could migrate cleanly
without duplicating cards. The alias was removed after the Phase 5 webview
migration.
- Grouped collectors scan every scoped folder once, bucket cards by the folder
currently being scanned, apply existing filtering and sorting per group, and
preserve the aggregate collector path when grouping is disabled.
- Empty groups are omitted after filtering. Item warnings remain with their
owning non-empty group; overall folder, note, and card totals count unique
scans/cards once.
- Added exact-owner coverage for root/child/grandchild cards, mixed notes and
to-dos, host-note exclusion, empty groups, done-window removal, group-local
warnings, ordering, sorting, both matrix modes, and single-layout regressions.
- Focused validation: 4 suites and 110 tests passed.
- Full validation: 15 suites and 219 tests passed.
- Production TypeScript/webpack build and `git diff --check` passed; the package
was created at `publish/com.victorwiebe.joplin.plugin.gtd-calendar.jpl`.
- No manual Joplin test is required for this collector-contract phase; grouped
payload and visual acceptance remain in Phases 48.
### Phase 4 — Plugin payloads and warning ownership
- [x] Update `handleGetKanban` and `handleGetMatrix` in `src/index.ts` to return
the discriminated layout plus SLICE8 `pageSize`.
- [x] Keep config warnings, notebook-resolution warnings, config parse errors,
and `scope: all` soft-cap warnings at the overall block level.
- [x] Send item-level warnings inside their notebook group in grouped mode.
- [x] Preserve the current top-level warning array in single mode.
- [x] Return a structurally valid empty layout when the source note cannot be
resolved.
- [x] Keep overall statistics visible once per source block; optionally show a
compact per-notebook card count in each generated heading, but do not add
separate scan totals that could imply folders were scanned repeatedly.
### Phase 4 completion record
- Kanban and matrix handlers now send the collector's discriminated `layout`
together with normalized `pageSize`, one overall statistics object, and the
existing global configuration fields.
- Missing-source responses use the requested single/notebook discriminator and
return a structurally valid empty layout, including matrix labels and scope
metadata.
- Configuration, YAML parse, notebook-resolution, and soft-cap warnings remain
overall. Single-mode item warnings retain the top-level warning path; grouped
item warnings remain inside their exact-owner notebook group.
- A temporary legacy `board` alias kept the single-view webview working during
this phase. It was removed in Phase 5 after the webview migrated to `layout`;
grouped mode never duplicated cards into an aggregate payload.
- Added pure payload-boundary helpers and tests for all missing-source layouts
and the no-aggregate compatibility behavior.
- Focused validation: 3 suites and 83 tests passed.
- Full validation: 16 suites and 221 tests passed.
- TypeScript with `--skipLibCheck`, `git diff --check`, and the production
webpack/JPL build passed. The current artifact is
`publish/com.victorwiebe.joplin.plugin.gtd-calendar.jpl`.
- No manual Joplin test is required for this payload-only phase; rendering and
manual acceptance remain in Phases 58.
### Phase 5 — Webview component reuse
- [x] Separate the existing kanban wrapper concerns (overall title, errors,
global warnings, stats) from rendering one kanban board component.
- [x] Do the same for one matrix component.
- [x] In single mode, render exactly one component with current markup and visual
behavior.
- [x] In grouped mode, iterate groups in payload order and render one full
component per group beneath a notebook-path heading.
- [x] Render each group's item warnings adjacent to that group.
- [x] Reuse the SLICE8 incremental-list helper inside every component; closure
state must not be shared between groups or buckets.
- [x] When `groups` is empty, render the existing empty board/matrix once rather
than showing a blank block.
- [x] Add only the spacing/heading CSS required to distinguish notebook views;
preserve column and matrix layout styles.
### Phase 5 completion record
- Split kanban and matrix rendering into overall wrappers plus reusable
one-board/one-matrix component functions. Single layouts retain the existing
component markup and visual behavior.
- Notebook layouts render every non-empty group in collector order beneath its
full notebook path, with item warnings adjacent to the owning view.
- Every generated component calls the existing SLICE8 incremental-list helper
separately for each column/quadrant, giving every bucket independent closure
state and the configured `pageSize`.
- Empty notebook layouts render one ordinary empty board or matrix, preserving
the established overall empty presentation.
- Removed the temporary top-level `board` payload alias. The webview consumes
`layout` directly, retaining only a defensive fallback for stale payloads.
- Added only shared notebook-view spacing and heading styles; existing board,
matrix, card, and batching styles remain unchanged.
- Full validation: JavaScript syntax, TypeScript with `--skipLibCheck`,
`git diff --check`, 16 suites/220 tests, and the production webpack/JPL build
all passed. The current artifact is
`publish/com.victorwiebe.joplin.plugin.gtd-calendar.jpl`.
- Browser-only interaction testing remains deliberately manual because the
repository has no DOM harness; Phase 6 records that boundary explicitly.
### Phase 6 — Automated tests
#### Scope/parser tests
- [x] `children` sets infinite depth plus grouping.
- [x] Numeric depth remains non-grouped, including a numeric string.
- [x] `this-folder`, invalid scope, and omitted scope remain non-grouped.
- [x] `scope: all` remains non-grouped and retains notebook-conflict warnings.
#### Folder-tree tests
- [x] Root plus multiple children and grandchildren.
- [x] Deterministic depth-first order regardless of input folder order.
- [x] Case-insensitive sibling ordering and folder-ID tie-breaker.
- [x] Duplicate titles represented by unambiguous full paths.
- [x] A `notebook:`-selected child root with its ancestor path retained.
- [x] Empty titles, orphan parents, and cycle protection.
#### Kanban collector tests
- [x] Root-owned and descendant-owned cards appear in separate exact-owner groups.
- [x] Every card appears once with no ancestor rollup.
- [x] Empty groups are omitted while non-empty grandchildren remain.
- [x] Done-window filtering can remove an otherwise non-empty group.
- [x] Per-group sorting and warnings are correct.
- [x] SLICE9 note and to-do cards group identically.
- [x] Aggregate stats and card totals are not double-counted.
- [x] Non-grouped scope results remain unchanged.
#### Matrix collector tests
- [x] Repeat exact-owner, omission, sorting, warning, mixed-card, and total-count
coverage for Skeleton and Eisenhower modes.
- [x] Confirm all four quadrants remain local to their owning notebook group.
- [x] Confirm non-grouped scope results remain unchanged.
#### Payload/rendering contracts
- [x] Test single and grouped payload construction at the thinnest practical
boundary without adding a new browser-test dependency solely for this slice.
- [x] If SLICE8 introduced reusable DOM tests, extend them to prove independent
visible counts across notebook components; otherwise cover this manually.
- [x] Run focused parser, folder-tree, kanban, and matrix tests.
- [x] Run the complete Jest suite and record suite/test totals.
- [x] Run `npm run dist` and record the produced `.jpl` path.
- [x] Run `git diff --check` and the prohibited-reference audit.
### Phase 6 completion record
- Parser coverage now explicitly locks literal `children` grouping, numeric and
default non-grouped scopes, invalid fallbacks, and `scope: all` notebook
conflict warnings for both kanban and matrix.
- Existing folder-tree coverage proves root/descendant traversal, deterministic
ordering, duplicate and blank titles, selected-root ancestor paths, orphans,
cycles, and unchanged legacy ID-only scope behavior.
- Collector coverage now includes exact ownership, no rollups, empty-group and
done-window omission, local sorting/warnings, mixed cards, unique totals, all
four local quadrants in both matrix modes, and single-layout regressions for
`this-folder`, numeric depth, recursive children, and `scope: all`.
- Pure layout helpers cover valid empty single/grouped payload construction.
The repository still has no DOM harness, so independent click-state remains
an explicit Phase 8 manual acceptance check rather than a test-only browser
abstraction.
- Focused validation: 5 suites and 121 tests passed.
- Full validation: 16 suites and 230 tests passed.
- JavaScript syntax, TypeScript with `--skipLibCheck`, `git diff --check`, and
the prohibited-reference audit passed.
- `npm run dist` produced
`publish/com.victorwiebe.joplin.plugin.gtd-calendar.jpl` (163,328 bytes,
SHA-256 `cc5eddd2dcd74453e5ebd2c2fd4635cb34eb5996bf736efd54441931f2a54930`).
The five-file tar archive was inspected and contains the grouped notebook-view
runtime markers.
- No manual Joplin acceptance was performed; that remains Phase 8.
### Phase 7 — Documentation
- [x] Update README.md to state that `scope: children` plus `group: notebook`
produces separate notebook views for kanban and matrix blocks.
- [x] Add a parent/child/grandchild example showing root ownership, omitted empty
notebooks, full-path headings, and no rollups.
- [x] State that ungrouped `children`, numeric depth, and `scope: all` remain
aggregated.
- [x] Explain that every generated bucket has independent SLICE8 expansion state.
- [x] Explain that SLICE9 notes group by their owning notebook.
- [x] Update SPEC.md scope parsing, folder traversal, layout payloads, warning
ownership, statistics, and rendering flow.
- [x] Add the SLICE10 feature to the v1.0.0 CHANGELOG entry.
### Phase 7 README progress record
- README option tables now document `group: notebook` for kanban and matrix and
constrain it to the literal `scope: children` combination.
- Added a parent/child/grandchild example covering exact ownership, no ancestor
rollups, omitted empty notebooks, non-empty descendants, full-path headings,
deterministic order, warning/statistics placement, and independent batching.
- Documented that ordinary note cards group exactly like to-dos and that all
other scope forms remain aggregated, warning when grouping is requested.
- Updated the development test count to 230 and release-version instructions to
include the lockfile.
- `SPEC.md` and the v1.0.0 `CHANGELOG.md` now record the grouped layout contract.
### Phase 8 — Manual Joplin acceptance
Create a root notebook containing direct cards, multiple children, grandchildren,
duplicate child titles under different parents, empty notebooks, and more than
one SLICE8 batch in at least two generated views. Include SLICE9 notes and to-dos.
- [x] Confirm root-owned cards render only in the root view.
- [x] Confirm every descendant card renders only in its owning notebook view.
- [x] Confirm empty notebooks are omitted but non-empty descendants still appear.
- [x] Confirm full paths distinguish duplicate titles.
- [x] Confirm view ordering is stable and matches the documented traversal.
- [x] Confirm both matrix modes create separate complete matrices.
- [x] Confirm per-group warnings appear with the correct notebook.
- [x] Confirm overall statistics equal the unique scanned/card totals.
- [x] Confirm independent `page-size` limits and "List more" state across groups,
buckets, and multiple source blocks.
- [x] Confirm reload resets all expanded buckets.
- [x] Confirm `this-folder`, numeric depths, and `scope: all` still render one
aggregated view.
- [x] Confirm notebook targeting plus `scope: children` roots grouping at the
resolved target and uses correct full paths.
- [x] Confirm sorting, styling, navigation, completion, recurrence, and note-card
behavior do not regress.
- [x] Record user sign-off here; do not mark manual acceptance complete before
confirmation.
### Phase 8 acceptance record
- **PASSED — USER SIGN-OFF RECEIVED 2026-07-28.** The user confirmed that all
Phase 8 manual tests passed against the current production JPL.
- Accepted workflows include exact root/descendant ownership, no rollups or
duplication, empty-group handling, full-path headings, deterministic order,
both matrix modes, warning/statistics ownership, independent batching across
groups and blocks, reload reset, notebook targeting, and all non-grouped scope
regressions.
- Sorting, styling, navigation, completion, recurrence, mixed note/to-do cards,
hover/focus controls, and configured page sizes were also manually accepted.
- This manual acceptance is recorded separately from Phase 6 automated
validation. Phase 7 documentation remains pending.
## Acceptance criteria
SLICE10 is complete only when:
- Literal `scope: children` renders separate root/descendant views with every
eligible card present exactly once in its owning notebook.
- Empty groups are omitted, headings are unambiguous, and ordering is stable.
- SLICE8 state is independent per bucket and SLICE9 cards group correctly.
- Warnings and statistics are correctly owned and never double-counted.
- Every other scope form retains its prior single-view behavior.
- Focused tests, the full suite, and the production package build pass.
- Manual Joplin acceptance is explicitly confirmed.
## Files expected to change
- `src/Gtd/types.ts`
- `src/Gtd/parseKanbanConfig.ts`
- `src/Gtd/parseMatrixConfig.ts`
- `src/Gtd/folderScope.ts` and/or a new `src/Gtd/folderTree.ts`
- `src/Gtd/collectKanban.ts`
- `src/Gtd/collectMatrix.ts`
- `src/index.ts`
- `src/gtd-calendar-webview.js`
- `src/event-calendar.css`
- `src/tests/Gtd/kanban.test.ts`
- `src/tests/Gtd/matrix.test.ts`
- `src/tests/Gtd/resolveNotebook.test.ts` and/or a new folder-tree test file
- `README.md`
- `SPEC.md`
- `CHANGELOG.md`
- `SLICE10.md`
- `TASKS.md`
## Out of scope
- Grouping numeric-depth or `scope: all` results.
- Ancestor rollups or duplicated cards across parent/child views.
- Nested notebook subsections inside a single kanban or matrix.
- Persisting expanded/collapsed notebook-view state.
- Notebook-level filtering controls or user-selectable grouping modes.
- Applying notebook grouping to calendar or Gantt views.
## Dependencies and resume point
Implement after SLICE8 and SLICE9 are complete. Start by preserving
`groupByNotebook` in parser tests, then build and test ordered folder metadata.
Introduce discriminated collector layouts before changing the webview. Complete
single-view regression tests before grouped manual acceptance.

View File

@ -1,216 +0,0 @@
# SLICE 11 — Complete ordinary notes with a `done` tag
> **State-saving rule:** update this file after every completed task and whenever
> work pauses. Keep automated validation and manual Joplin acceptance separate.
## Status
**COMPLETE.** Phases 16 are implemented and fully validated. The user confirmed
all Phase 7 manual Joplin acceptance tests pass on 2026-07-28. The production
JPL is built and inspected.
## Goal
Give ordinary notes containing a `gtd` block a natural completion state without
turning them into native Joplin to-dos. Completion follows the same tag-driven
progression already used by kanban:
```text
no workflow tag -> Backlog
in-progress -> In Progress
done -> Done
```
If an ordinary note carries both `in-progress` and `done`, **Done wins**.
## Confirmed behavior
- Only ordinary notes that already opt in through a found `gtd` block are
affected. A `done` tag does not opt a plain note into a view.
- The default completion tag is `done`.
- For ordinary notes, `done` takes priority over `in-progress` during kanban
bucketing.
- Existing native to-do completion remains authoritative for Joplin to-dos.
- The feature must preserve SLICE8 batching, SLICE9 mixed cards, and SLICE10
exact-owner notebook grouping.
## Confirmed design decisions
### 1. Configuration
**Recommendation:** add `done-tag:` to both `gtd-kanban` and `gtd-matrix`,
defaulting to `done`, just as `in-progress-tag:` defaults to `in-progress`.
Normalize tag names case-insensitively.
Warn when `done-tag` and `in-progress-tag` are identical because the workflow
would collapse directly to Done. Do not silently choose a different tag.
### 2. Which item types the tag affects
**Recommendation:** `done-tag` changes completion only for ordinary notes.
Native to-dos continue to use `todo_completed`; an incomplete to-do tagged
`done` remains incomplete. This avoids two competing completion authorities for
Joplin to-dos.
### 3. Kanban `done-window`
Ordinary notes have no native completion timestamp. Their `updated_time` is not
a reliable substitute because any later edit would make an old completion look
recent.
**Recommendation:** completed ordinary notes are not filtered by `done-window`;
all opted-in notes carrying `done-tag` appear in Done. `done-window` continues to
apply only to native completed to-dos. Document this explicitly.
Alternative if an unbounded Done column is unacceptable: add a completion date
to the `gtd` block in a later slice. Do not infer it from `updated_time`.
### 4. Matrix behavior
**Recommendation:** exclude `done` ordinary notes from both Skeleton and
Eisenhower matrices, matching the existing exclusion of completed to-dos. A
prioritization view should contain only unfinished work.
The exclusion happens before quadrant bucketing. Thus `done` also wins over
`in-progress`, `urgent`, and `important` tags.
### 5. Calendar behavior
**Recommendation for this slice:** leave calendars unchanged. A completed
ordinary note remains a scheduled/unscheduled calendar item and does not gain
strikethrough solely from `done-tag`.
Calendar completion styling can be considered separately because calendars may
serve as historical records, while kanban and matrix are workflow views.
### 6. Card representation and styling
Completed ordinary-note cards keep the 📄 fallback glyph and use the existing
completed title styling in kanban Done. They do not use a checked-checkbox glyph
and never become recurring.
The shared card model may represent `completed: true` with `isTodo: false`.
`completedTime` remains `0` for notes because no completion timestamp exists.
### 7. Removing or changing tags
- Removing `done` from a note makes it unfinished again.
- If `in-progress` remains, the note returns to In Progress.
- If neither workflow tag remains, it returns to Backlog.
- No tag is automatically added or removed; the plugin remains read-only.
## Precedence table
| Item | Native completion | `done` tag | `in-progress` tag | Kanban result | Matrix result |
|---|---:|---:|---:|---|---|
| Ordinary opted-in note | n/a | yes | either | Done | Excluded |
| Ordinary opted-in note | n/a | no | yes | In Progress | Existing mode rules |
| Ordinary opted-in note | n/a | no | no | Backlog | Existing mode rules |
| Ordinary note without `gtd` | n/a | any | any | Excluded | Excluded |
| Completed to-do | yes | any | any | Done, subject to `done-window` | Excluded |
| Incomplete to-do | no | yes | yes/no | Existing in-progress rule | Existing mode rules |
## Architecture
```text
eligible note + normalized tag titles
|
v
buildKanbanCard
to-do -> completion from todo_completed
note -> completion from configured done-tag
|
+-- kanban: completed wins -> Done
| note: no done-window filtering
| to-do: existing done-window filtering
|
+-- matrix: completed -> exclude before quadrant bucketing
```
Because `done-tag` is configurable per view, card construction may need either a
completion-tag argument or a small view-specific completion step after the
shared builder. Prefer one explicit data path shared by kanban and matrix; do not
hide the configured tag in a global constant lookup.
## Implementation plan
### Phase 1 — Configuration and types
- [x] Add `DONE_TAG` with the value `done`.
- [x] Add normalized `doneTag` fields to `KanbanConfig` and `MatrixConfig`.
- [x] Parse `done-tag` in both view blocks with a default of `done`.
- [x] Warn when `done-tag` and `in-progress-tag` are identical.
- [x] Add parser tests for defaults, custom tags, normalization, empty values,
unknown-option handling, and identical-tag warnings.
### Phase 2 — Shared completion model
- [x] Allow an ordinary `KanbanCard` to be completed from the configured tag.
- [x] Keep `isTodo: false`, `completedTime: 0`, and `isRecurring: false`.
- [x] Preserve native to-do completion regardless of `done-tag`.
- [x] Test ordinary-note, native-to-do, custom-tag, and both-tags precedence.
### Phase 3 — Kanban behavior
- [x] Bucket completed ordinary notes into Done before checking in-progress.
- [x] Keep all completed note cards regardless of `done-window`.
- [x] Preserve the existing to-do done-window behavior.
- [x] Sort completed notes and to-dos together with the configured Done sorter.
- [x] Cover single and SLICE10 notebook-grouped layouts with exact card totals.
### Phase 4 — Matrix behavior
- [x] Exclude completed ordinary notes before Skeleton/Eisenhower bucketing.
- [x] Prove `done` wins over in-progress, urgent, and important tags.
- [x] Preserve native completed-to-do exclusion.
- [x] Cover single and SLICE10 notebook-grouped layouts without double-counting.
### Phase 5 — Rendering and payload regression
- [x] Confirm completed note cards retain the 📄 glyph and completed styling.
- [x] Confirm no new payload branch is needed beyond the normalized card fields.
- [x] Confirm SLICE8 independent batching and SLICE10 grouped rendering remain
unchanged.
### Phase 6 — Documentation and automated verification
- [x] Document `done-tag`, precedence, note-only semantics, matrix exclusion,
and the done-window exception in README.md and SPEC.md.
- [x] Add an unreleased CHANGELOG entry.
- [x] Run focused parser/builder/kanban/matrix tests (103 passed).
- [x] Run the complete Jest suite (240 tests), TypeScript, JavaScript syntax, whitespace,
and prohibited-reference checks.
- [x] Build the production JPL.
### Phase 7 — Manual Joplin acceptance
- [x] Note with `done` only appears in Kanban Done.
- [x] Note with both `done` and `in-progress` appears only in Done.
- [x] Removing `done` returns the note to In Progress or Backlog as appropriate.
- [x] Custom `done-tag` works independently in kanban and matrix blocks.
- [x] Completed notes are excluded from both matrix modes.
- [x] Incomplete to-dos tagged `done` remain governed by native completion.
- [x] `done-window` filters native to-dos but not completed ordinary notes.
- [x] Glyph, styling, sorting, navigation, warnings, statistics, batching, and
child-notebook grouping do not regress.
- [x] Record explicit user sign-off separately from automated checks.
## Out of scope
- Mutating tags from a rendered card.
- Automatically removing `in-progress` when `done` is added.
- Inferring a completion time from `updated_time`.
- Adding a completion date to the `gtd` block.
- Changing calendar completion styling.
- Treating `done` as an opt-in for ordinary notes without a `gtd` block.
## Acceptance criteria
- An opted-in ordinary note carrying the configured done tag is complete.
- Done wins over in-progress and every matrix axis tag.
- Native to-do completion behavior is unchanged.
- Done-window semantics are explicit and deterministic without fabricated dates.
- Single and grouped views retain correct ownership, totals, sorting, warnings,
batching, styling, and navigation.
- Automated validation, production packaging, and explicit manual acceptance
all pass and are recorded separately.

View File

@ -1,276 +0,0 @@
# SLICE 12 — Filter project notes independently from task to-dos
> **State-saving rule:** update this file after every completed task and whenever
> work pauses. Keep implementation, automated validation, production packaging,
> and manual Joplin acceptance as separate status boundaries.
## Status
**COMPLETE.** Phases 17 are implemented and validated. Focused and full
automation passed, the production JPL was built, and the user confirmed manual
Joplin acceptance on 2026-07-28.
## Goal
Allow `gtd-kanban` and `gtd-matrix` blocks to include or exclude opted-in
ordinary notes independently from native Joplin to-dos. The user treats
ordinary `gtd` notes as projects and native to-dos as tasks, and needs focused
project-only, task-only, and combined views.
```yaml
# Projects only
notes: all
todos: none
```
```yaml
# Tasks only
notes: none
todos: all
```
```yaml
# Projects and tasks
notes: all
todos: all
```
## Confirmed configuration contract
- The option is named `notes`, plural, matching the existing `todos` option and
the calendar configuration vocabulary.
- `notes` is valid only in `gtd-kanban` and `gtd-matrix` blocks in this slice.
- Accepted values are `all` and `none`.
- The default is `all`, preserving current behavior for existing blocks.
- `notes: all` means all **eligible ordinary notes containing a found `gtd`
block**. It never means every plain note in scope.
- `notes: none` excludes every ordinary note, including notes tagged
`in-progress`, `done`, `urgent`, or `important`.
- A tag alone never opts an ordinary note into a view.
- The existing `todos` contract remains unchanged: `gtd-only | all | none`,
defaulting to `gtd-only`.
- `notes` and `todos` are independent. Every valid combination is supported.
- Invalid or empty `notes` values warn and fall back to `all`.
- The singular key `note` remains unknown and produces the normal unknown-option
warning; no alias is introduced.
## Inclusion table
| Item | `gtd` block | `notes` | `todos` | Result |
|---|---:|---|---|---|
| Ordinary note | yes | `all` | any | Included, then normal view rules apply |
| Ordinary note | yes | `none` | any | Excluded |
| Ordinary note | no | `all` | any | Excluded |
| Ordinary note | no | `none` | any | Excluded |
| Native to-do | yes | any | `gtd-only` or `all` | Included, subject to completion rules |
| Native to-do | no | any | `all` | Included, subject to completion rules |
| Native to-do | no | any | `gtd-only` or `none` | Excluded |
The host dashboard note remains excluded regardless of its type, block, or
configuration.
## Interaction with existing behavior
### Kanban
- `notes: all` preserves SLICE9 and SLICE11 note behavior.
- An admitted unfinished note uses `in-progress-tag` to select In Progress or
Backlog.
- An admitted note carrying `done-tag` enters Done, even if it also carries
`in-progress-tag`.
- Completed ordinary notes remain exempt from the native to-do `done-window`.
- `notes: none` excludes ordinary notes before tag lookup, card construction,
completion checks, sorting, grouping, statistics, and pagination.
- Native to-do behavior is unaffected by `notes`.
### Matrix
- `notes: all` preserves both Skeleton and Eisenhower note behavior.
- Admitted notes carrying `done-tag` remain excluded before quadrant bucketing.
- Other admitted notes use the existing date and tag axes for the selected mode.
- `notes: none` excludes ordinary notes before tag lookup, completion checks,
quadrant bucketing, sorting, grouping, statistics, and pagination.
- Native completed to-dos remain excluded, and incomplete to-do behavior is
unaffected by `notes`.
### Notebook grouping and statistics
- SLICE10 exact-owner grouping remains unchanged.
- A filtered note must not create a notebook group, warning, card count, or
pagination state.
- A group containing only filtered notes is omitted.
- Non-empty descendant groups remain visible even when their ancestors become
empty after filtering.
- `scannedFolders` and `scannedNotes` continue to describe scan work, not admitted
cards; `cardCount`, bucket totals, headings, and pagination use admitted cards.
- Malformed-block warnings are emitted only for items admitted by their type
filter. A malformed ordinary note under `notes: none` produces no warning.
### Rendering and payload
- No new rendering branch is required. Collectors return the same card and
grouped-layout payloads with filtered arrays and totals.
- SLICE8 independent `page-size` expansion state remains per rendered bucket.
- Empty project-only or task-only results use the existing empty-state behavior.
- Card glyphs, styling, drilldown, recurrence, and read-only behavior do not
change.
## Data-access and performance rules
Filtering should occur as early as correctness permits:
```text
scan note metadata/body
|
+-- host note -> exclude
+-- ordinary note + notes:none -> exclude
+-- to-do rejected by todos -> exclude
|
v
parse/admit gtd block -> warnings -> tag lookup -> card -> bucket
```
- Do not request tags for an item rejected by `notes` or `todos`.
- Ordinary notes require bodies to prove explicit `gtd` opt-in when
`notes: all`.
- `todos: gtd-only` requires bodies to determine to-do admission.
- `todos: all` still needs bodies because an optional `gtd` block can override
the to-do card date, title, colours, icon, and text. Do not optimize this away.
- Retain current body fetching whenever either admitted type can use a `gtd`
block. Any no-body optimization must be proven not to alter card overrides,
warnings, scan counts, or host-note handling.
## Architecture
Add a note-specific inclusion type rather than reusing `InclusionMode`, because
`gtd-only` would be redundant: all ordinary notes are already explicitly
`gtd`-only.
```ts
type NoteInclusionMode = "all" | "none";
interface KanbanConfig {
notes: NoteInclusionMode;
// existing fields
}
interface MatrixConfig {
notes: NoteInclusionMode;
// existing fields
}
```
Both collectors should apply the type filter before malformed-block warnings,
tag requests, and card construction. Keep parsing and filtering behavior shared
in intent, but avoid an abstraction that obscures the different kanban and
matrix completion paths.
## Implementation plan
### Phase 1 — Configuration and types
- [x] Add `NoteInclusionMode = "all" | "none"`.
- [x] Add `notes` to `KanbanConfig` and `MatrixConfig`.
- [x] Recognize `notes` in both parsers.
- [x] Parse case-insensitive `all | none`, defaulting to `all`.
- [x] Warn on invalid, empty, null, array, or object values and use `all`.
- [x] Preserve the unknown-option warning for singular `note`.
- [x] Add parser tests for defaults, valid values, normalization, invalid values,
and coexistence with every `todos` mode.
### Phase 2 — Kanban filtering
- [x] Exclude ordinary notes immediately when `config.notes === "none"`.
- [x] Preserve explicit `gtd` opt-in when `config.notes === "all"`.
- [x] Ensure rejected notes trigger no malformed-block warning or tag request.
- [x] Preserve Backlog, In Progress, Done, done-tag precedence, done-window,
sorting, and native to-do behavior.
- [x] Cover project-only, task-only, combined, and fully empty configurations.
- [x] Cover empty, valid, and malformed `gtd` blocks.
### Phase 3 — Matrix filtering
- [x] Exclude ordinary notes immediately when `config.notes === "none"`.
- [x] Preserve explicit `gtd` opt-in when `config.notes === "all"`.
- [x] Ensure rejected notes trigger no malformed-block warning or tag request.
- [x] Preserve done-note exclusion and native completed-to-do exclusion.
- [x] Cover project-only, task-only, combined, and fully empty configurations in
both Skeleton and Eisenhower modes.
- [x] Cover empty, valid, and malformed `gtd` blocks.
### Phase 4 — Grouping, totals, and performance
- [x] Verify single-layout `cardCount` and all bucket totals after filtering.
- [x] Verify exact-owner notebook groups omit groups emptied by filtering.
- [x] Verify non-empty descendants survive filtered empty ancestors.
- [x] Verify filtered notes do not generate group-local warnings.
- [x] Verify each surviving bucket retains independent SLICE8 pagination.
- [x] Assert no tag request occurs for filtered notes.
- [x] Audit `getNotesInFolder` body-field hints without removing to-do `gtd`
overrides or malformed-block warnings.
### Phase 5 — Documentation
- [x] Add `notes: all | none` to README kanban and matrix option tables.
- [x] Document that `all` means all opted-in ordinary `gtd` notes.
- [x] Add project-only, task-only, and combined examples.
- [x] Document independence from `todos`, note completion behavior, grouped-view
effects, and the default-preserving migration story.
- [x] Update SPEC.md configuration, inclusion matrix, collector flow, statistics,
and performance notes.
- [x] Add an unreleased CHANGELOG entry.
### Phase 6 — Automated validation and packaging
- [x] Run focused parser, kanban, matrix, grouping, and field-hint tests
(117 passed).
- [x] Run the complete Jest suite (251 tests across 16 suites).
- [x] Run TypeScript validation and webview JavaScript syntax checking.
- [x] Run whitespace and prohibited-reference audits.
- [x] Build the production JPL.
- [x] Inspect archive contents; SHA-256:
final v1.0.0 artifact
`0b28fb0f5ffcb60d92e017fb0972ecaa456fcc3c24b144891dce7d72dc7d98ef`.
### Phase 7 — Manual Joplin acceptance
- [x] Existing blocks without `notes` still show opted-in project notes.
- [x] Kanban `notes: all` plus `todos: none` shows projects only across Backlog,
In Progress, and Done.
- [x] Kanban `notes: none` plus `todos: all` shows tasks only.
- [x] Kanban `notes: all` plus `todos: all` shows both without duplication.
- [x] Matrix project-only and task-only configurations work in Skeleton mode.
- [x] Matrix project-only and task-only configurations work in Eisenhower mode.
- [x] A plain note without `gtd` remains excluded under `notes: all`.
- [x] A `done` project appears in Kanban Done and remains excluded from matrices.
- [x] Invalid `notes` warns and falls back to `all`; singular `note` warns as
unknown.
- [x] Filtered malformed notes do not produce warnings.
- [x] Single and grouped views have correct headings, totals, empty states,
sorting, pagination, and navigation.
- [x] Record explicit user sign-off separately from automated checks.
## Out of scope
- Allowing ordinary notes without a `gtd` block into kanban or matrix views.
- Adding `gtd-only` as a distinct `notes` value.
- Renaming or changing the existing `todos` option.
- Inferring project/task identity from tags, titles, notebooks, or note content.
- Mutating note types, tags, or completion state from a rendered view.
- Changing calendar inclusion semantics.
- Changing Gantt item admission.
- Persisting filters or pagination state outside the fenced block.
## Acceptance criteria
- Existing kanban and matrix blocks behave identically when `notes` is omitted.
- `notes: all` includes only ordinary notes containing a `gtd` block.
- `notes: none` excludes ordinary notes without affecting native to-dos.
- All `notes` and `todos` combinations behave independently and predictably.
- Filtering happens before tag access, warning creation, bucketing, grouping,
totals, sorting, and pagination.
- SLICE8 batching, SLICE10 grouping, and SLICE11 completion semantics do not
regress.
- Documentation clearly supports project-only, task-only, and combined views.
- Automated validation, production packaging, and explicit manual acceptance
are completed and recorded separately.

View File

@ -1,174 +0,0 @@
# SLICE 13 — Remove underlines from kanban and matrix cards
> **State-saving rule:** update this file after every completed task and whenever
> work pauses. Keep implementation, automated validation, production packaging,
> and manual Joplin acceptance as separate status boundaries.
## Status
**COMPLETE.** Phases 15 are implemented and validated. Automation and package
inspection passed, and the user confirmed manual Joplin acceptance on
2026-07-28.
## Goal
Remove the distracting underline from every kanban and matrix card title while
preserving the visual distinction for completed items and every existing card
interaction.
The change applies equally to:
- ordinary project notes;
- incomplete native to-dos;
- completed native to-dos in Kanban Done;
- completed ordinary notes in Kanban Done;
- cards in both Skeleton and Eisenhower matrices;
- single and notebook-grouped layouts.
## Confirmed diagnosis
Kanban and matrix cards are rendered by the shared `renderCard` function as:
```html
<div class="gtd-kanban-card gtd-calendar-clickable">
<div class="gtd-kanban-card-title">...</div>
</div>
```
The underline is real and comes from the generic clickable rule:
```css
.gtd-calendar-clickable {
cursor: pointer;
text-decoration: underline;
}
```
Calendar items already suppress that decoration through a calendar-scoped
override. Kanban and matrix cards do not, so the parent decoration propagates
through their title text.
Completed-card strikethrough is separate. `renderCard` applies an inline
`line-through` directly to `.gtd-kanban-card-title` when `card.completed` is
true. Removing the parent underline therefore must not remove Done styling.
## Confirmed design
Add a card-scoped override to the existing `.gtd-kanban-card` rule:
```css
.gtd-kanban-card {
text-decoration: none;
}
```
This selector is deliberately narrow:
- It affects both kanban and matrix because they share `renderCard`.
- It affects both notes and to-dos because both use the same card element.
- It does not change calendar events, unscheduled items, Gantt rows, debug
output, warnings, or other clickable elements.
- It does not change `cursor: pointer`, card borders, hover borders, custom
colours, glyphs, recurrence marks, details, pagination, or click navigation.
- It leaves the completed title's inline `line-through` authoritative.
Do not remove `text-decoration: underline` from the generic
`.gtd-calendar-clickable` rule. Other renderers may rely on that default, and a
global change would make this small visual correction unnecessarily broad.
## Visual state table
| Card | Before | After |
|---|---|---|
| Backlog/In Progress note | Underline | No decoration |
| Backlog/In Progress to-do | Underline | No decoration |
| Done ordinary note | Underline + line-through | Line-through only |
| Done native to-do | Underline + line-through | Line-through only |
| Matrix note or to-do | Underline | No decoration |
Custom foreground colours, including intentionally red text, remain unchanged;
only the underline is removed.
## Accessibility and interaction boundary
Cards remain visually identifiable as interactive controls through their card
container, border, hover-border change, glyph, and pointer cursor. Click-to-open
behavior is unchanged.
This slice does not redesign keyboard semantics or convert cards from `div`
elements to buttons/links. That would be a separate accessibility task with a
larger markup, focus, key handling, and styling surface.
## Implementation plan
### Phase 1 — CSS correction
- [x] Add `text-decoration: none` to `.gtd-kanban-card` in
`src/event-calendar.css`.
- [x] Keep the generic `.gtd-calendar-clickable` underline rule unchanged.
- [x] Keep the existing calendar-scoped decoration override unchanged.
- [x] Make no JavaScript or payload changes.
### Phase 2 — Static and rendering regression audit
- [x] Confirm kanban and matrix still use the shared `.gtd-kanban-card` element.
- [x] Confirm incomplete cards receive no inline text decoration.
- [x] Confirm completed cards still set `line-through` on the title row.
- [x] Confirm custom `fg-colour` and `bg-colour` remain inline card styles.
- [x] Confirm hover details, card-detail modes, click navigation, glyphs, and
recurrence marks use unchanged code paths.
- [x] Confirm single and notebook-grouped layouts use the same renderer.
### Phase 3 — Documentation
- [x] Add an unreleased CHANGELOG entry describing the visual cleanup.
- [x] Update README or SPEC only if existing text claims cards are underlined;
otherwise avoid unnecessary documentation churn.
- [x] Record the screenshot-derived acceptance intent in this file.
### Phase 4 — Automated validation and packaging
- [x] Run the complete Jest suite (251 tests across 16 suites).
- [x] Run TypeScript validation and webview JavaScript syntax checking.
- [x] Run CSS/build, whitespace, and prohibited-reference audits.
- [x] Build the production JPL.
- [x] Inspect the packaged `event-calendar.css` and confirm the scoped override
is present.
- [x] Inspect archive contents; SHA-256:
final v1.0.0 artifact
`0b28fb0f5ffcb60d92e017fb0972ecaa456fcc3c24b144891dce7d72dc7d98ef`.
### Phase 5 — Manual Joplin acceptance
- [x] Backlog project-note titles are not underlined.
- [x] In Progress project-note titles are not underlined.
- [x] Backlog and In Progress to-do titles are not underlined.
- [x] Done note titles show line-through without underline.
- [x] Done to-do titles show line-through without underline.
- [x] Skeleton matrix note and to-do titles are not underlined.
- [x] Eisenhower matrix note and to-do titles are not underlined.
- [x] Custom foreground/background colours still render correctly.
- [x] Hover borders and card-detail behavior still work.
- [x] Clicking every card type still opens its source note.
- [x] Single and notebook-grouped views render consistently.
- [x] Record explicit user sign-off separately from automated checks.
## Out of scope
- Changing the generic clickable style globally.
- Changing calendar, Unscheduled, Gantt, warning, or debug decorations.
- Changing completed-item semantics or removing line-through.
- Altering card colours, borders, spacing, typography, glyphs, or pagination.
- Changing card markup, focus behavior, keyboard behavior, or ARIA semantics.
- Changing kanban/matrix filtering, sorting, grouping, or data collection.
- Bumping the plugin version or performing the v1.0.0 release.
## Acceptance criteria
- No kanban or matrix card title is underlined solely because it is clickable.
- Completed kanban notes and to-dos retain line-through styling.
- The change applies identically to notes, to-dos, matrix modes, and grouped
layouts through the shared card renderer.
- No other clickable view element changes decoration.
- Automated validation, production packaging, and explicit manual acceptance
are completed and recorded separately.

View File

@ -1,405 +0,0 @@
# SLICE 14 — Opt-in kanban drag and drop (v2.0 foundation)
> **State-saving rule:** update this file after every completed task and whenever
> work pauses. Keep implementation, automated validation, production packaging,
> and manual Joplin acceptance as separate status boundaries.
## Status
**COMPLETE.** Phases 17 are complete. Focused and full automation passed, the
production JPL was built and inspected, and the user confirmed every Phase 7
manual Joplin acceptance test passed on 2026-07-31.
## Goal
Allow users to move kanban cards between Backlog, In Progress, and Done from the
rendered-note view. Moves persist to Joplin, so refreshes and other dashboards
derive the same workflow state.
````
```gtd-kanban
title: Editable projects and tasks
editable: yes
notes: all
todos: all
```
````
Existing blocks remain read-only when `editable` is omitted.
## Confirmed product contract
- Desktop rendered-note webview only; cards are DOM elements, not canvas pixels.
- `editable: yes` enables mutation; default `no` preserves every v1 block.
- Parse `yes | no` case-insensitively. Invalid, empty, or non-scalar values warn
and fall back to `no`.
- Editable boards provide pointer drag and a keyboard-operable **Move to…**
control. Dragging is never the only interaction.
- Successful moves persist through Joplin's data API, then refresh the affected
board from canonical data.
- Failed/stale moves show an inline error and converge to canonical state.
- Moves change workflow state, not free-form ordering; configured sorting remains
authoritative.
- Cards stay in their current notebook; cross-group moves are out of scope.
## State transitions
An intentional move canonicalizes configured workflow markers. Normal read-only
collection still treats Done as higher priority than In Progress.
### Ordinary opted-in notes
| Destination | Persisted change |
|---|---|
| Backlog | Remove `done-tag` and `in-progress-tag`. |
| In Progress | Add `in-progress-tag`; remove `done-tag`. |
| Done | Add `done-tag`; remove `in-progress-tag`. |
Notes remain ordinary notes. Their `gtd` block, content, dates, notebook, and
unrelated tags remain unchanged.
### Native Joplin to-dos
| Destination | Persisted change |
|---|---|
| Backlog | Set `todo_completed` to `0`; remove `in-progress-tag`. |
| In Progress | Set `todo_completed` to `0`; add `in-progress-tag`. |
| Done | Set `todo_completed` to current epoch milliseconds; remove `in-progress-tag`. |
`done-tag` remains note-only. Moving an already completed to-do to Done is a
no-op rather than rewriting its completion history.
### Required cases
| Item | Source | Destination | Result |
|---|---|---|---|
| Note | Backlog | In Progress | Add progress, remove done. |
| Note | Backlog/In Progress | Done | Add done, remove progress. |
| Note | Done | In Progress | Remove done, add progress. |
| Note | Done/In Progress | Backlog | Remove both. |
| To-do | Backlog | In Progress | Reopen if needed, add progress. |
| To-do | Backlog/In Progress | Done | Complete natively, remove progress. |
| To-do | Done | In Progress | Reopen, add progress. |
| To-do | Done/In Progress | Backlog | Reopen, remove progress. |
| Either | Any | Same column | No mutation or refresh. |
## Repeating to-dos
Native completion may trigger Repeating To-Dos to advance or replace an item.
Use Joplin's real `todo_completed` field; do not simulate recurrence. Always
refetch after completion and accept canonical state if the item changes during
the round trip. Explicitly test completing and reopening recurring to-dos.
## Architecture and trust boundary
```text
pointer drop or keyboard Move to…
-> constrained webview intent
-> main process reparses config and requires editable:yes
-> mutation service fetches current note and tag identities
-> validate eligibility, scope, group, type, and destination
-> apply minimal idempotent completion/tag operations
-> recollect and rerender the initiating board from canonical data
```
The webview sends intent only:
```ts
type KanbanDestination = "backlog" | "inProgress" | "done";
interface MoveKanbanCardIntent {
view: "kanban";
hostNoteId: string;
cardId: string;
destination: KanbanDestination;
rawConfig: string;
viewInstanceId: string;
}
```
Never accept arbitrary REST paths, patches, tag IDs, or tag-operation lists from
the webview. Reparse configuration and independently derive every write.
Add narrowly scoped, testable operations for:
- fetching one current note, including type, completion, parent, and body when
eligibility must be checked;
- listing tags with both ID and title;
- finding/creating a configured tag by normalized title;
- idempotently attaching/detaching a tag;
- patching only `todo_completed` on a native to-do.
Tag matching stays case-insensitive. Reuse an existing case variant. If duplicate
same-title tags exist, do not create another; select deterministically and test it.
### Fresh-state validation
Before writing, verify that the host note exists, the card is not the host, the
destination is fixed, the block still has `editable: yes`, the item remains
admitted by `notes`/`todos`, ordinary notes retain a found `gtd` block, and the
item remains in resolved scope. Grouped boards must retain exact notebook
ownership. Stale conditions make no write and trigger a canonical refresh.
### Multi-step safety
Joplin note/tag operations are separate and may not be transactional. Compute
changes first, skip already-correct writes, order changes sensibly, and refetch
after success or failure. Do not compensate with stale values that might
overwrite a concurrent user edit.
## Webview interaction
Prefer pointer events for consistent desktop behavior and click/drag separation.
- Show a drag affordance only on editable cards.
- Start after a movement threshold; ordinary clicks still open notes.
- Use pointer capture and visible dragged/valid-target/active-target states.
- Support long, scrolling, grouped, and paginated boards.
- Escape, cancellation, lost capture, outside drop, and same-column drop do not
write.
- Scope targets to the nearest board and `viewInstanceId`, never another block.
- Disable duplicate submissions and show pending/error feedback.
- Refresh canonically; sorting may place a moved card beyond the visible page.
Keyboard/accessibility requirements:
- provide a focusable **Move to…** control with fixed destinations;
- disable the current destination;
- preserve keyboard open-note behavior and restore focus after rerender;
- announce pending, success, failure, and destination through `aria-live`;
- give controls and targets useful accessible names.
## Rendering and refresh
- Carry stable `hostNoteId` and per-render `viewInstanceId` context.
- Multiple blocks retain independent request/pagination state.
- Refresh only the initiating instance and reuse collectors for sorting, totals,
warnings, grouping, and cards.
- Optimistic preview is allowed; canonical payload remains authoritative.
- Preserve expansion where practical or document/test consistent reset behavior.
- Reject stale async responses with a per-instance request sequence.
## Implementation plan
### Phase 1 — Configuration and protocol
- [x] Add `editable: boolean` to `KanbanConfig`, default `false`.
- [x] Parse/validate `editable: yes | no` and add complete parser tests.
- [x] Define destinations, constrained intents, success, stale, and error types.
- [x] Add stable host/view identity to payload/context.
- [x] Prove existing blocks retain identical read-only behavior.
### Phase 1 completion record
- Added strict scalar `editable: yes | no` parsing. Omitted and `no` remain
read-only; invalid, empty, non-string, array, and object values warn and use
`no`.
- Added fixed Kanban destinations, a constrained semantic move intent, typed
success/stale/error results, and a runtime destination guard. The protocol
contains no arbitrary paths, patches, tag IDs, or tag-operation lists.
- Every rendered placeholder now owns a stable `viewInstanceId` for its render.
Kanban payloads echo that identity and the canonically resolved host note ID,
including a null host identity on missing-source responses.
- Focused automation passed: 2 suites, 62 tests. TypeScript with
`--skipLibCheck`, webview JavaScript syntax, and `git diff --check` passed.
- Full regression automation passed: 17 suites, 261 tests. No production JPL
was built and no manual Joplin acceptance was performed in this phase.
### Phase 2 — Testable mutation service
- [x] Add fresh note/tag reads, find-or-create, membership changes, and native
completion patches behind a narrow adapter.
- [x] Implement every note and to-do transition above.
- [x] Make changes idempotent and preserve unrelated data.
- [x] Test transitions, conflicting tags, custom names/case, duplicate tags,
no-ops, partial failures, and stale/concurrent reads.
### Phase 2 completion record
- Added a writable adapter separate from the existing read-only collector
abstraction. Its surface is limited to fetching one note and tag identities,
listing/creating tags, attaching/detaching memberships, and patching only
`todo_completed`.
- Added canonical note and native-to-do transitions for all three destinations.
Operations are minimal and idempotent, tag matching is case-insensitive,
existing case variants are reused deterministically, duplicate attached tags
are removed deterministically, and unrelated fields/tags are untouched.
- Partial write failures propagate without stale compensating writes so the
later handler can refetch canonical state. Missing-card reads fail before any
write.
- Focused mutation automation passed: 1 suite, 13 tests. Full regression
automation passed: 18 suites, 274 tests. TypeScript with `--skipLibCheck` and
`git diff --check` passed. No package or manual test was performed.
### Phase 3 — Message validation
- [x] Register the kanban move message in the existing handler.
- [x] Reparse config and require `editable: yes` in the main process.
- [x] Reject malformed intent, arbitrary destinations, missing/host/filtered/
out-of-scope items, lost opt-in, and cross-group movement without writes.
- [x] Return typed user-safe results and keep diagnostic detail in logs.
- [x] Prove webview input cannot select arbitrary paths, fields, tags, or writes.
### Phase 3 completion record
- Registered `moveKanbanCard` and added a testable validation service between
the untrusted webview message and the Phase 2 mutation adapter.
- The handler requires an exact constrained message shape, fixed destination,
non-empty host/card/view identities, matching currently selected host, valid
reparsed YAML, and explicit `editable: yes`. Unknown fields are rejected, so
paths, patches, tag operations, and arbitrary write instructions cannot be
smuggled alongside a valid destination.
- Fresh folders and the freshly fetched card are used to revalidate notebook
targeting, scope, exact current parent ownership, host exclusion, item type
filters, and ordinary-note/to-do `gtd` eligibility immediately before writes.
- Expected stale conditions and failures return typed user-safe results.
Unexpected API detail is retained only in plugin-process diagnostic logging.
- Focused validation passed: 3 suites, 43 tests. Full regression validation
passed: 19 suites, 295 tests. TypeScript with `--skipLibCheck`, webview syntax,
and `git diff --check` passed. Packaging and manual acceptance remain pending.
### Phase 4 — Pointer drag and drop
- [x] Add editable-only affordances and complete pointer lifecycle handling.
- [x] Implement thresholds, capture, target detection, scrolling, cancellation,
Escape, lost capture, pending state, and failure UI.
- [x] Preserve clicks, details, styling, pagination, and navigation.
- [x] Prevent cross-block/group drops and duplicate submissions.
- [x] Refresh canonically and ignore stale responses.
### Phase 4 completion record
- Added an explicit editable-only drag handle to each Kanban card. Pointer drag
begins only after six pixels of movement; ordinary card clicks retain the
existing open-note path, while a completed/cancelled drag suppresses the
synthetic click that could otherwise open a note accidentally.
- Pointer capture, active-card styling, valid/active target styling, viewport
edge scrolling, Escape, pointer cancellation, lost capture, outside drops,
and same-column no-ops are implemented. Drop targets are restricted to the
originating board element, preventing movement across notebook groups or
stacked blocks.
- Per-view pending state prevents duplicate submissions and temporarily disables
handles. Inline pending/error feedback is shown without changing the card
arrays optimistically.
- Every accepted/rejected move triggers a canonical `getKanban` refresh for the
initiating `viewInstanceId`. Per-instance request sequencing and echoed
identity prevent older or foreign responses from replacing the current view.
- Full regression automation passed: 19 suites, 295 tests. TypeScript with
`--skipLibCheck`, webview syntax, and `git diff --check` passed. The repository
has no browser DOM harness, so pointer visuals and behavior remain explicit
Phase 7 manual acceptance items. No production JPL was built in this phase.
### Phase 5 — Keyboard and accessibility
- [x] Add keyboard-operable **Move to…** controls.
- [x] Preserve open-note keys and predictable focus.
- [x] Add accessible names, instructions, states, and live announcements.
- [x] Test keyboard-only movement among all columns.
### Phase 5 completion record
- Every editable card now exposes a native keyboard-operable **Move to…**
selector with fixed Backlog, In Progress, and Done options. The current
destination and placeholder are disabled; choosing another destination uses
the same constrained message and canonical refresh as pointer movement.
- Editable cards are keyboard-focusable links with accessible open-note names
and Enter/Space activation. The pointer-only handle is removed from the tab
and accessibility order so it does not create an inert keyboard stop.
- Move controls carry card-specific accessible names and visible focus styles.
Pending, success, stale, and error status messages use atomic live regions;
controls are disabled while a request is active.
- Canonical rerender restores focus to the moved card's selector when it remains
visible. If sorting or pagination removes the card from the rendered batch,
focus moves to the status region instead of being lost.
- Read-only boards retain their previous markup/focus behavior and expose no
drag handle, move selector, or mutation status UI.
- Full regression automation passed: 19 suites, 295 tests. TypeScript with
`--skipLibCheck`, webview syntax, and `git diff --check` passed. With no DOM
harness in this repository, end-to-end keyboard and screen-reader behavior
remains a Phase 7 manual acceptance boundary. No JPL was built in this phase.
### Phase 6 — Documentation, regression, and packaging
- [x] Document opt-in editing, transitions, sorting, recurrence caveats, keyboard
controls, and desktop scope in README.md and SPEC.md.
- [x] Revise text that says all cards are read-only or drag is merely planned.
- [x] Add an unreleased v2.0.0 CHANGELOG entry without releasing.
- [x] Verify calendars, matrices, Gantt, read-only boards, filters, groups,
warnings, totals, navigation, and styling do not regress.
- [x] Run focused tests, full Jest, TypeScript, webview syntax, whitespace,
prohibited-reference checks, and production JPL build/inspection.
### Phase 6 completion record
- README documents the exact `editable: yes` opt-in, pointer and keyboard
controls, note/to-do transitions, sorting/pagination authority, recurrence,
cancellation/failure behavior, preservation guarantees, and desktop scope.
- SPEC documents parsing, transition tables, constrained protocol and trust
boundary, mutation safety, request sequencing, canonical refresh, focus/live
announcements, and the remaining multi-dashboard refresh limitation. The old
planned-authoring roadmap is replaced with the implemented release-pending
contract. Historical v1 read-only text and current matrix/Gantt read-only text
remain intentionally scoped and accurate.
- CHANGELOG contains an unreleased 2.0.0 Kanban entry. Package and manifest
versions intentionally remain 1.0.0 until the consolidated v2 release phase;
nothing was published or pushed.
- Focused automation passed: 4 suites, 96 tests. Full automation passed: 19
suites, 295 tests. TypeScript with `--skipLibCheck`, webview JavaScript syntax,
`git diff --check`, stale-wording review, and packaged-output prohibited-path/
planning-reference audits passed.
- `npm run dist` passed. The fresh production artifact is
`publish/com.victorwiebe.joplin.plugin.gtd-calendar.jpl` (177,664 bytes,
embedded manifest version 1.0.0, SHA-256
`749db5d334457ba4818a075c9d0df4f47bda53a478f0dc9904cbab5f8d0d4e33`).
Its five expected runtime files and Kanban mutation, pointer, keyboard,
accessibility, and request-sequencing markers were inspected.
- No manual Joplin acceptance was performed; every Phase 7 item remains open.
### Phase 7 — Manual Joplin acceptance
- [x] Omitted/`no`/invalid `editable` states remain read-only as documented.
- [x] Pointer moves notes through all columns and persists exact configured tags.
- [x] Pointer moves to-dos through all columns and persists native completion and
progress state.
- [x] Conflicting note tags are canonicalized only by an intentional move.
- [x] Custom workflow tags preserve unrelated/similarly named tags.
- [x] Same-column, cancelled, outside, Escape, and interrupted drags do not write.
- [x] Click still opens; drag does not accidentally open.
- [x] Keyboard-only movement works, restores focus, and announces results.
- [x] Stale state/API failure cannot leave false UI or corrupt unrelated data.
- [x] Sorting, totals, pagination, colours, glyphs, details, and Done styling stay
correct after refresh.
- [x] Single, grouped, multiple-block, and long scrolling boards work.
- [x] Completing/reopening a recurring to-do works with Repeating To-Dos.
- [x] Record explicit user sign-off separately from automation.
### Phase 7 acceptance record
- **PASSED — USER SIGN-OFF RECEIVED 2026-07-31.** The user confirmed all Phase 7
manual tests passed against the inspected Phase 6 production JPL.
- Accepted coverage includes read-only opt-in boundaries, ordinary-note and
native-to-do transitions, conflicting/custom/unrelated tags, pointer
cancellation and click separation, keyboard movement and focus/live feedback,
stale/API-failure convergence, sorting/pagination/styling, grouped and stacked
boards, long scrolling, and Repeating To-Dos integration.
- This manual acceptance is recorded separately from Phase 6 automation and
packaging. Slice 15 has not begun.
## Out of scope
- Free-form ordering or persisted rank.
- Moving between notebooks/groups or changing `parent_id`.
- Editing card fields or arbitrary tags.
- Calendar, Gantt, or matrix drag (SLICE 15 covers Eisenhower).
- Skeleton drag, mobile support, or recurrence reimplementation.
- Publishing v2.0.0 before both slices and release acceptance are complete.
## Acceptance criteria
- Only an explicitly editable kanban can mutate data.
- Every destination produces the exact canonical state above.
- Main-process validation uses fresh Joplin data and constrained intent.
- Pointer and keyboard workflows are complete and accessible.
- Failures converge to canonical state without overwriting unrelated changes.
- All read-only and v1.0.0 behavior remains compatible.
- Automation, packaging, and manual acceptance are recorded separately.

View File

@ -1,257 +0,0 @@
# SLICE 14a — Compact move menu for editable Kanban cards
> **State-saving rule:** update this file after every completed task and whenever
> work pauses. Keep implementation, automated validation, production packaging,
> and manual Joplin acceptance as separate status boundaries.
## Status
**COMPLETE.** Phases 15 are complete. Focused and full automation passed, a
fresh production JPL was built and inspected, and the user confirmed manual
Joplin acceptance on 2026-07-31.
## Goal
Reduce visual noise on editable Kanban cards by removing the permanently visible
**Move to…** select. Keep the existing pointer-drag handle and place a compact
disclosure arrow directly beneath it. Activating the arrow opens a temporary
menu containing the available destination columns.
The menu remains closed by default. All Slice 14 persistence, validation,
canonical refresh, error handling, focus restoration, and accessibility
behavior remains authoritative.
## Confirmed product contract
- Remove the always-visible **Move to…** select from every editable card.
- Keep the existing drag handle at the upper-right of each editable card.
- Add a compact disclosure arrow directly beneath the drag handle.
- Activating the arrow opens a temporary dropdown/menu below the arrow.
- The menu lists the three fixed Kanban destinations: Backlog, In Progress, and
Done.
- The card's current column is unavailable and cannot submit a move.
- The menu is closed by default and closes after selection or cancellation.
- Pointer drag remains available and unchanged.
- Keyboard movement remains fully available; the disclosure and menu must be
operable without a pointer.
- Use the existing constrained `moveKanbanCard` intent and main-process mutation
pathway. This slice must not introduce new write capabilities.
- Preserve pending/error feedback, canonical refresh, stale-response rejection,
focus restoration, and live announcements.
- Read-only boards continue to expose no drag or move controls.
- Matrix, calendar, and Gantt rendering remain unchanged.
## Interaction model
### Closed state
Each editable card shows a compact two-control stack in its upper-right corner:
```text
┌───────────────┐
│ Card title ↕ │ pointer drag handle
│ ▾ │ move-menu disclosure
└───────────────┘
```
The disclosure arrow has an accessible name such as **Move “Card title” to…**.
It is a real keyboard-focusable button. The pointer-only drag handle remains out
of the tab order as established by Slice 14.
### Open state
Activating the disclosure displays a menu immediately beneath it:
```text
┌─────────────┐
│ Backlog │
│ In Progress │
│ Done │
└─────────────┘
```
- The current destination is disabled or omitted consistently; disabling it is
preferred so all menus retain the same predictable destination order.
- Arrow keys move among enabled menu items.
- Enter or Space selects an item.
- Escape closes the menu and returns focus to the disclosure.
- Clicking outside closes the menu without submitting.
- Selecting a destination closes the menu, submits one move, and enters the
existing pending state.
- Only one move menu should be open within a rendered view at a time.
## Accessibility and focus contract
- The disclosure communicates expanded/collapsed state with `aria-expanded` and
identifies its menu with `aria-controls`.
- Use appropriate button/menu semantics and accessible destination names.
- Opening moves focus predictably to the first enabled destination, or another
documented deterministic item.
- Closing without selection restores focus to the disclosure.
- After a successful or rejected canonical refresh, restore focus to the moved
card's disclosure when the card remains visible.
- If sorting or pagination removes the card from the current batch, retain Slice
14's status-region focus fallback.
- Pending, success, stale, and failure announcements continue through the
existing live region.
- Read-only cards gain no new focus stops.
## Layout and styling constraints
- The closed control stack must occupy only the narrow upper-right card area and
must not reserve a full row beneath every card title.
- Card titles and detail text should regain the vertical space previously used
by the visible select.
- The open menu may overlay nearby content and must not resize every card or
Kanban column.
- The menu must remain legible with custom card foreground/background colours
and Joplin light/dark themes.
- Preserve card borders, hover detail, completed styling, glyphs, recurrence
marks, pagination, and column sizing.
- Avoid clipping the menu inside card, column, or notebook-view containers.
## Implementation plan
### Phase 1 — Menu markup and state
- [x] Remove the always-visible `.gtd-kanban-move-select` control.
- [x] Add an editable-only disclosure button beneath the drag handle.
- [x] Render a closed-by-default destination menu with fixed destinations.
- [x] Disable the current destination.
- [x] Ensure only one menu per rendered view is open at a time.
- [x] Close on selection, Escape, outside click, rerender, and cancellation.
#### Phase 1 completion record
- Replaced the permanent select with an editable-only disclosure button beneath
the unchanged pointer drag handle. Its fixed Backlog, In Progress, and Done
menu is hidden by default and disables the card's current destination.
- Per-view `openMoveMenu` state closes an older menu before opening another.
Selection, Escape, outside pointer action, pending submission, and canonical
rerender all remove the open state and document-level listener.
### Phase 2 — Keyboard, focus, and announcements
- [x] Add `aria-expanded`, `aria-controls`, accessible names, and menu semantics.
- [x] Implement deterministic arrow-key navigation and Enter/Space selection.
- [x] Restore focus to the disclosure after cancellation.
- [x] Restore focus to the moved card's disclosure after canonical refresh.
- [x] Preserve status fallback when the moved card is no longer visible.
- [x] Preserve Slice 14 live announcements and pending-state behavior.
#### Phase 2 completion record
- The disclosure is a named button with `aria-expanded`, `aria-controls`, and
`aria-haspopup`. The popup and destination buttons use menu/menuitem semantics.
- Arrow Down opens and focuses the first enabled item. Arrow Up/Down, Home/End,
native Enter/Space activation, Escape restoration, outside dismissal, and Tab
closure are implemented deterministically.
- Canonical refresh now restores focus to the disclosure rather than the removed
select. Slice 14's status fallback, live regions, duplicate guard, and pending
control disabling remain shared and unchanged.
### Phase 3 — Styling and regression
- [x] Stack the disclosure beneath the drag handle without restoring a full-width
control row.
- [x] Position the open menu beneath the disclosure without resizing cards.
- [x] Verify theme compatibility, custom colours, long titles, completed cards,
hover details, grouped boards, scrolling, and paginated columns.
- [x] Confirm pointer drag and normal card click/open behavior do not regress.
- [x] Confirm read-only Kanban, matrix, calendar, and Gantt DOM remain unchanged.
#### Phase 3 completion record
- Added a narrow absolute-positioned control stack and overlay menu. The compact
arrow consumes no full-width content row; the menu uses Joplin theme colours,
overlays neighboring content, and leaves card/column widths unchanged.
- Styling remains scoped to editable Kanban control classes. Shared card title,
detail, completion, click, pointer drag, grouping, scrolling, pagination,
read-only, matrix, calendar, and Gantt paths were not structurally changed.
### Phase 4 — Documentation, automation, and packaging
- [x] Update README.md keyboard-control wording and any screenshots/examples that
imply the select is permanently visible.
- [x] Update SPEC.md interaction and accessibility details.
- [x] Add the Slice 14a UI refinement to the unreleased CHANGELOG entry.
- [x] Run focused protocol/mutation/handler and practical rendering-contract
checks.
- [x] Run the complete Jest suite, TypeScript, webview syntax, whitespace, and
prohibited-reference audits.
- [x] Build and inspect a fresh production JPL without publishing or versioning.
#### Phase 4 completion record
- README, SPEC, and the unreleased 2.0.0 CHANGELOG entry now describe the compact
disclosure and closed-by-default keyboard menu. No screenshot in the repository
depicted the removed select.
- Added a practical source/CSS rendering-contract suite covering removal of the
permanent select, disclosure/menu semantics, fixed destinations, current-state
disabling, keyboard/outside dismissal markers, and overlay styling.
- Focused validation passed: 4 suites, 46 tests. Full validation passed: 20
suites, 298 tests. TypeScript with `--skipLibCheck`, webview syntax,
`git diff --check`, stale-select wording, and packaged-output prohibited-path/
planning-reference audits passed.
- `npm run dist` produced and the archive inspection verified
`publish/com.victorwiebe.joplin.plugin.gtd-calendar.jpl` (180,736 bytes,
embedded manifest version 1.0.0, SHA-256
`80ec9c230ab2dad2a3fb11cb0c0390403a84e6655d31e10af58ba7a665305318`).
The five expected files and compiled disclosure/menu/accessibility markers are
present. Nothing was versioned, published, pushed, or committed.
- No manual Joplin acceptance was performed; every Phase 5 item remains open.
### Phase 5 — Manual Joplin acceptance
- [x] Editable cards no longer show an always-visible **Move to…** select.
- [x] Every editable card shows the drag handle with a disclosure arrow directly
beneath it.
- [x] The menu is closed by default and opening one does not resize all cards.
- [x] Pointer activation opens the correct card's menu beneath the disclosure.
- [x] Keyboard activation, arrow navigation, Enter/Space selection, and Escape
cancellation work.
- [x] The current destination is unavailable and cannot submit.
- [x] Outside click closes the menu without writing.
- [x] Only one menu is open per rendered view.
- [x] Selecting every valid destination persists the same exact note/to-do state
accepted in Slice 14.
- [x] Focus returns to the disclosure after cancellation and canonical refresh;
the status fallback works when sorting/pagination hides the card.
- [x] Pending/error states, live announcements, and duplicate-request prevention
remain correct.
- [x] Pointer drag, click-to-open, hover details, pagination, grouped boards,
multiple blocks, long scrolling, custom colours, and completed styling do
not regress.
- [x] Read-only Kanban boards expose neither control; matrix, calendar, and Gantt
remain unchanged.
- [x] Record explicit user sign-off separately from automation.
#### Phase 5 acceptance record
- **PASSED — USER SIGN-OFF RECEIVED 2026-07-31.** After testing the inspected
Slice 14a JPL, the user reported the compact move-menu result was perfect and
asked that Slice 14a be marked complete and pushed.
- This manual acceptance is recorded separately from Phase 4 automation and
packaging. Slice 15 has not begun.
## Out of scope
- Changing Slice 14 transition semantics or persistence rules.
- Adding destinations, arbitrary tag editing, or free-form ordering.
- Moving cards between notebook groups or blocks.
- Matrix or Skeleton editing.
- Changing card content, dates, note type, or notebook ownership.
- Replacing pointer drag with the menu.
- Publishing or bumping the plugin version.
## Acceptance criteria
- Editable cards are visually quieter because the full-width move select is
absent while the move menu remains discoverable beneath the drag handle.
- Pointer and keyboard users can reach every valid destination through compact,
accessible controls.
- Slice 14's mutation safety, canonical refresh, errors, announcements, focus,
sorting, pagination, grouping, and read-only defaults do not regress.
- Automated validation, production packaging, and explicit manual Joplin
acceptance are completed and recorded separately.

View File

@ -1,64 +0,0 @@
# SLICE 2 — `notebook:` option for all gtd block types
> **State-saving rule:** update this file immediately after EVERY completed task (tick the checkbox, update Status and Resume notes) and whenever pausing for input — automatically, without being asked. State on disk must always match reality.
## Status
**COMPLETE** (2026-07-15). SLICE1 done; all tasks landed; 112/112 tests pass.
## Goal
Every view block (`gtd-calendar`, `gtd-kanban`, `gtd-matrix`, later `gtd-gantt`)
accepts `notebook: <name-or-path-or-id>` to root the folder scan at a specific
notebook, possibly outside the host note's tree. `scope:` then applies relative
to that notebook.
## Design (agreed with user 2026-07-13)
- Accept, in order of resolution:
1. A raw 32-char hex folder id.
2. A `Parent/Child/...` title path (case-insensitive match on titles).
3. A bare title — if unique, use it; if ambiguous, warn and fall back to the
host folder (consistent with the plugin's warn-don't-fail convention).
- Unknown notebook → warning + fall back to host folder.
- Folder titles are needed: extend `DataAdapter.getFolders()` fields to include
`title` (currently id/parent_id only).
## Tasks
- [x] Extend `RawFolder` + `joplinAdapter.getFolders()` with `title`. Done:
`RawFolder.title` is optional (`title?: string`) — folder scoping uses
id/parent_id only, so scope-only test fixtures don't need it; the real
adapter always requests `["id","parent_id","title"]`.
- [x] New `src/Gtd/resolveNotebook.ts` (pure): folders + spec → `{ folderId,
warning }`. Resolution order id → path → unique bare title; any miss warns
and falls back to the host folder. Also exports `parseNotebookOption`
(shared trim/null normaliser used by all three config parsers).
- [x] Add `notebook` key to parseCalendarConfig / parseKanbanConfig /
parseMatrixConfig (`string | null`, default null; "notebook" added to each
knownKeys list).
- [x] Wire in `src/index.ts`: `resolveScanFolder()` helper resolves the notebook
(or returns host folder) and pushes any warning onto `config.warnings`;
all three handlers pass the resolved id to their collector.
- [x] Tests: `src/tests/Gtd/resolveNotebook.test.ts` — resolver (id, unknown id,
unique title, ambiguous title, missing title, path, path-disambiguation,
unresolvable path, empty), `parseNotebookOption`, config parsing for all
three views, and a collector-level integration (resolved notebook reroots
collectEvents' scan). 15 new cases.
- [x] Docs: README option tables (all three views) + SPEC.md §2.1/§2.3/§2.4
(row + example lines). Workspace planning files remain git-excluded.
- [x] Full test run: **112 passed, 112 total** (10 suites) on 2026-07-15.
`npx tsc --noEmit` clean for `src/` (only pre-existing api/ + node_modules
lib noise).
## Resume notes
SLICE2 complete. Files touched:
- `src/Gtd/types.ts` (`RawFolder.title?`, `notebook` on all three configs)
- `src/Gtd/resolveNotebook.ts` (new; resolver + `parseNotebookOption`)
- `src/Gtd/parseCalendarConfig.ts`, `parseKanbanConfig.ts`, `parseMatrixConfig.ts`
(notebook key + knownKeys)
- `src/index.ts` (getFolders title field, `resolveScanFolder`, 3 handler wirings)
- `src/tests/Gtd/resolveNotebook.test.ts` (new)
- `README.md` (3 tables), `SPEC.md` (§2.1 row + 3 example lines)
Design note: notebook resolution lives in `index.ts` (needs the folder tree),
so collectors stay unchanged — they still take a plain `folderId`. When
`notebook:` is set, `getFolders()` is fetched once in the handler and again
inside the collector; acceptable (only on notebook use), could be deduped later
if it matters. Next: SLICE3 (`scope: all`).

View File

@ -1,75 +0,0 @@
# SLICE 3 — `scope: all` (every notebook)
> **State-saving rule:** update this file immediately after EVERY completed task (tick the checkbox, update Status and Resume notes) and whenever pausing for input — automatically, without being asked. State on disk must always match reality.
## Status
**COMPLETE** (2026-07-15). SLICE1 & SLICE2 done; all tasks landed; 120/120
tests pass; full `npm run dist` builds clean.
## Goal
`scope: all` on any view block scans every notebook in the Joplin profile.
User explicitly wants this despite the cost ("very dangerous, yes").
## Design
- `resolveScopedFolderIds` gains an "all" path: return every folder id
(skip BFS entirely). Config parsers map `scope: all` → a sentinel
(e.g. scopeDepth = -1 or a separate `scopeAll: boolean`) — decide during
implementation, keep the type honest.
- Guardrails (agreed):
- Prominent "scanned N notebooks / M notes" footnote when scope is `all`
(stats plumbing already exists in every payload).
- Body-fetch optimization from SLICE1 applies.
- Soft warning in the rendered view when scanned notes exceed ~2000
(advisory only, never blocks).
- `notebook:` + `scope: all` together: `all` wins; warn about the ignored
`notebook:` key.
## Design decisions (as implemented)
- Representation: a separate **`scopeAll: boolean`** on each config (not a
`scopeDepth` sentinel) — `all` is not a tree depth, so an honest flag reads
cleaner and keeps `scopeDepth` meaningful.
- Scope resolution lives in `folderScope.ts`: `resolveScopedFolderIds` gains a
4th `scopeAll = false` param; when true it returns `folders.map(id)` and skips
BFS entirely (root/depth ignored).
- `notebook:` + `scope: all`: parser emits a warning and `all` wins;
`resolveScanFolder` short-circuits to the host folder (root is irrelevant when
every notebook is scanned).
- Soft cap: `index.ts` pushes a ⚠ warning when `scopeAll` and scanned notes
> 2000 (`SCOPE_ALL_SOFT_CAP`), advisory only. Footnote: payloads carry
`scopeAll`; the webview appends `· scope: all (every notebook)` to the existing
stats meta line in all three render paths.
## Tasks
- [x] Parser changes (all three view types) + warnings for conflicts. Done:
`scope: all``scopeAll=true` in calendar/kanban/matrix parsers;
notebook+all conflict warning in each.
- [x] `folderScope.ts` all-folders path + tests. Done (4th param; collectors
pass `config.scopeAll`).
- [x] Footnote + soft-cap warning in webview render paths. Done: `scopeAll` on
payloads + meta-line marker (3 paths); soft-cap warning via `warnings[]`
(rendered as ⚠, no extra webview work — existing warning path).
- [x] Tests: parser, scope resolution, collector with multi-root folders.
`src/tests/Gtd/scopeAll.test.ts` — parser (all three + conflict + no-false-
positive), folderScope scopeAll path + unchanged non-all behaviour, and a
multi-root collector integration proving `all` spans disconnected
notebooks. 8 new cases.
- [x] Docs: README (3 scope rows + calendar caution) + SPEC.md §2.1 row and all
three example blocks, including the performance/guardrail caution.
- [x] Full test run: **120 passed, 120 total** (11 suites) on 2026-07-15.
`npm run dist` builds clean; archive rule confirmed working.
## Resume notes
SLICE3 complete. Files touched:
- `src/Gtd/types.ts` (`scopeAll` on all three configs)
- `src/Gtd/folderScope.ts` (scopeAll param + all-folders path)
- `src/Gtd/parseCalendarConfig.ts`, `parseKanbanConfig.ts`, `parseMatrixConfig.ts`
(scope: all + notebook-conflict warning; kanban/matrix now parse notebook
before the return so the conflict check has it)
- `src/Gtd/collectEvents.ts`, `collectKanban.ts`, `collectMatrix.ts` (pass scopeAll)
- `src/index.ts` (`resolveScanFolder` short-circuit, `SCOPE_ALL_SOFT_CAP` +
`scopeAllSoftCapWarning`, `scopeAll` on 3 payloads)
- `src/gtd-calendar-webview.js` (footnote marker in 3 stats blocks)
- `src/tests/Gtd/scopeAll.test.ts` (new)
- `README.md`, `SPEC.md`
Next: SLICE4.

View File

@ -1,86 +0,0 @@
# SLICE 4 — gtd-gantt data layer
> **State-saving rule:** update this file immediately after EVERY completed task (tick the checkbox, update Status and Resume notes) and whenever pausing for input — automatically, without being asked. State on disk must always match reality.
## Status
**COMPLETE** (2026-07-15). Pure data layer landed; 140/140 tests pass;
`tsc --noEmit` clean. No rendering/index wiring yet — that is SLICE5.
Resolved open question (2026-07-15): dateless/invalid-date gantt items are
**excluded with a warning** (the recorded default) — no Unscheduled strip. If a
strip is wanted later it's an additive change to collectGantt + the payload.
## Goal
Pure data layer for the gantt: item opt-in parsing, project grouping,
collection, config parsing. No rendering yet.
## Design (agreed with user 2026-07-13)
- Items opt in with their own ```gtd-gantt fenced block (separate from ```gtd):
- **Notes** (bars): `project: bluesky`, `begin-date: yyyy-mm-dd`,
`end-date: yyyy-mm-dd` (accept same date formats as resolveDate.ts).
- **To-dos** (milestones): `project: bluesky`; the milestone date is the
to-do's native **due date**. begin/end on a to-do → warning, treated as
milestone anyway.
- Rows/swimlanes are grouped by `project:`.
- **Chart block vs item block disambiguation** (same fence name!): a
`gtd-gantt` block containing a `project:` key is an ITEM declaration;
without `project:` it is the CHART config block. The markdown-it renderer
must apply the same rule (SLICE5). Item blocks render as a small inline
badge ("gantt: bluesky"), not a chart.
- Chart config keys: `title`, `notebook`, `scope`, `filter-project`
(optional; limit to one project), `sort` / `sort-type` (row order within
project), `card-detail` semantics for hover text. Defaults follow the
existing parsers' conventions; invalid values warn + fall back.
- Semantics: completed to-do milestones render checked/struck (consistent
with calendar). Items with unparseable/missing dates → warning +
excluded (a gantt has no Unscheduled section — confirm with user if one
is wanted; see open questions).
- Reuse styling keys? Item gtd-gantt block also accepts `bg-colour`,
`fg-colour`, `title`, `text` mirroring the gtd block.
## Open questions for user (non-blocking, defaults chosen)
- Dateless gantt items: currently planned as warn+exclude. Alternative:
an "Unscheduled" strip like the calendar. DEFAULT: exclude with warning.
## Tasks
- [x] `src/Gtd/ganttBlock.ts``extractGanttBlock` mirrors gtdBlock.ts
(fence regex for `gtd-gantt`, same YAML/empty/malformed handling). Parses
project / begin-date / end-date + styling keys (bg-colour/fg-colour/title/
text). Classification is the caller's job via `block.project` (non-null ⇒
item, null ⇒ chart).
- [x] `src/Gtd/parseGanttConfig.ts` — chart config: title, scope (+ scope: all
+ notebook conflict warning), notebook, filter-project, sort, sort-type
(`begin-date`|`title`|`modified-date`), card-detail. Invalid → warn + default.
- [x] `src/Gtd/collectGantt.ts` — scope (scopeAll-aware) → fetch (always with
body) → item filter (project present, ≠ chart) → filter-project → bar vs
milestone → group by project → sort → compute range. Returns typed
`GanttChart { projects, rangeStart, rangeEnd, warnings, scannedFolders,
scannedNotes, itemCount }`.
Rules: notes → bars (begin+end required & valid, end≥begin else warn+exclude);
to-dos → milestones on their due date (begin/end on a to-do → warning;
no due date → warn+exclude); completed/recurring flags on milestones;
projects merged case-insensitively, output alphabetical.
- [x] Types in `src/Gtd/types.ts` — GanttSortType, GanttItemBlock,
GanttBlockResult, GanttConfig, GanttBar, GanttMilestone, GanttProject,
GanttChart.
- [x] Thorough unit tests — `src/tests/Gtd/gantt.test.ts` (20 cases): block
extraction (chart vs item, styling, Date coercion, malformed), config
(defaults/full/conflict/invalid), collector (grouping+range, ignore
chart/plain/host, bar bad/reversed dates, milestone due-date + begin/end
warning, milestone no-due-date, completed+recurring, filter-project,
begin-date sort + direction, case-insensitive project merge).
- [x] Full test run: **140 passed, 140 total** (12 suites) on 2026-07-15.
`tsc --noEmit` clean for src/.
## Resume notes
SLICE4 complete — pure data layer only. New files:
- `src/Gtd/ganttBlock.ts`, `parseGanttConfig.ts`, `collectGantt.ts`
- gantt types appended to `src/Gtd/types.ts`
- `src/tests/Gtd/gantt.test.ts`
NOT done here (belongs to SLICE5): the markdown-it renderer's chart-vs-item
disambiguation, the `getGantt` message handler in `src/index.ts`, and any
webview rendering. `collectGantt`'s signature mirrors the other collectors
(`adapter, hostNoteId, hostFolderId, config`) so index wiring will look like
handleGetKanban. Item blocks must render as a small inline badge (not a chart) —
that's a SLICE5 renderer concern. Next: SLICE5.

View File

@ -1,72 +0,0 @@
# SLICE 5 — gtd-gantt rendering
> **State-saving rule:** update this file immediately after EVERY completed task (tick the checkbox, update Status and Resume notes) and whenever pausing for input — automatically, without being asked. State on disk must always match reality.
## Status
**COMPLETE** (2026-07-15). All rendering code landed; 140/140 unit tests pass;
`tsc` clean; `npm run dist` builds the `.jpl`. User manually verified in Joplin
desktop. README + SPEC gantt docs added (beyond the original task list, at user
request). No automated front-end tests (per the earlier webview-testing
decision; disambiguation mirrors the unit-tested collectGantt).
## Goal
Render the gantt in the webview: fence interception, message plumbing, DOM, CSS.
## Tasks
- [x] `src/gtd-calendar-renderer.ts`: intercepts `gtd-gantt` fences. A block with
a non-empty `project:` value (cheap regex `GANTT_PROJECT_LINE`, no YAML
lib) → inline `<span class="gtd-gantt-badge">gantt: <project></span>`;
otherwise emits the chart placeholder (`data-block-type="gantt"`). Project
value is HTML-escaped and unquoted. Empty-project alignment matches
collectGantt (empty ⇒ chart).
- [x] `src/index.ts`: `handleGetGantt` + `getGantt` switch case, mirroring
handleGetKanban (parse → selectedNote → resolveScanFolder → collectGantt →
payload with title/cardDetail/scopeAll/configError/warnings+softcap/chart/
stats). No-selectedNote path returns an empty chart.
- [x] `src/gtd-calendar-webview.js`: `renderGantt` + `buildGanttGrid` and helpers
(offsetDays, clampOffset, monthTicks, todayLocalISO, tooltips). Single CSS
grid: label column (sticky) + `repeat(totalDays, 26px)`; month tick header
row; project header rows; bars via `grid-column` from day offsets (bg/fg
colour or `colourFromId` hash); milestones as ◆ markers (done → dimmed +
struck label, ↻ for recurring); today line spanning body rows; click →
openNote; hover text via native `title` (suppressed when card-detail:none);
title/configError/warnings/stats blocks as in other views (stats footnote
carries the scope: all marker).
- [x] `src/event-calendar.css`: `.gtd-gantt-*` styles (badge, scroll container,
grid, sticky corner/labels, ticks, bars, milestones, today line), using
the same rgba/theme-var conventions as existing styles.
- [x] Manual verification in Joplin desktop — CONFIRMED 2026-07-15. Verified
live: item-block badge (`gantt: bluesky`) renders inline (not a chart);
chart renders with title, month tick header, alphabetical project header
(`bluesky (3)`), a note **bar** spanning begin→end (Jul 114), ◆
**milestones** on to-do due dates, and the red **today** line at Jul 15.
Footnote reports item/note/folder counts. Host-note exclusion confirmed
(an item block in the chart's own note is correctly omitted — put items in
separate notes). Unknown-option warning fires correctly for stray keys like
`todos` (gantt has no notes/todos inclusion option).
- [x] Docs (added at user request; not in the original task list): README gantt
section (item vs chart, bar/milestone rules, chart options table) + SPEC.md
§2.5 (dual-role disambiguation rule, item/chart schemas). Screenshots not
committed — verified interactively instead.
## Resume notes
Code complete. Files touched:
- `src/gtd-calendar-renderer.ts` (gantt fence + badge/chart disambiguation)
- `src/index.ts` (handleGetGantt + switch case + imports)
- `src/gtd-calendar-webview.js` (dispatch + renderGantt/buildGanttGrid + helpers)
- `src/event-calendar.css` (.gtd-gantt-* styles)
Design choices worth noting for a resumed session:
- Grid layout is a single `display:grid` with explicit gridRow/gridColumn per
element (label col = 1, day columns = 2..N+1). Bars: `grid-column: off+2 /
endOff+3` (inclusive end). Milestones: `off+2 / span 1`. Today line:
`grid-row: 1 / <rowIndex>` at the today column.
- Hover uses native `title` tooltips (not the calendar's hover-card DOM) — simpler
and reliable; revisit if richer hover cards are wanted. card-detail:none
suppresses tooltips; "always" currently behaves like "hover" (no inline text on
bars) — a possible future refinement, noted but out of scope.
- No automated renderer/webview tests by design (front-end testing deferred, per
the SLICE-boundary discussion). Disambiguation regex mirrors the unit-tested
collectGantt classification.
Next after manual sign-off: SLICE6.

View File

@ -1,55 +0,0 @@
# SLICE 6 — Polish, docs, full verification
> **State-saving rule:** update this file immediately after EVERY completed task (tick the checkbox, update Status and Resume notes) and whenever pausing for input — automatically, without being asked. State on disk must always match reality.
## Status
**COMPLETE** (2026-07-18). Docs, CHANGELOG, roadmap, consistency sweep, and
tests all done. Cross-view end-to-end pass in Joplin signed off by the user:
dashboard note stacking gtd-calendar + gtd-kanban + gtd-matrix (skeleton &
eisenhower) + gtd-gantt all render independently; scope/depth, todos: all,
colour/icon/title/text overrides, recurring ↻, kanban states, matrix quadrants,
gantt bars/milestones (incl. to-do begin/end-ignored warning), dual-role note,
drilldown/hover, and invalid-config fallback warnings all verified. All spot
checks pass. → proceed to SLICE7 (final review / release prep).
Versioning decision (2026-07-15): **split** into 0.6.0 (notebook + scope: all)
and 0.7.0 (gantt). **0.6.0 is functionally COMPLETE** (notebook targeting +
scope: all + body-fetch, verified in the 2026-07-18 E2E pass); 0.7.0 (gantt) is
likewise complete. Note: the actual manifest/package version bump + publish is
the separate release step NOT part of this slice —
current version on disk is still 0.5.0.
## Tasks
- [x] README.md: "Gantt chart" section (item-block + chart options tables) added
(during SLICE5, at user request). `notebook:`/`scope: all` rows in all view
tables + `all` performance caution were landed in SLICE2/SLICE3. Screenshots
not committed (verified interactively).
- [x] SPEC.md: §2.5 gtd-gantt (chart + item schema, dual-role rule) added in
SLICE5; notebook/scope-all semantics in §2.1 (SLICE2/3); §9 roadmap updated
here with SHIPPED v0.6.0 (notebook + scope: all + body-fetch) and v0.7.0
(gantt) sections.
- [x] CHANGELOG.md: 0.7.0 (Gantt) and 0.6.0 (notebook + scope: all) entries added
per the split decision.
- [x] Consistency sweep: verified notebook-conflict warning wording identical
across all 4 parsers; `scope: all` handled in all 4; card-detail/unknown-
option wording consistent; doc default tables match parser defaults.
Pre-existing note (NOT changed — out of scope, avoids behaviour change): the
calendar's `sort-type` uses `modified_date` (underscore) while kanban/matrix/
gantt use `modified-date` (hyphen); this cross-block spelling difference is
already documented in SPEC §9's reconciliation note.
- [x] Full jest run green: **140 passed, 140 total** (12 suites), up from 88 at
the start of this effort.
- [x] End-to-end pass in Joplin desktop across all four view types, including a
dashboard note stacking all four blocks. **Signed off by user 2026-07-18;
all spot checks pass.** publish/*.jpl (built SLICE5) is current — docs-only
changes since don't affect the bundle.
## Resume notes
SLICE6 fully COMPLETE as of 2026-07-18 — E2E signed off by the user (dashboard
note stacking all four view types rendered independently; all spot checks pass).
Files touched here: `CHANGELOG.md` (0.6.0 + 0.7.0 entries), `SPEC.md` (§9 roadmap
SHIPPED sections). README/SPEC gantt schema + notebook/scope docs landed earlier.
Next: SLICE7 (final review / release prep). Release version bump (0.6.0/0.7.0)
and publish are deferred to the separate release step, NOT this
slice — version on disk is still 0.5.0.

View File

@ -1,58 +0,0 @@
# SLICE 7 — Release
> **State-saving rule:** update this file immediately after EVERY completed task (tick the checkbox, update Status and Resume notes) and whenever pausing for input — automatically, without being asked. State on disk must always match reality.
## Status
**COMPLETE** (2026-07-18) — v0.7.0 published to npm (registry confirms
`latest: 0.7.0`) and manual sanity-open in Joplin desktop signed off by user.
All seven slices DONE. Nothing outstanding.
**Release-cut decision made with user (2026-07-18): ONE release, tagged 0.7.0.**
Rationale: gantt is woven through all four shared files (73 refs — 33 webview,
19 types.ts, 12 renderer, 9 index.ts), so a gantt-free 0.6.0 would be error-prone
surgery on an intermediate nobody tested; the 2026-07-18 E2E sign-off was on the
combined state. So npm goes 0.5.0 → 0.7.0 (0.6.0 not separately published); the
CHANGELOG keeps BOTH the 0.7.0 (gantt) and 0.6.0 (notebook/scope) entries as a
documentation split. Version bumped to 0.7.0 in src/manifest.json + package.json.
## Tasks
- [x] Decide with user: **ONE release, tagged 0.7.0** (see Status). CHANGELOG keeps
both 0.6.0 + 0.7.0 entries as documentation.
- [x] Bump `version` in BOTH `src/manifest.json` and `package.json` → 0.7.0 (match).
- [x] CHANGELOG.md entry finalized (0.7.0 at top, both feature entries present —
written in SLICE6).
- [x] **Archived existing publish/*.jpl first** — predist (scripts/archive-jpl.sh)
moved it to build-archive/com.victorwiebe...-0.7.0-20260718-115536.jpl
(no overwrite; three 0.5.0 archives preserved). NB the archive filename is
stamped with the *current* package.json version (0.7.0), not the version the
archived jpl was built at — cosmetic, git-ignored dir, left as-is.
- [x] `npm run dist` — GREEN. jest 140/140 first. New publish/*.jpl (156 KB)
embeds version 0.7.0 (verified in publish/*.json).
- [x] Sanity-open the .jpl in Joplin desktop — **DONE** (2026-07-18): user
completed manual visual check, signed off.
Path: publish/com.victorwiebe.joplin.plugin.gtd-calendar.jpl
- [x] `npm publish`**DONE** (2026-07-18): user re-logged in and published.
Registry (https://registry.npmjs.org/) confirms `latest: 0.7.0`.
- [x] **Committed + pushed to Gitea** (2026-07-18): commit 187fd34
"v0.7.0: Gantt chart, notebook targeting & profile-wide scope" (30 files,
+2300/-40), pushed 9780d95..187fd34 master->master. Per user: `.npmrc`
(browser=true) was included. workspace planning files and build-archive/ correctly
excluded from the release commit. NB: no git tag created — tagging was abandoned after 0.3.0 (0.4.0
& 0.5.0 untagged); left consistent with recent practice.
- [x] Mark all SLICE files DONE; summarize outcome here. **DONE**
SLICE16 already COMPLETE; SLICE7 now COMPLETE (this file).
## Outcome
2026-07-18: **v0.7.0 shipped.** The full slice effort (notebook targeting,
profile-wide `scope: all`, and the gtd-gantt view) is released as a single
0.7.0 (0.6.0 documented in CHANGELOG but never separately published, per the
one-release decision). Final state:
- Version 0.7.0 in src/manifest.json + package.json (match).
- Committed + pushed to Gitea (commit 187fd34, master).
- `npm run dist` green, 140/140 tests, clean 0.7.0 jpl (embedded version verified).
- **Published to npm — registry confirms `latest: 0.7.0`.**
- Manual sanity-open in Joplin desktop signed off by user.
All seven slices DONE. No follow-ups outstanding.
## Resume notes
Nothing to resume — SLICE17 all COMPLETE, v0.7.0 live on npm.

275
SLICE8.md
View File

@ -1,275 +0,0 @@
# SLICE 8 — Incremental card limits for kanban and matrix views
> **State-saving rule:** update this file and `TASKS.md` after every completed
> task and whenever work pauses. Automated checks and manual Joplin acceptance
> must be recorded separately.
## Status
**COMPLETE (2026-07-27). All seven phases passed, including automated validation, production packaging, documentation, and user-confirmed manual Joplin acceptance.**
## Goal
Keep large kanban columns and matrix quadrants readable by rendering cards in
configurable batches. Every bucket initially shows 10 cards by default. A local
"List more" control reveals the next batch without changing the source note or
persisting UI state. Reloading the rendered note resets all buckets.
## Confirmed behavior
- Add `page-size:` to both `gtd-kanban` and `gtd-matrix` blocks.
- Default: `10`.
- Valid values: positive integers only.
- Invalid values produce a warning and fall back to 10.
- Apply the limit independently after existing filtering and sorting to:
- Backlog, In Progress, and Done on kanban boards.
- All four quadrants in both matrix modes.
- "List more" reveals the next `page-size` entries in only that bucket.
- Show the control only when hidden entries remain.
- Hovering or focusing the control shows the exact number of additional entries
remaining, for example `13 more entries`.
- Clicking updates the remaining count and removes the control after the final
batch.
- Expansion exists only in the current webview DOM. It is not written to the
note, settings, plugin process, or project data.
- Column/quadrant headings continue to show total card counts, not visible counts.
- Collector results and overall statistics continue to represent all cards.
## Architecture and data flow
```text
gtd-kanban / gtd-matrix YAML
|
v
parseKanbanConfig / parseMatrixConfig
validates page-size -> normalized pageSize
|
v
handleGetKanban / handleGetMatrix
returns pageSize alongside the complete sorted board
|
v
renderKanban / renderMatrix
gives each column or quadrant its own visibleCount
|
v
renderIncrementalCardList
renders slice(0, visibleCount), remaining summary, and List more
```
The collectors (`collectKanban.ts` and `collectMatrix.ts`) must not paginate or
truncate. Keeping pagination in the webview avoids corrupting totals, changing
sort semantics, or requiring another plugin-process request on every click.
## Implementation plan
### Phase 1 — Configuration contract
- [x] Add `pageSize: number` to `KanbanConfig` and `MatrixConfig` in
`src/Gtd/types.ts`.
- [x] Add `page-size` to each parser's known-key list.
- [x] Normalize omitted `page-size` to 10.
- [x] Accept numeric or YAML numeric-string values only when they resolve to a
finite positive integer.
- [x] Warn with consistent wording for zero, negative, fractional, non-numeric,
`NaN`, or infinite values and use 10.
- [x] Add parser tests in `src/tests/Gtd/kanban.test.ts` and
`src/tests/Gtd/matrix.test.ts` for:
- omitted/default value;
- `1`, `10`, and a larger valid override;
- numeric-string compatibility;
- zero, negative, fractional, and non-numeric fallback;
- recognition as a known key (no unknown-option warning).
## Progress log
- 2026-07-27 — Phase 1 complete. Added shared `page-size` normalization and
required `pageSize` fields for kanban/matrix config. Focused validation:
2 suites passed, 59/59 tests. Full validation: 12 suites passed, 164/164
tests. No manual testing applies yet because the value is not sent to or used
by the webview until later phases.
### Phase 2 — Plugin-to-webview payload
- [x] Add `pageSize: config.pageSize` to successful kanban and matrix responses
in `src/index.ts`.
- [x] Add the same normalized value to missing-source-note/error responses so
rendering behavior is structurally consistent.
- [x] Do not change collector arguments, result shapes, sorting, card totals, or
scope behavior.
- 2026-07-27 — Phase 2 complete. Added normalized `pageSize` to successful
and missing-source-note responses for both kanban and matrix handlers. No
collector or rendering changes. Full Jest validation: 12 suites passed,
164/164 tests. `npx tsc --noEmit --skipLibCheck` passed; the unmodified
command remains blocked by unresolved generated Joplin declarations and the
repository's Jest/TypeScript library mismatch. No manual testing applies yet.
### Phase 3 — Shared incremental renderer
- [x] Add one shared helper in `src/gtd-calendar-webview.js`, tentatively
`renderIncrementalCardList(cards, pageSize, detail, contentScriptId)`.
- [x] Keep `visibleCount` in the helper's closure, initialized to
`Math.min(pageSize, cards.length)`.
- [x] Render cards through the existing `renderCard` function so styling and
click-to-open behavior remain unchanged.
- [x] On each click, increase `visibleCount` by `pageSize`, capped at the full
card count, then render only the newly revealed cards or refresh the local
list without affecting other buckets.
- [x] Reuse the helper from `renderColumn` and `matrixQuadrant`; pass
`payload.pageSize || 10` from both top-level renderers as a defensive
fallback for stale payloads.
- [x] Preserve the existing total-count text in every column/quadrant heading.
- 2026-07-27 — Phase 3 complete. Added one incremental card-list helper and
wired it to all three kanban columns and all four matrix quadrants. Each
bucket owns independent closure state, initially renders one batch, appends
one batch per click, removes the basic control when complete, and preserves
total heading/stat counts. Validation: JavaScript syntax and `git diff --check`
passed, `npx tsc --noEmit --skipLibCheck` passed, and 12 Jest suites passed
with 164/164 tests. Full-file formatting was intentionally not applied because
the legacy webview does not match the default formatter and doing so rewrites
roughly 2,000 unrelated lines. Tooltip, accessibility association, and final
CSS remain Phase 4; manual Joplin acceptance is deferred until then.
### Phase 4 — Control, summary popup, and accessibility
- [x] Render "List more" as a real `button` with `type="button"`.
- [x] Place the button outside card elements so its click cannot bubble into a
card's note-open handler.
- [x] Add a dedicated tooltip element whose text is generated from the remaining
count with correct singular/plural wording (`1 more entry`, `N more entries`).
- [x] Associate the button and tooltip with `aria-describedby` using a unique ID.
- [x] Show the tooltip on both `:hover` and `:focus-visible`; do not rely only on
the native `title` attribute.
- [x] Add focused styles in `src/event-calendar.css` for the control, tooltip,
positioning, keyboard focus, and light/dark theme compatibility.
- [x] Update or remove the button and tooltip atomically after every expansion.
- 2026-07-27 — Phase 4 complete. Added a real button, unique tooltip IDs,
`aria-describedby`, exact singular/plural remaining counts, an updating
accessible label, and pointer-hover/keyboard-focus popup behavior. New cards
insert before the control so focus survives intermediate expansion; the
control is removed only after the final batch. Added scoped, theme-aware CSS.
Validation: JavaScript syntax, `npx tsc --noEmit --skipLibCheck`, and
`git diff --check` passed; 12 Jest suites passed with 164/164 tests. Visual
and keyboard behavior still requires manual Joplin acceptance after a current
package is built.
### Phase 5 — Automated verification
The current Jest configuration uses `jest-environment-node` and the webview is a
browser IIFE with no DOM-test harness. This slice will not add a new DOM runtime
solely for one interaction. Automated coverage will therefore focus on the
stable configuration contract, while DOM behavior receives explicit manual
acceptance. If implementation exposes a genuinely reusable pure batching helper
without duplicating browser logic, add focused unit tests for it; otherwise do
not create a test-only abstraction.
- [x] Run focused parser tests for kanban and matrix configuration.
- [x] Run the complete Jest suite and record suite/test totals.
- [x] Run `npm run dist`, which includes the repository's full test/build/package
workflow, and record the produced `.jpl` path.
- [x] Run `git diff --check`.
- [x] Re-run the repository-wide prohibited-reference audit.
- 2026-07-27 — Phase 5 complete. Focused kanban/matrix validation passed
2/2 suites and 59/59 tests; the full suite passed 12/12 suites and 164/164
tests. JavaScript syntax, `npx tsc --noEmit --skipLibCheck`, and
`git diff --check` passed. `npm run dist` archived the previous package to
`build-archive/com.victorwiebe.joplin.plugin.gtd-calendar-0.7.0-20260727-150417.jpl`
and created `publish/com.victorwiebe.joplin.plugin.gtd-calendar.jpl`
(158,720 bytes, manifest version 0.7.0). The JPL tar contains all five expected
runtime files, and its webview contains the `pageSize` and tooltip markers.
Both prohibited-reference audits returned no matches. No browser-only test
abstraction was added because the batching helper remains a DOM-owning closure;
its visual and keyboard behavior stays within explicit manual acceptance.
### Phase 6 — Documentation
- [x] Add `page-size` to the kanban and matrix option tables in README.md.
- [x] Add examples showing the default and a custom batch size.
- [x] Explain that expansion is per bucket, advances one batch per click, and
resets on reload.
- [x] Update SPEC.md with config validation, rendering ownership, and transient
state semantics.
- [x] Add an unreleased SLICE8 entry to CHANGELOG.md without changing the package
version until release scope is decided.
- 2026-07-27 — Phase 6 README work complete. Added `page-size` to both
option tables, an explicit default kanban example, a custom matrix example,
and the per-bucket expansion, remaining-count popup, full-total, and reload
reset behavior. Updated the README test count to the current 164. README diff
validation passed. SPEC.md and CHANGELOG.md were completed after Phase 7
acceptance; Phase 6 and SLICE8 are now complete.
### Phase 7 — Manual Joplin acceptance
**PASSED — USER SIGN-OFF RECEIVED 2026-07-27.** All manual checks below
passed against the Phase 5 package.
Use a test dashboard that includes a kanban, Skeleton matrix, and Eisenhower
matrix. Prepare buckets containing 0, 1, 10, 11, 20, and 21+ cards.
- [x] Confirm 010 cards show no "List more" control.
- [x] Confirm 11 cards show 10 initially and reveal the final card with one click.
- [x] Confirm 21+ cards expand by exactly 10 per click with the default.
- [x] Confirm a custom `page-size` changes both initial and subsequent batches.
- [x] Confirm the hover and keyboard-focus popup always reports the exact number
still hidden.
- [x] Confirm expansion in one column/quadrant does not alter another.
- [x] Confirm multiple blocks in one note maintain independent state.
- [x] Confirm card order, styling, hover detail, recurrence glyphs, completion
styling, and click-to-open behavior remain unchanged.
- [x] Confirm headings and overall statistics retain full totals.
- [x] Confirm reloading the rendered note resets every expanded bucket.
- [x] Confirm invalid `page-size` values warn and render with the default of 10.
- [x] Record user sign-off here; do not mark manual acceptance complete before
confirmation. **Confirmed by user 2026-07-27: all passed.**
- 2026-07-27 — User reported "all passed" for the complete Phase 7 checklist.
## Acceptance criteria
SLICE8 is complete only when:
- Every kanban column and matrix quadrant initially renders no more than its
normalized `page-size`.
- Every click reveals at most one further batch in only the selected bucket.
- The remaining-count popup is exact and usable with pointer and keyboard.
- Fully expanded buckets have no "List more" control.
- Reloading resets expansion.
- Full card collections, ordering, headings, statistics, navigation, styling,
and warnings remain correct.
- Focused tests, the full suite, and the production package build pass.
- Manual Joplin acceptance is explicitly confirmed.
## Files expected to change
- `src/Gtd/types.ts`
- `src/Gtd/parseKanbanConfig.ts`
- `src/Gtd/parseMatrixConfig.ts`
- `src/index.ts`
- `src/gtd-calendar-webview.js`
- `src/event-calendar.css`
- `src/tests/Gtd/kanban.test.ts`
- `src/tests/Gtd/matrix.test.ts`
- `README.md`
- `SPEC.md`
- `CHANGELOG.md`
- `SLICE8.md`
- `TASKS.md`
## Out of scope
- Server-side pagination or lazy fetching from Joplin.
- Persisting expanded state across reloads.
- A global "expand all" control.
- Limits for calendar, unscheduled, or Gantt views.
- Notebook grouping from SLICE10.
- Inclusion of normal note cards from SLICE9.
## Outcome and resume point
SLICE8 is complete. `page-size` is implemented for kanban and matrix views, the
current production JPL is in `publish/`, all 164 automated tests pass, and manual
Joplin acceptance is signed off. Nothing remains in this slice; proceed to SLICE9.

347
SLICE9.md
View File

@ -1,347 +0,0 @@
# SLICE 9 — Include opted-in note cards in kanban and matrix views
> **State-saving rule:** update this file and `TASKS.md` after every completed
> task and whenever work pauses. Automated checks and manual Joplin acceptance
> must be recorded separately.
## Status
**COMPLETE.** Phases 17 are complete. Automated validation, production packaging, and manual Joplin acceptance all passed.
## Goal
Allow ordinary Joplin notes (`is_todo: 0`) containing a `gtd` fenced block to
appear as cards in kanban and matrix views. Preserve existing to-do behavior and
continue excluding normal notes that have not explicitly opted in.
## Confirmed behavior
- A normal note is eligible only when `extractGtdBlock` reports `found: true`.
- An empty `gtd` block is a valid opt-in, matching existing calendar semantics.
- A malformed `gtd` block still opts the note in and surfaces the existing
warning; fallback card properties come from the source note.
- The existing `todos:` option continues to govern only to-dos. It does not
disable or broaden normal-note inclusion.
- No new `notes:` option is introduced in this slice.
- Kanban placement for normal notes:
- configured in-progress tag -> In Progress;
- otherwise -> Backlog;
- never -> Done.
- Matrix placement for normal notes:
- Eisenhower mode uses the existing important and urgent tags;
- Skeleton mode uses the existing in-progress tag plus urgent tag/date rules.
- A note date comes only from the `gtd` block. To-do date resolution remains
block date first, then `todo_due`.
- Note cards reuse block title, colours, icon, and text; missing values fall back
exactly as existing cards do.
- Normal notes are never completed and never show the recurring-to-do marker,
even if they happen to carry the configured recurrence tag.
- The host kanban/matrix note remains excluded from its own results.
- SLICE8 batching counts note and to-do cards together after final sorting.
## Inclusion decision table
| Item | `gtd` block | `todos:` | Kanban | Matrix |
|---|---:|---|---|---|
| Normal note | absent | any | excluded | excluded |
| Normal note | present/empty/malformed | any | included | included |
| To-do | absent | `gtd-only` | excluded | excluded |
| To-do | absent | `all` | included | included if incomplete |
| To-do | present | `gtd-only` or `all` | included | included if incomplete |
| To-do | any | `none` | excluded | excluded |
| Completed to-do | any included mode | any | Done-window rules | excluded |
## Architecture and data flow
```text
scoped RawNote
|
+-- host note? ------------------------------> exclude
|
+-- ordinary note
| extract gtd block
| absent -> exclude
| found -> build note card -> tags -> bucket
|
+-- to-do
existing todos/completion rules
-> extract block -> tags -> build card -> bucket
combined bucket -> existing sort -> SLICE8 incremental rendering
```
Collection remains responsible for eligibility, card normalization, tags,
bucketing, sorting, warnings, and totals. The webview should not need separate
rendering logic for note cards.
## Implementation plan
### Phase 1 — Card model and shared builder
- [x] Add an explicit card discriminator to `KanbanCard` in `src/Gtd/types.ts`,
preferably `isTodo: boolean`, so completion/recurrence rendering does not
infer item kind indirectly.
- [x] Extract duplicated kanban/matrix card construction into a small shared
helper only if doing so reduces real duplication without changing public
collector contracts.
- [x] For normal notes set:
- `isTodo: false`;
- `completed: false` and `completedTime: 0`;
- `isRecurring: false`;
- date via `resolveEventDate(note, block)`, which correctly ignores `todo_due`
for normal notes.
- [x] For to-dos preserve current completion and recurrence behavior and set
`isTodo: true`.
- [x] Update `renderCard` only as needed to choose a neutral note glyph when a
normal note has no custom icon; retain current checkbox glyphs for to-dos.
- [x] Decide and document the neutral fallback glyph during implementation using
an existing project-compatible symbol; do not alter custom icons.
### Phase 1 completion record
- Added `isTodo` to the shared card model and centralized kanban/matrix card
normalization in `buildKanbanCard`.
- Normal-note cards are forced incomplete and non-recurring; to-do completion,
recurrence, due-date, styling, and override behavior remain unchanged.
- The neutral fallback is `📄`, matching the existing calendar note glyph.
Custom icons still take precedence, and absent `isTodo` values retain the
legacy checkbox fallback for compatibility.
- Focused validation: 3 suites and 63 tests passed.
- Full validation: 13 suites and 168 tests passed.
- TypeScript validation and webview JavaScript syntax checks passed.
- No manual Joplin test is required for Phase 1 because normal-note collection
is not enabled until later phases.
### Phase 2 — Kanban eligibility and bucketing
- [x] Refactor the `collectKanban.ts` loop so host exclusion occurs first, then
item-kind-specific eligibility.
- [x] For to-dos, preserve `todos: none`, `gtd-only`, and `all` behavior exactly.
- [x] For normal notes, call `extractGtdBlock` and exclude only when no block is
found.
- [x] Surface malformed-block warnings for included notes with the existing
message format.
- [x] Fetch tags only after an item is eligible.
- [x] Bucket completed to-dos into Done using the existing done-window cutoff.
- [x] Bucket incomplete to-dos and normal notes by the configured in-progress
tag; untagged eligible notes go to Backlog.
- [x] Sort the combined note/to-do arrays with the existing configured sorter.
### Phase 2 completion record
- Kanban eligibility now branches by item kind after host-note exclusion.
- Ordinary notes require a found `gtd` block; to-dos retain the existing
`none`, `gtd-only`, and `all` rules.
- Eligible note cards use existing warning, tag, bucketing, sorting, styling,
count, and navigation paths. They can enter Backlog or In Progress, never Done.
- Tag lookup occurs only after eligibility is established.
- Focused validation: 2 suites and 38 tests passed.
- Full validation: 13 suites and 174 tests passed.
- TypeScript, whitespace, and prohibited-reference checks passed.
- Phase 4 still must request bodies in `todos: none` mode before opted-in notes
work with that setting against the Joplin data adapter.
- No manual Joplin test is required yet; mixed-view acceptance remains Phase 7.
### Phase 3 — Matrix eligibility and bucketing
- [x] Apply the same host, item-kind, and shortcode eligibility split in
`collectMatrix.ts`.
- [x] Preserve completed-to-do exclusion.
- [x] Allow eligible normal notes through because they have no completion state.
- [x] Fetch tags only after eligibility is established.
- [x] Reuse the current Eisenhower tag axes without note-specific exceptions.
- [x] Reuse the current Skeleton active/due-soon rules; dateless normal notes
naturally fall into the not-due-soon column unless marked urgent.
- [x] Sort combined note/to-do quadrant arrays with the existing sorter.
### Phase 3 completion record
- Matrix eligibility now branches by item kind after host-note exclusion.
- Completed to-dos remain excluded; ordinary notes require a found `gtd` block
and are not excluded by to-do completion fields.
- Eligible notes reuse the existing Eisenhower and Skeleton axes, sorting,
warnings, tags, styling, totals, and navigation paths.
- `todos: none` continues to exclude only to-dos at the collector level.
- Tag lookup occurs only after eligibility is established.
- Focused validation: 2 suites and 37 tests passed.
- Full validation: 13 suites and 176 tests passed.
- TypeScript, webview syntax, whitespace, and prohibited-reference checks passed.
- Phase 4 still must request bodies in `todos: none` mode for the real Joplin
adapter; the collector contract itself is covered.
- No manual Joplin test is required yet; mixed-view acceptance remains Phase 7.
### Phase 4 — Body-fetch contract
Normal-note eligibility can be known only by reading the body. Consequently,
kanban and matrix collectors must request bodies for every scanned folder even
when `todos: none`. The previous `needsBody = config.todos !== "none"`
optimization is no longer valid for these two views.
- [x] Set `includeBody: true` for kanban and matrix folder-note fetches.
- [x] Update collector comments so they no longer claim `todos: none` guarantees
an empty board.
- [x] Update `src/tests/Gtd/fieldsHint.test.ts` to expect body fetches in all
kanban/matrix modes.
- [x] Leave calendar and Gantt body-fetch behavior unchanged.
- [x] Document this intentional performance tradeoff in SPEC.md: explicit note
opt-in requires body inspection, but tag requests remain limited to
eligible items.
### Phase 4 completion record
- Kanban and matrix now request bodies for every in-scope note, including under
`todos: none`, so ordinary-note `gtd` opt-in can always be evaluated.
- Calendar body-fetch behavior remains unchanged; Gantt code was not modified.
- Tag requests remain deferred until an item passes eligibility.
- `fieldsHint.test.ts` now records the new board contract in all to-do modes.
- SPEC.md documents the intentional performance tradeoff.
- Focused validation: 3 suites and 76 tests passed.
- Full validation: 13 suites and 176 tests passed.
- TypeScript, webview syntax, whitespace, and prohibited-reference checks passed.
- No manual Joplin test is required for this data-fetch contract; mixed-view
acceptance remains Phase 7.
### Phase 5 — Automated tests
#### Kanban
- [x] Include a normal note with a valid block.
- [x] Include a normal note with an empty block.
- [x] Include a malformed-block note and retain its warning.
- [x] Exclude a normal note without a block.
- [x] Exclude the host note even when it has a block.
- [x] Place tagged notes in In Progress and untagged notes in Backlog.
- [x] Prove normal notes never enter Done and never become recurring.
- [x] Preserve all `todos:` modes and done-window behavior.
- [x] Verify mixed sorting for due date, title, and modified date.
#### Matrix
- [x] Cover all four Eisenhower quadrants with normal notes.
- [x] Cover Skeleton active/inactive and due-soon/not-due-soon combinations.
- [x] Cover dateless and explicitly urgent normal notes.
- [x] Include empty and malformed blocks; exclude absent blocks and the host.
- [x] Preserve completed-to-do exclusion and all `todos:` modes.
- [x] Verify mixed sorting for due date, title, and modified date.
#### Shared/integration contracts
- [x] Update existing `KanbanCard` fixtures for the item discriminator.
- [x] Verify tags are not requested for ineligible plain notes or excluded to-dos.
- [x] Verify `cardCount` includes all eligible note and to-do cards once.
- [x] Verify SLICE8 works on the combined ordered arrays without renderer changes.
- [x] Run focused kanban, matrix, and fields-hint suites.
- [x] Run the complete Jest suite and record suite/test totals.
- [x] Run `npm run dist` and record the produced `.jpl` path.
- [x] Run `git diff --check` and the prohibited-reference audit.
### Phase 5 completion record
- Added comprehensive mixed note/to-do coverage for valid, empty, malformed,
absent, and host-note blocks.
- Covered kanban placement, both matrix modes, all to-do modes, completion and
recurrence invariants, source IDs, warnings, and exact card totals.
- Verified mixed due-date, title, and modified-date sorting.
- Verified `page-size: 1` does not truncate complete collector arrays or totals;
existing SLICE8 rendering consumes those combined arrays unchanged.
- Focused validation: 4 suites and 89 tests passed.
- Full validation: 13 suites and 185 tests passed.
- TypeScript validation passed.
- Production build passed: `publish/com.victorwiebe.joplin.plugin.gtd-calendar.jpl`
(155 KB; SHA-256
`7257bb2bdbb8340539cdad3f88a2ac5f8b55bfa960816f16e472e047311db1cc`).
- `git diff --check` and the prohibited-reference audit passed.
- No manual Joplin acceptance was performed; that remains Phase 7.
### Phase 6 — Documentation
- [x] Update README.md kanban and matrix sections with a normal-note example.
- [x] State clearly that normal notes require a `gtd` block and that `todos:`
controls only to-dos.
- [x] Document kanban placement, both matrix placement modes, note date rules,
malformed/empty block behavior, and fallback glyph behavior.
- [x] Update SPEC.md inclusion tables, collection flow, card model, and body-fetch
tradeoff.
- [x] Add an unreleased SLICE9 entry to CHANGELOG.md without changing the package
version until release scope is decided.
### Phase 6 completion record
- README.md now documents ordinary-note opt-in, kanban placement, both matrix
modes, note dates, `todos:` isolation, empty/malformed blocks, fallback
glyphs, custom icons, and clickable source-note behavior.
- SPEC.md now records the inclusion table, shared card model, collection flow,
rendering rules, body-fetch tradeoff, and completed unreleased feature.
- CHANGELOG.md includes the unreleased SLICE9 feature and validation results
without changing the package version.
- The strict prohibited-reference and contradictory-wording audits passed.
### Phase 7 — Manual Joplin acceptance
Use mixed views containing ordinary notes, incomplete to-dos, completed to-dos,
empty blocks, malformed blocks, and plain notes.
**Acceptance package:** `publish/com.victorwiebe.joplin.plugin.gtd-calendar.jpl`
(SHA-256
`7257bb2bdbb8340539cdad3f88a2ac5f8b55bfa960816f16e472e047311db1cc`).
**Accepted:** the user reported all manual checks passed on 2026-07-27.
- [x] Confirm only opted-in normal notes appear.
- [x] Confirm empty blocks opt in and malformed blocks warn without disappearing.
- [x] Confirm normal-note Backlog/In Progress placement on kanban.
- [x] Confirm normal notes never appear in Done.
- [x] Confirm all four quadrants in both matrix modes behave as specified.
- [x] Confirm block title/date/colour/icon/text overrides render on normal notes.
- [x] Confirm neutral note glyph and custom-icon precedence.
- [x] Confirm normal notes do not show completion or recurrence styling.
- [x] Confirm clicking note cards opens the correct source note.
- [x] Confirm `todos: none`, `gtd-only`, and `all` still affect only to-dos.
- [x] Confirm SLICE8 limits and remaining counts include the mixed card total.
- [x] Confirm sorting, warnings, headings, statistics, and reload behavior.
- [x] Record user sign-off here; do not mark manual acceptance complete before
confirmation.
## Acceptance criteria
SLICE9 is complete only when:
- Every normal note with a found `gtd` block appears exactly once in its expected
bucket, while every plain normal note remains excluded.
- Existing to-do inclusion, completion, recurrence, done-window, and matrix rules
remain unchanged.
- Note styling, dates, navigation, warnings, totals, and sorting are correct.
- Mixed arrays obey SLICE8 batching without special renderer branches.
- Focused tests, the full suite, and the production package build pass.
- Manual Joplin acceptance is explicitly confirmed.
## Files expected to change
- `src/Gtd/types.ts`
- `src/Gtd/collectKanban.ts`
- `src/Gtd/collectMatrix.ts`
- Optional shared card-builder module if justified by implementation
- `src/gtd-calendar-webview.js`
- `src/tests/Gtd/kanban.test.ts`
- `src/tests/Gtd/matrix.test.ts`
- `src/tests/Gtd/fieldsHint.test.ts`
- `README.md`
- `SPEC.md`
- `CHANGELOG.md`
- `SLICE9.md`
- `TASKS.md`
## Out of scope
- Including normal notes that lack a `gtd` block.
- Adding a general `notes: all` or `notes: none` option.
- Giving normal notes a completion workflow.
- Editing notes or tags from a kanban/matrix card.
- Changing calendar or Gantt inclusion behavior.
- Child-notebook grouping from SLICE10.
## Dependency and resume point
Implement after SLICE8. Start with collector tests and the explicit card-kind
model, then change eligibility/body fetching. Finish automated regression checks
before documentation and manual acceptance.

View File

@ -1,370 +0,0 @@
# SLICE 15 — Eisenhower drag and drop with Skeleton quadrant names
> **Filename note:** this file is named `SLIDE15.md` as requested. Its feature
> sequence and heading identify it as SLICE 15.
>
> **State-saving rule:** update this file after every completed task and whenever
> work pauses. Keep implementation, automated validation, production packaging,
> and manual Joplin acceptance as separate status boundaries.
## Status
**COMPLETE.** Phases 17 are complete. Focused and full automation passed, the
production JPL was built and inspected, and the user confirmed the manual Joplin
acceptance tests passed on 2026-07-31.
## Goal
Make Eisenhower matrices editable by moving cards between quadrants. Each move
persists the urgent/important tag combination represented by the destination.
At the same time, replace the classic action labels with the more constructive
Skeleton labels while retaining Eisenhower's urgent/important axes:
| | **Urgent** | **Not urgent** |
|---|---|---|
| **Important** | **Do Next** | **Scheduled** |
| **Not important** | **On Deck** | **Backlog** |
This means the current **Eliminate** quadrant becomes **Backlog**. The underlying
quadrant remains “not urgent and not important”; only its displayed name changes.
## Confirmed product contract
- The label change applies to every Eisenhower matrix, editable or read-only.
- Eisenhower's axes remain `urgent-tag` and `important-tag`; no date-derived
urgency or in-progress semantics are introduced.
- All four Eisenhower quadrant names adopt Skeleton's names:
`Do Next`, `Scheduled`, `On Deck`, and `Backlog`.
- Skeleton mode remains available and unchanged in v2.0.0, but does not receive
drag and drop in this slice.
- `editable: yes` enables Eisenhower mutation and defaults to `no`.
- Setting `editable: yes` with `mode: skeleton` warns that editing is unavailable
and renders a read-only Skeleton matrix. It must not expose inert handles.
- Both ordinary opted-in notes and incomplete native to-dos can be moved.
- Completed to-dos and notes carrying `done-tag` remain excluded; dragging does
not change completion state.
- A move changes only the configured urgent/important tag memberships. It does
not edit dates, in-progress state, completion, notebook, or unrelated tags.
- Pointer drag and keyboard **Move to…** use the shared SLICE 14 behavior.
- Sorting remains controlled by `sort` and `sort-type`; drag does not establish
manual ordering within a quadrant.
- Moves remain within the current notebook group.
## Eisenhower transition rules
| Destination | Urgent tag | Important tag | Persisted change |
|---|---:|---:|---|
| Do Next (top-left) | yes | yes | Add both configured tags. |
| Scheduled (top-right) | no | yes | Remove urgent; add important. |
| On Deck (bottom-left) | yes | no | Add urgent; remove important. |
| Backlog (bottom-right) | no | no | Remove both configured tags. |
The rules are identical for ordinary notes and native to-dos. Joplin note type,
native completion, `done-tag`, `in-progress-tag`, `gtd` block, dates, and all
unrelated tags remain unchanged.
If `urgent-tag` and `important-tag` normalize to the same title, the two axes
cannot represent four persistent states. Existing parser behavior must be
audited. For an editable Eisenhower matrix, treat this configuration as
non-editable with a clear warning rather than applying ambiguous mutations.
Read-only collection may retain its existing precedence for compatibility.
## Label migration
Update the canonical `matrixLabels("eisenhower")` result:
| Position | v1 label | v2 label |
|---|---|---|
| top-left | Do First | Do Next |
| top-right | Schedule | Scheduled |
| bottom-left | Delegate | On Deck |
| bottom-right | Eliminate | Backlog |
Remove duplicated fallback label literals in the webview or update them from
the same shared contract so an absent/legacy payload cannot silently restore
the old wording. Update code comments, tests, README.md, SPEC.md, and accessible
labels. No saved data migration is needed because quadrant labels are not
persisted.
## Architecture
SLICE 15 extends, rather than duplicates, the SLICE 14 pathway:
```text
matrix pointer drop or keyboard Move to…
|
v
{ view: "matrix", hostNoteId, cardId, destination, rawConfig, viewInstanceId }
|
v
shared message validation
- config reparses as mode:eisenhower + editable:yes
- card remains admitted, incomplete, in scope, and in the same group
|
v
shared tag mutation primitives apply urgent/important target state
|
v
canonical matrix recollection, sorting, totals, pagination, and rerender
```
Use a matrix-specific semantic destination union (`topLeft | topRight |
bottomLeft | bottomRight`, or equivalent named states). Do not let the webview
submit arbitrary tag titles or add/remove lists. The main process derives tag
operations from freshly parsed configuration and the fixed destination table.
### Fresh-state validation
Before writing, confirm all SLICE 14 host, scope, type, grouping, and `gtd`
eligibility checks, plus:
- mode is still `eisenhower`;
- `editable` is still enabled;
- urgent and important tag names remain distinct after normalization;
- the item is still incomplete under native or note `done-tag` semantics;
- the destination is one of the four fixed quadrants.
If the item became complete or the block changed mode/configuration during a
drag, make no write, report stale state, and refresh the matrix.
## Interaction and rendering rules
- Reuse SLICE 14 pointer thresholds, capture, cancellation, pending/error states,
request sequencing, focus recovery, live announcements, and canonical refresh.
- Highlight only quadrants in the same matrix instance and notebook group.
- Use the new quadrant labels everywhere, including **Move to…** options and
announcements.
- Do not infer a source quadrant in the main process from stale DOM data; fetch
current tags and calculate a minimal target-state change.
- Same-quadrant moves are no-ops.
- A canonical refresh may sort the card to a different position or beyond the
currently visible `page-size`; counts and **List more** remain authoritative.
- Read-only Eisenhower matrices receive the label change without drag handles,
move menus, mutation requests, or new focus stops.
- Skeleton matrices receive the label-independent shared rendering regressions
only and remain read-only.
## Implementation plan
### Phase 1 — Label change and compatibility tests
- [x] Change Eisenhower labels to Do Next, Scheduled, On Deck, and Backlog.
- [x] Update comments and remove/update fallback literals that contain Do First,
Schedule, Delegate, or Eliminate.
- [x] Update unit and rendering tests for single and grouped layouts.
- [x] Confirm Skeleton labels and both modes' row/column axes remain unchanged.
- [x] Confirm the label-only change applies when `editable` is omitted.
#### Phase 1 completion record
- Canonical Eisenhower labels and the defensive legacy-payload webview fallback
now use Do Next, Scheduled, On Deck, and Backlog. Labels remain display-only;
no card data or persisted state changed.
- Updated matrix source comments, current README/SPEC contracts, and the
unreleased CHANGELOG entry. Historical version notes remain historical.
- Added exact label/axis tests for both modes. Eisenhower still uses
Important/Not important rows and Urgent/Not urgent columns; Skeleton still
uses Active/Not active and Due soon/Not due soon.
- Focused validation passed: 3 suites, 78 tests. Full regression validation
passed: 20 suites, 299 tests. TypeScript with `--skipLibCheck`, webview syntax,
and `git diff --check` passed. No JPL or manual test was performed in Phase 1.
### Phase 2 — Matrix configuration and protocol
- [x] Add `editable: boolean` to `MatrixConfig`, defaulting to `false`.
- [x] Parse `editable: yes | no` using the SLICE 14 validation contract.
- [x] Warn and disable editing for `mode: skeleton`.
- [x] Warn and disable editable Eisenhower when normalized axis tags are equal.
- [x] Define constrained matrix move intents and fixed destinations using the
shared host/view identity and typed result model.
- [x] Add parser/protocol tests for defaults, invalid values, mode interaction,
equal-axis tags, and backward compatibility.
#### Phase 2 completion record
- Added strict scalar `editable: yes | no` parsing to matrix configuration,
defaulting to read-only. Invalid/non-scalar values warn and use `no`.
- `editable: yes` is effective only for Eisenhower mode with distinct normalized
urgent/important tags. Skeleton and equal-axis configurations warn clearly and
return `editable: false`, preventing inert mutation UI.
- Added fixed positional matrix destinations, a constrained matrix intent, and a
runtime guard. Matrix payloads now echo the per-render `viewInstanceId`, the
canonical host note ID, and effective editable state.
- Focused validation passed: 3 suites, 76 tests. Full regression validation
passed: 20 suites, 310 tests. TypeScript with `--skipLibCheck` and
`git diff --check` passed. No JPL or manual testing occurred in Phase 2.
### Phase 3 — Eisenhower mutation planning
- [x] Implement the four urgent/important target states through shared tag
primitives from SLICE 14.
- [x] Calculate minimal, idempotent tag membership changes from fresh data.
- [x] Preserve completion, progress, dates, body, note type, notebook, and
unrelated tags.
- [x] Cover notes and to-dos, custom tag names, casing, duplicate tags, all 12
cross-quadrant moves, same-quadrant no-ops, and partial failures.
- [x] Prove a completed or newly filtered item cannot be mutated.
#### Phase 3 completion record
- Exported and reused Slice 14's deterministic case-insensitive find/create,
attach, and detach primitives. Matrix mutation maps each fixed destination to
the exact urgent/important target state and performs only missing changes.
- Covered every source/destination combination for ordinary notes and native
to-dos: 24 cross-state cases and eight same-state no-ops across both item
types. No completion or note-field adapter operation is available to the
matrix service.
- Tests cover custom names, existing case variants, duplicate attached tags,
missing-tag creation, unrelated-tag preservation, fresh-state rejection,
disappeared cards, and partial failures without stale compensation.
- Focused validation passed: 2 suites, 50 tests. Full regression validation
passed: 21 suites, 347 tests. TypeScript with `--skipLibCheck` and
`git diff --check` passed. Packaging and manual acceptance remain pending.
### Phase 4 — Message validation and refresh
- [x] Add the matrix move message to the main-process handler.
- [x] Reparse and validate editable Eisenhower configuration and fresh eligibility.
- [x] Reject arbitrary destinations, tag operations, fields, modes, host notes,
out-of-scope items, cross-group moves, and stale completions.
- [x] Return typed results and refresh only the initiating matrix instance.
- [x] Guard against older responses overwriting newer renders.
#### Phase 4 completion record
- Registered `moveMatrixCard` and added a testable validation service between
untrusted webview intent and the Phase 3 mutation service.
- Exact message keys, fixed positional destinations, matching selected host,
reparsed effective editable Eisenhower mode, fresh notebook/scope ownership,
item filters, `gtd` opt-in, native completion, and note `done-tag` state are
validated before writes. Skeleton, equal-axis, completed, filtered, host,
out-of-scope, malformed, and expanded requests cannot write.
- User-safe success/stale/error results reuse the Slice 14 result model;
unexpected API details remain in plugin-process logs. The Phase 5 webview will
use echoed `viewInstanceId` and request sequencing for initiating-only refresh.
- Focused validation passed: 3 suites, 79 tests. Full regression validation
passed: 22 suites, 370 tests. TypeScript with `--skipLibCheck` and
`git diff --check` passed. Packaging and manual acceptance remain pending.
### Phase 5 — Pointer and keyboard interaction
- [x] Reuse the SLICE 14 drag affordance and pointer lifecycle for quadrants.
- [x] Constrain active targets to the same matrix instance and notebook group.
- [x] Add new-label drag feedback, pending/error states, and cancellation.
- [x] Add keyboard **Move to…** options for the other three quadrants.
- [x] Preserve click-to-open, hover details, pagination, scrolling, focus, and
screen-reader announcements.
- [x] Verify read-only Eisenhower and all Skeleton matrices expose no mutation UI.
#### Phase 5 completion record
- Editable Eisenhower cards now reuse Slice 14a's compact control stack: a
pointer drag handle with a disclosure-controlled keyboard menu beneath it.
The menu uses the payload's Do Next, Scheduled, On Deck, and Backlog labels
and disables the card's current quadrant.
- Pointer targets are resolved only within the card's own matrix grid, so a
grouped notebook board cannot accept a drop from another group. Threshold,
capture, Escape cancellation, edge scrolling, click suppression, pending
state, and canonical post-move rendering match the Kanban interaction.
- Matrix moves post only fixed positional destinations with the host and view
identities, then refresh only the initiating matrix through a guarded request
sequence. Live success/error status and focus recovery survive rerendering.
- Controls are created only from the main process's effective `editable` payload;
read-only Eisenhower and all Skeleton matrices add no handles, menus, or focus
stops. Existing card opening, hover detail, sorting, and List more rendering
remain shared and unchanged.
- Focused validation passed: 3 suites, 67 tests. Full regression validation
passed: 22 suites, 374 tests. Webview syntax and `git diff --check` passed.
Packaging and manual Joplin acceptance remain pending.
### Phase 6 — Documentation and complete validation
- [x] Update README.md examples, option tables, quadrant table, read-only text,
controls, and limitations.
- [x] Update SPEC.md configuration, label contract, transition table, protocol,
mutation safety, accessibility, and refresh behavior.
- [x] Update the unreleased v2.0.0 CHANGELOG entry.
- [x] Decide whether Skeleton remains documented as a supported alternative or
is marked as legacy/non-editable; do not remove it in this slice.
- [x] Run focused parser, labels, collector, mutation, handler, render, grouping,
pagination, and accessibility tests.
- [x] Run the complete Jest suite, TypeScript, webview syntax, CSS/build,
whitespace, and prohibited-reference checks.
- [x] Build and inspect the production JPL without publishing v2.0.0 yet.
#### Phase 6 completion record
- README, specification, and unreleased changelog now document the opt-in
Eisenhower control, all four persisted tag states, fresh validation, scoped
pointer/keyboard behavior, canonical refresh, accessibility, and unchanged
completion/sorting/pagination semantics. Skeleton remains a supported,
intentionally read-only default mode rather than being marked legacy.
- Focused interaction/mutation/handler validation passed at 3 suites and 67
tests; the complete Jest run passed at 22 suites and 374 tests. TypeScript
(`--skipLibCheck`), webview JavaScript syntax, whitespace, and current-source
prohibited visible-label checks passed.
- `npm run dist` completed. The inspected production artifact is a POSIX-tar JPL
containing the manifest and four expected runtime assets, including bundled
matrix mutation UI and CSS. This pre-version-bump artifact was version 1.0.0,
193,024 bytes, with SHA-256
`a24ca6bf464c2f4e0aa0cc3815d6a4dc7d565c084b35a8a7c962af1443dcb903`.
- The build could not embed Git metadata because the sandbox denied its internal
Git subprocess; this does not affect the plugin assets. No publish, version
bump, or manual Joplin acceptance was performed.
- After Phase 7 user acceptance, the package, lockfile root package, and Joplin
manifest were advanced to 2.0.0. The rebuilt 2.0.0 JPL is 193,024 bytes with
SHA-256 `fcffaecda3ded4ee11d70e21bd86fa8ede7a7d018b98feb857bce0aae2b67503`.
This version change remains uncommitted and unpushed as requested.
### Phase 7 — Manual Joplin acceptance
- [x] Every Eisenhower matrix shows Do Next, Scheduled, On Deck, and Backlog.
- [x] No visible or accessible Eisenhower UI says Do First, Schedule, Delegate,
Eliminate, or “Drop this.”
- [x] Read-only Eisenhower matrices retain click, hover, sorting, and pagination
behavior without mutation affordances.
- [x] An editable Eisenhower matrix moves an ordinary note through all four
quadrants and persists the exact urgent/important tag state after reload.
- [x] An editable Eisenhower matrix moves a native to-do through all four
quadrants without changing native completion or progress state.
- [x] Custom distinct axis tags work; equal axis tags warn and disable editing.
- [x] Same-quadrant, cancelled, outside, Escape, and interrupted drags do not write.
- [x] A normal click opens the note; a drag does not accidentally open it.
- [x] Keyboard-only **Move to…** reaches every quadrant and restores focus.
- [x] Stale completion, filtering, scope, group, and API-failure cases converge
to canonical state without unrelated data changes.
- [x] Single, grouped, multiple-block, scrolling, and paginated matrices work.
- [x] Skeleton matrices remain correct and read-only.
- [x] Record explicit user sign-off separately from automated checks.
#### Phase 7 completion record
- The user reported all manual Slice 15 tests passed on 2026-07-31 after using
the editable Eisenhower acceptance block. This sign-off is recorded separately
from the 374-test automated run and production package inspection.
## Out of scope
- Skeleton drag and drop.
- Removing Skeleton mode or migrating Skeleton blocks to Eisenhower.
- Date-derived urgency changes.
- Changing completion or in-progress state from an Eisenhower move.
- Manual ordering within a quadrant.
- Dragging between notebook groups or changing notebook ownership.
- Editing arbitrary tags or card fields.
- Calendar, Gantt, or mobile drag and drop.
- Publishing v2.0.0 before consolidated release acceptance.
## Acceptance criteria
- Eisenhower retains urgent/important semantics but consistently uses Skeleton's
four constructive quadrant names.
- Mutation occurs only for an explicitly editable, valid Eisenhower block.
- Every destination persists exactly the configured urgent/important state and
preserves completion, progress, content, notebook, and unrelated tags.
- Pointer and keyboard interaction share SLICE 14's validated safety and
accessibility foundation.
- Skeleton mode remains available, correct, and read-only.
- Automated validation, production packaging, and explicit manual Joplin
acceptance are completed and recorded separately.

537
SPEC.md
View File

@ -1,537 +0,0 @@
# GTD Calendar — Project Specification
*Status: living document, current through v1.0.0. Sections 15 describe the shipped design; §9 tracks what is still ahead.*
A fork of [WeMakeMachines/joplin-plugin-event-calendar](https://github.com/WeMakeMachines/joplin-plugin-event-calendar) (MIT) that inverts the plugin's data model: instead of events living as YAML inside the calendar note, the plugin's views are **populated by real Joplin notes and todos** in the surrounding folder tree, with clickable drilldown to the source note. What started as a single calendar view has grown into four composable views—calendar, kanban, priorities matrix, and Gantt—sharing one data layer.
**Package name:** `joplin-plugin-gtd-calendar` (npm)
**Manifest id:** `com.victorwiebe.joplin.plugin.gtd-calendar`
---
## 1. Concept
A note anywhere in a folder contains a ` ```gtd-calendar ` fenced block. That block renders as a Day/Week/Month calendar populated by notes and todos in scope. Items opt in (by default) via a ` ```gtd ` fenced block in their body. Clicking a calendar event opens the underlying note.
Dateless items are not lost — they collect in an **Unscheduled** section below the calendar grid, functioning as a GTD next-actions / someday-maybe bucket.
---
## 2. Schema Reference
### 2.1 The `gtd-calendar` block (lives in the calendar note)
```
```gtd-calendar
view: month # day | week | month | month-grid (default: day)
title: Editorial Schedule
scope: this-folder # this-folder | children | <integer depth> | all (default: this-folder)
notebook: Work/Ideas # notebook name | Parent/Child path | folder id (default: this note's folder)
notes: gtd-only # gtd-only | all | none (default: gtd-only)
todos: gtd-only # gtd-only | all | none (default: gtd-only)
sort: asc # asc | desc (default: asc)
sort-type: title # title | modified_date (default: title)
```
| Key | Values | Default | Notes |
|---|---|---|---|
| `view` | `day`, `week`, `month` (also `d`/`w`/`m`), `month-grid` (also `grid`) | `day` | Grouping granularity. `day`/`week`/`month` render the compact tile strip spanning all events; `month-grid` renders a classic 7-column grid of the entire *current* calendar month with weekday headers, dimmed adjacent-month days, today highlighted, and clickable event chips in each day cell. Events outside the displayed month are counted in a footnote. |
| `title` | string | none | Rendered as a heading above the calendar. |
| `scope` | `this-folder`, `children`, an integer, or `all` | `this-folder` | `children` = unlimited recursion into subfolders. An integer *n* = this folder plus *n* levels of descendants (`0``this-folder`). `all` = **every notebook in the profile**, ignoring the root folder and tree position entirely — an explicit, deliberately expensive opt-in for a global dashboard. Guardrails: the stats footnote reports scanned notebooks/notes and is marked `scope: all`; a soft ⚠ warning appears when more than ~2000 notes are scanned (advisory, never blocks). `notebook:` is ignored under `scope: all` (with a warning). Documentation cautions that broad scopes scan every note body in the tree. |
| `notebook` | notebook title, `Parent/Child` title path, or 32-char folder id | this note's folder | Roots the folder scan at a chosen notebook, possibly outside the host note's tree; `scope` then applies relative to it. Resolution order: (1) a raw folder id; (2) a `Parent/Child/…` case-insensitive title path; (3) a bare title, used only if unique. Any miss — unknown id, unresolvable path, ambiguous or absent title — appends a ⚠ warning and falls back to the host note's own folder (warn-don't-fail). |
| `notes` | `gtd-only`, `all`, `none` | `gtd-only` | Which plain notes participate. `all` includes notes without a `gtd` block (they receive default styling and are Unscheduled unless... see §3.2). |
| `todos` | `gtd-only`, `all`, `none` | `gtd-only` | Same semantics for todos. |
| `sort` | `asc`, `desc` | `asc` | Order of events **within** each day/week/month grouping and within Unscheduled. Groupings themselves always run chronologically. |
| `sort-type` | `title`, `modified_date` | `title` | Field used by `sort`. `modified_date` maps to Joplin's `updated_time`. |
| `unscheduled-todos` | `yes`/`no` (also `true`/`false`, `show`/`hide`) | `yes` | Whether the Unscheduled **to-dos** sub-section is shown. |
| `unscheduled-notes` | `yes`/`no` (also `true`/`false`, `show`/`hide`) | `yes` | Whether the Unscheduled **notes** sub-section is shown. Independent of `unscheduled-todos` — lets a dashboard split the two lists across stacked calendar blocks instead of duplicating both. |
Unscheduled items (no resolvable date) are split into two labelled sub-sections — to-dos first, then notes — each hidden when empty or toggled off.
Unknown keys are ignored with a warning. A malformed block renders an inline warning above the calendar rather than failing silently.
### 2.2 The `gtd` block (lives in participating notes/todos)
```
```gtd
date: 2026-07-04 # required for notes to be scheduled; optional override for todos
bg-colour: teal # any CSS color value
fg-colour: white
icon: 🦴
title: Override title # optional; defaults to the note's own title
text: Hover text # optional; tooltip detail, as upstream
```
| Key | Applies to | Behavior |
|---|---|---|
| `date` | notes | Required for the item to appear **in the grid**; without it the note goes to Unscheduled. Accepts `yyyy-mm-dd` (and `mm-dd-yyyy` for upstream parity). |
| `date` | todos | Optional. If present, **overrides** `todo_due` (explicit beats implicit). |
| `bg-colour` / `bgColor` | both | Event tile background. Both spellings accepted; `bg-colour` is canonical for this fork. |
| `fg-colour` | both | Event tile text color (new vs. upstream). |
| `icon` | both | Emoji/short string, as upstream. Defaults: type indicator (see §4). |
| `title` | both | Overrides the note title on the tile. |
| `text` | both | Tooltip body. |
An **empty** ` ```gtd ` block is valid and meaningful: it is the opt-in signal with all defaults.
### 2.3 The `gtd-kanban` block (lives in the board note)
```
```gtd-kanban
title: Editorial Board
scope: this-folder # this-folder | children | N | all — same semantics as gtd-calendar
notebook: Work/Ideas # same semantics as gtd-calendar (default: this note's folder)
notes: all # all | none (default: all; gtd notes only)
todos: gtd-only # gtd-only | all | none (default: gtd-only)
sort-type: due-date # due-date | title | modified-date (default: due-date)
sort: asc # asc | desc (default: asc)
in-progress-tag: in-progress # default: "in-progress"
done-tag: done # ordinary-note completion; default: "done"
card-detail: hover # hover | always | none (default: hover)
page-size: 10 # positive integer (default: 10)
done-window: 7 # days, or "all" (default: 7)
editable: no # yes | no; persisted moves (default: no)
```
Three columns are always shown: **Backlog**, **In Progress**, and **Done**.
Ordinary notes participate only when their body contains a `gtd` block; empty
and malformed blocks both opt in, with malformed YAML producing a warning and
fallback card values. `notes: all` admits those opted-in ordinary notes while
`notes: none` excludes them; it never admits a note without a `gtd` block. The
`notes` and `todos` options apply independently.
Bucketing: completion wins regardless of progress tags. Native to-dos use
`todo_completed`; opted-in ordinary notes use `done-tag`. Incomplete items
carrying `in-progress-tag` enter In Progress; everything else enters Backlog.
The done tag neither completes a native to-do nor opts in a plain note.
`done-window` filters native completed to-dos only; completed notes remain in
Done because no completion timestamp is inferred from `updated_time`. Sorting
is per-column across the combined note/to-do array.
Under `due-date`, dateless cards sort after dated cards regardless of direction.
`page-size` must be a positive integer; invalid values warn and fall back to
10. Each column initially renders at most that many cards and reveals another
batch per **List more** click. Expansion is independent per column and resets
when the rendered note reloads; headings and statistics retain complete totals.
`editable` accepts scalar `yes | no` case-insensitively and defaults to `no`.
Invalid, empty, or non-scalar values warn and remain read-only. With `yes`, the
desktop rendered-note view exposes a pointer handle with a compact disclosure
arrow beneath it. The arrow opens a keyboard-accessible **Move to…** menu;
destinations are fixed semantic states, never arbitrary patches:
| Destination | Ordinary opted-in note | Native Joplin to-do |
|---|---|---|
| Backlog | remove `done-tag` and `in-progress-tag` | set `todo_completed: 0`; remove `in-progress-tag` |
| In Progress | add `in-progress-tag`; remove `done-tag` | set `todo_completed: 0`; add `in-progress-tag` |
| Done | add `done-tag`; remove `in-progress-tag` | set `todo_completed` to current epoch ms; remove `in-progress-tag` |
Same-state moves are idempotent. Content, dates, note type, notebook,
unrelated tags, and free-form ordering are preserved. Completing a recurring
to-do uses native completion and may activate the Repeating To-Dos plugin.
### 2.4 The `gtd-matrix` block (lives in the matrix note)
```
```gtd-matrix
mode: skeleton # skeleton | eisenhower (default: skeleton)
editable: no # yes | no; Eisenhower moves (default: no)
title: Priorities
scope: this-folder # this-folder | children | N | all — same semantics as gtd-calendar
notebook: Work/Ideas # same semantics as gtd-calendar (default: this note's folder)
notes: all # all | none; gtd notes only (default: all)
todos: gtd-only
sort-type: due-date
sort: asc
urgent-tag: urgent # default: "urgent"
important-tag: important # Eisenhower mode only; default: "important"
in-progress-tag: in-progress # Skeleton mode only; default: "in-progress"
done-tag: done # note completion in both modes; default: "done"
urgent-window: 3 # Skeleton mode only, days; default: 3
card-detail: hover
page-size: 10 # positive integer; per quadrant (default: 10)
```
A 2×2 grid with labelled axes. It includes incomplete to-dos admitted by
`todos:` plus unfinished ordinary notes admitted by `notes:`. `notes: all`
still requires an explicit `gtd` block; `notes: none` excludes all notes. Completed
to-dos and notes carrying `done-tag` are excluded before either mode buckets
them, so done wins over active, urgent, and important tags. Native to-dos ignore
`done-tag` and remain governed by `todo_completed`. The board is internally
positional; each mode supplies its own axis and quadrant labels.
**`mode: skeleton`** (default) — rows = active (`in-progress-tag`, the same tag the kanban uses); columns = due soon (resolved card date within `urgent-window` days, overdue included, or the `urgent-tag` as a manual override for dateless items). Quadrants: **Do Next** (active + due soon), **Scheduled** (active, not due soon — includes dateless in-progress work), **On Deck** (not active + due soon — the early-warning quadrant), **Backlog** (neither). Designed to complement the calendar and kanban directly: same in-progress tag, dates drive urgency instead of a second opinion-tag, and nothing is ever condemned as "Eliminate" — a Backlog item is simply low priority. In-progress + `urgent-tag` + no date resolves to Do Next (the urgent tag is an explicit override, so it wins over the absence of a date).
**`mode: eisenhower`** — rows = `important-tag`; columns = `urgent-tag`.
Quadrants use the constructive shared labels while retaining Eisenhower
semantics: Do Next (both), Scheduled (important only), On Deck (urgent only),
Backlog (neither).
`editable` accepts scalar `yes | no` case-insensitively and defaults to `no`.
It is effective only for Eisenhower mode with distinct normalized axis tags.
Skeleton or equal-axis configurations warn, remain read-only, and expose no
mutation controls. An editable move persists only the destination's tag state:
Do Next = both tags, Scheduled = important only, On Deck = urgent only, and
Backlog = neither. The handler reparses the block, validates selected host,
scope/group, item eligibility, completion, mode, axes, and fixed destination
against fresh state before applying minimal tag writes. `page-size` has the same
validation and transient behavior as kanban, independently per quadrant.
### 2.5 The `gtd-gantt` block (dual-role: chart **and** item declaration)
Unlike the other views, gantt items opt in through their **own** `gtd-gantt` block rather than the shared `gtd` block, and every item carries a `project:` that groups items into swimlanes. The same fence name therefore serves two roles, disambiguated by a single rule:
> **A `gtd-gantt` block containing a non-empty `project:` key is an ITEM declaration; one without is the CHART config block.**
The markdown-it content script applies this rule with a cheap line scan (no YAML lib): an item block renders in place as a small inline badge (`gantt: <project>`) and is never drawn as a chart; a chart block emits the usual webview placeholder. `collectGantt` applies the identical rule when scanning (empty/whitespace project ⇒ treated as a chart block, i.e. not an item).
**Item block** (lives in any participating note/todo):
```
```gtd-gantt
project: bluesky # REQUIRED — the swimlane; its presence marks this an item
begin-date: 2026-07-01 # notes only — bar span start (yyyy-mm-dd or mm-dd-yyyy)
end-date: 2026-07-14 # notes only — bar span end
bg-colour: teal # optional styling, mirroring the gtd block
fg-colour: white
title: Design phase # optional; defaults to the note title
text: Hover detail # optional; tooltip
```
```
| Key | Applies to | Behavior |
|---|---|---|
| `project` | both | Required. Groups the item into a swimlane (case-insensitive; first-seen casing is the display name). Empty/absent ⇒ this is a chart block, not an item. |
| `begin-date` / `end-date` | notes → bars | Both required and valid, `end-date``begin-date`, else the item is **excluded with a warning**. |
| (native due date) | todos → milestones | A to-do becomes a ◆ **milestone** on its own `todo_due`. `begin-date`/`end-date` on a to-do are ignored (warning). No due date ⇒ excluded with a warning. |
| `bg-colour`/`fg-colour`/`title`/`text` | both | As the gtd block. Bars default to the stable pastel hash when no colour is set. |
Completed to-do milestones render dimmed and struck through; recurring to-dos get the ↻ mark. There is **no Unscheduled section** — an item with no usable date is simply excluded (the warning explains why).
**Chart block** (lives where the timeline is drawn):
```
```gtd-gantt
title: Roadmap
scope: children # this-folder | children | N | all — same semantics as gtd-calendar
notebook: Work/Ideas # same semantics as gtd-calendar; ignored under scope: all
filter-project: bluesky # optional — limit to a single project (case-insensitive)
sort-type: begin-date # begin-date | title | modified-date (default: begin-date)
sort: asc # asc | desc (default: asc)
card-detail: hover # hover | always | none (default: hover)
```
```
There is deliberately **no `notes`/`todos` inclusion option**: items opt in solely via their `project:` block, so those keys are unknown here (and warn as such). Projects render alphabetically; within each project, rows sort by `sort-type`/`sort`. The renderer draws a month tick header, a "today" line when today falls within the chart's date envelope, and scrolls horizontally for wide ranges. The host note is always excluded from its own scan (as with every view). Bars and milestones are clickable (open the source note); hover detail uses a native tooltip, suppressed by `card-detail: none`. Read-only.
---
## 3. Data Model Rules
### 3.1 Date resolution
1. **Todos:** `todo_due` from the Joplin API. A `date:` in the `gtd` block overrides it.
2. **Notes:** `date:` from the `gtd` block only. No fallback to created/updated time — those are not event dates and would produce misleading calendars.
3. **No resolvable date** → the calendar renders the item in **Unscheduled**; kanban and matrix retain it in the appropriate dateless bucket.
### 3.2 Inclusion matrix
An item appears on the calendar iff its type's mode admits it:
| Mode | Has `gtd` block | No `gtd` block |
|---|---|---|
| `gtd-only` (default) | ✅ included | ❌ excluded |
| `all` | ✅ included | ✅ included (default styling) |
| `none` | ❌ excluded | ❌ excluded |
The calendar note itself is always excluded from results, as is any note whose only `gtd-calendar` block makes it a calendar (a note may, however, be both an event and contain a calendar if it has both block types — edge case, supported, documented).
Kanban and matrix use a deliberately asymmetric inclusion contract:
| Item | Has `gtd` block | No `gtd` block |
|---|---|---|
| Ordinary note with `notes: all` (default) | Included | Excluded |
| Ordinary note with `notes: none` | Excluded | Excluded |
| To-do with `todos: gtd-only` | Included | Excluded |
| To-do with `todos: all` | Included | Included |
| To-do with `todos: none` | Excluded | Excluded |
An empty or malformed `gtd` block counts as found. Malformed admitted blocks
warn but do not disappear. The independent `notes` and `todos` filters run
before warnings and tag lookup. Each kanban/matrix host note is excluded before
eligibility and tag lookup.
### 3.3 Completed todos
**Calendar:** completed to-dos render with strikethrough and a checked indicator.
**Kanban:** completed to-dos enter Done when inside the configured done window;
opted-in ordinary notes carrying `done-tag` also enter Done without timestamp
filtering. **Matrix:** completed to-dos and done-tag notes are excluded. Ordinary
notes never become recurring.
---
## 4. Rendering
- **Calendar** reuses upstream's grouping/renderer architecture (`DayGrouping`/`WeekGrouping`/`MonthGrouping` + DOM renderers + CSS asset), including the empty-grouping placeholders and current-date highlight, plus the fork's own `month-grid` (a 7-column grid of the entire current month).
- **Fixed upstream bug:** `Calendar/index.ts` imported `WeekGrouping` from `Month/MonthGrouping`, so the week view silently grouped by month. Corrected in the fork.
- **Type distinction:** to-dos render with a checkbox glyph (☐ / ☑ when completed); ordinary notes render with 📄. A custom `icon` takes precedence. Recurring to-dos (per the `recurring` tag) get a ↻ suffix; ordinary notes never do, even if tagged `recurring`.
- **Unscheduled section:** below the calendar grid, split into two labelled sub-sections — to-dos first, then notes — each hidden when empty or switched off (`unscheduled-todos` / `unscheduled-notes`).
- **Kanban** renders eligible notes and to-dos in three fixed columns (Backlog / In Progress / Done) of compact cards; a "hover" mode shows date and hover text only on mouseover (`card-detail`). Each column independently renders cards in `page-size` batches.
- **Editable kanban:** only `editable: yes` adds a pointer handle and a compact
disclosure arrow that opens the keyboard **Move to…** menu. Pointer movement
has a threshold and is scoped to the
originating board/notebook group. Pending, success, stale, and failure states
are announced through an atomic live region; focus returns to the moved card
or status fallback after canonical rerender.
- **Matrix** renders eligible notes and incomplete to-dos in a 2×2 grid with axis and quadrant labels supplied per mode; cards are visually and behaviourally identical to kanban cards. Each quadrant owns an independent batch counter.
- **Editable matrix:** effective Eisenhower `editable: yes` reuses the pointer
handle and compact disclosure menu. Targets are limited to the originating
matrix/notebook group. Pending/error live announcements, Escape cancellation,
focus recovery, and guarded canonical rerender match editable kanban. Skeleton
and read-only Eisenhower matrices add no mutation UI or focus stops.
- **Incremental card lists:** collectors always return complete sorted arrays and full statistics. The main process sends normalized `pageSize`; the webview owns transient `visibleCount` state per column/quadrant and appends one batch per **List more** click. A linked tooltip (`aria-describedby`) reports the exact remaining count on pointer hover and keyboard focus. No expansion state is persisted; rerender/reload resets it.
- **Drilldown:** every tile, card, hover-card row, and chip across all four view types is clickable and opens the source note.
---
## 5. Architecture
Upstream is a single synchronous markdown-it content script with no data API access. The fork is a three-part plugin serving four view fence types (`gtd-calendar`, `gtd-kanban`, `gtd-matrix`, and `gtd-gantt`) through the same pipeline:
```
┌─────────────────────────────┐
│ 1. Markdown-it content │ Intercepts the four view fence types
│ script (renderer) │ and parses their YAML configuration,
│ │ emits a placeholder <div> tagged with
│ │ data-block-type and the config as a
│ │ data attribute.
└──────────────┬──────────────┘
│ (rendered HTML)
┌──────────────▼──────────────┐
│ 2. Webview asset script │ Finds placeholders, calls
│ (runs in rendered note) │ webviewApi.postMessage(config) with
│ │ getEvents / getKanban / getMatrix / getGantt
│ │ depending on block type. Builds the
│ │ calendar / board / matrix / Gantt DOM from
│ │ returned payload. Click → postMessage
│ │ ({type: "openNote", noteId}); editable
│ │ kanban/matrix send constrained move intent.
└──────────────┬──────────────┘
│ postMessage / onMessage
┌──────────────▼──────────────┐
│ 3. Main plugin process │ joplin.contentScripts.onMessage routes
│ (index.ts) │ by message type to a handler per block
│ │ type, each of which:
│ │ • resolves current note's folder
│ │ (joplin.workspace.selectedNote)
│ │ • builds folder tree, applies scope
│ │ (src/Gtd/folderScope.ts)
│ │ • fetches notes/todos, paginated
│ │ • parses ```gtd blocks from bodies
│ │ (src/Gtd/gtdBlock.ts)
│ │ • fetches tag titles per eligible item
│ │ (getNoteTagTitles) for recurrence /
│ │ in-progress / urgent / important
│ │ detection
│ │ • applies the inclusion matrix, date
│ │ resolution, and view-specific
│ │ bucketing (collectEvents.ts /
│ │ collectKanban.ts / collectMatrix.ts /
│ │ collectGantt.ts)
│ │ • handles openNote via
│ │ joplin.commands.execute
│ │ • reparses editable view config, refetches
│ │ host/card/scope/tags, derives minimal writes,
│ │ then returns a typed move result
└─────────────────────────────┘
```
The calendar, kanban, and matrix collectors share `folderScope.ts`,
`gtdBlock.ts`, and `resolveDate.ts`. Kanban and matrix also share
`buildKanbanCard.ts`, which normalizes the explicit `isTodo` discriminator,
source ID, title/style overrides, resolved date, completion state, recurrence
state, and modification time.
Their collection flow is: resolve scoped folders → exclude the host note →
evaluate item-kind eligibility → parse and warn on the `gtd` block → fetch
tags only for eligible items → build a normalized card → bucket → sort the
combined note/to-do arrays. Complete arrays and totals cross the plugin boundary;
SLICE8 batching remains transient webview state.
### Performance notes
- Folder tree and note list fetched per render request; paginated API calls (`page`/`has_more`).
- Body scanning is limited to items in scope. Calendar may omit bodies when both
item inclusion modes are `none`. Kanban and matrix always request bodies,
including when `todos: none`, because ordinary notes opt in through a `gtd`
block and `todos:` governs only to-dos. This is an intentional performance
tradeoff: block discovery requires the body, while tag-title requests remain
limited to items that pass host, type, completion, and shortcode eligibility.
For large scopes, pre-filter with Joplin's search API before fetching full
bodies; cache by `updated_time` if needed — not yet required at observed
real-world scales (largest tested: ~285 notes, single folder scope), but worth
revisiting if a `children`-scope board on a large tree feels slow, especially
with Persistent Layout keeping several boards rendered at once.
- Refresh model: normal background edits appear when Joplin rerenders the note.
An editable kanban or matrix move immediately refetches and rerenders only its initiating
`viewInstanceId`; request sequencing rejects stale responses. Other open
dashboards remain stale until their own rerender.
### Platform caveats
- Desktop is the target. Mobile plugin support is partial and this plugin's core mechanisms (custom fence rendering, webview↔plugin messaging) are not expected to work there; documented explicitly in the README. Underlying to-dos still sync and appear in Joplin's native mobile views. The "Embed Search" plugin pairing remains a documented pattern for cross-scope aggregation generally, not specifically a mobile workaround.
---
## 6. Implementation Phases
1. **Phase 0 — Scaffolding.** Fork, rename (manifest id, plugin name, file names), build pipeline verified (`npm install && npm run dist`), upstream week-view bug fixed, upstream tests green.
2. **Phase 1 — Plumbing.** Placeholder rendering for `gtd-calendar` fences; webview ⇄ plugin message round-trip proven with a hardcoded payload.
3. **Phase 2 — Data layer.** Folder tree + scope resolution; note/todo fetching; `gtd` block parsing; inclusion matrix; date resolution; unit tests for all of it (the upstream Jest setup carries over).
4. **Phase 3 — Rendering.** Wire real data into the reused renderers; Unscheduled section; type glyphs; fg-colour support; sort options.
5. **Phase 4 — Drilldown & polish.** Click-to-open; error boxes for malformed YAML; docs/README; scope-performance warnings.
---
## 7. Decision Log / Open Items
| # | Decision | Status |
|---|---|---|
| D1 | Defaults: `notes: gtd-only`, `todos: gtd-only` | ⚠️ **Confirm** — earlier discussion said todos are *always* included (i.e. `todos: all` default); the final message specified `gtd-only` as default for both. Spec follows the final message. |
| D2 | `gtd` `date:` overrides `todo_due` (vs. emitting a warning) | Adopted; revisit if confusing in practice. |
| D3 | Completed todos: shown struck-through in v1; `completed:` option reserved | Proposed, unconfirmed. |
| D4 | `sort`/`sort-type` act within groupings only; grid is always chronological | Confirmed. |
| D5 | Colour key spelling: `bg-colour`/`fg-colour` canonical, `bgColor` accepted for upstream parity | Proposed. |
| D6 | Future options parked: `completed:`, tag-based filtering, week-start day, live refresh | Backlog. |
---
## 8. Licensing & Attribution
Upstream is MIT (© Franco Speziali / WeMakeMachines). The fork retains the MIT license, preserves the original copyright notice, and credits upstream in the README per standard practice.
---
## 9. Roadmap (post-1.0 / post-launch)
Recorded direction, not commitments. Each gets a focused design pass before work begins; real-world use of the shipped plugin may reorder this.
### v0.2.0 — Repeating to-dos (visualisation) — SHIPPED · strip horizon — NOT YET BUILT
Recurrence itself is delegated to an existing community plugin (open-environment approach; we don't reimplement it). Candidates evaluated: BeatLink's original *Repeating To-Dos* (unmaintained), the *[Revived]* fork (maintained, GitHub issues only), and *Repeating To-dos v2* by TheScriptingGuy (`com.github.TheScriptingGuy.joplin-plugin-repeating-todos`, best-documented). All share the same mechanic: on completion the to-do's alarm date is reset to the next occurrence and the to-do is unmarked — so at any moment a recurring to-do is a **single** to-do at its next due date, with the recurrence rule stored in the note body.
- **Option A (shipped in 0.2.0):** detect recurrence and mark those events with a ↻ glyph. *Implementation note:* v2 of the plugin stores recurrence in Joplin's `userData` (not the note body) and maintains a `recurring` **tag** as its query index — so detection is by tag presence on to-dos (constant `RECURRING_TAG`), not body-scanning. Cleaner than the originally-planned body parse, and reuses the tag-fetching the kanban work will need. No series projection — the to-do shows at its next due date.
- **Option B (in pocket):** parse the recurrence rule and project all future occurrences across the visible window. Deferred because the body format has proven to be a moving target across forks; promote to a 0.2.x only if Option A's single-occurrence view proves insufficient, and target a specific fork's format at that time.
- **README:** recommend the maintained fork (v2 first, Revived as fallback); warn about the known "recurrence block injected into all notes" failure mode; note coexistence (their plugin moves the anchor, ours draws the marker).
**Configurable strip horizon.** `day`/`week`/`month` strip views currently span first-event to last-event (plus today). Add a horizon option to `gtd-calendar` (working name `weeks:` or `horizon:`, default ~8 weeks forward) controlling how far ahead the strip extends — relevant both generally and for how many future occurrences Option B would draw. **`month-grid` is explicitly excluded**: it always shows the entire current calendar month, by design, and that is not configurable.
*Note: multiple `gtd-calendar` blocks in one note already work as of 0.1.0 (each fence renders independently) — e.g. a "week ahead" strip plus a "month grid" in a single dashboard note. No work required; document as a recommended pattern.*
### v0.3.0 — Kanban board (read-only) — SHIPPED
Decision: a **second block type within this plugin**, not a separate plugin. Rationale: a kanban needs the entire existing data layer (`folderScope`, `collectEvents` inclusion matrix, `getNoteTagTitles`, config parsing, recurrence detection, webview messaging); a separate plugin would duplicate all of it or require a shared-library split whose overhead isn't justified for "three tag-driven columns." One plugin also serves the intended use — a calendar and a kanban in one dashboard note — with a single install and a unified config/tag vocabulary.
**The `gtd-kanban` block.** Parallel to `gtd-calendar`; lives in its own note or stacked beneath a calendar (multiple blocks already render independently).
```
```gtd-kanban
title: Editorial Board
scope: this-folder # this-folder | children | N — same semantics as gtd-calendar
todos: gtd-only # gtd-only (default) | all | none
sort-type: due-date # due-date (default) | title | modified-date
sort: asc # asc | desc
in-progress-tag: in-progress # configurable; default "in-progress"
card-detail: hover # hover (default) | always | none
done-window: 7 # days; integer or "all"; default 7
```
**Columns (always three, always shown even when empty):** Backlog · In Progress · Done.
**Bucketing rules:**
- **Done wins.** Any to-do with `todo_completed > 0`*Done*, regardless of tags.
- Else, carrying the in-progress tag → *In Progress*.
- Else → *Backlog*.
- **Backlog shows everything** uncompleted and untagged — dated or not, near or far. (Acknowledged: can grow long; compact cards mitigate.)
**Sorting (within each column):**
- `sort-type: due-date | title | modified-date`, with `sort: asc | desc`.
- Under `due-date`, to-dos **without** a due date sort *after* all dated ones.
**Done column window:** shows only to-dos completed within `done-window` days (default 7); `all` shows the full history. Prevents unbounded growth.
**Cards:** compact by default—title, resolved date, optional recurrence mark for to-dos, and click-to-open. `card-detail` controls the hover panel: `hover` (default), `always` (details inline), `none` (no panel). Ordinary notes containing a `gtd` block now participate under the same placement and styling rules.
**Read-only in 0.3.0:** reflects state, never mutates it. Proves columns/bucketing cheaply before write-back.
**Architecture:** reuses the calendar's pipeline. New `gtd-kanban` markdown-it fence → placeholder → webview requests data → plugin runs scope + inclusion + tag fetch (already built) + bucketing → returns three columns → webview renders. `IN_PROGRESS_TAG` constant mirrors `RECURRING_TAG`, exposed via `in-progress-tag:` config from the start.
**`sort-type` vocabulary reconciliation:** the calendar block's `sort-type` (currently `title | modified_date`) is unified toward `due-date | title | modified-date` across both blocks so they read consistently. For the calendar, `due-date` is accepted but falls back to chronological where less meaningful. (Note the spelling shift `modified_date``modified-date` for cross-block consistency; the old spelling stays accepted as an alias to avoid breaking existing calendars.)
### v0.4.0 — Eisenhower Matrix (`gtd-matrix` block) — SHIPPED
### v0.5.0 — Skeleton Matrix (`mode:` on gtd-matrix) — SHIPPED
Real-world feedback: Eisenhower's opinion axes (and "Eliminate" in particular) fit triage, not a vault of already-committed work. The **Skeleton Matrix** replaces them with two derived axes the system already maintains: rows = active (`in-progress-tag`, shared with the kanban); columns = due soon (`urgent-window` days, default 3, overdue included; `urgent` tag as manual override — in-progress + urgent + dateless is Do Next by design). Quadrants: Do Next / Scheduled / On Deck / Backlog; dateless active work → Scheduled; nothing is ever "Eliminate". Implemented as `mode: skeleton | eisenhower` on the same block — positional MatrixBoard (topLeft…bottomRight), labels supplied per mode via payload, shared rendering. Skeleton is the default (it matches the plugin's own tag/date vocabulary); classic Eisenhower preserved via `mode: eisenhower`.
A fourth display block: a 2×2 prioritisation matrix, tag-driven like the kanban. Chosen ahead of drag-and-drop deliberately: it is on the **views axis** (another rendering of tagged to-dos, reusing the whole data layer, read-only) rather than the **authoring axis** (vault mutation), so it deepens what the plugin already is without committing to write-back.
- **Quadrants** from two configurable tags (`urgent-tag:` default `urgent`; `important-tag:` default `important`):
- urgent + important → **Do Next**
- important only → **Scheduled**
- urgent only → **On Deck**
- neither → **Backlog**
- **Completed to-dos are excluded entirely** — the matrix is a prioritisation view, not a tracking view; kanban's Done column is where completions live.
- Incomplete to-dos plus ordinary notes containing a `gtd` block; same `scope` / `todos` / `sort-type` / `sort` / `card-detail` options as kanban and the same compact clickable cards.
- Own block (`gtd-matrix`), not a kanban mode — different layout, different tag semantics.
### v0.6.0 — Notebook targeting & profile-wide scope — SHIPPED
Two scope-axis additions shared across all four view blocks (see §2.1 / §2.5 for the normative schema):
- **`notebook:`** roots the folder scan at a chosen notebook rather than the host note's own folder, so a dashboard note can live anywhere and aggregate a different tree. Resolution order: raw 32-char folder id → `Parent/Child` case-insensitive title path → unique bare title. Any miss (unknown id, unresolvable path, ambiguous or absent title) warns and falls back to the host folder (warn-don't-fail). `scope` applies relative to the resolved notebook. Implemented as a pure `resolveNotebook.ts` (folders + spec → id + warning) plus a shared `parseNotebookOption`; resolution lives in `index.ts` (needs the folder tree), collectors stay unchanged.
- **`scope: all`** scans every notebook in the profile, ignoring root and depth — an explicit, deliberately expensive opt-in ("very dangerous, yes"). Represented as a `scopeAll` flag (not a `scopeDepth` sentinel); `folderScope` returns all folder ids when set. Guardrails: the stats footnote reports scan size and is marked `scope: all`; a soft ⚠ appears past ~2000 scanned notes (advisory, never blocks). `notebook:` + `scope: all``all` wins, with a warning.
- **Body-fetch contract:** calendar retains its safe inclusion-mode optimization and Gantt retains its item-block behavior. Kanban and matrix now always request bodies because ordinary-note opt-in must be discovered even under `todos: none`; tag requests remain eligibility-gated.
### v0.7.0 — Gantt chart (`gtd-gantt` block) — SHIPPED
A fifth view: a project-grouped timeline, on the **views axis** like the matrix (another read-only rendering of existing notes/to-dos), not the authoring axis. Unlike the other blocks, gantt items opt in through their **own** `gtd-gantt` block carrying a `project:`, and the same fence name is disambiguated by that key (item vs chart — see §2.5). Notes become bars (`begin-date``end-date`), to-dos become ◆ milestones on their due date; grouped into per-project swimlanes, no Unscheduled section (dateless/invalid items are excluded with a warning). Data layer is pure and unit-tested (`ganttBlock.ts`, `parseGanttConfig.ts`, `collectGantt.ts`); rendering is a CSS-grid timeline (month ticks, today line, horizontal scroll, click-to-open). Item blocks render in place as a small `gantt: <project>` badge. Read-only. Open follow-ups (deferred): richer hover cards (currently native tooltips), `card-detail: always` inline text on bars, an optional Unscheduled strip.
### Unreleased — Incremental kanban/matrix card limits — COMPLETE
Kanban columns and matrix quadrants now render in configurable `page-size`
batches (default 10). Parsing is shared and rejects zero, negative, fractional,
non-numeric, or non-finite values with a warning and fallback to 10. Collectors
remain complete and unchanged; normalized `pageSize` crosses the plugin payload
boundary, while the webview owns independent transient expansion state for every
bucket. **List more** reveals one batch, reports the exact remaining count through
an accessible hover/focus tooltip, preserves focus between intermediate clicks,
and disappears at completion. Reload resets all expansion. Automated validation
passed at 164 tests and manual Joplin acceptance passed 2026-07-27.
### Unreleased — Opted-in note cards for kanban/matrix — COMPLETE
Ordinary notes containing a `gtd` block now participate in kanban and matrix
views. Empty and malformed blocks opt in; malformed YAML warns and falls back to
source-note values. Notes use `gtd` dates only, share title/colour/icon/text
overrides, render with 📄 when no icon is supplied, and never become completed or
recurring. `todos:` continues to govern only to-dos. Kanban places notes in
Backlog or In Progress; both matrix modes reuse their existing tag/date axes.
Body inspection is always enabled for these two collectors, while tag lookup
remains eligibility-gated. Automated validation passed at 185 tests, the
production JPL built successfully, and manual Joplin acceptance passed
2026-07-27.
### v2.0.0 — Opt-in kanban authoring — IMPLEMENTED, RELEASE PENDING
Kanban write-back is implemented behind `editable: yes`; existing blocks remain
read-only. Pointer and keyboard controls submit constrained intent. The main
process reparses configuration, validates the selected host and fresh scope/item
eligibility, derives minimal tag/completion writes, and refreshes the initiating
view from canonical data. Failures do not use stale rollback values. Cross-group
movement and free-form ordering remain excluded. Release and manual Joplin
acceptance are tracked in `SLICE14.md`.
### Future authoring — create from view
**Create-to-do from kanban.** A "+" affordance per column creates a to-do with that column's state (In Progress column → in-progress tag; Backlog → no tag). Easier than create-from-calendar: the column *is* the metadata, no date geometry. Open: target notebook (leaf dashboards obvious, cockpit murky), trigger reliability in the rendered viewer.
**Create-to-do from calendar.** Click/affordance on a day cell creates a to-do due that day. Open questions: trigger mechanism in the read-only viewer (double-click/right-click may be claimed by Joplin; per-cell "+" affordance likelier), target notebook rule, whether created to-dos get an empty gtd block so they appear under `todos: gtd-only`. Prototype the trigger before designing the rest.

211
TASKS.md
View File

@ -1,211 +0,0 @@
# TASKS — SLICE8 through SLICE14
Use this file as the cross-slice implementation checklist. Update it whenever a
task is completed or its status changes. Keep automated validation results and
manual Joplin acceptance recorded separately.
## Overall status
- [x] SLICE8 complete — incremental card limits
- [x] SLICE9 complete — opted-in note cards
- [x] SLICE10 complete — child-notebook boards and matrices
- [x] SLICE11 complete — ordinary-note completion with a `done` tag
- [x] SLICE12 complete — independent project-note and task-to-do filters
- [x] SLICE13 complete — underline removal for kanban and matrix cards
- [x] SLICE14 complete — opt-in kanban drag and drop
- [x] SLICE14a complete — compact move menu
Slices 813 are complete. SLICE14 is the next implementation slice.
## SLICE8 — Incremental card limits for kanban and matrix views
### Configuration and types
- [x] Add normalized `page-size` fields to `KanbanConfig` and `MatrixConfig` in
`src/Gtd/types.ts`.
- [x] Recognize `page-size` in `parseKanbanConfig.ts` and
`parseMatrixConfig.ts`.
- [x] Default `page-size` to 10.
- [x] Accept positive integer overrides.
- [x] Warn and fall back to 10 for zero, negative, fractional, or non-numeric
values.
- [x] Add parser tests for defaults, valid overrides, and invalid fallbacks.
- [x] Send normalized `pageSize` in successful and missing-source-note kanban
and matrix responses.
- [x] Preserve collector arguments, result shapes, sorting, totals, and scope
behavior while adding the payload field.
### Rendering and interaction
- [x] Add reusable incremental-list behavior to
`src/gtd-calendar-webview.js`.
- [x] Initially render at most `page-size` cards in each kanban column.
- [x] Initially render at most `page-size` cards in each matrix quadrant.
- [x] Add a separate "List more" control to every bucket with hidden cards.
- [x] Reveal the next `page-size` cards in only the clicked bucket.
- [x] Update the remaining-entry count after every click.
- [x] Remove the control when no hidden cards remain.
- [x] Reset all expansion state when the rendered note reloads.
- [x] Keep complete collection results and total-card statistics; apply limits
only while rendering.
### Accessibility and styling
- [x] Style the button and remaining-count popup in `src/event-calendar.css`.
- [x] Show the exact remaining-entry count on pointer hover.
- [x] Make the same summary available on keyboard focus.
- [x] Ensure activating "List more" never triggers a card's note-open action.
### Tests, documentation, and acceptance
- [x] Test empty buckets and buckets smaller than one batch.
- [x] Test buckets containing exactly one batch.
- [x] Test one card beyond a batch boundary.
- [x] Test repeated expansion and final-control removal.
- [x] Test independent expansion across columns, quadrants, and stacked blocks.
- [x] Document `page-size` and transient expansion in README.md.
- [x] Document the behavior and data-flow boundary in SPEC.md.
- [x] Add the feature to CHANGELOG.md.
- [x] Run the full automated test suite and record command/results.
- [x] Run the production package build and record command/artifact.
- [x] Manually verify kanban with more than 20 cards in one column in Joplin.
- [x] Manually verify Skeleton and Eisenhower matrices with more than 20 cards
in one quadrant in Joplin.
- [x] Confirm sorting, card navigation, styling, warnings, and statistics remain
unchanged.
- [x] Mark SLICE8 complete in `SLICE8.md` and this file.
## SLICE9 — Include opted-in note cards in kanban and matrix views
### Collection and types
- [x] Update kanban collection to inspect ordinary notes (`is_todo: 0`).
- [x] Include an ordinary note only when its body contains a `gtd` block.
- [x] Keep ordinary notes without a `gtd` block excluded.
- [x] Keep the host view note excluded even when it contains a `gtd` block.
- [x] Preserve current `todos:` behavior for to-do notes.
- [x] Place eligible normal notes with the configured in-progress tag in the
kanban In Progress column.
- [x] Place other eligible normal notes in the kanban Backlog column.
- [x] Never place normal notes in the kanban Done column.
- [x] Apply existing urgent/important tag bucketing to normal notes in an
Eisenhower matrix.
- [x] Apply existing date bucketing to normal notes in a Skeleton matrix.
- [x] Reuse `gtd` title, date, colour, icon, and text overrides on note cards.
- [x] Ensure normal notes are never marked completed or recurring.
- [x] Update card types to represent notes and to-dos accurately.
- [x] Fetch note bodies wherever opted-in notes may be collected while retaining
only safe body-fetch optimizations.
### Tests
- [x] Test kanban inclusion for a note containing a valid `gtd` block.
- [x] Test kanban exclusion for a plain note.
- [x] Test kanban Backlog and In Progress placement for note cards.
- [x] Test host-note exclusion and malformed `gtd` block warnings.
- [x] Test that current to-do and Done behavior is unchanged.
- [x] Test note-card bucketing in Skeleton matrix mode.
- [x] Test note-card bucketing in Eisenhower matrix mode.
- [x] Test completed-to-do exclusion remains unchanged in matrices.
- [x] Test mixed note/to-do sorting and click-to-open payloads.
- [x] Test that SLICE8 limits use the combined note and to-do card count.
### Documentation and acceptance
- [x] Document note-card opt-in rules and examples in README.md.
- [x] Update SPEC.md with mixed note/to-do behavior.
- [x] Add the feature to CHANGELOG.md.
- [x] Run the full automated test suite and record command/results.
- [x] Run the production package build and record command/artifact.
- [x] Manually verify mixed note/to-do kanban views in Joplin.
- [x] Manually verify mixed note/to-do views in both matrix modes in Joplin.
- [x] Confirm existing to-do filtering, completion, sorting, styling, warnings,
and navigation do not regress.
- [x] Mark SLICE9 complete in `SLICE9.md` and this file.
## SLICE10 — Separate child-notebook kanban and matrix views
### Scope and folder grouping
- [x] Preserve explicit `scope: children` plus `group: notebook` intent in
normalized kanban and matrix configuration.
- [x] Keep numeric scope, `this-folder`, and `scope: all` semantics unchanged.
- [x] Add a shared folder-tree helper that returns notebook IDs, titles, paths,
and parent relationships in deterministic tree/path order.
- [x] Handle nested notebooks and duplicate notebook names correctly.
- [x] Create one result group for cards owned directly by the root notebook.
- [x] Create one result group for cards owned directly by each descendant
notebook.
- [x] Ensure every in-scope card appears in exactly one notebook group.
- [x] Omit empty notebook groups.
- [x] Preserve one overall empty-state presentation when all groups are empty.
### Payloads and rendering
- [x] Add named per-notebook kanban and matrix groups to collection result types.
- [x] Retain compatible single-view results unless both `scope: children` and
`group: notebook` are selected.
- [x] Provide aggregate and per-view statistics without double-counting notes or
cards.
- [x] Preserve warnings at the appropriate overall or notebook-view level.
- [x] Update plugin-to-webview messages and handlers for grouped results.
- [x] Render a complete kanban or matrix component for every non-empty group.
- [x] Use the full notebook path as each view heading.
- [x] Give every generated view independent SLICE8 expansion state.
- [x] Apply the configured `page-size` independently within every view bucket.
- [x] Group SLICE9 note cards by owning notebook in the same way as to-do cards.
### Tests
- [x] Test preservation of explicit `children` plus `group: notebook` intent.
- [x] Test deterministic folder ordering, nested paths, duplicate names, and
empty groups.
- [x] Test that kanban cards appear exactly once in their owning notebook group.
- [x] Test the same invariant in Skeleton and Eisenhower matrices.
- [x] Include both note and to-do cards in grouping tests.
- [x] Test aggregate statistics and warnings for double-counting regressions.
- [x] Test independent batching and "List more" state across generated views.
- [x] Test that ungrouped `children`, numeric scope, `this-folder`, and
`scope: all` still produce one view with unchanged behavior.
### Documentation and acceptance
- [x] Document `scope: children` plus `group: notebook` multi-view behavior
in README.md.
- [x] Add a parent/child/grandchild notebook example to README.md or SPEC.md.
- [x] Update architecture and payload behavior in SPEC.md.
- [x] Add the feature to CHANGELOG.md.
- [x] Run the full automated test suite and record command/results.
- [x] Run the production package build and record command/artifact.
- [x] Manually verify a parent notebook containing multiple children and
grandchildren in Joplin.
- [x] Confirm headings, ordering, empty-group handling, no duplication, sorting,
styling, navigation, warnings, and independent expansion.
- [x] Mark SLICE10 complete in `SLICE10.md` and this file.
## Final cross-slice verification
- [x] Run the complete automated suite after all three slices are integrated.
- [x] Build the final production `.jpl` and record its path and version.
- [x] Verify a dashboard containing kanban and both matrix modes together.
- [x] Verify more than 20 mixed note/to-do cards across multiple child notebooks.
- [x] Confirm reload resets every expanded list to its configured initial size.
- [x] Record manual Joplin acceptance without conflating it with automated checks.
- [x] Re-run the repository-wide prohibited-reference audit.
- [x] Ensure README.md, SPEC.md, CHANGELOG.md, TASKS.md, and SLICE810 reflect the
final implementation state.
## Subsequent slice status
- [x] SLICE11 complete. Implementation, focused and full automation, production
JPL inspection, and manual Joplin acceptance were completed on 2026-07-28.
- [x] SLICE12 complete. Implementation, focused and full automation, production
JPL inspection, and manual Joplin acceptance were completed on 2026-07-28.
- [x] SLICE13 complete. Implementation, automation, production package
inspection, and manual Joplin acceptance were completed on 2026-07-28.
- [x] SLICE14 complete. Phases 17 passed; explicit manual Joplin acceptance was
received on 2026-07-31. Slice 15 remains not started.
- [x] SLICE14a complete. Phases 15 passed and explicit manual acceptance was
received on 2026-07-31.
- [x] SLICE15 complete. Phases 17 passed, production packaging was inspected,
and explicit manual Joplin acceptance was received on 2026-07-31.

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "joplin-plugin-gtd-calendar",
"version": "2.0.0",
"version": "2.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "joplin-plugin-gtd-calendar",
"version": "2.0.0",
"version": "2.1.0",
"license": "MIT",
"dependencies": {
"date-fns": "^2.29.3",

View File

@ -1,9 +1,8 @@
{
"name": "joplin-plugin-gtd-calendar",
"version": "2.0.0",
"version": "2.1.0",
"scripts": {
"test": "jest",
"predist": "bash scripts/archive-jpl.sh",
"dist": "webpack --env joplin-plugin-config=buildMain && webpack --env joplin-plugin-config=buildExtraScripts && webpack --env joplin-plugin-config=createArchive",
"prepare": "npm run dist",
"update": "npm install -g generator-joplin && yo joplin --node-package-manager npm --update --force",

View File

@ -1,38 +0,0 @@
#!/usr/bin/env bash
#
# Archive existing publish/*.jpl builds before a new `npm run dist`.
#
# Never overwrite a previous .jpl. This moves each current
# publish/*.jpl into build-archive/ with a version + timestamp suffix so a
# fresh build can't clobber the last one. No-op when no .jpl is present.
#
# The archive lives at repo root (NOT under publish/) deliberately: webpack's
# buildMain step runs `fs.removeSync(publishDir)` and would wipe an in-publish
# archive, and `package.json` "files": ["publish"] would ship it to npm.
# build-archive/ is git-ignored and outside both.
#
# Wired as the `predist` npm script, so it runs automatically before every
# `npm run dist`. Safe to run manually too.
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
publish_dir="$repo_root/publish"
archive_dir="$repo_root/build-archive"
shopt -s nullglob
jpls=("$publish_dir"/*.jpl)
if [ ${#jpls[@]} -eq 0 ]; then
echo "archive-jpl: no publish/*.jpl to archive."
exit 0
fi
version="$(node -p "require('$repo_root/package.json').version")"
timestamp="$(date +%Y%m%d-%H%M%S)"
mkdir -p "$archive_dir"
for jpl in "${jpls[@]}"; do
base="$(basename "$jpl" .jpl)"
dest="$archive_dir/${base}-${version}-${timestamp}.jpl"
mv "$jpl" "$dest"
echo "archive-jpl: moved $(basename "$jpl") -> build-archive/$(basename "$dest")"
done

View File

@ -5,7 +5,7 @@ import {
RawNote,
RECURRING_TAG,
} from "./types";
import resolveScopedFolderIds from "./folderScope";
import resolveEffectiveFolderScope from "./effectiveFolderScope";
import extractGtdBlock from "./gtdBlock";
import { resolveEventDate } from "./resolveDate";
@ -35,12 +35,15 @@ export default async function collectEvents(
const warnings: string[] = [];
const folders = await adapter.getFolders();
const folderIds = resolveScopedFolderIds(
const effectiveScope = resolveEffectiveFolderScope(
folders,
calendarFolderId,
config.scopeDepth,
config.scopeAll
config.scopeAll,
config.excludeNotebooks
);
const folderIds = effectiveScope.folderIds;
warnings.push(...effectiveScope.warnings);
let scannedNotes = 0;
const events: GtdEvent[] = [];

View File

@ -6,7 +6,7 @@ import {
KanbanLayout,
NotebookKanbanGroup,
} from "./types";
import resolveScopedFolderIds from "./folderScope";
import resolveEffectiveFolderScope from "./effectiveFolderScope";
import resolveScopedFolderMetadata from "./folderTree";
import extractGtdBlock from "./gtdBlock";
import buildKanbanCard from "./buildKanbanCard";
@ -42,17 +42,23 @@ export default async function collectKanban(
const warnings: string[] = [];
const folders = await adapter.getFolders();
const effectiveScope = resolveEffectiveFolderScope(
folders,
kanbanFolderId,
config.scopeDepth,
config.scopeAll,
config.excludeNotebooks
);
warnings.push(...effectiveScope.warnings);
const effectiveIds = new Set(effectiveScope.folderIds);
const folderMetadata = config.groupByNotebook
? resolveScopedFolderMetadata(folders, kanbanFolderId)
? resolveScopedFolderMetadata(folders, kanbanFolderId).filter((folder) =>
effectiveIds.has(folder.id)
)
: [];
const folderIds = config.groupByNotebook
? folderMetadata.map((folder) => folder.id)
: resolveScopedFolderIds(
folders,
kanbanFolderId,
config.scopeDepth,
config.scopeAll
);
: effectiveScope.folderIds;
let scannedNotes = 0;
const backlog: KanbanCard[] = [];

View File

@ -6,7 +6,7 @@ import {
MatrixLayout,
NotebookMatrixGroup,
} from "./types";
import resolveScopedFolderIds from "./folderScope";
import resolveEffectiveFolderScope from "./effectiveFolderScope";
import resolveScopedFolderMetadata from "./folderTree";
import extractGtdBlock from "./gtdBlock";
import buildKanbanCard from "./buildKanbanCard";
@ -45,17 +45,23 @@ export default async function collectMatrix(
const warnings: string[] = [];
const folders = await adapter.getFolders();
const effectiveScope = resolveEffectiveFolderScope(
folders,
matrixFolderId,
config.scopeDepth,
config.scopeAll,
config.excludeNotebooks
);
warnings.push(...effectiveScope.warnings);
const effectiveIds = new Set(effectiveScope.folderIds);
const folderMetadata = config.groupByNotebook
? resolveScopedFolderMetadata(folders, matrixFolderId)
? resolveScopedFolderMetadata(folders, matrixFolderId).filter((folder) =>
effectiveIds.has(folder.id)
)
: [];
const folderIds = config.groupByNotebook
? folderMetadata.map((folder) => folder.id)
: resolveScopedFolderIds(
folders,
matrixFolderId,
config.scopeDepth,
config.scopeAll
);
: effectiveScope.folderIds;
let scannedNotes = 0;
const topLeft: KanbanCard[] = [];

View File

@ -0,0 +1,43 @@
import resolveScopedFolderIds from "./folderScope";
import { resolveNotebookReference } from "./resolveNotebook";
import { RawFolder } from "./types";
export interface EffectiveFolderScope {
folderIds: string[];
excludedFolderIds: Set<string>;
warnings: string[];
}
/** Resolve positive scope, then subtract every valid excluded notebook tree. */
export default function resolveEffectiveFolderScope(
folders: RawFolder[],
rootFolderId: string,
depth: number,
scopeAll: boolean,
excludeNotebooks: string[]
): EffectiveFolderScope {
const positiveIds = resolveScopedFolderIds(folders, rootFolderId, depth, scopeAll);
const excludedFolderIds = new Set<string>();
const warnings: string[] = [];
for (const spec of excludeNotebooks) {
const resolved = resolveNotebookReference(folders, spec);
if (!resolved.folderId) {
warnings.push(`exclude-notebook: ${resolved.problem} (nothing excluded)`);
continue;
}
for (const folderId of resolveScopedFolderIds(
folders,
resolved.folderId,
Infinity
)) {
excludedFolderIds.add(folderId);
}
}
return {
folderIds: positiveIds.filter((folderId) => !excludedFolderIds.has(folderId)),
excludedFolderIds,
warnings,
};
}

View File

@ -14,7 +14,7 @@ export default function resolveScopedFolderIds(
depth: number,
scopeAll = false
): string[] {
if (scopeAll) return folders.map((folder) => folder.id);
if (scopeAll) return [...new Set(folders.map((folder) => folder.id))];
const childrenByParent = new Map<string, string[]>();
for (const folder of folders) {
@ -24,6 +24,7 @@ export default function resolveScopedFolderIds(
}
const inScope: string[] = [rootFolderId];
const visited = new Set<string>([rootFolderId]);
let frontier = [rootFolderId];
let level = 0;
@ -31,6 +32,8 @@ export default function resolveScopedFolderIds(
const next: string[] = [];
for (const folderId of frontier) {
for (const childId of childrenByParent.get(folderId) || []) {
if (visited.has(childId)) continue;
visited.add(childId);
inScope.push(childId);
next.push(childId);
}

View File

@ -1,7 +1,7 @@
const YAML = require("yaml");
import extractGtdBlock from "./gtdBlock";
import resolveScopedFolderIds from "./folderScope";
import resolveEffectiveFolderScope from "./effectiveFolderScope";
import { moveKanbanItem } from "./kanbanMutation";
import { isKanbanDestination } from "./moveProtocol";
import parseKanbanConfig from "./parseKanbanConfig";
@ -107,9 +107,13 @@ export async function validateAndMoveKanbanCard(
if (!config.scopeAll && config.notebook) {
scanFolderId = resolveNotebook(folders, config.notebook, host.parentId).folderId;
}
const folderIds = new Set(
resolveScopedFolderIds(folders, scanFolderId, config.scopeDepth, config.scopeAll)
);
const folderIds = new Set(resolveEffectiveFolderScope(
folders,
scanFolderId,
config.scopeDepth,
config.scopeAll,
config.excludeNotebooks
).folderIds);
const outcome = await moveKanbanItem(
mutationAdapter,
candidate.cardId,

View File

@ -1,7 +1,7 @@
const YAML = require("yaml");
import extractGtdBlock from "./gtdBlock";
import resolveScopedFolderIds from "./folderScope";
import resolveEffectiveFolderScope from "./effectiveFolderScope";
import { moveMatrixItem } from "./matrixMutation";
import { isMatrixDestination } from "./moveProtocol";
import parseMatrixConfig from "./parseMatrixConfig";
@ -109,9 +109,13 @@ export async function validateAndMoveMatrixCard(
if (!config.scopeAll && config.notebook) {
scanFolderId = resolveNotebook(folders, config.notebook, host.parentId).folderId;
}
const folderIds = new Set(
resolveScopedFolderIds(folders, scanFolderId, config.scopeDepth, config.scopeAll)
);
const folderIds = new Set(resolveEffectiveFolderScope(
folders,
scanFolderId,
config.scopeDepth,
config.scopeAll,
config.excludeNotebooks
).folderIds);
const outcome = await moveMatrixItem(
mutationAdapter,
candidate.cardId,

View File

@ -1,5 +1,8 @@
import { CalendarConfig, InclusionMode } from "./types";
import { parseNotebookOption } from "./resolveNotebook";
import {
parseExcludeNotebookOption,
parseNotebookOption,
} from "./resolveNotebook";
const VIEW_ALIASES: Record<string, CalendarConfig["view"]> = {
day: "day",
@ -28,6 +31,7 @@ export default function parseCalendarConfig(raw: any): CalendarConfig {
"title",
"scope",
"notebook",
"exclude-notebook",
"notes",
"todos",
"sort",
@ -77,6 +81,10 @@ export default function parseCalendarConfig(raw: any): CalendarConfig {
// notebook (id / title / Parent/Child path); resolved later against the
// folder tree. Empty → null (host folder). Ignored under scope: all.
const notebook = parseNotebookOption(input.notebook);
const excludeNotebooks = parseExcludeNotebookOption(
input["exclude-notebook"],
warnings
);
if (scopeAll && notebook) {
warnings.push(
`"notebook" is ignored when scope: all (scanning every notebook)`
@ -129,6 +137,7 @@ export default function parseCalendarConfig(raw: any): CalendarConfig {
scopeDepth,
scopeAll,
notebook,
excludeNotebooks,
notes,
todos,
sort,

View File

@ -6,7 +6,10 @@ import {
KanbanSortType,
NoteInclusionMode,
} from "./types";
import { parseNotebookOption } from "./resolveNotebook";
import {
parseExcludeNotebookOption,
parseNotebookOption,
} from "./resolveNotebook";
import parsePageSize from "./pageSize";
const INCLUSION_MODES: InclusionMode[] = ["gtd-only", "all", "none"];
@ -26,6 +29,7 @@ export default function parseKanbanConfig(raw: any): KanbanConfig {
"scope",
"group",
"notebook",
"exclude-notebook",
"notes",
"todos",
"sort-type",
@ -81,6 +85,10 @@ export default function parseKanbanConfig(raw: any): KanbanConfig {
}
const notebook = parseNotebookOption(input.notebook);
const excludeNotebooks = parseExcludeNotebookOption(
input["exclude-notebook"],
warnings
);
if (scopeAll && notebook) {
warnings.push(
`"notebook" is ignored when scope: all (scanning every notebook)`
@ -205,6 +213,7 @@ export default function parseKanbanConfig(raw: any): KanbanConfig {
scopeAll,
groupByNotebook,
notebook,
excludeNotebooks,
notes,
todos,
sortType,

View File

@ -9,7 +9,10 @@ import {
DONE_TAG,
NoteInclusionMode,
} from "./types";
import { parseNotebookOption } from "./resolveNotebook";
import {
parseExcludeNotebookOption,
parseNotebookOption,
} from "./resolveNotebook";
import parsePageSize from "./pageSize";
const INCLUSION_MODES: InclusionMode[] = ["gtd-only", "all", "none"];
@ -30,6 +33,7 @@ export default function parseMatrixConfig(raw: any): MatrixConfig {
"scope",
"group",
"notebook",
"exclude-notebook",
"notes",
"todos",
"sort-type",
@ -98,6 +102,10 @@ export default function parseMatrixConfig(raw: any): MatrixConfig {
}
const notebook = parseNotebookOption(input.notebook);
const excludeNotebooks = parseExcludeNotebookOption(
input["exclude-notebook"],
warnings
);
if (scopeAll && notebook) {
warnings.push(
`"notebook" is ignored when scope: all (scanning every notebook)`
@ -222,6 +230,7 @@ export default function parseMatrixConfig(raw: any): MatrixConfig {
scopeAll,
groupByNotebook,
notebook,
excludeNotebooks,
notes,
todos,
sortType,

View File

@ -7,6 +7,11 @@ export interface ResolvedNotebook {
warning: string | null;
}
export interface NotebookReferenceResolution {
folderId: string | null;
problem: string | null;
}
const HEX_ID = /^[0-9a-f]{32}$/i;
/**
@ -19,6 +24,79 @@ export function parseNotebookOption(value: any): string | null {
return trimmed.length > 0 ? trimmed : null;
}
/** Normalize scalar/list `exclude-notebook:` input while retaining valid entries. */
export function parseExcludeNotebookOption(
value: any,
warnings: string[]
): string[] {
if (value === undefined || value === null) return [];
const values = Array.isArray(value) ? value : [value];
const result: string[] = [];
for (let index = 0; index < values.length; index += 1) {
const entry = values[index];
if (
typeof entry !== "string" &&
typeof entry !== "number" &&
typeof entry !== "boolean"
) {
warnings.push(
`Invalid exclude-notebook entry ${index + 1} (expected a notebook name, path, or id)`
);
continue;
}
const trimmed = String(entry).trim();
if (!trimmed) {
warnings.push(`Invalid exclude-notebook entry ${index + 1} (empty value ignored)`);
continue;
}
result.push(trimmed);
}
return result;
}
/** Resolve a notebook reference without applying the positive-root fallback. */
export function resolveNotebookReference(
folders: RawFolder[],
spec: string
): NotebookReferenceResolution {
const trimmed = (spec || "").trim();
if (!trimmed) return { folderId: null, problem: "is empty" };
if (HEX_ID.test(trimmed)) {
const match = folders.find((f) => f.id.toLowerCase() === trimmed.toLowerCase());
return match
? { folderId: match.id, problem: null }
: { folderId: null, problem: `no notebook with id "${trimmed}"` };
}
const segments = trimmed
.split("/")
.map((segment) => segment.trim())
.filter((segment) => segment.length > 0);
if (segments.length > 1) {
const resolved = resolvePath(folders, segments);
if (resolved.length === 1) return { folderId: resolved[0], problem: null };
return resolved.length === 0
? { folderId: null, problem: `path "${trimmed}" not found` }
: {
folderId: null,
problem: `path "${trimmed}" is ambiguous (${resolved.length} matches)`,
};
}
const wanted = trimmed.toLowerCase();
const matches = folders.filter((folder) =>
(folder.title || "").toLowerCase() === wanted
);
if (matches.length === 1) return { folderId: matches[0].id, problem: null };
return matches.length === 0
? { folderId: null, problem: `no notebook named "${trimmed}"` }
: {
folderId: null,
problem: `"${trimmed}" is ambiguous (${matches.length} notebooks share that name — qualify it as a Parent/Child path)`,
};
}
/**
* Resolve a `notebook:` option to a folder id, per SPEC.md §2.
*
@ -38,52 +116,11 @@ export default function resolveNotebook(
): ResolvedNotebook {
const trimmed = (spec || "").trim();
if (!trimmed) return { folderId: hostFolderId, warning: null };
// 1. Raw folder id.
if (HEX_ID.test(trimmed)) {
const match = folders.find((f) => f.id.toLowerCase() === trimmed.toLowerCase());
if (match) return { folderId: match.id, warning: null };
return {
folderId: hostFolderId,
warning: `notebook: no notebook with id "${trimmed}" (using this note's folder)`,
};
}
// 2. Path (Parent/Child/...). A single segment with no slash falls through
// to the bare-title branch below.
const segments = trimmed
.split("/")
.map((s) => s.trim())
.filter((s) => s.length > 0);
if (segments.length > 1) {
const resolved = resolvePath(folders, segments);
if (resolved.length === 1) return { folderId: resolved[0], warning: null };
if (resolved.length === 0) {
return {
folderId: hostFolderId,
warning: `notebook: path "${trimmed}" not found (using this note's folder)`,
};
}
return {
folderId: hostFolderId,
warning: `notebook: path "${trimmed}" is ambiguous (${resolved.length} matches — using this note's folder)`,
};
}
// 3. Bare title — anywhere in the tree, if unique.
const wanted = trimmed.toLowerCase();
const matches = folders.filter((f) => (f.title || "").toLowerCase() === wanted);
if (matches.length === 1) return { folderId: matches[0].id, warning: null };
if (matches.length === 0) {
return {
folderId: hostFolderId,
warning: `notebook: no notebook named "${trimmed}" (using this note's folder)`,
};
}
const resolution = resolveNotebookReference(folders, trimmed);
if (resolution.folderId) return { folderId: resolution.folderId, warning: null };
return {
folderId: hostFolderId,
warning: `notebook: "${trimmed}" is ambiguous (${matches.length} notebooks share that name — qualify it as a Parent/Child path; using this note's folder)`,
warning: `notebook: ${resolution.problem} (using this note's folder)`,
};
}

View File

@ -15,6 +15,8 @@ export interface CalendarConfig {
* Ignored when `scopeAll` is true.
*/
notebook: string | null;
/** Notebook roots whose complete descendant trees are removed from scope. */
excludeNotebooks: string[];
notes: InclusionMode;
todos: InclusionMode;
sort: "asc" | "desc";
@ -151,6 +153,8 @@ export interface KanbanConfig {
groupByNotebook: boolean;
/** Notebook to root the scan at (id/title/path); null = host folder. */
notebook: string | null;
/** Notebook roots whose complete descendant trees are removed from scope. */
excludeNotebooks: string[];
notes: NoteInclusionMode;
todos: InclusionMode;
sortType: KanbanSortType;
@ -245,6 +249,8 @@ export interface MatrixConfig {
groupByNotebook: boolean;
/** Notebook to root the scan at (id/title/path); null = host folder. */
notebook: string | null;
/** Notebook roots whose complete descendant trees are removed from scope. */
excludeNotebooks: string[];
notes: NoteInclusionMode;
todos: InclusionMode;
sortType: KanbanSortType;

View File

@ -2,7 +2,7 @@
"manifest_version": 1,
"id": "com.victorwiebe.joplin.plugin.gtd-calendar",
"app_min_version": "2.7",
"version": "2.0.0",
"version": "2.1.0",
"name": "GTD Calendar",
"description": "Day, week, and month calendars populated by your notes and to-dos, with click-through to the source note. Configure with simple YAML blocks. A GTD-friendly fork of Event Calendar by Franco Speziali.",
"author": "Victor Wiebe",

View File

@ -0,0 +1,178 @@
import collectEvents from "../../Gtd/collectEvents";
import collectKanban from "../../Gtd/collectKanban";
import collectMatrix from "../../Gtd/collectMatrix";
import resolveEffectiveFolderScope from "../../Gtd/effectiveFolderScope";
import parseCalendarConfig from "../../Gtd/parseCalendarConfig";
import parseKanbanConfig from "../../Gtd/parseKanbanConfig";
import parseMatrixConfig from "../../Gtd/parseMatrixConfig";
import { DataAdapter, RawFolder, RawNote } from "../../Gtd/types";
const folders: RawFolder[] = [
{ id: "work", parent_id: "", title: "Work" },
{ id: "ibm", parent_id: "work", title: "IBM" },
{ id: "ibm-child", parent_id: "ibm", title: "Projects" },
{ id: "archive-work", parent_id: "work", title: "Archive" },
{ id: "personal", parent_id: "", title: "Personal" },
{ id: "archive-personal", parent_id: "personal", title: "Archive" },
];
function note(id: string, parentId: string): RawNote {
return {
id,
title: id,
parent_id: parentId,
is_todo: 1,
todo_due: 0,
todo_completed: 0,
updated_time: 0,
body: "```gtd\n```",
};
}
function adapter(calls: string[]): DataAdapter {
return {
getFolders: async () => folders,
getNotesInFolder: async (folderId) => {
calls.push(folderId);
return [note(`note-${folderId}`, folderId)];
},
getNoteTagTitles: async () => [],
};
}
describe("exclude-notebook config parsing", () => {
test.each([
["calendar", parseCalendarConfig],
["kanban", parseKanbanConfig],
["matrix", parseMatrixConfig],
] as const)("%s accepts omitted, scalar, and list forms", (_name, parse) => {
expect(parse({}).excludeNotebooks).toEqual([]);
expect(parse({ "exclude-notebook": " IBM " }).excludeNotebooks).toEqual([
"IBM",
]);
const config = parse({ "exclude-notebook": [" IBM ", "Archive", "IBM"] });
expect(config.excludeNotebooks).toEqual(["IBM", "Archive", "IBM"]);
expect(config.warnings).toEqual([]);
});
test.each([parseCalendarConfig, parseKanbanConfig, parseMatrixConfig])(
"retains valid list entries and warns in input order",
(parse) => {
const config = parse({
"exclude-notebook": ["IBM", " ", { title: "Archive" }, ["Personal"]],
});
expect(config.excludeNotebooks).toEqual(["IBM"]);
expect(config.warnings).toEqual([
"Invalid exclude-notebook entry 2 (empty value ignored)",
"Invalid exclude-notebook entry 3 (expected a notebook name, path, or id)",
"Invalid exclude-notebook entry 4 (expected a notebook name, path, or id)",
]);
}
);
});
describe("effective notebook scope", () => {
test("subtracts complete, overlapping trees from scope: all", () => {
const result = resolveEffectiveFolderScope(
folders,
"work",
0,
true,
["IBM", "Work/IBM/Projects", "IBM"]
);
expect(result.folderIds).toEqual([
"work",
"archive-work",
"personal",
"archive-personal",
]);
expect([...result.excludedFolderIds].sort()).toEqual(["ibm", "ibm-child"]);
expect(result.warnings).toEqual([]);
});
test("resolves entries independently and warns without fallback", () => {
const result = resolveEffectiveFolderScope(
folders,
"work",
Infinity,
false,
["Archive", "Nope", "Personal"]
);
expect(result.folderIds).toEqual(["work", "ibm", "archive-work", "ibm-child"]);
expect(result.warnings).toEqual([
'exclude-notebook: "Archive" is ambiguous (2 notebooks share that name — qualify it as a Parent/Child path) (nothing excluded)',
'exclude-notebook: no notebook named "Nope" (nothing excluded)',
]);
});
test("supports paths, root exclusion, empty scope, and cycle safety", () => {
expect(
resolveEffectiveFolderScope(folders, "work", Infinity, false, ["Work"]).folderIds
).toEqual([]);
const cyclic: RawFolder[] = [
{ id: "a", parent_id: "b", title: "A" },
{ id: "b", parent_id: "a", title: "B" },
];
expect(
resolveEffectiveFolderScope(cyclic, "a", Infinity, false, ["b"]).folderIds
).toEqual([]);
});
test("accepts a 32-character folder id", () => {
const id = "0123456789abcdef0123456789abcdef";
const idFolders = [
...folders,
{ id, parent_id: "personal", title: "Secret" },
];
const result = resolveEffectiveFolderScope(
idFolders,
"work",
0,
true,
[id.toUpperCase()]
);
expect(result.folderIds).not.toContain(id);
expect(result.warnings).toEqual([]);
});
});
describe("collector exclusion integration", () => {
test.each([
["calendar", async (data: DataAdapter) => collectEvents(data, "host", "work", parseCalendarConfig({ scope: "all", notes: "all", todos: "all", "exclude-notebook": "IBM" }))],
["kanban", async (data: DataAdapter) => collectKanban(data, "host", "work", parseKanbanConfig({ scope: "all", todos: "all", "exclude-notebook": "IBM" }))],
["matrix", async (data: DataAdapter) => collectMatrix(data, "host", "work", parseMatrixConfig({ scope: "all", todos: "all", mode: "eisenhower", "exclude-notebook": "IBM" }))],
] as const)("%s never fetches excluded folders", async (_name, collect) => {
const calls: string[] = [];
const result = await collect(adapter(calls));
expect(calls).toEqual(["work", "archive-work", "personal", "archive-personal"]);
expect(result.scannedFolders).toBe(4);
expect(result.scannedNotes).toBe(4);
});
test.each([
["kanban", async (data: DataAdapter) => collectKanban(data, "host", "work", parseKanbanConfig({ scope: "children", group: "notebook", todos: "all", "exclude-notebook": "IBM" }))],
["matrix", async (data: DataAdapter) => collectMatrix(data, "host", "work", parseMatrixConfig({ scope: "children", group: "notebook", todos: "all", mode: "eisenhower", "exclude-notebook": "IBM" }))],
] as const)("%s grouped layout omits the excluded tree", async (_name, collect) => {
const calls: string[] = [];
const result = await collect(adapter(calls));
expect(calls).toEqual(["work", "archive-work"]);
expect(result.layout.kind).toBe("notebooks");
if (result.layout.kind !== "notebooks") throw new Error("grouped expected");
expect(result.layout.groups.map((group) => group.folderId)).toEqual([
"work",
"archive-work",
]);
});
test("an entirely excluded calendar scope uses an empty result", async () => {
const calls: string[] = [];
const result = await collectEvents(
adapter(calls),
"host",
"work",
parseCalendarConfig({ "exclude-notebook": "Work" })
);
expect(calls).toEqual([]);
expect(result).toMatchObject({ events: [], scannedFolders: 0, scannedNotes: 0 });
});
});

View File

@ -122,6 +122,16 @@ describe("validateAndMoveKanbanCard", () => {
expect(writes).toEqual(["attach:progress"]);
});
test("rejects a card in an excluded notebook tree without writes", async () => {
const { read, mutation, writes } = setup(card({ parent_id: "child" }));
const result = await validateAndMoveKanbanCard(
intent({ rawConfig: "editable: yes\nscope: children\nexclude-notebook: Child" }),
host, read, mutation
);
expect(result).toMatchObject({ status: "stale" });
expect(writes).toEqual([]);
});
test.each([
[card({ is_todo: 0 }), "editable: yes\nnotes: none\ntodos: all"],
[card({ is_todo: 1, body: "plain" }), "editable: yes\nnotes: all\ntodos: gtd-only"],

View File

@ -110,6 +110,15 @@ describe("validateAndMoveMatrixCard", () => {
expect(writes).toEqual(["attach:urgent", "attach:important"]);
});
test("rejects a card in an excluded notebook tree without writes", async () => {
const { read, mutation, writes } = setup(card({ parent_id: "child" }));
const moveResult = await validateAndMoveMatrixCard(intent({
rawConfig: "mode: eisenhower\neditable: yes\nscope: children\nexclude-notebook: Child",
}), host, read, mutation);
expect(moveResult).toMatchObject({ status: "stale" });
expect(writes).toEqual([]);
});
test.each([
[card({ is_todo: 0 }), "mode: eisenhower\neditable: yes\nnotes: none\ntodos: all"],
[card({ is_todo: 1, body: "plain" }), "mode: eisenhower\neditable: yes\nnotes: all\ntodos: gtd-only"],