← All guides

Authoring a domain — corpus, vocabulary, curriculum

What you write by hand, and the contracts each artefact must satisfy. The API walkthrough covers the calls; this covers the content those calls carry.

A domain is built from three authored things: a corpus (the source of truth), a vocabulary (which words become concepts), and a curriculum (what the engine is taught, and how it is assessed).


1. Corpus

The corpus is what EARL learns its facts from. Every sentence you upload is read on its own and can become a fact — including notes, disclaimers, rationale and block quotes. So write the corpus as a list of plain facts, and keep rules, conditions and explanations in your application (or in a README you do not upload).

Upload Markdown, or .txt, .html, .pdf, .docx, .csv (converted to Markdown; the original is kept). Converted documents follow the same rules — read the converted text before training. One document per topic is easiest to maintain.

The rules

  1. One fact per sentence: subject, verb, object, in the natural direction. "A purchase requires an approval." "Helix supplies software."
  2. Name things the way claims and questions will. If the corpus says "the provider" and a claim says "Northstar", nothing connects them.
  3. One verb per relation, used every time. settles throughout teaches one relation; settles/handles/processes teaches three weak ones.
  4. No conditions inside a sentence. when, if, unless, above/below the threshold, only after — the condition is dropped and the rest is learned as a flat fact. Put thresholds, amounts and conditional rules in your application's own logic.
  5. A denial only when it is true without conditions. "A receipt does not prove ownership." is a good denial: it contests the affirmative claim and gives a definite No. "A purchase below the threshold requires no tender." is not: it is learned as purchase not_requires tender, which contradicts purchase requires tender.
  6. No rationale, commentary or disclaimers in the uploaded corpus. "The register is a closed list: the absence of a supplier is not a statement about its ability" becomes noise facts (register not_is closed, category not_is ability). Explanations belong in a README.
  7. No values as facts. Numbers, dates and amounts are not concepts ("The threshold is 100,000.").
  8. Tables are not read as relations. Keep a table if it helps a human reader, but state each fact it holds in a sentence as well.
  9. Nouns go in the vocabulary; verbs stay out of it (§2).

The platform checks rules 4-8 for you at upload. Every document carries corpus_warnings (the Corpus check column under Documents; GET /api/domains/{id}/documents): each flagged sentence with its line, the rule (condition, qualified_denial, rationale, value, block_quote, table, long_sentence) and why. They are advisory - nothing is blocked, and a flagged sentence may be fine.

What these rules change — measured on the reference procurement domain (2026-09-26)

Written in corpus v1 Learned Corpus v2
"A purchase below the tender threshold requires no tender." purchase not_requires tender and threshold not_requires tender, contradicting purchase requires tender and threshold requires tender removed; the threshold is a rule in app.py
"Screening certifies a supplier when no sanctions match the supplier." screening not_certifies supplier "Helix is certified by screening." for each supplier
"The register is a closed list: the absence …" register is closed and register not_is closed, plus unrelated denials moved to the README

Corpus v1 trained to 106 facts, 27 of them denials (3 contradicting held facts). Corpus v2 trains to 67 facts with no denials and no contradictions. Every claim verdict and decision in the reference application stayed the same, because they only ever depended on the plain facts.

Check the store after every training

The domain page's Ask panel opens with a store check (GET /api/domains/{id}/store-check): the number of facts, every taught denial, every fact held together with its own denial, and facts with a function word as an endpoint. The training helper in the SDK examples logs the same summary. Then open Knowledge graph › Beliefs (or GET /api/domains/{id}/kg/beliefs) and look for:

Fix the sentence (or the band), retrain, and read the store again. Do not reword the question or the claim to work around a bad fact.

Avoid also: bare bullet fragments and long sentences whose subject and object drift far apart.


2. Vocabulary module

Decides which words become first-class concepts. A word that is not grounded cannot be an endpoint of any verdict — claims about it come back HALLUCINATED no matter what the corpus says.

CLUSTERS = [
    ("acquirers",  "acquirer",   ["acquirer", "atlas", "northstar", "processor"]),
    ("schemes",    "scheme",     ["scheme", "visa", "mastercard", "amex"]),
    ("settlement", "settlement", ["settlement", "currency", "eur", "usd"]),
    # One band per NAMED entity your claims are about, anchored on it, listing
    # the specific things the corpus says about it. See "Assert the pairs your
    # claims are about" below -- without these bands no claim about Atlas and
    # EUR can ever verify, however plainly the corpus states it.
    ("atlas_capabilities",     "atlas",     ["atlas", "visa", "mastercard", "eur", "gbp"]),
    ("northstar_capabilities", "northstar", ["northstar", "visa", "mastercard", "usd"]),
]

