Skip to content

Design principles

These are the rules that decide the shape of new code. They came out of an architecture audit, and they are worth a read before your first contribution, because half of them are enforced by a gate and the other half are conventions that reviewers hold you to. The useful part is knowing which is which, so this page says so, checked against the code rather than repeated from the old architecture note. It assumes you have read Layers and import contracts.

The primary caller is an agent, not a person. The MCP server is the interface that gets designed first, and it exposes five tools, status, find, keep, report and share, plus one resource for reading an exact artifact revision. That is the whole surface, and it is deliberately small enough that a model can hold it in working memory without a per-tool prompt.

The consequences run deep. find returns evidence rather than an answer, because an agent can weigh evidence and cannot check an answer. Writes are cheap and asynchronous, because an agent that has to wait on extraction stops writing. The web app is a second-class citizen by design and calls the same Memory service in src/aizk/memory.py that the MCP tools do.

Everything that is not the memory model itself is somebody else’s maintained project. This is the rule that keeps the codebase small enough to hold in your head.

Concern Who does it
identity, organizations, membership Logto
document conversion Docling
malware scanning ClamAV
object storage SeaweedFS, reached through obstore
vector search C-SPANN on CockroachDB or VectorChord on PostgreSQL
job queuing the portable CockroachDB queue or PgQueuer on PostgreSQL
model serving an OpenAI-compatible hosted endpoint or vLLM
chunking chonkie
the MCP protocol FastMCP
row level security machinery the house rls package

The PostgreSQL profile includes one model server in src/services/gliner/app.py because GLiNER has no vLLM-compatible image. The CockroachDB profile adds a small queue adapter because PgQueuer uses PostgreSQL behavior CockroachDB does not provide. Database profiles owns the exact boundary.

The eight rules and what actually enforces them

Section titled “The eight rules and what actually enforces them”
Where a new piece of code belongs, decided by three questionsyesnoyesnoyesnonew codecomposes SQL?store, or the twelvebuilds a service?Runtime.assembleemits user markdown?a Jinja templatebeside its code

1. SQL lives in the store. Enforced, with documented exceptions. The forbidden import contract stops seventeen packages from importing sqlmodel or sqlalchemy at all, and ruff overlays in src/aizk/mcp/ruff.toml and src/aizk/api/ruff.toml stop the two transports from building a statement or opening a session. Twelve packages do compose SQL, so the honest version of this rule is that SQL lives in the store or beside the query it serves, and never in a transport.

2. Queries are model classmethods. Convention, and a strong one. A statement is a classmethod on the model that owns its primary table or view, so Document.scope_sets and Usage.Event.capture sit on the models they read. Nothing checks this directly, but rule 1 makes the alternative awkward from a transport, which is most of the enforcement in practice.

3. Patos base models over hand-written constructors. Convention. Ninety-one files in src/aizk/ import from patos, and the pattern is that constrained types and validators on Model or FrozenModel replace __init__ bodies and raise-on-bad-input blocks, so an invariant is declared once and checked at every boundary. No gate counts this.

4. Maintained libraries over hand clients. Convention, and the table above is its evidence. In practice a reviewer will ask which upstream project you considered before merging a hand-rolled protocol client.

5. One composition root. Convention with structural help. Runtime.assemble in src/aizk/runtime.py builds every shared service once from settings, and the layer contract puts runtime above the transports so nothing below it can construct one. Two boundaries are deliberately still process-global and the class docstring says so, which are the cached Database.app() and Database.owner() engine pair, because every User session resolves its engine there, and the Logto snapshot cache TTLs, which are bound at class build time. A third softer exception is the module-level settings object that several modules import directly.

6. Span-based usage accounting. Enforced by construction in src/aizk/usage.py. Handlers call annotate_operation, which stamps the operation, the touched scopes and the item count onto the current OpenTelemetry span, and transport middleware in src/aizk/api/middleware.py and src/aizk/mcp/middleware.py measures request and response bytes and duration around the call. No counter is threaded through a call site. The one nuance worth knowing is that the ledger row is built from a ContextVar that mirrors the span attributes, because a span is write-only, so the accounting state lives in two places on purpose.

7. Template-owned markdown. Real, and narrower than it sounds. There are exactly two templates, src/aizk/retrieval/templates/find.md.j2 for the find answer and src/aizk/artifacts/templates/source.md.j2 for the artifact header, both rendered through the async Jinja environment in src/aizk/common/templates.py. The rule is that a new user-facing markdown surface adds a third template rather than a formatting function.

8. No duplicate projections. Convention. One row model per projection shape, shared by every reader. StatusReport in src/aizk/status.py is the clean example, since the MCP status tool, the browser API /status route and the CLI client all return the same class, so a field change cannot fork the wire format between them.

Rule Enforced by
SQL lives in the store import-linter, ruff TID251, tests/test_contracts.py
Queries are model classmethods review, helped by rule 1
Patos base models review
Maintained libraries review
One composition root review, helped by the layer contract
Span-based usage the design of usage.py
Template-owned markdown review
No duplicate projections review

That split is the useful part. Four of the eight are checked by a machine on every push, and the rest survive only because somebody reads the diff.