Compare commits
2 Commits
main
...
mai/kahn/p
| Author | SHA1 | Date | |
|---|---|---|---|
| 1020d60c75 | |||
| 6e4fabfab9 |
175
docs/plans/phase-7-entity-model.md
Normal file
175
docs/plans/phase-7-entity-model.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# Phase 7 — entity model on mBrian: projects / tasks / tasklists + hybrid CalDAV-vs-mBrian task backend
|
||||
|
||||
**Status**: Phase A design (inventor — no code). Branch `mai/kahn/phase-7a-entity-model`.
|
||||
**Author**: kahn (inventor), 2026-06-01.
|
||||
**Predecessor**: Phase 6 — mBrian is now projax's canonical backend (reads direct-DB, writes via mBrian's scoped `/api/projax` HTTP surface; `PROJAX_BACKEND=mbrian` live on 7c84c96). See `docs/plans/mbrian-backend-migration.md` + `docs/plans/slice-c-writepath-contract.md`.
|
||||
**m's vision (verbatim)**: *"Nestable project components: Projects (completely nestable into sub-projects), tasks, tasklists (as checklists, too), CalDAV connections for tasks."*
|
||||
**Watchword**: keep the DB simple (m's hard constraint from Phase 6).
|
||||
|
||||
---
|
||||
|
||||
## §0 — m's decisions (2026-06-01, relayed via head)
|
||||
|
||||
m approved the design and answered all six §9 questions. Five matched the inventor pick; Q4 was overridden.
|
||||
|
||||
- **Q1 (Tasklist)**: (a) — one container + `metadata.projax.render='checklist'` hint, no new type. *(pick)*
|
||||
- **Q2 (Done-state)**: (a) — reuse `status='done'`. *(pick)*
|
||||
- **Q3 (Hybrid selector)**: (a) — `caldav-list` link presence ALONE selects the backend; `management` is orthogonal, NOT a gate. *(pick)*
|
||||
- **Q4 (CalDAV + mBrian tasks on one project)**: **(b) — OVERRIDE.** Show **both** as separate sub-sections, don't hide the mBrian tasks. *(pick was (a) hide; m wants both visible — so a CalDAV-bound project can still carry mBrian-native tasks alongside its VTODOs, both rendered.)*
|
||||
- **Q5 (Ordering)**: (a) — `created_at` order only; defer drag-reorder. *(pick)*
|
||||
- **Q6 (Task edge)**: (a) — `child_of` + render-filter tasks out of `/tree`+`/graph`. *(pick)*
|
||||
|
||||
**Cross-repo ask** (POST `/api/projax/nodes` accept `type ∈ {project, task}`): head opened it with mBrian/head (msg 2719); the slug-control fix (2711) is also in flight. mBrian-native task CREATE is gated on the `type` field landing; everything else builds now.
|
||||
|
||||
Implementation tracked under `t-projax-7b-tasks-implement` (branch `mai/kahn/phase-7b-tasks`).
|
||||
|
||||
---
|
||||
|
||||
## §1 — Diagnosis: what exists, what's missing
|
||||
|
||||
**Verified against the live system (not docs):**
|
||||
|
||||
- mBrian node types today (msupabase, `mbrian.nodes`): `project` (76), `mai-managed` (43 co-type), `event` (242), plus contact/book/idea/note/concept/question/… — **no `task` or `todo` type exists.** projax tasks are not mBrian nodes today.
|
||||
- projax projects/areas: `type=['project']`; areas additionally carry `metadata.projax.kind='area'`; nesting is arbitrary-depth via `child_of` edges (Phase 6). Confirmed holds.
|
||||
- **Tasks today live OUTSIDE projax.** PRD §2.1: *"Task — atomic work item. Lives outside projax (CalDAV todos, Gitea issues, mai.tasks). projax references and aggregates them; it does not own them."* The only task-shaped surface projax owns end-to-end is the **CalDAV VTODO read/write** path (PRD §5): a `caldav-list` link (self-edge `rel='projax-caldav-list'`, `metadata.url`) binds a project to a calendar; the detail page + dashboard + timeline aggregate its VTODOs and write them back (complete/reopen/edit/delete/add) with ETag optimistic concurrency.
|
||||
- `management text[]` = `self` / `mai` / `external` (empty = unmanaged), already per-project.
|
||||
|
||||
**The gap Phase 7 closes:** m wants first-class tasks + tasklists/checklists for projects that AREN'T CalDAV-bound, while keeping CalDAV as the task backend where he's already wired a calendar. This **supersedes PRD §2.1's "tasks live outside projax"** — but only for non-CalDAV projects; CalDAV-bound projects keep CalDAV as their task store. §7 reconciles the PRD.
|
||||
|
||||
---
|
||||
|
||||
## §2 — Entity types + mBrian mapping
|
||||
|
||||
The design adds **exactly one new node type** (`task`) and **zero new edge rels** — everything else reuses Phase-6 structures. This is the DB-simple path.
|
||||
|
||||
| entity | mBrian shape | new? |
|
||||
|---|---|---|
|
||||
| **Area** | `type=['project']` + `metadata.projax.kind='area'`, root-level (no parent `child_of`). | unchanged |
|
||||
| **Project** | `type=['project']`, nestable via `child_of` edges, arbitrary depth. | unchanged |
|
||||
| **Task** | `type=['task']`, attached to its parent (project or task) via a `child_of` edge. Done-state reuses the existing `status` lifecycle (`active`→`done`→`archived`); `metadata.projax.{due, order, priority}` optional. | **NEW type** |
|
||||
| **Tasklist / checklist** | **NOT a new type.** Any container (project OR task) with `metadata.projax.render='checklist'` renders its child tasks in compact checklist mode. A "tasklist" is a project (or task) whose children are tasks; "checklist" is a render hint, not a type. | render flag only |
|
||||
|
||||
**Why task reuses `status` not a new `done` bool**: the lifecycle `active→done→archived` already exists on every node (`metadata.projax.status`), the PATCH-projax partial already writes it, and the reader already unpacks it. A task is "done" when `status='done'`. Zero new field, zero new API surface. (Open Q2 confirms.)
|
||||
|
||||
**Why one container concept, not three types** (m's "tasklists as checklists too" + the parked micro-project idea): projects, tasklists, and checklists are the same structural thing — a container of tasks — differing only in render density. Modelling them as one container + a `render` hint keeps the type vocabulary at two (`project`, `task`) instead of four. A checklist sub-item is just a task nested under a task. (Open Q1 confirms.)
|
||||
|
||||
**Task attachment — `child_of` vs a dedicated edge** (inventor pick: `child_of`): tasks attach via the same `child_of` edge projects use. Pros: reuses the reader's graph walk, path derivation, and the write API's existing `child_of` support — no new edge rel (the `/api/projax/edges` allowlist is `child_of | projax-*`). Con: tasks become structural children, so they'd surface in `/tree` and `/graph` unless those renderers filter by type. That filter is cheap — `Item.Kind` already carries `['task']`, so the project surfaces render `type=['project']` only and the Tasks section renders `type=['task']` children. The alternative (a dedicated task edge for hard separation from the project DAG) is cleaner-on-paper but adds a rel + a reader change for no functional gain at m's scale. (Open Q6.)
|
||||
|
||||
---
|
||||
|
||||
## §3 — The hybrid CalDAV-vs-mBrian task backend (m's decision)
|
||||
|
||||
m already decided task storage is **hybrid, segmented per-project** — not uniformly mBrian. His words: *"If we decide to use caldav, we use that - and mBrian for others. We already have the managed classification so we can separate."*
|
||||
|
||||
### §3.1 — Per-project backend-selection rule
|
||||
|
||||
**The selector is the `caldav-list` link.** A project's task backend is decided per-project, the same way m already decides it — by binding (or not binding) a CalDAV list:
|
||||
|
||||
```
|
||||
project has a caldav-list link → CalDAV-backed (tasks = that calendar's VTODOs)
|
||||
project has NO caldav-list link → mBrian-native (tasks = type=['task'] child nodes)
|
||||
```
|
||||
|
||||
`management` (`self`/`mai`/`external`) is the **existing classification that makes this clean** (m's "we already have the managed classification so we can separate") — it is orthogonal *who-runs-it* metadata, not the switch itself. The caldav-list link is the switch. (Open Q3 checks whether m wants `management` to ALSO gate — e.g. force mBrian-native for `self`, or suppress task-authoring on `external`.)
|
||||
|
||||
### §3.2 — CalDAV sync model (CalDAV-bound projects)
|
||||
|
||||
**No change — it already works.** PRD §5's VTODO surface is already full read/write (complete/reopen/edit/delete/add, ETag optimistic concurrency, ICS round-trip). A CalDAV-bound project's tasks ARE its VTODOs; Phase 7 just formalizes "this project is CalDAV-backed" as a first-class concept rather than an aggregation side-effect. No new CalDAV writeback is needed. (The §5 read-only stance on VEVENTs is unchanged — events aren't tasks.)
|
||||
|
||||
### §3.3 — Uniform task shape (one shape, two sources)
|
||||
|
||||
Both backends present through one `Task` view-shape, exactly the slice-B adapter pattern (one `Item` shape, two backends):
|
||||
|
||||
```
|
||||
Task { ID, Title, Done bool, Due *date, Source "caldav"|"mbrian",
|
||||
Order int, ParentItemID, Priority? , raw-source-handle }
|
||||
```
|
||||
|
||||
- CalDAV source: materialised from a VTODO (`SUMMARY`→Title, `STATUS`→Done, `DUE`→Due, UID+calendar_url as the handle for writeback).
|
||||
- mBrian source: materialised from a `type=['task']` node (`title`→Title, `status='done'`→Done, `metadata.projax.due`→Due, node id as the handle).
|
||||
|
||||
The detail-page Tasks section, dashboard task rollups, and timeline render the uniform shape — they don't care which backend produced it. Writes dispatch on `Source`: CalDAV → existing VTODO PUT path; mBrian → `/api/projax` node PATCH/POST.
|
||||
|
||||
---
|
||||
|
||||
## §4 — Nesting + ordering
|
||||
|
||||
- **Project nesting**: unchanged — arbitrary depth via `child_of`.
|
||||
- **Task nesting**: tasks nest under a project (`task child_of project`) and under a task (`subtask child_of task`) — arbitrary depth, reusing `child_of`. Sub-tasks ARE checklist items; a task with a `render='checklist'` hint shows its sub-tasks as a compact checklist. This folds in the parked micro-project/checklist-sub-item idea for free.
|
||||
- **Ordering within a list**: `metadata.projax.order` (int) for manual reorder; default order is `created_at` when `order` is unset. Drag-to-reorder writes `order` via PATCH-projax. (Open Q5: ship manual reorder in v1, or `created_at`-only and defer drag?)
|
||||
- **CalDAV task ordering**: VTODOs have no portable order field; CalDAV-backed lists keep their existing sort (due-date / priority / summary). Manual reorder is mBrian-native-only.
|
||||
|
||||
---
|
||||
|
||||
## §5 — UI / render implications
|
||||
|
||||
- **Detail page Tasks section** (`/i/{path}`): today CalDAV-only. Extend to render the uniform Task shape. For a CalDAV-bound project → VTODOs (as now). For an mBrian-native project → its `type=['task']` child nodes, with an "add task" affordance, a done checkbox (→ `status` flip), inline title/due edit, and delete — mirroring the CalDAV affordances so the two sources feel identical. **Per m's Q4=(b): a CalDAV-bound project that ALSO has mBrian-native task nodes renders BOTH — a CalDAV sub-section (VTODOs) and an mBrian sub-section (task nodes) — rather than hiding the mBrian tasks. The backend selector (§3.1) decides where a *newly created* task lands (CalDAV-bound → VTODO), but both sources always render when present.**
|
||||
- **Checklist render mode**: a container with `metadata.projax.render='checklist'` renders tasks as a dense checkbox list (no per-row chrome) vs the default roomier task rows.
|
||||
- **Tree / graph / tiles**: filter to `type=['project']` so tasks don't clutter the project DAG. `Item.Kind` already distinguishes — a one-line predicate in each renderer. (If Q6 picks the dedicated-edge alternative, this filtering is unnecessary.)
|
||||
- **Dashboard / timeline**: task rollups + the chronological spine consume the uniform shape, so mBrian-native tasks appear alongside CalDAV ones with no per-surface special-casing.
|
||||
- **Task creation**: on a project detail page, "add task" creates a `type=['task']` node `child_of` the project (mBrian-native projects) or a VTODO (CalDAV-bound) — the backend selector (§3.1) decides which, transparently.
|
||||
|
||||
---
|
||||
|
||||
## §6 — Write-API needs (cross-repo with mBrian/head)
|
||||
|
||||
mBrian owns the `/api/projax` write surface + `db.ts`. Phase 7's mBrian-native tasks need **one** real API addition; everything else is already supported.
|
||||
|
||||
| need | status |
|
||||
|---|---|
|
||||
| **Create a `type=['task']` node** | **NEW ASK.** Today `POST /api/projax/nodes` *forces* `type=['project']` (slice-C contract §2). Ask: accept an optional `type` field allowlisted to `{project, task}` (default `project`; reject anything else to preserve the projax-scoping model). Areas keep `type=['project']`+`metadata.projax.kind='area'`, so no `area` type needed. |
|
||||
| Task done-toggle | **No ask** — `PATCH /api/projax/nodes/{id}` already shallow-merges `projax:{status:'done'}`. |
|
||||
| Task due / order / priority | **No ask** — same PATCH-projax partial (`metadata.projax.{due,order,priority}`). |
|
||||
| Task→project / subtask→task edges | **No ask** — `POST /api/projax/edges` already allows `rel='child_of'`. |
|
||||
| Task slug control | **Confirmed-incoming** — head has already opened the explicit-slug fix with mBrian/head (slug on `POST`+`PATCH`); tasks need user-meaningful slugs, so this design assumes slug-control is restored (do not design around the generate-from-title bug). |
|
||||
| Reader picks up tasks | **No ask** — task nodes carry `metadata.projax_origin` (created via the API), so `MBrianReader`'s `projax_origin` scoping already includes them; `Item.Kind=['task']` flows through. |
|
||||
|
||||
**Net cross-repo ask: one field** (`type` on `POST /api/projax/nodes`). Batched to head with the open questions.
|
||||
|
||||
---
|
||||
|
||||
## §7 — Migration of existing CalDAV-task usage
|
||||
|
||||
- **CalDAV-bound projects**: zero migration. Their tasks are VTODOs and keep working through the existing surface; Phase 7 only re-labels them "CalDAV-backed."
|
||||
- **Non-CalDAV projects**: gain the ability to hold mBrian-native tasks. No data to migrate — there are no mBrian task nodes today (verified: 0 `task`-type nodes).
|
||||
- **PRD reconciliation**: PRD §2.1's "tasks live outside projax" is amended — tasks are now first-class for non-CalDAV projects, external (CalDAV/Gitea) for the rest. Phase 7 extends, doesn't contradict: the aggregation surfaces (dashboard/timeline) still consume external tasks; they additionally consume mBrian-native ones. A short PRD addendum captures this.
|
||||
- **No `projax.items` involvement**: tasks are mBrian-only from day one (no legacy projax.items.task rows ever existed). Slice E's table drop is unaffected.
|
||||
|
||||
---
|
||||
|
||||
## §8 — Implementation slicing
|
||||
|
||||
Depends on the §6 cross-repo `type`-field ask landing first (gates A).
|
||||
|
||||
- **A — task read adapter + uniform shape**: define the `Task` shape; the reader materialises `type=['task']` nodes; the detail Tasks section renders CalDAV + mBrian tasks uniformly per the §3.1 selector. (Read-only mBrian tasks first — mirrors the slice-B-before-C discipline.)
|
||||
- **B — mBrian task write path**: create / done-toggle / edit / delete mBrian-native tasks via `/api/projax` (node POST with `type=task`, PATCH `status`/`due`, edge POST for `child_of`, node DELETE). Reuses the slice-C `MBrianWriter`.
|
||||
- **C — checklist render mode + sub-tasks**: `metadata.projax.render='checklist'`, task-under-task nesting, compact render.
|
||||
- **D — ordering / drag-reorder**: `metadata.projax.order` + drag affordance (only if Q5 wants it in v1).
|
||||
- **E — tree/graph/tiles type-filter**: exclude `type=['task']` from project surfaces (skip if Q6 picks the dedicated-edge model).
|
||||
|
||||
Slice A is the cheapest win and de-risks the rest. B is the bulk. C/D/E are additive polish.
|
||||
|
||||
---
|
||||
|
||||
## §9 — Open questions
|
||||
|
||||
Batched to **projax/head** (not chip-asked to m — projax routes all forks through head). Inventor pick listed first.
|
||||
|
||||
1. **Tasklist model** — one container concept (project/task) + `metadata.projax.render='checklist'` render hint **[pick]**, or a dedicated `type=['tasklist']`? Pick keeps the type vocabulary at two and satisfies "checklists too" via render mode.
|
||||
2. **Task done-state** — reuse the existing `status` lifecycle (`done`) **[pick]**, or a separate `metadata.projax.done` bool? Pick adds zero fields/API.
|
||||
3. **Hybrid selector** — `caldav-list` link presence alone decides the backend **[pick]**, or does `management` ALSO gate (e.g. force mBrian-native for `self`, suppress authoring on `external`)?
|
||||
4. **CalDAV-bound project that also has mBrian task nodes** — CalDAV wins for the Tasks section, mBrian tasks hidden there **[pick]**; or show both (two sub-sections); or forbid creating mBrian tasks under a CalDAV-bound project?
|
||||
5. **Manual task ordering in v1** — ship `metadata.projax.order` + drag-reorder now, or **`created_at` order only and defer drag [pick]** (simpler v1; reorder is additive)?
|
||||
6. **Task attachment edge** — `child_of` + render-filter tasks out of project surfaces **[pick]**, or a dedicated task edge for hard separation from the project DAG? Pick reuses everything; the alternative is cleaner separation at the cost of a new rel + reader change.
|
||||
|
||||
**Cross-repo ask (to mBrian/head via head, not m):** extend `POST /api/projax/nodes` to accept `type ∈ {project, task}` (§6) — the one new API field Phase 7 needs.
|
||||
|
||||
---
|
||||
|
||||
## §10 — Out of scope (Phase 7)
|
||||
|
||||
- Recurring tasks (RRULE) — CalDAV app territory; mBrian-native recurrence deferred.
|
||||
- Task dependencies / blocked-by graph — possible later via a `projax-blocks` edge; not now.
|
||||
- Gitea issues as first-class tasks — issues stay aggregated (read/writeback) as today, not converted to task nodes.
|
||||
- Bidirectional CalDAV↔mBrian task mirroring — a project is one backend or the other, never synced both ways.
|
||||
- Reminders / notifications — Otto-PWA's domain.
|
||||
Reference in New Issue
Block a user