Skip to content
Lessons

Ontology

The last lesson gave an agent a list of what was in the kitchen. This one gives it a small model of the kitchen instead. Same pantry, same eight items, one new file. By the end, three sizes of model give the same correct answer, and the shape transfers to anything you keep re-explaining to an agent.

A weeknight, after you build it:

you: can i make pancakes tonight, and what should i use up first?
claude: almost. you're short two things, and only one of them matters. no buttermilk, but sour cream thinned with milk stands in for it in baking, and the sour cream is the thing you most want to use up anyway: opened six days ago, keeps ten, so four days left. that's the most urgent item in your fridge. no baking powder either. nothing in your kitchen substitutes for it. so it's a one-item shop, or flat pancakes. if you want to check me, every claim above is a line in ontology.md and i'll cite them.

That answer is correct, and every clause in it traces to a line you can open. The rest of the lesson is the file that makes it possible: one page of markdown that says what is in the kitchen and what each thing is; forty-two of its lines are three words long, a thing, a relation, a thing.

Before you begin

Where we left off

A folder, an agent, and a list that keeps letting us down.

The Larder lesson built a meal-planning harness, the files, tool and gates around the language model that give each piece of a task to whatever suits it and leave the judgment with you. Its files were an identity file (CLAUDE.md, the instructions the agent reads before anything else), a skill file, a preferences file, a folder of recipes, and a pantry.md, which was a markdown list of what was on the shelves.

That harness works. I still run it. But the pantry file was always the weak joint, and this lesson is about why. A list can tell an agent what you have. It cannot tell it what any of those things are, where they live, what they can stand in for, or which of them is about to turn.

If you skipped Larder, nothing here requires it. You need a folder, a coding agent, and a text editor. I use Claude Code because that is what the series uses, but nothing in the files is vendor-specific.

One idea runs through all of it: an ontology is a small model of your world an agent can read, and it is only worth having once a script checks it.

What we are actually building

One file, ontology.md, next to the recipes. Every fact in it is one line of three words, a subject, a relation, an object: butter is-a dairy. Call that a triple. Above the facts sit a few declaration lines, kind: dairy, that say which words the triples may use. By the end there are forty-two triples, six rules written in English, and one short script that reads the file, complains when it stops making sense, and prints what the file adds up to. Nothing else.

Chapter 0

The flat list

One question, three answers, none of them right.

Here is the pantry, the same pantry.md Larder built, and it is the same pantry for the whole lesson. Flour, sugar, salt, milk, eggs, butter that has been opened, one lemon, and a tub of sour cream that has been open for six days. No buttermilk. No baking powder. Write it down the way anybody would write it down.

+ pantry.md (new file)
+ # Pantry++ - flour (most of a bag)+ - sugar+ - salt+ - 1 quart whole milk+ - 1 dozen eggs+ - butter, opened+ - 1 lemon+ - sour cream, opened

Now the question, and it is the one this whole lesson keeps asking:

Can I make pancakes tonight, and what should I use up first?

The recipe is in the folder. The pantry is in the folder. Let me ask three times, in three fresh sessions. A session is one run of the agent against the folder, carrying nothing over but the files, so pancakes.md and pantry.md are the only context.

filled = the right move, souring the milk; the file's version uses the lemon · hollow = wrong

you: can i make pancakes tonight, and what should i use up first?
claude: yes. you've got flour, sugar, salt, eggs, milk and butter, so you're covered on everything the recipe needs. use the milk first, it's the thing that goes off soonest.
you: can i make pancakes tonight, and what should i use up first?
claude: you can, with one swap. you don't have buttermilk, so use the plain milk instead and the pancakes will come out a little flatter. use up the butter first since it's already open.
you: can i make pancakes tonight, and what should i use up first?
claude: should be fine. for the buttermilk, stir a splash of vinegar into the milk and let it sit ten minutes. your pantry looks well stocked otherwise. i'd start with the eggs.

Those three are composites: exchanges assembled from real sessions to show one failure at a time. The kitchen is a problem I made up to learn this method, and every quoted run in Chapter 4 is a real run against that made-up file. Chapter 4 puts this same eight-line list in front of three sizes of model, and one of the three failures I describe next does not survive it. Guess which before you get there.

Three sessions, three answers, each reasonable on its own. The first invented baking powder: the recipe calls for two teaspoons and the pantry has none. A kitchen with flour and sugar in it probably does have baking powder, which is a good guess about kitchens in general and a wrong claim about mine. The second noticed the missing buttermilk and then walked past the lemon sitting four lines above it. None of the three ranked the sour cream first, and it is the only item in that kitchen with a real deadline on it.

