From df741e43eeaa77dc7069c9a8bc604d2146bd2981 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Fri, 15 May 2026 22:21:27 +0200 Subject: [PATCH] docs(09): research etapes implementation --- .planning/phases/09-etapes/09-RESEARCH.md | 257 ++++++++++++++++++++ .planning/phases/09-etapes/09-VALIDATION.md | 76 ++++++ 2 files changed, 333 insertions(+) create mode 100644 .planning/phases/09-etapes/09-RESEARCH.md create mode 100644 .planning/phases/09-etapes/09-VALIDATION.md diff --git a/.planning/phases/09-etapes/09-RESEARCH.md b/.planning/phases/09-etapes/09-RESEARCH.md new file mode 100644 index 0000000..e22eb77 --- /dev/null +++ b/.planning/phases/09-etapes/09-RESEARCH.md @@ -0,0 +1,257 @@ +--- +phase: 09 +slug: etapes +status: complete +created: 2026-05-15 +--- + +# Phase 9: Etapes - Research + +## RESEARCH COMPLETE + +This research answers what the planner needs to know to implement Phase 9 without breaking the existing task kanban. + +## Phase Summary + +Phase 9 adds one-level etapes inside a tablo. An etape is a parent wrapper for tasks, not a nested hierarchy. Tasks may be unassigned or assigned to one etape. The first UI is a compact top strip of etape chips with counts, including `Unassigned`; selecting a chip filters the existing four-column kanban. + +The core implementation constraint is preserving the current task reorder model: task order remains scoped by tablo and status, not by etape. + +## Existing System Facts + +### Task Schema + +Current task schema is in `backend/migrations/0004_tasks.sql`. + +- `task_status` enum order is `todo`, `in_progress`, `in_review`, `done`; this order matches the visual kanban columns. +- `tasks` has `tablo_id`, `title`, `description`, `status`, `position`, timestamps. +- `tasks_tablo_id_status_idx` indexes `(tablo_id, status, position)`. + +### Task Query Surface + +Current task queries are in `backend/internal/db/queries/tasks.sql`. + +- `ListTasksByTablo` returns all tasks in a tablo ordered by `status, position, created_at`. +- `InsertTask` inserts title, description, status, and position. +- `GetTaskByID` verifies task-to-tablo binding with `WHERE id = $1 AND tablo_id = $2`. +- `UpdateTask` overwrites title, description, status, and position. +- `MaxPositionByTabloAndStatus` scopes new task position to `(tablo_id, status)`. + +### Task Handler Surface + +Task handlers live in `backend/internal/web/handlers_tasks.go`. + +- `loadOwnedTablo` verifies authenticated ownership for tablo-scoped routes. +- `loadOwnedTabloForTask` verifies both tablo ownership and task membership. +- `TaskCreateHandler` computes `MaxPositionByTabloAndStatus + 100`, then inserts. +- `TaskUpdateHandler` preserves existing status and position when updating title/description. +- `TaskReorderHandler` fetches each task by ID and tablo before updating status/position. +- Reorder currently calls `UpdateTaskParams` with only title, description, status, and position. After Phase 9 adds `etape_id`, this path must preserve the existing task's `etape_id` or drag/drop will silently unassign tasks. + +### Template Surface + +Task templates live in `backend/templates/tasks.templ`. + +- `KanbanBoard(tabloID, csrfToken, tasks []sqlc.Task)` accepts a flat task slice and groups by status. +- `TaskCreateFormFragment` and `TaskEditFragment` are the right places for an etape selector. +- `TaskCardOOB` returns a new task card and resets the add-task slot through an out-of-band swap. +- `TasksTabFragment` in `backend/templates/tablos.templ` currently wraps only `KanbanBoard`. + +### Test Surface + +Task tests live in `backend/internal/web/handlers_tasks_test.go`. + +- Existing tests cover rendering, create, validation, update, cross-column reorder, same-column reorder, delete, order persistence, and ownership. +- DB-backed tests use `setupTestDB` and will skip when `TEST_DATABASE_URL` is absent. +- These tests are the closest template for etape handler tests. + +## Recommended Data Model + +Add a migration after `0006_social_identities.sql`, likely `0007_etapes.sql`. + +Recommended shape: + +- `etapes` + - `id uuid primary key default gen_random_uuid()` + - `tablo_id uuid not null references tablos(id) on delete cascade` + - `title text not null` + - `description text` + - `position integer not null default 100` + - `created_at timestamptz not null default now()` + - `updated_at timestamptz not null default now()` +- `tasks.etape_id uuid null references etapes(id) on delete set null` + +Recommended indexes: + +- `etapes_tablo_id_position_idx on etapes(tablo_id, position)` +- `tasks_tablo_id_etape_id_idx on tasks(tablo_id, etape_id)` +- Keep the existing `(tablo_id, status, position)` index because task ordering remains status-scoped. + +Nested etapes are prevented by structure: `etapes` has no `parent_id`. Do not add an etape-to-etape relationship. + +Use `ON DELETE SET NULL` for `tasks.etape_id`; this directly implements ETAPE-04 and avoids a manual cleanup race. The down migration must drop the task foreign key/column before dropping `etapes`. + +## Recommended Query Changes + +Create `backend/internal/db/queries/etapes.sql` with: + +- `ListEtapesByTablo` +- `InsertEtape` +- `GetEtapeByID` +- `UpdateEtape` +- `DeleteEtape` +- `MaxEtapePositionByTablo` +- `UpdateEtapePosition` + +Extend task queries to include `etape_id` everywhere a task is returned or updated: + +- `ListTasksByTablo` should select `etape_id`. +- `InsertTask` should accept nullable `etape_id`. +- `GetTaskByID` should select `etape_id`. +- `UpdateTask` should accept and preserve nullable `etape_id`. +- Add `ListTasksByTabloAndEtape` for a specific etape. +- Add `ListUnassignedTasksByTablo` for the `Unassigned` chip. + +For counts, either: + +- add `CountTasksByEtapeForTablo` plus `CountUnassignedTasksByTablo`, or +- fetch all tasks and count in Go for the first version. + +Counting in Go is acceptable for Phase 9 because the current Tasks tab already loads all tasks and the first UI only needs task counts in the chip strip. If filtered views should avoid loading all tasks later, the count queries can be added separately. + +## Recommended Route And Handler Shape + +Follow existing `/tablos/{id}/...` patterns in `backend/internal/web/router.go`. + +Candidate routes: + +- `GET /tablos/{id}/tasks?etape={uuid|unassigned|all}` renders the Tasks tab or fragment with etape filter context. +- `GET /tablos/{id}/etapes/new` renders an inline create form for the top strip. +- `POST /tablos/{id}/etapes` creates an etape. +- `GET /tablos/{id}/etapes/{etape_id}/edit` renders edit form. +- `POST /tablos/{id}/etapes/{etape_id}` updates title/description. +- `GET /tablos/{id}/etapes/{etape_id}/delete-confirm` renders confirmation. +- `POST /tablos/{id}/etapes/{etape_id}/delete` deletes the etape; DB `ON DELETE SET NULL` unassigns tasks. +- `POST /tablos/{id}/etapes/reorder` updates etape positions. + +Add `EtapesDeps` or extend `TasksDeps`. Prefer a dedicated `EtapesDeps` if handlers become large; both should hold `*sqlc.Queries` and use the same ownership helpers. + +Important authorization invariant: every etape route must first load the owning tablo with `loadOwnedTablo`, then fetch etape with `WHERE id = $1 AND tablo_id = $2`. + +## Recommended Template Shape + +Extend `TasksTabFragment` to accept: + +- `tasks []sqlc.Task` +- `etapes []sqlc.Etape` +- current filter: all, unassigned, or etape UUID +- task counts per etape + +Add an `EtapeStrip` component above `KanbanBoard`. + +The strip should include: + +- an `All` chip if needed for returning to the full board +- an `Unassigned` chip with count +- one chip per etape with count +- minimal create/edit/delete/reorder controls + +Use HTMX links/buttons that target the Tasks tab container or `#kanban-board`, depending on the selected fragment boundary. The least risky boundary is the whole Tasks tab fragment because filtering changes both active chip state and task cards. + +Keep cards and columns stable: + +- Do not split kanban columns by etape. +- Do not add etape lanes. +- Do not change `TaskColumns`. + +## Reorder Pitfalls + +### Pitfall 1: Filtered Reorder Can Overwrite Hidden Task Positions + +The current Sortable.js code posts only tasks visible in `.sortable-column`. If the board is filtered by etape and the handler recalculates positions from visible tasks, only those visible tasks will be renumbered. + +That is acceptable for Phase 9 if task order remains status-scoped and the handler only updates submitted tasks. Hidden tasks keep their previous positions. This may create position collisions across filtered/unfiltered views, but the current model already tolerates sparse positions and does not require uniqueness. + +Planner should require a regression test that: + +- creates two tasks in the same status with different etapes, +- filters to one etape, +- reorders the visible task or moves it to another status, +- verifies the hidden task still exists and keeps its etape assignment. + +### Pitfall 2: Reorder Must Preserve Etape Assignment + +Once `tasks.etape_id` exists, every call to `UpdateTask` must include the current `etape_id` unless the handler intentionally changes assignment. + +This affects: + +- `TaskUpdateHandler` +- `TaskReorderHandler` array path +- `TaskReorderHandler` single-task path + +Missing this will satisfy compile checks but break ETAPE-03 during drag/drop. + +### Pitfall 3: Task Creation Under A Filter + +When the current filter is an etape, the add-task form should include the active `etape_id` so new tasks land in the filtered etape. When the filter is `Unassigned`, the form should submit no etape. When the filter is `All`, default to no etape unless the user selects one in the form. + +This keeps the visible result intuitive without changing task position semantics. + +## Security Notes + +Threats to cover in PLAN.md: + +- Cross-tablo etape assignment: a user must not assign a task in tablo A to an etape from tablo B. +- Cross-user etape access: non-owners must receive 404, matching existing task behavior. +- CSRF on create/update/delete/reorder routes: all mutation forms must use `@ui.CSRFField(csrfToken)` and live under existing CSRF middleware. +- XSS through titles/descriptions: templ escaped text output should be used; do not render etape titles or descriptions through `templ.Raw`. +- Delete behavior: deleting an etape must not delete tasks. + +## Suggested Plan Breakdown + +Because the phase is MVP mode, prefer vertical slices. However, this feature has a schema dependency that all UI slices need. A pragmatic four-plan sequence is: + +1. Schema, SQLC, and RED tests for etapes and optional task assignment. +2. Etape CRUD and top-strip management in the Tasks tab. +3. Task assignment in create/edit forms plus filtered kanban rendering. +4. Reorder preservation and final browser/UAT checkpoint. + +Plan 1 should be wave 1 and blocking. Plans 2 and 3 can be sequential because the strip and forms share template signatures. Plan 4 should depend on the previous plans and focus on regression coverage plus human verification. + +## Validation Architecture + +### Automated Coverage + +Use `go test ./... -count=1` from `backend` as the full command. + +Add or extend tests in `backend/internal/web/handlers_tasks_test.go` or a new `handlers_etapes_test.go`: + +- ETAPE-01: create etape inside owned tablo and render chip with count. +- ETAPE-02: edit, delete, and reorder etapes inside a tablo. +- ETAPE-03: create/update a task with zero or one etape. +- ETAPE-04: delete etape and verify assigned tasks still exist with null etape. +- ETAPE-05: filter by etape and unassigned while preserving status columns. +- ETAPE-06: schema has no parent field; assignment rejects etape IDs outside the task's tablo. +- Regression: drag/drop reorder preserves `etape_id`. +- Regression: current task tests still pass. + +### Manual Verification + +Manual browser verification should confirm: + +- top strip appears above the kanban, +- chip counts update after create/assign/delete, +- selecting an etape filters the existing kanban, +- selecting `Unassigned` shows only unassigned tasks, +- task drag/drop still works after filtering, +- deleting an etape unassigns tasks rather than deleting cards permanently. + +### Commands + +- Generate code: `cd backend && just generate` +- Full test suite: `cd backend && go test ./... -count=1` +- Whitespace sanity: `git diff --check` + +## Open Planning Decisions + +No user-facing product decisions remain open. The planner may choose exact route names and fragment boundaries as long as the context decisions are preserved. diff --git a/.planning/phases/09-etapes/09-VALIDATION.md b/.planning/phases/09-etapes/09-VALIDATION.md new file mode 100644 index 0000000..13eb3a8 --- /dev/null +++ b/.planning/phases/09-etapes/09-VALIDATION.md @@ -0,0 +1,76 @@ +--- +phase: 09 +slug: etapes +status: draft +nyquist_compliant: true +wave_0_complete: false +created: 2026-05-15 +--- + +# Phase 09 - Validation Strategy + +> Per-phase validation contract for feedback sampling during execution. + +--- + +## Test Infrastructure + +| Property | Value | +|----------|-------| +| **Framework** | Go test | +| **Config file** | `backend/sqlc.yaml`, `backend/justfile` | +| **Quick run command** | `cd backend && go test ./internal/web -count=1` | +| **Full suite command** | `cd backend && go test ./... -count=1` | +| **Estimated runtime** | ~30-90 seconds, DB-backed tests skip unless `TEST_DATABASE_URL` is configured | + +--- + +## Sampling Rate + +- **After every task commit:** Run `cd backend && go test ./internal/web -count=1` +- **After every plan wave:** Run `cd backend && go test ./... -count=1` +- **Before `$gsd-verify-work`:** Full suite must be green +- **Max feedback latency:** 90 seconds for local non-DB path; DB-backed path depends on `TEST_DATABASE_URL` + +--- + +## Per-Task Verification Map + +| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status | +|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------| +| 09-01-01 | 01 | 1 | ETAPE-01, ETAPE-06 | T-09-01 | Etapes are tablo-owned rows with no parent relationship | migration/sqlc | `cd backend && just generate && go test ./internal/web -count=1` | yes | pending | +| 09-01-02 | 01 | 1 | ETAPE-03, ETAPE-04 | T-09-02 | `tasks.etape_id` is nullable and `ON DELETE SET NULL` | migration/sqlc | `cd backend && just generate && go test ./internal/web -count=1` | yes | pending | +| 09-02-01 | 02 | 2 | ETAPE-01, ETAPE-02 | T-09-03 | Only tablo owners can create/edit/delete/reorder etapes | handler integration | `cd backend && go test ./internal/web -count=1` | yes | pending | +| 09-03-01 | 03 | 3 | ETAPE-03, ETAPE-05 | T-09-04 | Task assignment rejects etapes outside the task tablo | handler integration | `cd backend && go test ./internal/web -count=1` | yes | pending | +| 09-04-01 | 04 | 4 | ETAPE-05 | T-09-05 | Reorder preserves `etape_id` and hidden filtered tasks | regression integration | `cd backend && go test ./internal/web -count=1` | yes | pending | +| 09-04-02 | 04 | 4 | ETAPE-01..06 | - | Full backend suite remains green | full suite | `cd backend && go test ./... -count=1` | yes | pending | + +--- + +## Wave 0 Requirements + +- Existing Go test infrastructure covers the phase. +- Add RED tests before implementation for etape schema behavior, ownership, delete-unassign, assignment, filtering, and reorder preservation. +- `just generate` must run after migration/query/template changes to refresh sqlc and templ generated code. + +--- + +## Manual-Only Verifications + +| Behavior | Requirement | Why Manual | Test Instructions | +|----------|-------------|------------|-------------------| +| Top-strip chip interaction and kanban filtering | ETAPE-05 | HTMX/browser interaction and active chip state are easier to confirm visually after automated handler tests pass | Start `cd backend && just dev`, open a tablo, create etapes, assign tasks, click etape and `Unassigned` chips, confirm the existing four kanban columns remain usable | +| Drag/drop after filtering | ETAPE-05 | Sortable.js behavior depends on browser DOM events | With an etape filter active, drag a task across columns, reload, and confirm status and etape assignment persist | + +--- + +## Validation Sign-Off + +- [x] All tasks have automated verify commands or Wave 0 dependencies +- [x] Sampling continuity: no 3 consecutive tasks without automated verify +- [x] Wave 0 covers all MISSING references +- [x] No watch-mode flags +- [x] Feedback latency target documented +- [x] `nyquist_compliant: true` set in frontmatter + +**Approval:** pending