Three layers¶
kennis is built as three layers with a one-way dependency, and the rule is enforced by a test rather than by convention.
Nothing points back. A test walks the imports of every module and fails if one reaches the wrong way.
The engine knows what is true¶
Everything kennis can do lives under kennis.engine. It returns typed
results and raises domain exceptions. It never prints, never formats for a
particular interface, and never imports one - no click, no rich, no
prompt_toolkit.
Progress leaves through an event stream. An operation emits events; a front end subscribes. Nothing in the engine knows what a progress bar is, which is what lets the same indexing run report itself to a terminal, to a log, or to nothing at all.
The render layer chooses the words¶
kennis.render turns engine values into English. It may read the engine; the
engine may not read it.
This exists because of a rule that is easy to state and easy to violate:
The engine names things; it does not phrase them
Where a value already carries the facts as fields, it must not also carry a sentence built from them.
A sentence built in the engine is the one a front end would otherwise reach
for, and once it exists the command line can no longer restyle it, rewrap it,
or combine it with anything else. So SearchResult carries a score, a rank
and a chunk index; the phrase "relevance: high" is composed in render.
Three things are exempt, because each is quoted rather than composed: a resolution that is a command string, an error's message, and text kennis is repeating from a document.
The command line decides the layout¶
kennis.cli handles arguments, indentation, colour and terminal width. It is
one front end and is not privileged: the MCP server is another and the
graphical interface a third. None of the three imports another.
This is why a colour is named by role rather than by hue. identifier,
md_fence, operation are roles; the theme maps them to one of the eight
standard ANSI colours, never a 256-colour index and never a hex value, so
kennis's output resolves against whatever palette you have set.
One process acts¶
The three front ends do not run the engine themselves. Each is a client of
the kennis service, the one process per corpus that reads and
writes it, and asks it for one typed operation at a time. A test holds that
cli and mcp import nothing from the layer that acts. So a search typed
in a terminal, asked by an agent and run in the window is one function in
one process, and two writes from different front ends queue rather than
collide.
Why it is worth the discipline¶
The practical test is whether a second interface would be a rendering or a
reimplementation. The guided setup makes this concrete: which questions to
ask, and the knowledge that openai needs an API key while fastembed does
not, live in the engine. The command line only prompts. A web form would ask
the same questions in the same order without importing anything from cli.