ALIASES = {
    "amex": ["american express"],
    "usd":  ["united states dollars"],
}

Every member is one lowercase word. The engine grounds a token exactly as written and resolves a multi-word concept only in its underscore form, at most four words (journal_entry). A member written with a space (journal entry) is injected as a token nothing can ever reach: the school scores zero on every assessment that names it (F31). The validator refuses such members before the school starts. Prefer the head noun (entry) and, when the phrase matters, declare it as an alias of the head noun; use the underscore form only when the corpus uses that exact phrase and the head noun alone would be ambiguous.

# refused (F31):  ("entry_types", "journal entry", ["journal entry", "manual entry"])
# grounded:       ("entry_types", "entry", ["entry", "manual", "recurring"])


def anchor_chain():
    return [
        ("acquirer", "scheme"),
        ("scheme", "settlement"),
    ]

Nouns only

Ground nouns only: the things claims are about. Never put a verb in a band (settles, supports, covers, holds). A grounded verb is a concept to the verifier, and a concept can be chosen as a claim's endpoint: with supplies grounded, "Helix supplies software" is adjudicated on the pair (helix, supplies) and comes back UNCERTAIN however plainly the corpus states it, and a reversed claim can even verify on a co-occurrence belief (findings F13/F14, measured 2026-09-23). The relation is read from the sentence; it does not need to be a concept. The vocabulary validator rejects members that are connector or relation words (plus, and, versus, …), which the extractor consumes as relations, and the curriculum validator warns when a band member occupies the verb position of a lesson proposition.

The same holds for the curriculum: a proposition like helix passed sanctions teaches passed as a concept. Use verbs the engine's relation lexicon knows where you can (requires, enables, certifies, oversees, funds, …).

The contract

Rule Why
CLUSTERS is a non-empty list of (cluster_name, anchor, [tokens]) Each cluster is a densely connected concept band
The anchor must appear in its own token list It is the band's hub; a hub outside its band connects nothing
anchor_chain() returns a list of 2-tuples linking anchors The prerequisite spine — without it the bands are disconnected islands
ALIASES is optional: canonical → other surface forms See below
Every value must be a literal See below

Literals only

The module is parsed as data — the platform reads its syntax tree and never executes it. Comprehensions, variables, imports, and computed returns cannot be resolved and are rejected with a message saying so.

BASE = ["a", "b"]
CLUSTERS = [("band", "a", BASE)]          # rejected: not a literal
def anchor_chain(): return [(x, x) for x in BASE]   # rejected: not a literal

Inline the lists and tuples instead. This is deliberate: the draft arrives over HTTP, from an operator or a language model, and executing it would be arbitrary code execution on the server.

Aliases

Declare an alias whenever the corpus spells a concept out but a claim would plausibly use the short form, or the reverse.

Without {"amex": ["american express"]}, a corpus sentence saying "cannot process an American Express transaction" and a claim saying "supports the amex scheme" land on different tokens, so the denial cannot constrain the claim. With it, both resolve to amex and the evidence applies.

Rules: the canonical must be a token in some cluster; a surface is at most 4 words; it may not duplicate a real token (that would shadow it); and it may not be declared for two different canonicals. Violations are reported when the draft is saved, not hours later during a build.

Aliases are declared, never inferred — nothing guesses that two words mean the same thing.


3. Curriculum

A DAG of lessons. Each teaches propositions and is assessed against what the engine can reconstruct from its own substrate, with no teacher present.

{
  "subject": "PaymentsRouting",
  "lessons": [
    {
      "id": "PR.1",
      "title": "Acquirers and Schemes",
      "grade": "Kindergarten",
      "requires": [],
      "propositions": [
        "an acquirer processes a transaction.",
        "a membership admits a scheme."
      ],
      "assessments": [
        {"a": "acquirer", "b": "transaction",
         "prompt": "Does an acquirer process a transaction?"}
      ],
      "mastery": 0.95
    }
  ]
}
Field Notes
id Unique; referenced by other lessons' requires
requires Prerequisite lesson ids. A lesson stays Locked until all are mastered
propositions The facts taught, each a short subject verb object sentence
assessments (a, b) concept pairs the engine must connect unaided
mastery Threshold to pass, typically 0.95

Teaching and assessment are deliberately separate: teaching pushes facts in, assessment measures what actually took hold. A proposition the gate rejected, or a link never formed, surfaces as a gap rather than being assumed learned.

What passes an assessment (engine v0.66.0-rc24 on). An (a, b) item passes only when the brain holds a fact between the two concepts: a three-word belief a RELATION b or b RELATION a (a taught denial such as a not_requires b counts; the lesson-order relations taught_after / taught_before do not).

Before rc24 an item also passed on any belief that merely named both words. That included the verifier's own record that it had found a graph path between them, written for every claim it checks. It also passed on any graph path of up to four hops. Those passes hid sentences the engine had misread.