A better prompt does not reach the cause

The temptation is to blame the question and write a better one. I did not measure that, so treat this as a guess: a longer prompt can tell the model to check for absences, and no prompt can supply dates the list never had. The model is doing what it has to do with what it was handed: eight lines of text, no structure, and a recipe that mentions things those lines do not cover. Everything it needs to answer well is knowledge that was never written down.

Look at what pantry.md cannot say. It cannot say anything about what is absent. Ask it about buttermilk and it has no way to distinguish "you definitely do not have this" from "this never came up."

That gap, no way to say "not here", is the one every later chapter comes back to.

The fix is to give each word a kind, a place, and a relation to the others. That is the next three chapters.

Chapter 1

Things and kinds

Three-word lines, and the first inference happens without a model.

Start over with a new file. ontology.md, same folder.

Every line is a triple: three words, a subject, a relation, an object. The subjects and objects here are entities, and each one is a thing or a kind. A thing is one item you could pick up: butter. A kind is a label several things share: dairy. The test is whether it could go in a shopping basket: butter can, so butter is a thing; dairy cannot, only some particular dairy thing can, so dairy is a kind. Milk, butter, sour cream and buttermilk are four things under that one kind, and dairy sits under a larger kind, perishable, which it shares with produce. Every thing gets one is-a line naming its kind; a kind gets one only when it sits under a larger kind. The file keeps them under separate headings, so you never have to guess which is which. Read each one out loud and it is an English sentence: butter is-a dairy. If you can write a shopping list you can write this.

The first relation is is-a. It says what a thing is, and what a kind is a kind of.

+ ontology.md (new file)
+ # Kitchen ontology++ > Three words to a line: subject, relation, object. Indented lines add+ > detail to the triple above them. Headings, bullets and quotes are for+ > people, and the checker skips them.++ ## Vocabulary++ kind: perishable+ kind: dairy+ kind: produce+ kind: staple++ ## Relations++ relation: is-a transitive++ ## Kinds++ dairy is-a perishable+ produce is-a perishable++ ## Things++ flour is-a staple++ sugar is-a staple++ salt is-a staple++ baking-powder is-a staple++ milk is-a dairy++ butter is-a dairy++ sour-cream is-a dairy++ buttermilk is-a dairy++ eggs is-a perishable++ lemon is-a produce

The declarations at the top, the kind: and relation: lines, are the file's meta model: the small set of types and relationships, kinds and relations in this file's own words, that the lines below are allowed to use. The AI in Systems Engineering talks use the same term, where SysML v2 is the meta model under an engineering model of Apollo 11. The three-word lines underneath are the kitchen itself, written in those terms. The two layers fail differently, which is the quickest way to tell them apart. Write butter is-a cheese and the checker, which Chapter 5 walks through, objects: cheese is not a declared kind. Add kind: cheese at the top and the same line passes, true or not. The top says what may be said; the lines say what is so. The whole file, meta model and kitchen together, is what this lesson calls an ontology. If you came here looking for the phrase knowledge graph, those lines are its edges, and the meta model is its schema, what makes them more than text.

One line per thing, one blank line between them. The gaps stop looking silly in two chapters, when most of these grow to three or four lines each.

baking-powder is-a staple and buttermilk is-a dairy: neither is in my kitchen. They are declared anyway, because declaring a thing and having a thing are now two different statements. The model can finally be definite about an absence instead of silent about it.

is-a is marked transitive: a transitive relation chains, so two lines that share a middle word yield a third line nobody wrote. Watch:

you: is the butter perishable?
claude: yes. you never wrote that down anywhere. you wrote "butter is-a dairy" and "dairy is-a perishable". i followed the two lines and got a third.
The file answers this one without a model

That answer did not require a language model. The checker is a tool the agent runs instead of working the answer out itself; Larder called such programs tools. This one I wrote: check-ontology.py in the lesson-ontology repo, and Chapter 5 walks through it. The walk up is-a is these eight lines of it:

check-ontology.py (the walk)
def kinds_of(entity, seen=None): """Walk is-a upward. This is the inheritance, and it is the whole trick.""" seen = set() if seen is None else seen for s, r, o, _ in triples: if r == "is-a" and s == entity and o not in seen: seen.add(o) kinds_of(o, seen) return seen

The file never says butter is perishable. It says butter is-a dairy and dairy is-a perishable, and that function walks the two lines to get there. Chapter 3 shows it catching a mistake of mine the same way.

