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.