Measured on the hosted example curricula, the passes that disappeared were all facts the engine never formed: - "an acquirer settles a currency." was read as settles related_to currency; - "a vector lives in a hilbert space." formed nothing, because the concept is two words; - "a commutator relates position and momentum." formed nothing, because the sentence has two objects.

A school run on rc24 can therefore master fewer lessons than the same curriculum did before. That is the engine reporting the gap. Read the lesson's gaps and the belief store, and rewrite the proposition: - one subject, one verb, one object; - single-word concepts.

Assess two concepts, never a concept and a verb. An item such as (compatible, commute) cannot pass on rc24. A verb is not a concept the engine holds facts about.

Order lessons so each builds on grounded ground. A failed lesson holds its dependents Locked and stalls the grade — which is the intended behaviour, not a bug to route around.

Use tokens your vocabulary grounded. Assessment is inflection-sensitive: if the vocabulary grounds side, an assessment on sides has no path and honestly fails.

Generated drafts are grounded by construction and fully recorded. When you ask the platform to generate a curriculum (POST /tuning-runs/{id}/generate-lessons), the generator receives your approved vocabulary as a hard constraint and makes one corrective pass if its first proposal assesses tokens the vocabulary does not ground (F27) or puts a member in verb position (F14). Whatever remains is listed in validation_warnings, and the school refuses to start over it unless you override with a logged reason. Every vocabulary and lesson draft carries a generation_log: each LLM attempt with the problems found in it, feedback regenerations, every human edit (which lessons, members or aliases changed) and the approval, oldest first. That log is the authoring record — read it before you approve a generated draft, and keep it: it says what the model proposed, what the platform corrected and what a person changed.


What the engine reads from a sentence (engine v0.66.0-rc6, SHIP-1)

The trainer and the verifier now read a sentence the same way. Knowing the rules lets you write a corpus, a curriculum and claims in ordinary English instead of phrasing them around the engine.

The relation is the clause that states it. A belief is formed from the two grounded concepts of one clause and the verb between them, in the order the clause gives them. Two concepts that only ever appear in different sentences are an association (related_to), never a typed relation, so a claim about them can be grounded but never VERIFIED. A line break ends a clause; so does a markdown table cell: a table row is a list of cells, not a sentence, and no relation or denial is ever read across cells. State facts in prose as well as in tables.

Every stated relation on a pair is kept. "A suite requires a supervisor." and "A supervisor inspects a suite." in the same document form two beliefs, one per verb and direction (rc6, F32); before rc6 only the nearest clause survived. The other side of that rule: every clause that puts a verb-shaped word between two grounded concepts also forms a belief, so explanatory prose ("sending a manager to every studio would take the manager away from the reviews the manager owns") yields studio take manager and studio reviews manager (F36, disclosed). Write explanations in sentences that name at most one grounded concept, or read the belief store after training and reword the sentences behind any artefact. A denial contests the affirmative claim. With "A studio does not require a manager." in the corpus, the claim "A studio requires a manager." is CONTESTED affirmative_claim_negated_in_substrate (rc6, F33), and the denial itself verifies as a negated claim; every claim result reports negated.

Voice. "Vega is certified by screening" is read as screening certified Vega on both sides: the corpus sentence trains that belief and a claim in the passive is checked in that direction. Write curriculum propositions in their natural direction ("screening certifies vega"), not in the order you expect the verifier to flank them.

The copula is a verb. "An observable is hermitian" and "spin is quantised" are typed relations (is); a claim restating them verifies against them, and a different adjective on the same subject is unasserted.

Negation. One denying clause is recorded beside any affirmation of the same pair; the substrate then carries the contradiction and an affirmative claim on that pair is CONTESTED. A denial marker that is the pair's own verb ("the handler declined the claim") is the relation, not a negation. A negated claim is supported only by the store's own denial of the same pair with the same verb, never by an affirmation.

Named concept over type noun. "A manual entry requires a controller approval" grounds on (manual, controller): inside one noun phrase the concept that names the thing wins over the noun that types it, and if the store asserts a pair among the phrases' tokens, that pair is the claim's. Claims no longer have to be phrased on the bare pair.

Verbs. Connectors ("plus", "versus") are never relation words. A word the sentence marks as a verb by an auxiliary ("do not commute") is read as the verb even when it is not verb-shaped; a bare noun by elimination is not. A coordinated pair as the subject of an intransitive verb ("position and momentum do not commute") takes its verb from the clause tail.