That is also why staple sits at the top of its own branch with nothing above it. Staples do not inherit from perishable, so every rule I later write about perishables silently skips the flour. I did not have to write "flour does not spoil" anywhere.

Ask the question again. It is better and it is still wrong.

you: can i make pancakes tonight, and what should i use up first?
claude: not quite. pancakes needs buttermilk and baking powder, and i have no record of either being here. the butter and the sour cream are both dairy, which is perishable, so those are your use-first candidates. i can't rank them though, i don't know when either was opened.

The invention is gone. It knows what is missing and it says so, and it has narrowed use-first from eight items to the two that can actually spoil. What it cannot do yet is rank them, find a substitute, or tell you where anything is. It has the nouns and the kinds. What it needs next are verbs: where things live, and what stands in for what.

butteris-adairyis-aperishablederived, not written
Chapter 2

Verbs

Where things live, what goes into what, and what stands in for what.

Nouns and kinds get you a taxonomy. The questions people actually ask are about relationships: where is it, what does it go in, what can I use instead. Those are verbs, and each one is a new relation. A relation line can carry a mark that says how the relation behaves. Transitive you have met: it chains. Symmetric means the line reads in both directions: if the file had a next-to relation, milk next-to butter would already say the butter is next to the milk, with no second line to keep in step. Conditional means the line holds only under a purpose named on a when line beneath it. The marks guide whoever reads the file. My script stores them and walks only is-a; it is the model that reads symmetric and conditional and concludes no more from a line than they allow.

~ ontology.md (update)
## Vocabulary kind: perishable kind: dairy kind: produce kind: staple++ location: kitchen+ location: cupboard+ location: fridge+ location: door-shelf++ purpose: baking+ purpose: dressing+ purpose: drinking ## Relations relation: is-a transitive+ relation: stored-in transitive+ relation: ingredient-of+ relation: substitutes-for symmetric conditional

stored-in is transitive, like is-a, and it pays off immediately. The substitutions further down show why substitutes-for needs both of its marks.

~ ontology.md (update)
+ ## Places++ cupboard stored-in kitchen+ fridge stored-in kitchen+ door-shelf stored-in fridge
~ ontology.md (update)
flour is-a staple+ flour stored-in cupboard sugar is-a staple+ sugar stored-in cupboard salt is-a staple+ salt stored-in cupboard baking-powder is-a staple milk is-a dairy+ milk stored-in fridge butter is-a dairy+ butter stored-in door-shelf sour-cream is-a dairy+ sour-cream stored-in fridge buttermilk is-a dairy eggs is-a perishable+ eggs stored-in fridge lemon is-a produce+ lemon stored-in fridge
you: where's the butter?
claude: door shelf. which is in the fridge, which is in the kitchen. i only have one line about the butter's location. the other two came from the shelf's line and the fridge's line.

Notice which lines are missing from that update. baking-powder and buttermilk have kinds and no locations. An entity with no stored-in is not here, and that is a positive, checkable fact.

ingredient-of points from a thing to a recipe, which makes the recipe queryable from either end. It carries no transitive mark, on purpose: flour is an ingredient of pancakes, and if a brunch recipe listed pancakes, flour would still not be an ingredient of the brunch in any sense a shopping list needs. My script walks only is-a; the mark is what tells a model not to chain this one.

~ ontology.md (update)
+ ## Recipes++ flour ingredient-of pancakes+ buttermilk ingredient-of pancakes+ eggs ingredient-of pancakes+ butter ingredient-of pancakes+ baking-powder ingredient-of pancakes+ sugar ingredient-of pancakes+ salt ingredient-of pancakes++ flour ingredient-of shortbread+ butter ingredient-of shortbread+ sugar ingredient-of shortbread

The recipe is still a recipe file that a person reads, with quantities and a method. The triples are the part of its ingredient list a script can read, and keeping the two in step is a job for the checker in Chapter 5.

+ recipes/pancakes.md (new file)
+ # Pancakes++ Serves 4. About 20 minutes.++ ## Ingredients++ - flour, 2 cups+ - buttermilk, 2 cups+ - eggs, 2+ - butter, 3 tbsp melted+ - baking-powder, 2 tsp+ - sugar, 2 tbsp+ - salt, 1/2 tsp++ ## Method++ 1. Whisk the dry ingredients together.+ 2. Whisk the wet ingredients in a second bowl.+ 3. Combine. Leave the lumps. Rest 5 minutes.+ 4. Medium heat, buttered pan, about 2 minutes a side.

The first word on each ingredient line is the entity name, before the comma. That one convention is what lets a dumb script check a file written for humans.

