damien-kaiser/taskloft

Taskloft — team task boards (owner / member / viewer), card history, my plate, overdue, blockers, AI checklists and summaries, usable from Claude and ChatGPT over MCP.

A remix template · 0 remixes · Remix damien-kaiser/taskloft

NOTES.md

Design notes

Short notes on the decisions that shape Taskloft, and why.

1. One set of use cases, two doors. application/use_cases.py is the only place an action happens. doors/web.py and doors/mcp.py translate HTTP or MCP into a call and the domain's exceptions into 403/404/422 or a tool error. The tests drive the same scenario through both doors.

2. Rules live in the domain. Board.require_view / require_edit / require_owner, list names, label limits, due dates, "who can be assigned" (owner or member, not a viewer) and the blocker loop check are plain Python in domain/model.py, testable in milliseconds.

3. A board you can't see doesn't exist. Resolving a board or a card the person isn't on raises NotFound, never Forbidden: no one can probe for other teams' boards or card numbers, and an AI connected as you gets exactly your view. Every read is scoped by the caller inside the use case.

4. People talk in names, so references are forgiving. A board is its id or its title ("the Ops board"), a list its id or its name ("doing"), a card its number ("12", "#12") or its id. An ambiguous title is refused with the candidates' ids rather than guessed.

5. Card numbers are claimed, not counted. #12 must be unique and stable. DataK3 has no sequences, and an upsert lets every writer win, so the repository claims a number with a plain INSERT into card_numbers (primary key = the number): under a race one writer is acknowledged, the other gets 23505 and takes the next number.

6. Every card change is a diff. Card.edit(**changes) applies only what really changes and returns {field: [before, after]}; that diff is what the history stores. Saving a card with nothing changed writes no event.

7. Drag and drop is a position, not a reshuffle. Cards carry a float position; dropping a card above another sets it halfway between its neighbours, so a move writes one row.

8. "Finished" is a property of a list. A list can be marked done (the default Done list is). Cards there are never overdue and never on anyone's plate; blockers in a done list stop blocking.

9. Migrations are additive and run at start-up. A fresh remix gets every table from schema.sql. A bucket made by an older version gets the new tables and columns from MIGRATIONS (CREATE TABLE IF NOT EXISTS, ALTER TABLE … ADD COLUMN IF NOT EXISTS), so redeploying never needs a manual step. Nothing is ever renamed or dropped.

10. Built for DataK3's behaviour. Transient faults (40001, 08006, 58030, XX000) are retried, real errors are not; at most 8 connections at once (a bucket serves 16 concurrent reads); natural keys everywhere (board:email for memberships, card>blocker for links).

11. AI suggests; a person decides — unless they asked. suggest_checklist changes nothing. turn_into_checklist is the explicit request "turn this card into a checklist", so its steps are added (and logged). The daily summary is written only from the board's own facts and says so in its prompt. All AI goes through Dodil Models with the app's service account. kimi-k2.6 reasons before it answers (15–60 s), so a summary is stored under a hash of the facts it was written from: the same board in the same state, the same day, answers instantly; any change to the board writes a fresh one.

12. The page is one file. No build step and no external assets: it calls the same JSON routes the tests call. A viewer gets no drag handles and read-only fields, but the API refuses their edits anyway: the page never enforces a rule on its own.

13. Meaning lives next to the rows. Each card's text (title, labels, start of the description) is embedded with Dodil Models into card_vectors.embedding VECTOR(2048) in the same bucket; a hash of that text means a card is re-embedded only when it changed. "Find cards like this one" and "search by meaning" are one cosine KNN (<=>) filtered to the boards the person can see — the filter is in the SQL, so no other team's card can come back, even as a near neighbour.