Architecture
How the server is shaped
Section titled “How the server is shaped”- Event sourcing. Every state change is an event appended to a JSONL log
under
~/.kanna/data. On boot the server replays the log tail after the last snapshot, then compacts. Nothing is edited in place. - Separate read and write paths. Commands write events; read models derive the views the client needs from them; subscribers are pushed a fresh snapshot (or a small list of operations) on every change. The client never asks the server to recompute anything.
- One process. The WebSocket router, the agent coordinator, the event store, the cron scheduler and the Kanna MCP tools all live in one Bun process. Agent runtimes (the Claude Agent SDK, the Codex app-server) and plugin servers are child processes.
- Shared contracts.
src/shared/holds the protocol and domain types both sides import; a type is defined there once.
The IO seal
Section titled “The IO seal”Side effects are sealed by ESLint at error level across src/shared/**,
src/client/** and src/server/**: node:fs, node:child_process,
node:http, bun:sqlite, Bun.spawn, Bun.file, process.env and friends
fail bun run lint outside the exempt files.
| You need IO in… | Do this |
|---|---|
src/shared or src/client |
Move the code to the server, or take the operation as a typed parameter |
| a server file whose only job is the IO | Name it *.adapter.ts; adapters are exempt |
| a server file mixing domain logic and IO | Extract the IO into a sibling *-io.adapter.ts and import it |
| server domain code | Accept a typed port from the caller’s adapter |
Adapters are leaf modules: they wrap one primitive and hold no domain logic, so
anything can import them. There is no escape valve; do not add
eslint-disable comments.
C3 component docs
Section titled “C3 component docs”Kanna uses C3 component docs at .c3/.
Before coding
Section titled “Before coding”Run /c3 query <topic> (or c3x lookup <file>) to load component context, refs, and rules. Do not skip this — even for small edits. Skipping leads to stale assumptions and wrong patches.
After coding
Section titled “After coding”If a change touches component boundaries, refs, public contracts, or rules, run /c3 change (or /c3 sweep for audit) to update .c3/ docs in the same PR. Code-doc drift is a blocker.
Operations
Section titled “Operations”| Op | Purpose |
|---|---|
query |
Look up component context, refs, rules for a topic |
audit |
Check a component against its docs |
change |
Update docs after a code change |
ref |
Add or fix a ref between components |
sweep |
Bulk audit across all components |
File lookup
Section titled “File lookup”c3x lookup <file-or-glob> maps files/directories to components + refs.
c3-skill:c3 auto-triggers on /c3 or architecture phrases.
Always review what c3x wrote
Section titled “Always review what c3x wrote”c3x can rewrite docs your change never touched. Run git status .c3/
after any c3x write — repair and change apply alike — and
git checkout -- anything you did not intend. .c3/c3.db is gitignored, so the
markdown is the only source of truth and git is the only thing that catches
this.
Most of the churn is cosmetic re-canonicalisation, but text can be lost, in two shapes worth being able to recognise:
\ |in a table cell is damage. A literal pipe in a cell must be\|; rewritten as\ |it escapes a space instead, the pipe then reads as a column separator, and the next serialisation truncates the row. One row lost 533 bytes mid-sentence this way, and carried the broken escape for months.- A glob or emphasis character inside backticks in a cell. The backtick
strip leaves
*unguarded, and the next pass eats it as markdown emphasis:`mermaid-*.js`→mermaid-.js. A cell showingglob-.extwhere a*belongs is damaged.
repair is idempotent on this tree, so any diff it produces is about your
change and is worth reading. Three authoring mistakes make it produce one:
- An
.c3/adr/file with no YAML frontmatter is deleted, not skipped. Usec3x add adr, or write the frontmatter block yourself. - One unsealed fact blocks every cache rebuild, and the error names only that
file while the visible symptom is elsewhere — unrelated
c3x lookupcalls start answering “no component mapping” for files that are correctly bound. - A wrapped list item loses its continuation indent. Keep list items on one
line in
.c3/prose.
Keeping c3x lookup working
Section titled “Keeping c3x lookup working”Every production-significant component has a spec at .c3/eval/c3-NNN.yaml with
a code: list; that is what maps a file back to its owning component. When you
add an architecture-significant file, add it to that code: list and to the
matching entry in .c3/code-map.yaml, then run c3x repair (expect no
warnings) and c3x lookup <file> to confirm. Renames and deletions need the
same treatment — stale anchors surface as warnings, so keep those clean.