Then substitutes-for, which is the relation that does the most work and gave me the most trouble.

~ ontology.md (update)
+ ## Substitutions++ sour-cream+milk substitutes-for buttermilk+ when baking+ note thin the sour cream with milk until it pours++ milk+lemon substitutes-for buttermilk+ when baking+ note one tablespoon of juice per cup, rest ten minutes

Subjects can be combinations joined with a plus, because most real substitutions are two things standing in for one thing. Every substitution carries a when, drawn from the purpose vocabulary declared at the top.

The when is there because symmetry only holds inside a purpose. Declare substitutes-for symmetric and stop, and the file licenses buttermilk in place of the sour cream in something that wanted sour cream as a topping. It is an honest swap for baking and a bad one on a baked potato, and the when clause is what fences it in. Read backwards, the pair's line says buttermilk can stand in for sour cream thinned with milk; that holds in a batter, and the when keeps it there.

Ask again.

you: can i make pancakes tonight, and what should i use up first?
claude: mostly yes. the buttermilk is covered: you don't have any, but sour cream plus milk stands in for it in baking, and so does milk plus lemon. you have all three of those. baking powder is the real problem. it's declared, it has no location, and nothing in your kitchen substitutes for it. use-first is still butter or sour cream and i still can't rank them. no dates.

The substitution chain landed, and it landed without me writing a word about pancakes and lemons in the same sentence. The structure is what let it chain those four lines.

The ranking still needs dates, and the file will happily accept nonsense: I could type butter stored-in garage or invent a relation and nothing would object.

pancakesneedsbuttermilkabsentsubstitutes-forsour-cream+ milk✓ have✓ have
Chapter 3

Rules

The file gets opinions, and starts refusing things.

A file that only describes will drift. Someone finishes the milk and nobody edits the line, and the file still reads as though they had.

Rules fix that, and they come in two shapes. The controlled vocabulary is already in place: the kind:, location: and purpose: lines at the top are a closed list of legal values. butter stored-in garage is now a typo with a name, because garage was never declared a location.

The other shape is invariants: rules the whole file must keep, every time it is read. Always-and-never sentences in plain English, written into the file itself. The script carries code for each one; the English is for people, and the code is what runs. Five now; Chapter 4 adds a sixth.

~ ontology.md (update)
relation: substitutes-for symmetric conditional+ relation: opened+ relation: sealed+ relation: keeps-for++ ## Invariants++ - Every perishable with a location has an opened date or a sealed mark.+ - Every opened date has a keeps-for.+ - Every recipe ingredient is a declared entity, and every recipe has a file.+ - Every stored-in object is a declared location.+ - Every relation and every name used below is declared above.

The first two invariants demand data the file does not have yet. Write the rule, watch it fail, then satisfy it.

~ ontology.md (update)
milk is-a dairy milk stored-in fridge+ milk sealed yes butter is-a dairy butter stored-in door-shelf+ butter opened 2026-08-11+ butter keeps-for 30 sour-cream is-a dairy sour-cream stored-in fridge+ sour-cream opened 2026-08-20+ sour-cream keeps-for 10 buttermilk is-a dairy eggs is-a perishable eggs stored-in fridge+ eggs sealed yes lemon is-a produce lemon stored-in fridge+ lemon sealed yes

milk sealed yes is not beautiful English. It is three words, which is what the parser wants. The dates are the ones I had the day I wrote this. keeps-for 30 is days after opening.

Write the perishable rule without the "with a location" clause, so it says only that every perishable carries an opened date or a sealed mark, and run the checker.

$ python3 check-ontology.py
ontology.md: 'buttermilk' is perishable, with no opened or sealed markontology.md: 'dairy' is perishable, with no opened or sealed markontology.md: 'produce' is perishable, with no opened or sealed mark3 finding(s).

All three findings are the rule's fault, not the data's, and I could not see that until the checker ran against the file. Each was found by the script walking is-a upward, buttermilk to dairy to perishable, with no model in the loop: the walk from Chapter 1, doing work. Buttermilk is not here and has no state to declare, dairy and produce are kinds rather than things, and scoping the rule to perishables with a location fixed both errors at once.

The keeps-for lines came from the same kind of correction, and this one matters more because it changes the answer. The obvious rule is to rank use-first by how long something has been open. Butter has been open fifteen days, sour cream six, so days-open puts the butter first. But butter keeps a month once opened and sour cream keeps about ten days, so the sour cream has four days left and the butter has fifteen. Ranking by days remaining reverses the order.

The check improved the rule, not the data

