Ontologies
An ontology is one YAML file that does three jobs for a knowledge base:
- It declares the topics — the validated first segment of every fact path.
- It describes them to the agent. Topic descriptions are shipped as part of the MCP server instructions on every session. This is prompt text, not documentation.
- It carries validation rules — small JavaScript expressions evaluated against every fact on write. A fact that fails one is rejected, and the agent is shown your error message.
The third job is what separates an ontology from a folder layout. A taxonomy tells an agent where to file a claim; a validation rule decides whether the claim may enter the corpus at all. The ontology is therefore where you encode what counts as a well-formed fact here — and it is enforced on write, not offered as advice.
Most knowledge bases never need a custom one. knomit ships
two presets, and code in particular is a working
answer for any codebase. Read on when you want topics the presets don’t have,
or a standard of evidence they don’t enforce.
Where it lives, and when it is read
Section titled “Where it lives, and when it is read”Inside the knowledge base repo, at .knomit/ontology.yaml — a private path, so
it is configuration rather than a fact, and knomit reads it by name. Facts
themselves land under the ontology root, kb/ by default and set by
ontology_root in knomit.toml:
kb/<topic>/<category…>/<uuid8>.mdThe ontology is parsed once, when the repo is opened — at server start, or when the repo is created or adopted. It is not re-read per request.
The file format
Section titled “The file format”id: acme-support # required — stable identity; also controls preset upgradesname: Acme Support Knowledge # required — human labeldescription: >- # optional What the support team knows about customers, policy, and incidents.
validations: # optional — ROOT rules, run for every fact - name: title-states-a-claim message: "titles must state a claim, not ask a question" rule: "!fact.title.trim().endsWith('?')"
topics: # required — at least one policy: description: Rules agents must follow — refunds, escalation, SLAs validations: # optional — run for facts under this topic - name: policy-must-anchor message: "a policy fact must cite the document or code that enforces it" rule: "fact.refs.length > 0" children: # optional, and nestable refunds: description: Eligibility, limits, approvals escalation: description: When to escalate, and to whom| Field | Where | Required | Notes |
|---|---|---|---|
id | root | ✓ | Stable slug. Matching an embedded preset’s id opts you into auto-upgrade. |
name | root | ✓ | Human label. |
description | root | — | Not shown to the agent. |
topics | root | ✓ | Must be non-empty. |
validations | root, and any node | — | A list of {name, message, rule}. |
description | node | — | Top-level topic descriptions are sent to the agent; child descriptions are not. |
children | node | — | Recursive, with no depth limit. |
Key naming. Topic keys and their direct children must match
^[a-z0-9]+(-[a-z0-9]+)*$ — lowercase kebab-case. A violation is a parse error.
Deeper keys are not regex-checked, but every path lookup lowercases each
segment, so an uppercase grandchild key parses cleanly and is then unreachable.
Keep the whole tree kebab-case.
How paths are validated
Section titled “How paths are validated”ValidatePath walks a fact’s path against the tree:
- The first segment must be a declared topic. An unknown topic is rejected.
- Later segments are matched against declared
children. At the first segment that does not match, the walk stops, and everything after it is accepted as freeform. - Lookups are case-insensitive, since keys are canonically lowercase.
categoryis required separately byknomit_learn, so the shallowest legal placement istopic/category.
Against the example above:
| Path | Result |
|---|---|
policy/refunds | ✅ declared child |
policy/refunds/eu/vat | ✅ eu/vat is a freeform tail |
policy/anything/at/all | ✅ freeform from anything onward |
Policy/Refunds | ✅ lookups are case-insensitive |
marketing/campaigns | ❌ unknown topic "marketing" |
policy | ❌ rejected by knomit_learn — category required |
The consequence is worth stating plainly: declaring children does not close
the set. Children shape the agent’s mental model and give you somewhere to
hang rules, but they never restrict what may be created below a topic. Only the
top-level topic list is closed. If you want a genuinely constrained category,
enforce it with a rule on fact.path — see the cookbook below.
Validation rules
Section titled “Validation rules”Each rule is a JavaScript expression that passes by returning a truthy value. Rules are compiled once when the ontology is parsed, then evaluated in a sandboxed interpreter — a fresh one per fact.
Order. Root rules run first, then the topic’s rules, then each matched child’s rules, descending. The first failure wins, so the agent sees exactly one error:
validation "policy-must-anchor" at policy: a policy fact must cite the document or code that enforces itRoot rules report at the pseudo-topic <root>.
What a rule can see
Section titled “What a rule can see”One global, fact:
| Field | Type |
|---|---|
fact.kind | string — epistemic | pragmatic |
fact.type | string — leaf type (observation, policy, hypothesis, …) |
fact.domain | string[] |
fact.entities | string[] |
fact.refs | string[] |
fact.title | string |
fact.body | string |
fact.path | string — the full path, e.g. kb/policy/refunds/a1b2c3d4.md |
fact.confidence | number, 0–1 |
fact.sources | number — count of independent corroborations |
fact.origin | string — authored | distilled | discovered |
fact.evidence_weight | number — provenance mass carried by a derived fact |
The last three are the provenance axes, and they are what let an ontology
enforce a standard of evidence rather than a shape. sources counts
independent corroborations and defaults to 1. origin records which pipeline
minted the fact: authored for anything a human or agent asserted, distilled
for synthesis-pipeline output, discovered for the emergent facts the
discovery engine surfaces. evidence_weight is the mass a
derived fact inherits from the facts it was built on.
What the write path does to the values
Section titled “What the write path does to the values”Rules run at three moments, and two of them hand a rule values no single caller supplied. Worth knowing before you write a provenance rule:
| Field | On a fresh knomit_learn write | On the re-run after a subsumption merge |
|---|---|---|
sources | as supplied, default 1 | summed — new + existing |
confidence | as supplied, default 0.7 | max of the two |
domain, entities | as supplied | union of both |
origin, type, kind | as supplied, resolved | inherited from whichever fact wins the merge |
evidence_weight | always 0 | always 0 |
Two consequences:
fact.sources >= 2gates a claim’s first write, then passes on every one after it, because the merge sums. That is usually the semantics you want from a corroboration rule, but it is not a per-write floor.evidence_weightis only meaningful onknomit_update. Onknomit_learnit is not a caller-supplied field at all — it is computed and stamped after validation has already run, so a rule always sees 0 andfact.evidence_weight > 0rejects every learn write.knomit_updatevalidates a fact parsed from disk, so there the stored weight is real.
Sandbox and limits
Section titled “Sandbox and limits”- A fresh interpreter per fact, so no state leaks between evaluations.
- Plain JavaScript, not Node: no
require, noprocess. The standard library (Array,String,JSON,RegExp, …) is available. factis bound non-enumerably, soObject.keys(globalThis)is empty and a rule cannot discover what else exists.- A 100 ms interrupt per rule.
- No I/O, no network, no access to other facts. A rule is a pure function of one fact — cross-fact checks like “is this a duplicate of…” are out of scope by design, and belong to subsumption.
Where rules run
Section titled “Where rules run”knomit_learn, on every fact written — and again on the merged fact after a subsumption merge.knomit_update, on the reassembled fact.- Skipped entirely for private state — facts written to an explicit
.knomit/<area>/…path — including root rules. That exemption is deliberate: a background job’s bookkeeping has no ontology placement, and without it any root rule would let a job create its state slot and then refuse every update to it.
Rule cookbook
Section titled “Rule cookbook”# Require a citationrule: "fact.refs.length > 0"
# Conditional — "if type is X then Y must hold". The idiom is !isX || Y.rule: "fact.type !== 'hypothesis' || fact.confidence <= 0.5"
# Constrain kind and type for a topicrule: "fact.kind === 'pragmatic' && fact.type === 'policy'"
# Require a tag from a fixed setrule: "['billing','auth','api'].some(function(d){ return fact.domain.indexOf(d) >= 0 })"
# Mutually exclusive tagsrule: "!(fact.domain.includes('global') && fact.domain.length > 1)"
# Enforce category depth — children cannot do this, because of the freeform tailrule: "fact.path.split('/').length >= 5"
# A body naming a dollar threshold must also name the account-age condition,# so the fact cannot compress into a false sloganrule: "!/\\$\\d/.test(fact.body) || /\\bdays?\\b/.test(fact.body)"
# Confidence floor for a topicrule: "fact.confidence >= 0.8"
# Require independent corroboration before a claim may land hererule: "fact.sources >= 2"
# Keep a topic for human and agent assertions only — no pipeline outputrule: "fact.origin === 'authored'"
# Tie confidence to provenance: an emergent fact stays provisional until# someone corroborates itrule: "fact.origin !== 'discovered' || fact.confidence <= 0.5 || fact.sources >= 2"Note the escaping in the regex example: \\$ and \\b, because the rule is a
double-quoted YAML scalar before it is JavaScript.
A worked example
Section titled “A worked example”This is the ontology behind a research pack that reads arXiv papers. It is worth reading in full, because it shows the thing that distinguishes an ontology from a folder list: the topics encode an epistemic stance, and the rules encode a standard of evidence.
id: research-literaturename: Research Literature Knowledgedescription: >- Claims extracted from research papers, together with the experimental setup that bounds them. Written for a reader at a build decision, not for a literature review.
validations: - name: refs-non-empty message: "every fact must cite at least one source" rule: "fact.refs.length > 0" - name: hypothesis-stays-provisional message: "a hypothesis is a single-source claim: confidence must be <= 0.5 until independent replication graduates it to an observation" rule: "fact.type !== 'hypothesis' || fact.confidence <= 0.5"
topics: findings: description: What a paper established, and the setup that bounds it children: empirical: description: Measured results, stated with models, scale and benchmark negative: description: What did not work, and the cause the authors give replicated: description: Confirmed by independent work — graduated from hypothesis scaling: description: How a result moves with model size, data or compute methods: description: Techniques and architectures, and the rationale behind their design children: architectures: description: Structural designs and what they buy training: description: Objectives, curricula, data construction inference: description: Decoding, sampling, test-time compute evaluation: description: How a method measures itself contested: description: Where credible papers disagree, and the conditions that decide it children: unresolved: description: Live disagreement, no condition yet separates the results conditional: description: Both hold — the conditions separating them are known limits: description: Boundary conditions, scaling walls, impossibility results children: empirical: description: Observed ceilings and where a technique stops paying theoretical: description: Proved bounds and impossibility results benchmarks: description: What a benchmark measures — and how it misleads children: construction: description: How it was built, and what that bakes in saturation: description: Headroom, contamination, ceiling effects open-problems: description: Gaps the literature itself names as unsolvedFour things in it are worth copying, whatever your subject.
The root rules encode a research standard, not a schema. refs-non-empty
means an uncited claim cannot enter the corpus at all. hypothesis-stays-provisional
is the more interesting one: it ties a fact’s type to a confidence ceiling,
so a single-source claim structurally cannot masquerade as settled. Confidence
0.8 on a hypothesis is rejected; the same 0.8 on an observation passes. The
ontology enforces an epistemic norm that a human reviewer would otherwise have
to police by hand.
findings/replicated is a promotion target. The taxonomy names the state
change — hypothesis becomes replicated observation — so graduating a claim is a
move within the tree rather than an ad-hoc edit. Topics can model a lifecycle.
contested splits on the reason for the disagreement. unresolved (no
condition separates the results) versus conditional (both hold, and the
separating condition is known) changes what a reader does next. That is the test
for whether a child earns its place: does landing here rather than there change
the reader’s next action?
open-problems declares no children at all. The shape of that space is not
known in advance, so it is left freeform. Not every topic needs a fixed
substructure.
The built-in presets
Section titled “The built-in presets”Two ontologies ship embedded. Both are good starting points to copy and edit.
default — id general
Section titled “default — id general”A broad taxonomy aligned with Wikipedia’s main topic classifications: people,
technology, science, society, culture, geography, history, health,
philosophy, religion, business, reference, plus meta. Each carries
three to five children — technology has software, hardware, networking,
data; science has formal, natural, applied. There are no validation
rules at all; it is deliberately permissive.
Use it for personal knowledge bases, chat-derived knowledge, or anything whose
subject matter isn’t known in advance. It is also what mode: "preset" picks
when you omit ontology_preset — it is not a fallback for a repo whose own
ontology failed to load, which opens read-only instead.
code — id source-code
Section titled “code — id source-code”Knowledge categories for an agent working in a codebase. The topics are
epistemic roles, not subject areas — which is the real contrast with
default:
| Topic | Purpose | Children |
|---|---|---|
invariants | Load-bearing rules that must never be violated | architecture, concurrency, data, protocol |
architecture | What lives where, and how it connects | modules, flows, integrations |
conventions | House style — idioms, patterns | testing, logging, errors, naming, git |
decisions | Design choices with rationale | accepted, superseded, rejected |
gotchas | Sharp edges and surprises | tools, libraries, runtime |
incidents | Past bugs and their fixes | bugs, near-misses |
meta | Knowledge about the knowledge | reasoning, ontology |
principles | Designer-authored intent | mission, philosophy, anti-patterns, ux |
principles is the only topic carrying rules, and they are a good template for
your own: entities must include designer, kind and type must be pragmatic
and policy, and domain must be non-empty and must not mix global with an
area path. Together they make the topic writable only through the
/knomit-principle flow.
Which shape to choose
Section titled “Which shape to choose”default organises by subject. code and the research example above
organise by epistemic role. For an agent-facing knowledge base, prefer the
second: “show me the invariants for this area” is a question an agent can act
on, in a way “show me the technology facts” is not. Subject is better carried by
domain tags and entities, which cut across the tree instead of partitioning
it.
Preset upgrades, and how to opt out
Section titled “Preset upgrades, and how to opt out”Presets evolve. On every repo open, knomit checks whether the stored ontology’s
id matches an embedded preset — general maps to default, source-code to
code. If it does and the stored ontology is a strict subset of that preset,
the stored file is overwritten in place with the newer preset, so repos
created against an older version pick up added topics automatically. Subset
comparison matches validations by name only, which is how rule wording fixes
ship this way too.
If the stored ontology has added anything the preset lacks, it has diverged: knomit logs that an upgrade is available and leaves your file alone.
Choosing an ontology
Section titled “Choosing an ontology”The ontology is chosen when the knowledge base is created, via
POST /api/v1/repos:
curl -X POST http://localhost:19278/api/v1/repos \ -H 'Content-Type: application/json' \ -d '{ "name": "research", "mode": "custom", "ontology_yaml": "id: research-literature\nname: Research Literature Knowledge\ntopics:\n findings:\n description: What a paper established\n" }'mode | Remote? | Ontology source |
|---|---|---|
preset | local only | ontology_preset: "default" or "code". Omitted, it uses default. |
custom | local only | ontology_yaml: the full YAML, inline. |
clone | remote | Taken from the chosen branch of the origin. Passing ontology_preset or ontology_yaml is rejected, not ignored — a clone would otherwise drop it silently. 409 if that branch has no ontology. |
initialize | remote | ontology_preset or ontology_yaml — required, since writing it is the act that makes the branch a knowledge base — committed onto knomit’s own branch. 409 if that branch already has one. |
The two remote modes are the two halves of one question about the branch you
chose — does it already carry .knomit/ontology.yaml? — answered by
POST /api/v1/repos:probe-initialized. clone joins a branch that is
already a knowledge base and lets its ontology govern; initialize turns
a branch that isn’t one into one.
The response is newline-delimited JSON progress ending in {"type":"done"} or
{"type":"error"}. Preflight establishes shape for both remote modes before
the stream opens, so a mismatched mode arrives as a problem+json 409 rather
than as an error line inside a committed 200. The web UI wraps all four modes
in a guided wizard that derives the mode
from what it probed, rather than asking you to classify the remote yourself.
Validating a draft before you commit to it
Section titled “Validating a draft before you commit to it”Three endpoints exist so a draft can be checked without creating anything. The
wizard’s editor is built on them, and they are equally usable from curl:
| Endpoint | What it gives you |
|---|---|
POST /api/v1/ontologies:validate | Parses a raw YAML body and returns every diagnostic with its line position, not just the first. Always 200 — a document that fails validation answers ok: false; a 4xx is reserved for a body the server could not read at all. |
GET /api/v1/ontologies/presets | The built-in presets: each one’s preset name, ontology id, title, description and topic list. |
GET /api/v1/ontologies/presets/{name} | A preset’s YAML in exactly the form create would commit — the right starting point for a custom ontology. |
GET /api/v1/ontologies/schema | The field reference for the YAML above — every key with a one-line description. A reflection test over the parser’s own structs fails if a key it accepts is missing here, so the reference cannot quietly fall behind the parser. |
curl -X POST http://localhost:19278/api/v1/ontologies:validate \ -H 'Content-Type: text/yaml' --data-binary @ontology.yamlUse this rather than repo creation as your validation step: unlike a create, it costs nothing and reports all the problems at once.
How the ontology reaches the agent
Section titled “How the ontology reaches the agent”MCP server instructions are generated per session from the write repo’s
ontology. The top-level topic list, with each topic’s description, is
interpolated straight into the section the model reads before it does anything:
- **topic**: A validated top-level folder. Available topics: benchmarks — What a benchmark measures — and how it misleads contested — Where credible papers disagree, and the conditions that decide it findings — What a paper established, and the setup that bounds it ...That has four practical consequences:
- Descriptions are prompt engineering. “Sharp edges and surprises” routes facts better than “gotchas”. Write them as routing instructions addressed to the agent, not as glossary entries.
- Only top-level descriptions are shipped. Child descriptions shape the file tree and the UI, and a human browsing will read them, but the model never sees them in its instructions. Front-load the meaning into the topic line.
- Topic names affect federation: a lens skips read mounts whose ontology lacks the topic being browsed, so a shared topic vocabulary across repos is what makes cross-repo browsing coherent.
- Topics feed the OKF export, where the OKF
typeis derived from the topic, singularised.
Gotchas
Section titled “Gotchas”- A missing or unparseable ontology makes the repo read-only. It is not replaced by the default; refused writes and an error log are the signal.
- Deleting topics from a preset while keeping its
idis silently undone on the next repo open. Change theid. childrenare suggestions. The freeform tail means they never restrict what gets created; use afact.pathrule for a real constraint.fact.sourcesis summed on a subsumption merge, so a corroboration floor gates a claim’s first write and passes on every one after it — andfact.evidence_weightis always 0 onknomit_learn, because it is stamped after validation.- Only the first failing rule is reported per write.
- Comments and key order are not preserved — the stored file is re-serialised with sorted keys.
- Grandchild keys skip the kebab-case check, and are then unreachable if they aren’t lowercase.
- Rules are compiled at parse time, so a syntax error takes down the whole ontology, not just that rule — and then gotcha 1 applies.
- Private-state facts bypass ontology validation entirely, root rules included.
- Regex rules need doubled backslashes inside a double-quoted YAML scalar.