Aliases over foundational n-grams. The foundational KB carries general-English n-grams ("sanctions_screening"). A declared alias now takes precedence over such a token in both the trainer and the verifier, so you may map the phrase onto the concept your bands declare: ALIASES = {"screening": ["sanctions screening"]}. The KB builder reports the remap; it refuses only a single-word alias that shadows a real token. That refusal comes from the builder at POST /tuning-runs/{id}/build-vocab-kb (the foundational KB's tokens are read then), not from the draft validator — the draft validator checks the module against its own clusters. The message names the alias (alias 'housekeeper' shadows an existing token text); remove it or alias a different surface, save, approve, build again. The domain and its documents are kept: create the next tuning run on the same domain rather than a new domain (the reference helper takes domain_id= for exactly this).

Which verbs the engine knows. A claim's verb must be a relation the engine recognises, or the verdict is UNCERTAIN relation_unrecognized. Recognised: every verb your own corpus trained as a typed relation (the propositions beliefs lists — inspects, cleans, contains, performs … whatever your sentences used), plus the built-in lexicon: requires, enables, defines, generalizes, specializes, extends, models, contrasts, inverts, reverses, precedes, underlies, uses, applies, solves, computes, includes, provides, issues, processes, handles, leads, funds, disburses, finances, and the copula is — each with its -s/-ed/-ing forms. A verb outside both sets can appear in a claim only after a corpus sentence has trained it on some pair.

contradictions is not the denial count. The contradictions field counts substrate contradiction edges the grounding path crossed. A typed denial in the store ("A studio does not require a manager.") contests the affirmative claim through demotion_reason: affirmative_claim_negated_in_substrate and can leave contradictions: 0; read the reason, negated and asserted_by, not the counter (clean-room finding R5).

What a verdict reports. Every claim result carries endpoints in semantic order (subject, object), predicate, passive, asserted_by (the supporting belief) and asserted_source (the document and clause that formed it), so a test can check the pair, the relationship, the direction, the polarity and the source, not only the verdict.

Before the school. The school refuses a curriculum the vocabulary cannot ground: an assessment on a token that is not a member, or a member used as a verb, is reported with the lesson and token named. Generated curricula receive the grounded vocabulary as a hard constraint and are corrected once automatically; a valid draft needs no manual repair. An explicit override (accept_ungrounded with a reason) exists for research fixtures and is logged on the run.

Assert the pairs your claims are about

This is the rule the other three sections build towards, and the one most first domains get wrong. VERIFIED requires that a trained belief asserts the claim's entity pair (engine ≥ v0.64.0). Grounding — a path between the two endpoints — is necessary but not sufficient; a claim can be fully grounded and still come back UNCERTAIN with demotion_reason: relation_unasserted.

A belief about a pair is formed at training time only when both hold:

  1. the two grounded concepts and the verb between them sit in one clause (engine v0.66.0-rc6, "What the engine reads from a sentence" above: "Atlas settles the EUR currency." yields atlas settles eur; a line break or a table cell ends a clause). Engine v0.65.0 used a 25-mention window across sentences instead; that rule no longer applies. Two things still produce beliefs the text did not state: a clause that names two grounded concepts around a verb-shaped word ("the suite is inspected in the same way as the studio" forms suite inspected studio), and the school's consolidation step, which writes requires/enables edges between lesson concepts at confidence 0.9. Read the belief store after training and remove or reword the sentence behind any artefact you find; and
  2. the grounded KB already carries an edge between the two tokens. Edges come from the vocabulary: every band links its anchor to each member and its members to each other, and anchor_chain() links the pairs it lists. Type-level bands (acquirers, settlement) never link atlas to eur.

So for every named entity your claims will be about, add a band anchored on that entity whose members are the specific things the corpus says about it. Keep it truthful: a band is a declaration of which pairs may be asserted, so list what the corpus supports and nothing more — northstar_capabilities omits amex precisely because Northstar does not take Amex. Then check the belief store after training (GET /domains/{id}/kg/beliefs?query=atlas): the pairs you need must appear as three-word propositions (atlas settles eur). If they do not, the sentence or the band is missing, and no curriculum change will substitute for either.

What the substrate cannot represent

Design around these rather than discovering them in production:


A working loop

  1. Write the corpus. Upload it.
  2. Draft the vocabulary from the nouns and verbs claims will actually use. Approve; build the domain KB.
  3. Draft the curriculum bottom-up: parts and names first, relations after. Approve.
  4. Run school. Read the summary — unmastered lessons name their gaps. The platform refuses to accept a school with unmastered lessons unless you say so explicitly and give a reason (accept_unmastered), because locked lessons never teach their concepts and the trained brain then lacks them.
  5. Fix the vocabulary or corpus the gaps point at, not the assessment. An assessment bent until it passes measures nothing.
  6. Train, then test claims you already know the answers to — including negations of true claims, which should come back CONTESTED.
  7. Freeze and deploy.