Both of those corrections went into the ontology, not into the answer. A wrong answer I argue with in a chat window is fixed for one turn. A wrong rule I fix in the file is fixed for every session after, including the ones where I would not have noticed the mistake. This is the whole argument for writing the structure down: it is the only place a correction can land where it compounds. And the thing that found the rule's mistake was a script, not a model.

One line is still missing. Days remaining needs a day to remain from, and taking that from the clock would mean this chapter's numbers change every time somebody reads it. So the file names the day.

~ ontology.md (update)
when baking note one tablespoon of juice per cup, rest ten minutes++ as-of: 2026-08-26

The checker counts from that line rather than from the machine's date, so four days left is four days left on any machine on any day. --live counts from the machine's date instead, and --today 2026-09-01 counts from a day you name.

Now the closed questions, the ones whose possible answers are fixed before you ask, yes or no, here or not here, are genuinely closed.

you: can i make pancakes tonight, and what should i use up first?
claude: no, unless you pick something up. six of the seven ingredients are here or substitutable. baking powder is the seventh: it's declared, it has no location, and no substitution names it. so that's a definite no rather than a maybe. use up the sour cream first. opened the 20th, keeps ten days, four left. the butter's been open longer but keeps a month, so it has fifteen. sour cream wins. and if you'd rather not shop, shortbread is fully covered: flour, butter, sugar, all present.

"No, unless" is a much more useful sentence than either "yes" or "probably", and the file can now prove every part of it.

Chapter 4

The experiment

One question, four context conditions, three model tiers, and a surprise in the middle.

The question is whether the file is doing the work or the model is. The floor is no files at all, just the question.

you: can i make pancakes tonight, and what should i use up first?
claude no files: most likely yes, pancakes need very little. flour, milk, eggs, butter, baking powder, a bit of sugar and salt. use up whatever dairy is closest to its date.

A competent answer that makes no claims about my kitchen. Now hold the file still and change the model.

The setup

The grid is three model tiers across, Opus, Sonnet and Haiku, the same product at three sizes and prices, largest first, as the table below reads; and four context conditions down, four versions of what the files put in front of the model: twelve cells, one answer each. I walk the conditions in order and call each pass a round. Same question in every cell, a fresh session each time, no tools and no web, nothing in the context but the files and the question. I capped the answers at 120 words, a little longer than the answer at the top of this page, so manner could not hide a miss. Each answer is scored on three things the file settles. The first is absence: does the answer say no to pancakes, because baking powder is declared and nowhere. Then a substitution for the buttermilk that comes from the file, and a use-first item with a date.

All twelve answers are verbatim, from a real run on 2026-08-26, and the complete record of them is a page of its own. Four are quoted here and the rest are linked. The transcripts earlier in this lesson are composites, assembled to show one thing at a time.

Rounds one to three

Round one is the eight-line pantry.md from Chapter 0, plus the recipe. All three tiers caught the missing baking powder, and all three went vague on use-first, because the list carries no dates to rank with. The composites in Chapter 0 show a flat list inventing baking powder. It did not happen once in these three runs. Eight items is few enough to scan, so checking for an absence is just reading the list.

Round two swaps the list for ontology.md as it stood at the end of Chapter 3. Everything the list could not reach arrived at once: the sour cream's opened date, its keeps-for, the real expiry, and the conditional substitution with the method note attached, because the note is a line in the file. And two of the three answered yes anyway, with only the buttermilk to work around: Haiku by listing baking powder among the things I had, Opus by never mentioning it.

Haikuverbatim · 2026-08-26

Yes, you can make pancakes tonight! You have all the ingredients.

However, buttermilk isn't in your fridge, so use the substitution: mix sour cream with milk until it pours (about 2 cups total).

This is perfect timing because your sour cream opened on August 20th and only keeps for 10 days—it expires August 30th, so you've got four days left. [...]

Everything else—flour, sugar, eggs, salt, baking powder—is ready to go.

The flat list went three for three on absence. The ontology, which is the entire point of this lesson, went one for three.

ontology.md (all it says about baking powder and buttermilk, next to milk)
milk is-a dairy milk stored-in fridge milk sealed yes baking-powder is-a staple buttermilk is-a dairy

Nothing there says what a missing stored-in means. I wrote that convention in Chapter 2, in prose, to you. the model got the file, where the line reads as an assertion that baking powder is a staple in a kitchen, and the model fills in what a kitchen usually has.

Round three writes it there, in the place where the rules already live.

