flc/crewboard

Crewboard - team task board (boards, lists, cards, roles, history, drag and drop, MCP, AI extras). FastAPI + DataK3 + appid + Models.

A remix template · 1 remixes · Remix flc/crewboard

DESIGN.md

Design notes

One place for each rule. Roles, list rules, assignee rules and dependency loops live in domain/model.py. application/use_cases.py loads, checks, writes and logs. The web routes and the MCP tools are one line each onto the same use case, so neither door can skip a rule. Tests run the rules in memory and then through both doors.

Identity comes only from the gateway. The person is read from X-Dodil-User, never from a request body or a tool argument. Every read goes through _board(), which turns "not on this board" into "no such board": an agent can't even learn that a board exists.

Short card ids. A board gets a key from its title (Ops → OPS); cards are OPS-1, OPS-2… Both are claimed with a plain INSERT (DataK3 has no sequences): under a race one writer wins, the other gets 23505 and takes the next number. People and AI can then say "card 12"; it resolves among the caller's boards and asks which one when two boards have it.

No lost updates. Cards carry a version. Every write is UPDATE … WHERE id = ? AND version = ?; zero rows means someone else wrote first, and the use case answers 409 without writing. The web page sends If-Match with the version it opened. Updates name only the columns they change (DataK3 upserts overwrite every column in VALUES).

History is data, not prose. Each event stores changes as {field: [before, after]}, written in the same use case as the change. A save that changes nothing logs nothing.

Lists belong to the board. They are an ordered JSON array on the board row. The last list counts as done: done cards are never overdue and never block anything. A list can be removed only when empty, and a board keeps at least two.

Order without rewrites. A card's position is a float; dropping between two cards takes the midpoint, so a move writes one row.

Dependencies. blocked_by holds card ids on the same board. Adding one walks the graph and refuses a loop. "What is blocking X" follows chains and skips finished cards.

AI suggests, people decide. suggest_checklist, similar_cards and board_summary change nothing. A checklist reaches the card only when a member applies the items they chose. The summary prompt carries only the board's own facts. Embeddings come from Dodil Models (jina-embeddings-v4) and are cached in memory by card text hash.

Operations. At most 8 concurrent DataK3 connections (the bucket serves 16), retried on the transient codes. POST / is the self-check dodil ignite invoke calls: it opens the bucket with the app's own credential and counts rows per table. /healthz stays database-free.