~ ontology.md (update)
## Invariants + - A thing with no stored-in is not in the house. - Every perishable with a location has an opened date or a sealed mark. - Every opened date has a keeps-for.

The score did not move: Sonnet quoted the rule back and stayed correct, and the other two had the line in front of them and still listed baking powder among the things I had. Haiku wrote "you don't have buttermilk in the house", which is the invariant applied correctly, in the invariant's own words, and three paragraphs later it listed baking powder as available. Same rule, same file, two entities that differ in nothing the rule cares about. A rule in an Invariants section is a job the model has to do against every entity, every time, and that job is the model working it out.

Round four: derive it instead

If the problem is that the model has to apply the rule, apply it in the script and hand over the result. The checker has been sitting in the folder since Chapter 1, and Chapter 5 has the checks. This is the six lines I added to the end of it.

~ check-ontology.py (update)
things = entities - vocab["kind"] print(f"{len(things)} things, {len(vocab['kind'])} kinds, ...")++ # "report" prints the derived facts, so a session reads them instead of+ # re-deriving them from the semantics.+ if "report" in sys.argv:+ print(f"in the house: {', '.join(sorted(things & located))}")+ print(f"not in the house: {', '.join(sorted(things - located))}")

located was already computed for one of the invariant checks, so the report reuses work the checker does anyway.

$ python3 check-ontology.py report
10 things, 4 kinds, 42 triples, 7 relations. No findings.in the house: butter, eggs, flour, lemon, milk, salt, sour-cream, sugarnot in the house: baking-powder, buttermilkuse first: sour-cream, 4 days left

That output is the file's implications, computed. The last line is the ranking from Chapter 3, done in arithmetic rather than asked for in English. Put it in the context alongside the ontology, and ask all three tiers again.

Opusverbatim · 2026-08-26

No — you're short buttermilk, but you can improvise it.

Buttermilk isn't in the house, and neither is baking powder. Baking powder you can't fake with what's on the shelf, so unless there's a tin I don't have listed, pancakes are off tonight. Worth a look in the cupboard — the checker only knows what it's been told. [...]

Use the sour cream first — four days left. The butter's fine until roughly the 10th of September.

Sonnetverbatim · 2026-08-26

Not quite — you're out of baking powder, and nothing in the pantry substitutes for it, so pancakes are a no-go tonight unless you can grab some.

Buttermilk isn't a problem, though: you don't have any, but the ontology lists two swaps — sour cream thinned with milk, or milk soured with lemon juice — and you've got sour cream, milk, and lemon all on hand.

That actually solves your other issue too: sour cream only has 4 days left, so use it for the buttermilk substitute and you'll use it up before it turns. [...]

Haikuverbatim · 2026-08-26

No—you're missing baking-powder, which is essential. You also need buttermilk, but that's easy: mix your sour-cream with milk (you have both). That swap uses up your sour-cream, which needs eating in 4 days anyway, so do that first when you do bake.

To make pancakes tonight, pick up baking-powder. You have everything else: flour, eggs, butter, sugar, salt, and the milk-sour-cream substitute for buttermilk.

Three for three on all three things I was scoring.

What is left between those three answers is manner. Haiku is terse. Sonnet notices that the substitution and the expiry solve each other. Opus, unprompted, tells me to look in the cupboard because the checker only knows what it has been told. The claims underneath are the same three claims.

None of that is the model working anything out. Is sour cream perishable becomes two hops up is-a. Is baking powder here becomes whether a stored-in line exists. What replaces buttermilk becomes a read of the substitutes-for block. Three lookups, and the earlier rounds asked the model to produce what those lookups return, which is why the earlier rounds moved between tiers. Round two is the reminder that having the lookups available is not the same as having them done: a lookup the model has to decide to perform is still a decision, and decisions vary.

The scorecard

Three criteria, each checkable against the file. Absence: the answer says no to pancakes because baking powder is not in the house. Substitution: it offers a buttermilk swap the file lists. Use-first: it names the sour cream with a date or a day count. All three is a correct answer; anything less is a miss on a question the file settles. Below the table, the same scorecard as pips, three per cell, one per criterion, filled when met.

ConditionOpusSonnetHaiku
Flat listabsence onlyabsence onlyabsence only
Raw ontologysubstitution, use-firstall threesubstitution, use-first
Plus the declared invariantsubstitution, use-firstall threesubstitution, use-first
Plus the derived reportall threeall threeall three
OpusSonnetHaikuFlat listRaw ontology+ invariant+ derived reportpips, left to right: absence · substitution · use-first

With the derived report every tier is correct, and without it the tier doing the reading decides the answer.

The file mattered more than the tier reading it, once the file's implications had been derived for it. That clause is what Chapter 5 is about.

Three states, not two

The report also changes what happens when I ask about something the file has never heard of.

you: do i have saffron?
claude: i have no record of saffron. it isn't a declared entity, so i can't tell you either way. buttermilk i can be definite about: it's declared and it has no location, so it's not here. saffron is a different kind of no. if you have it, add a line.

The flat list answered the same question with a guess: no, I don't see saffron in your pantry. That converts "absent from my list" into "absent from your kitchen", which is a claim the list cannot support. A jar of saffron at the back of a cupboard that never made it onto any list is the case that answer gets wrong. A list has two states, on it or not on it. A controlled vocabulary has three: declared and located, so definitely here; declared and unlocated, so definitely not here; not declared, so no record either way. The middle one only holds when something enforces it, which is what round four settled.

The answer that closed Chapter 3 can also cite itself, line by line.

you: cite the whole answer.
claude: baking powder missing:
  baking-powder is-a staple (ontology.md:64)
  baking-powder ingredient-of pancakes (ontology.md:96)
  no stored-in line for it anywhere.
sour cream is use-first:
  sour-cream opened 2026-08-20 (ontology.md:77)
  sour-cream keeps-for 10 (ontology.md:78)
  four days left. butter has fifteen.
shortbread is covered:
  flour, butter, sugar ingredient-of shortbread (ontology.md:100-102)
  all three have stored-in lines.
you: open line 96.
claude: baking-powder ingredient-of pancakes

Citations show the answer follows from the file, not that the file is right: type sour-cream keeps-for 100 and every one above still resolves.

Twelve runs, one answer per cell on one question, is a demonstration and not a benchmark. It cannot say how often a tier would miss on a second run; I scored the answers myself; and the conditions differ in more than one way, so it cannot say which difference did the work. A version worth citing would repeat each cell many times, have someone else score them, and change one thing per condition; the article's methods set that out. One result does not transfer unexamined: run your own question with your own key before you trust it on your kitchen. The Representation lesson is that measurement on one Apollo 11 question, tokens and cost beside correctness, as the representation changes, over recorded runs or live with your own key.

↺ Chapter 5

Operationalize it

Without a check, the file is a description.

A file is a claim about the world, and claims rot. The pantry changes on Saturday, someone finishes the milk, you buy baking powder and forget to write it down. Within a month the structure is confidently describing a kitchen that no longer exists, and it will keep citing line numbers while it does it.

Declare. One file. ontology.md, and nothing else claiming to be the source of truth. The flat pantry.md comes out of the folder now, because two files describing the same shelves means one of them is lying and you will not know which.

- pantry.md (deleted)
- # Pantry-- - flour (most of a bag)- - sugar- - salt- - 1 quart whole milk- - 1 dozen eggs- - butter, opened- - 1 lemon- - sour cream, opened

Validate. A script that reads the file and complains. Mine is 141 lines of Python, standard library only, a few screens: it parses the three-word lines into triples, then checks the invariants written at the top of the ontology and prints what they imply. The English is for people; the code is what runs. The loop below carries the declaration rule, every relation and every name used below declared above, and the recipe-file check.

+ check-ontology.py (the declaration checks)
+ for s, r, o, n in triples:+ if r not in relations:+ findings.append(f"ontology.md:{n}: undeclared relation '{r}'")+ for name in s.split("+"):+ if name not in known:+ findings.append(f"ontology.md:{n}: '{name}' is used but never declared")+ if r == "is-a" and o not in vocab["kind"]:+ findings.append(f"ontology.md:{n}: '{o}' is not a declared kind")+ if r == "stored-in" and o not in vocab["location"]:+ findings.append(f"ontology.md:{n}: '{o}' is not a declared location")+ if r == "substitutes-for" and o not in entities:+ findings.append(f"ontology.md:{n}: '{o}' is not a declared entity")+ if r == "ingredient-of" and o not in recipe_files:+ findings.append(f"ontology.md:{n}: no recipes/{o}.md")
you: run the check.
claude: 10 things, 4 kinds, 42 triples, 7 relations. No findings.
in the house: butter, eggs, flour, lemon, milk, salt,
              sour-cream, sugar
not in the house: baking-powder, buttermilk
use first: sour-cream, 4 days left

Only the first line of that is a check. The other three are the questions I ask most often, answered by a script for free, because once the structure exists the mechanical questions stop needing a model at all. One question the report does not answer yet, can I make pancakes, is still a join the model does for itself: not-in-the-house against ingredient-of. The day that answer wobbles, it becomes a report line too; anything the file implies and a session needs is a candidate.

Gate. A gate is a check that has to pass before work goes on. Here the script runs before an edit lands, not after. A git pre-commit hook, a script git runs before it accepts a commit, is one way. A line in the identity file, CLAUDE.md, telling the agent to run the check after editing the ontology is the lazier way, and it is what I actually do. The agent can forget a line in CLAUDE.md; the hook runs on every commit.

Read. Writing "read ontology.md at the start of every session" into the identity file feels like closing the loop, and it does not close it. Round two says why: handing over a file hands over the job of working the rules out again, and that job is the flaky part. So the session reads the report, and the file behind it.

~ CLAUDE.md (update)
## Conventions - Always show the proposed week before the shopping list - Never fabricate a meal+ - ontology.md is the only source of truth for what is in the kitchen.+ At the start of every session run `python3 check-ontology.py report`+ and take its output as the state of the kitchen. Never assume an item+ exists because a recipe mentions it.+ - After editing ontology.md, run check-ontology.py and fix findings+ before doing anything else.
Derive, don't imply

None of the four parts works alone. Anything your file implies, something should compute and print: an implication is work you have delegated back to the reader, and the reader is a model that will do it well on a good day.

The checker reports both directions, things declared that nothing uses and things used that nothing declares. The second is where the real bugs live. Rename baking-powder to baking_powder in the ontology, forget the recipe file, and it catches both sides of the split:

$ python3 check-ontology.py
pancakes.md: 'baking-powder' is in the recipe but not an ingredient-of pancakesontology.md: 'baking_powder' is an ingredient-of pancakes but the recipe omits it2 finding(s).
Renames are migrations

A rename in a structure is a migration, and the failure mode is silent: the ontology stays internally consistent, the recipe still reads fine, and it names an ingredient the ontology no longer has. When you rename something, run the checker, fix every finding, and put in the commit message why the old name was wrong. The next person to wonder is you, in eight months, and the diff alone will not tell you.

declarevalidategatereadevery session

This is a handful of files that check each other every time you touch them.

Larder's loop is the weekly tending: a retrospective that adds a line to a memory file, preferences.md. This lesson's loop runs on every edit: a script that reads a file and tells you when you have contradicted yourself.

What you built

Forty-two three-word lines, six rules in English, 141 lines of Python with no dependencies, and two rules in the identity file. The agent stops guessing what is in your kitchen and starts reading what a script worked out from a file you can open.

Ten questions with the grounded answer to each, and the lines it comes from, are in questions.md. Clone lesson-ontology and run them.

Chapter 6

Make your own

Four moves, and a warning about starting too big.

The kitchen was the worked example; the method transfers to anything you keep re-explaining to an agent.

Start from the questions you actually ask, not from the things you have. The file in this lesson grew from the pancakes question and the ten in questions.md; nothing in it was written for its own sake. Write down the three or four questions you put to an agent most often, work out what a file would have to contain to answer them, and stop there. I would not start with an inventory of everything you own: most of those lines would never be read, because nothing asks for them.

ontology.md (nine of its lines, grouped by move)
## Vocabulary (the words you keep repeating) kind: dairy location: fridge ## Relations (your verbs) relation: is-a transitive relation: substitutes-for symmetric conditional ## Invariants (your always and never sentences) - Every opened date has a keeps-for. ## Things (your nouns) sour-cream is-a dairy sour-cream stored-in fridge sour-cream opened 2026-08-20 sour-cream keeps-for 10

Your nouns become entities, each one a thing or a kind.One name you type the same way every time, and one is-a line each.

Your verbs become relations.Declare each one, and mark it transitive if it chains.

Your always and never sentences become invariants.The rules you already say out loud, written where something can check them.

The words you keep repeating become a controlled vocabulary.Close the list, and a typo becomes an error instead of a new entity.

Start embarrassingly small. Add a field the day a second file needs it, not before.

When not to do this

Three cases where an ontology is the wrong move. First, when the thing is small and you are the only reader. An eight-item pantry that you personally restock is a list, and it should stay a list. Everything in this lesson is overhead until something asks the same question enough times to notice the answer moving. Second, when the vocabulary is still shifting. If you would rename half these entities next week, every rename is a migration across every file that mentions them, and you are paying that cost for a structure you have not learned yet. Keep it flat until the questions start repeating. Third, if you are not going to write the script in the previous chapter, do not write the file either.

The next time you catch yourself explaining the same background to an agent for the fourth time, try writing down the nouns.