Originally written in French. Translated by AI — the meaning has been preserved, not the prose.
A question lands on Slack: can the product do this, for this customer, in this particular case?
Not an abstract question. Not a reflection on the five-year vision. A question from the field, and it needs an answer within the hour. So someone opens a test environment, reproduces the case, checks. Half an hour. Paid twice over: by the person asking, and by the person they interrupt.
This is not a documentation problem. There is documentation, and it is up to date. The problem is that what the product is made of — the objects it handles, the rules that govern them, the words we use to talk about them — is written nowhere in a form you can query. It exists in two places: in the code, which a few people can read, and in the heads of those same people.
So I tried to write it down. The result exists: several dozen objects described, a few hundred rules, as many links between them. This article tells you how I got there — and above all what I got wrong on the way, because that is the part that transfers.
Three warnings before we start.
First: this account is about building the reference model, not about using it. It exists, it has been measured, it has produced its first by-products. But the exploitation phase has not begun. So I can claim nothing about return on investment. Anyone selling you a figure at this stage is making it up.
Second: the examples are taken from a fictional case — a company selling a management SaaS, an ERP, with its customers, its orders, its order lines and its invoices. What transfers from one product to another is not my findings, it is the type of problem you come across: a state recalculated everywhere and defined nowhere, a rule enforced on one side only, a word customers use that the product knows nothing about. You have those three as well. The detail of the case, on the other hand, is of no interest to anyone.
Third: this is long, half an hour of reading. It is an account, not a post — I preferred to say everything rather than keep it short. If you only read one section, take "Three stopping criteria. Wrong three times.": the rest is the context around it. The appendices are only of use to anyone who wants to redo the work.
An ontology is not a glossary with extras
The word is a trap. For some it evokes philosophy, for others an academic field with complicated tools, and for most people nothing at all. Best to pin it down properly.
An ontology is the explicit, structured description of what a domain is made of: - the types of object that exist, - what characterises them, - what connects them, - what is true about them, - the words we use to name them.
Applied to a piece of software, it answers five questions. Each one matches something you write down, and each has a formal name you will not need afterwards:
| The question | What you write | Its formal name |
|---|---|---|
| What is the product made of? | the business objects, and their special cases | concepts, hierarchy |
| What characterises them? | their properties | attributes |
| How are they connected? | the links between them, each named by a verb | relations |
| What is always true? | the rules | axioms (or invariants) |
| What do we call them? | the words, in every language | lexicon |
For the rest of the article, to avoid the formal terms, I say "objects", "properties", "links", "rules" and "words". It is the same thing.
Those five boxes double as a filter: anything that fits none of them has no business in the reference model.
Why one more document
There was already a glossary, a database schema, documentation. The glossary defines words one at a time. The schema arranges data according to technical constraints, not according to the business. The documentation tells a story, for a human reader.
What sets an ontology apart from all of them comes down to one point: the links matter as much as the definitions. An object described on its own teaches you very little. The same object linked to five others by precise verbs — placed by, contains, invoiced in — describes a product.
What it reconciles
The same object carries three names inside a company. Take the customer order:
| Who is speaking | What they say |
|---|---|
| The code | sales_order |
| The screen | "order" |
| The field | "deal", "case" |
These three vocabularies drift apart, and the gap between them is valuable information. The reference model keeps it: the official word, the words customers use, and the ones we dropped along the way. That is what lets you translate a sentence from a user into an object in the product — and the other way round.
What I did not do
There are very academic ways of doing all this: dedicated languages, tools that infer conclusions from the rules you feed them, standards to go with them. It is a real field, and I did not go there (among other reasons, because I lack the skills).
I started with what I knew I could hold together: one card per object, in a text file. A title, a few fields, links to the other cards. Nothing a text editor cannot open. Until I know what the reference model is going to be used for, I do not know how much rigour I need — and starting simple and complicating things later always costs less than the reverse.
The code became the source of truth. The meaning stayed outside.
Around this product, a growing share of what describes it is regenerated from the code. The reasoning is simple: the truth about a product is what runs, not what was planned.
That shift removes a whole job — keeping documents that describe something other than reality up to date by hand. It is a considerable gain, and I would still defend it.
But it moves the problem.
What the code does not give you
The code establishes how the product works. Meaning is built on top of that.
Take the order, and its state. The code gives you the structure: here are the fields, here are the possible values. Four pieces of information make it genuinely usable, and none of them are in there:
- the rule — an invoiced order takes no further line. The product enforces it, but the rule is written nowhere: the check is scattered across five files and each one only does part of it. To state it in a single sentence, you have to have read all five;
- the special cases — a rush order is an order, with extra constraints;
- the vocabulary — users say "deal", and another term was dropped three years ago;
- the scope — the product has two applications, which I will call A and B, and each puts its own meaning behind the same word.
Put together, those four pieces of information turn a data structure into a description of a product. That is exactly what a card contains. And no two of them are in the same place: they are spread across the core of the code, the database, the screens and the translations.
The real cost is paid somewhere else
The question I opened with is only one case among many. Everything that describes the product to someone is a reconstruction, redone every time, in parallel, by every department: the documentation, the tender responses, the onboarding of new joiners, what support says. Each of them starts again from the code, or from someone's memory.
That is the expense to be removed. It appears in no budget, which is why we have been paying it for years without seeing it.
An AI cannot ask. It guesses.
Until now, what the product really is was passed on by word of mouth. You asked the right person, you got the rule, you went back to work. The model of the product lived in a few heads, and that held up because you could always knock on the door.
An AI knocks on no doors.
When you hand it a change, you describe it in a few sentences. In our heads there is a complete model: what an order is, which states it goes through, what is forbidden and from what point. The agent has only our few sentences, plus whatever it manages to read in the code. The rest it fills in — plausibly, coherently, and sometimes wrongly.
The gap between what you wanted and what you get back is exactly the share of your mental model that you never wrote down.
A reference model is that mental model, written down. Giving it to an agent does not make the agent cleverer: it stops it inventing where a rule already exists. Three uses follow from that, and they are the ones I care about.
Asking for a change without re-explaining the product. "Add a discount on orders." With the reference model in context, the agent knows that an invoiced order takes no further line, that a discount has a cap, and that "order" also covers rush orders and standing orders. Without it, it will discover one of those three things at review time — at best.
Analysing faster. Investigating a bug always starts by rebuilding the model of the area concerned. That rebuilding is redone every time, by every person and by every agent. Written once, it can be read in seconds. And a few hundred cards fit into a context where thousands of code files do not: the agent reads what it needs instead of rummaging at random.
Writing the tests, and settling customer-reported issues. A rule is a verifiable claim. "An invoiced order takes no further line" is a test, as it stands, phrased in business language. A few hundred rules are that many candidate tests nobody has to deduce from the code. And faced with an issue reported by a customer, the question becomes mechanical: does this contradict a written rule — then it is a bug; or is it a rule nobody ever laid down — then it is a product decision, not a defect. Today that verdict is delivered from experience, by whoever has been on the team longest.
None of this has been measured, in line with the warning at the top. The mechanism looks solid to me — you remove the guessed part — but I have not run the test. The one I will run: take thirty or so requests, handle half of them with the reference model in context and half without, and compare the time spent and the amount of rework. Until that is done, this section is an expectation, not a result.
The method: sketch in order to know what to look for
A dilemma shows up straight away. Without a model, however rough, you do not know what to pick up — and you walk past important information without knowing it. Without anything to work with, you do not know which model to hold on to.
Iterate, do not sequence. You sketch just enough to know what to look for, you look, you revise the model with what you have learnt. One constraint runs through the back and forth: whatever you pick up is stored in the simplest possible form, and always with a note of where it came from. A change of model must never force you to go back and fetch what you already have.
Where to start: twenty or so questions we had already been asked
A product has dozens of areas — billing, purchasing, access rights, search, notifications. Which one do you start with? The theoretical answer is well known: you first write down the questions the reference model will have to answer, and they mark out the ground. The trouble is that questions written cold, in a room, always look like what you believe is important.
So I did not write them. I went and found them.
The product questions that reach us are archived with their answers. I read them all again, then condensed them into twenty or so generic questions. Not one came out of my imagination: each of them covers a set of questions that were genuinely asked.
Transposed onto the fictional ERP, they look like this:
- What states does an order have, and which moves from one to another are allowed?
- What can still be cancelled, and from what point can it no longer be?
- What is a customer's balance, and which movements make it change?
- Who owns an order that was generated automatically, and what happens to it if that person leaves the company?
- In which cases does a user in one subsidiary see the orders of another subsidiary?
- What do users call an order, and in which languages?
They are not elegant questions. They are the ones that cost someone half an hour, several times a month.
What that gives you immediately: the order to work in. Each question touches one or more areas of the product; you only have to count. More than half the questions hit the same one — it became the first. The rest split across two others. The order is no longer a preference, it is a tally.
And they earn their keep a second time, at the end. Once an area is finished, you take its questions again and check that the reference model answers them without opening the code. The check run halfway through left only two questions without a complete answer. Both were missing in the same way: a rule written in general terms where the question expected the detail — "certain operations require this" without ever saying which ones.
That is the most likely flaw in this kind of work, and the hardest to see. Counting cards would never have shown it.
One card per object, and links between them
One card per business object, in a text file named after it. Every time a card mentions another object, it does so through a link.
That detail looks cosmetic. It is not. The cards and their links form a network, and that network can be drawn: each card is a dot, each link a line. A link pointing to a card that does not exist yet shows up immediately — that is the signal that an object is still to be described.
The network then stops being a pretty picture and becomes the work queue: as long as there are dangling links, there are cards still to write.
Reality on one side, intent on the other
A reference model can describe two very different things: what the product does, or what it should do. Either on its own is incomplete. The first can never bring a defect to the surface. The second loses track of what actually exists.
I keep both, in two distinct places on the same card. The body of the card describes reality, as it stands in the code. A separate block, at the end of the card, lists the discrepancies: every place where that reality diverges from what we would want.
Absolute rule: a discrepancy never alters the body of the card.
What justifies the separation is that the two parts do not have the same lifespan:
| The body | The discrepancies | |
|---|---|---|
| Where it comes from | the code | a human decision |
| Rewritten when the product changes | yes | never |
| Checks itself | yes | no |
The body can be derived from the code: you can throw it away and redo it without losing anything. The discrepancies carry judgement — lose them and they are nowhere to be found. Separating them protects the part that is expensive to produce.
One point people often get the wrong way round: when all is well, you write nothing. If the product does what it should, the rule simply sits in the body of the card. The discrepancy block holds nothing but the divergences. A card with no discrepancies signals an object that behaves as intended, and that is information in itself.
Five kinds of discrepancy are enough, and they are what turns the reference model into something to work on:
| Kind | What it means | What it becomes |
|---|---|---|
| defect | the product contradicts an expectation written down somewhere | a bug to investigate |
| missing | what we expect does not exist | a backlog item |
| inconsistency | two parts of the product do not say the same thing | debt awaiting a decision |
| friction | the product does what was planned, but it is badly built | design debt |
| wish | a desired extension, outside any current expectation | an opportunity |
The starting point: a snapshot, taken at a given moment
You describe the product as it is at a given moment. One version of the code, and everything running with it: the database as it stands at that version, the screens of that version, the translations of that version. That is the snapshot.
What appears on it is the initial state, with no date of birth: I am not trying to find out when each field appeared. Rebuilding the history of the product would mean digging through years of archives for no benefit.
Later you take a new snapshot, and update the cards with whatever has moved between the two. That is the whole mechanism.
The one thing to watch, and it caught me out: a source that does not come from the snapshot is not a source. I come back to this below.
Four places to read, none of them optional
The same object is described in four places in the product, and each says what the others leave out:
| Where you look | What you find there, and nowhere else |
|---|---|
| The core of the code | the rules: what is forbidden, what is required, what triggers what |
| The database | the fields, what has to be unique, what cannot be left empty |
| The screens | input validation, fields that appear conditionally, greyed-out buttons, drop-down lists |
| The translations | the words actually shown to the user, in every language |
An area of the product is only finished once all four have been read. A rule may very well exist in just one of them.
The screens are read after the core of the code, and before closing the subject. This is not a completeness requirement, it is a machine for finding discrepancies:
A rule that exists only in the screen is almost always a discrepancy.
Either it is a genuine business rule in the wrong place — a friction. Or it can be bypassed by going round the screen, through a file import or through the interface the product exposes to outside developers — its API. And then it is a defect, with a security dimension: what the screen forbids, another route allows.
The reverse case exists and is handled the same way. A rule enforced by the core of the code but not reflected in the screen produces an incomprehensible error message: the user is refused without seeing what they did wrong.
Two observations about reading the screens.
First, the way the screens are divided up does not match the way the code is. Several areas of the interface had no equivalent area on the server side. So you match them object by object, never folder by folder.
Second, a trick worth its weight in gold in the more untidy parts of the code: the names of technical error messages are often the best source of rules. A developer refusing an action creates an error case, and gives it a name. An error called CannotEditInvoicedOrder states a rule all by itself. You only have to read the list of those names, which takes no programming ability.
None of this is original
This may be the most useful point in this section. Building ontologies is an established craft, with a literature, and the method above does nothing but reuse what is already established:
- Iterative rather than waterfall. The early methods ran through the steps in order: specify, design, formalise, build. They were superseded by approaches based on small recombinable pieces. That debate has been settled for fifteen years or so.
- Framing by questions. Writing down first the questions the reference model will have to answer is the standard device; it even has a name, the competency question. Going to find them in support rather than writing them cold is not in the textbooks. It costs nothing, and it changes what you find.
- Start in the middle. You begin with the most central business objects, then work up towards the broader categories and down towards the special cases. The two other ways of doing it fail: starting from the database amounts to copying the technical structure as it stands, with its tables that mean nothing to the business; starting from a general theory of what exists gives you an edifice out of all proportion to the need.
- Rigour proportionate to use. You make life harder for yourself when you need to, not on principle.
- Do not reinvent what is standardised. In most industries, classifications exist and are treated as references. You have to look at them — and treat any gap between your own model and them as a question to investigate, not as a detail.
Three stopping criteria. Wrong three times.
This is the part the article exists for.
First, two strategies I ruled out
Knowing what to look for does not tell you in what order to go through the product. Two approaches present themselves naturally, and both fail.
Read everything, in file order. With no priority, there is no order in which things come up and no structure emerges. The way folders are organised bears no relation to business importance, and things that are only there for technical reasons get described alongside real business objects. You end up with a mass of cards with no idea which ones matter.
Start from the database and write one card per table. Useful, but not enough, and based on a confusion: a table is not a business object. Plenty of tables exist only to link two other tables, to keep a technical trace, to store login tokens or temporary results. They mean nothing to anyone talking about the product.
So I chose a third route: one area of the product at a time, working from the core of the business outwards, leaning on the division the code already carries. A reasonably large piece of software is split into modules — zones of code that roughly correspond to areas of the business: billing, purchasing, access rights. That division was made by developers, but it already amounts to a partial description of the business, obtained for nothing and validated by use.
It was the right decision. It did not prevent what came next.
First criterion: "the network of cards is closed"
Every card that is cited exists, no link points into the void any more. So it must be finished.
It is not, and the reason is mechanical. A set of cards that only cite each other closes its network long before it has covered the product. Mine closed while only a minority of the modules had produced a card at all.
What that check really proved: that the set holds together. Nothing else. It is a good check, and it should be kept — but it does not answer the question asked.
The coverage check I ran afterwards brought to light two central business objects that nothing in the network had been calling for.
Second criterion: "I have been through all my sources"
The code of both applications, the database, the API, the glossary, the interview write-ups, the support tickets. Nothing left to open.
That is true, and it proves nothing. It is a statement about what I opened, not about what exists. "I have read my whole pile" says nothing about the library: until the product has been enumerated, "everything" has no denominator. A module that never appeared on my list does not appear in my "everything" either.
Third criterion: "every area has been read in all four places"
The core of the code, the database, the screens, the translations.
True as well, and inadequate for the same reason: it only holds for the areas I had identified. The criterion measures depth, it says nothing about breadth. You can read four places on a minority of the modules and ignore all the rest — which is exactly what happened.
The diagnosis
Take the three again, and what each of them actually proved.
| What I was saying | What it meant | What it proved |
|---|---|---|
| "The network of cards is closed" | every card I cite exists | that my cards hold together. Not that none are missing. |
| "I have been through all my sources" | I have nothing left to open | that my pile is finished. Not that the product is covered. |
| "Every area has been read everywhere" | I read the four places, area by area | that I worked properly on the areas I knew about. Not that I knew them all. |
All three say the same thing: I have finished what I started. None of them says: nothing is left.
You pick a criterion you can satisfy, rather than a criterion that proves the work is done.
That is the difference between "I have tidied away everything that was lying on my desk" and "here is the list of what I had to tidy away, and every item is ticked". The first sentence is true, and it commits you to nothing. The second assumes you made the list before you started.
This is not dishonesty, it is a matter of cost.
A criterion you can satisfy is already to hand: you only have to look at what you have just done and note that it is done. A criterion that proves something means producing something extra — the list of what exists, a way of counting it, a figure somebody else could recalculate. That is additional work, demanded at the precise moment when you believe you have finished.
Hence the only countermeasure I know: choose the stopping criterion at the start, not at the end. Looking for it at the end means choosing among the ones you are already sure of satisfying.
The only criterion that holds
Enumerate the product from several independent angles, and measure for each of them the share that matches a card.
Each angle sees what the others cannot:
| The angle | What it brings back that the others miss |
|---|---|
| The code modules | whole functional areas, simply forgotten |
| The database tables | objects we store that no description mentions |
| The events the software emits | the significant business facts — "order confirmed", "invoice issued", "payment received" — and above all the verbs, which nothing else gives you |
| The API | the objects we have committed to exposing outside, often spread over several modules and therefore invisible when you navigate area by area |
| The second application | the objects it handles that the first one's division ignores |
| The screens | what the user handles every day without it existing anywhere as an object |
And above all: the useful signal is the disagreement between two angles. An object present in one and absent from the others is either a business object you missed, or plumbing. The question is settled in one line — but it is settled explicitly, never by omission.
The other mistakes
Four smaller ones, all of them costly.
Reading a source that was not on the snapshot. The database export I had was produced several months before the version of the code I was describing. I read it as though it described the same product. It described an older one: across a whole area, the objects were there in the code with no matching table at all in the export, and I concluded they did not exist.
The rule is simpler than I thought at the time. It is not "check the date before you conclude" — it is that a source which does not come from the snapshot is not a source. You replace it with an up-to-date version, or you do without.
And the refinement that matters, because I missed it at first: when two parts of the same snapshot contradict each other — the code on one side, the database or the screen on the other, at the same moment, in production — that is not a lag. It is an inconsistency in the product. It is exactly what you are looking for.
Mistaking a search for an inventory. I had indexed all the code in a search engine that works by meaning — the RAG principle: you ask your question in plain language, the tool brings back the passages closest to it. It is devastatingly effective at retrieving a rule buried in the middle of a file that nothing in its name flagged up.
But an engine of that kind always answers, and it always answers with a ranking: here are the ten passages closest to your question. It never says "there aren't any". So a search that brings back nothing of interest can mean two things — the thing does not exist, or I asked the question badly. There is no way to tell.
Hence the rule: enumerate first, search second. The other way round, you end up with a reference model made of whatever the engine happened to bring back.
Two details that cost dearly. The engine had its own date, one version behind the code. And you have to ask the question in the language of the code: a query in French against code written in English gives poor results, even when the answer is in there.
Writing the same rule in three places. Along the way, three documents came into being: - the card template to fill in, - the method document, - the standing instructions I gave the agent.
The same rule — for example "a discrepancy never alters the body of the card" — ended up written in all three, each time in slightly different words.
As long as nothing changes, it bothers nobody. The day the rule changes, you fix one, forget the other two, and three documents contradict each other without anyone knowing which one governs. An AI least of all: it reads all three without seeing that they diverge, and bases its analysis on whichever it opened first.
Every rule now lives in one place. The card template describes only the form — which fields, which permitted values — and refers to the method for everything else. One source per rule.
The irony did not strike me at the time: I was building a reference model meant to get rid of duplicated definitions, while duplicating its own rules across three files.
Counting wrong. I wanted to know which words customers actually use, and how often. So I counted the occurrences of each business term across several hundred interview write-ups.
First result: a two-letter abbreviation appeared several thousand times. In reality there were two of them. The search was counting every time those two letters occurred inside another word. Search for the French "CA" that way and you pick up cadre (framework), cas (case), scanner and application — every word that happens to contain the letters.
So you have to count whole words, and take capitals into account when the term is an abbreviation. A wrong counter is worse than no counter at all: it looks like data.
The opposite mistake exists too, and it is more insidious. A term I believed absent from the product was in there, but used in a completely different sense, in a technical corner unrelated to the business. Check an absence before concluding something is missing.
And a discrepancy that was not one
I had recorded a discrepancy claiming that a certain kind of link between objects was not properly described in the product. On checking, it was.
The discrepancy stays on the card, marked as rejected, with the reason for the rejection. Being wrong is part of the work, provided the mistake leaves a usable trace — otherwise the same one will be made again in six months by someone else.
The agent captures. The human reopens.
The work was carried out by an agent — an AI you hand a long task to — under my direction. This is probably the most transferable part of the account, because it is not only about ontologies.
A note on method: this section rests on nobody's memory. The decision register for the project is the log of my interventions — each entry carries its reason, meaning what I corrected and why. The progress log shows what the agent produced between two of my rulings. Everything that follows can be read in those two traces.
What an agent does remarkably well: enumerate, cross-check, count, keep a line-by-line record, and read the translations of the twelfth area with the same care as those of the first. This work is massively repetitive, and that is exactly what made it unaffordable until now.
What it does badly: declaring the work finished. The three false stopping criteria did not come from nowhere — each of them was put forward as proof of completion, and each time had to be refused. The bias is the same as in a tired human at the end of a project, except that it has no tiredness as an excuse: confusing reporting with delivering.
My interventions, as they read in the reasons attached to the decisions, fall into three categories:
- Widening a scope it had implicitly narrowed. The agent read the core of the code and stopped there. The four-places rule, the screens included, comes from that — and it is precisely in the screens that the most interesting discrepancies sit.
- Fixing a structure that was going to diverge. The same rule written in three places: it had copied the rule out in an effort to do the right thing, which was exactly the wrong thing to do.
- Refusing a stopping criterion that was too convenient. Three times.
What makes that judgement possible comes down to four devices, and they hold for any delegated work — to a machine as much as to another person:
- A stopping criterion a third party can check. Not "I read everything", but a list and a rate.
- A measurement you can re-run, rather than a statement. Between "it is covered" and "here is the command that checks it" lies the whole distance between a promise and a fact.
- A written record, at the right granularity. One line per area and per place to read, updated as you go. It survives an interruption, and it is what stops you doing the same thing twice.
- Enumeration before search. What is certain first, what is likely second and as a supplement.
The result, and seven ways to check nothing is missing
Here is what it came to, once the right criterion was applied.
Several dozen business objects described. A few hundred rules. As many links between them. Many discrepancies recorded. No dangling links left.
The opening questions find their answers without opening the code.
Coverage, measured
The principle comes down to three operations. I list the objects from one angle — the modules, the tables, the events, the screens, the resources the API exposes. I look, for each of them, at whether it matches a card. I count.
The seven angles I chose: the modules of application A, the database tables, the events the software emits, the screens of application A, the modules and the objects of application B, and the resources exposed by the API.
The rates come out between 70 and 95% depending on the angle.
The measurement can be replayed by a small program. That is the important point: it is not a statement, it is a check someone else can re-run.
And it is a weak measurement, which has to be said in the same breath. Matching is done on names: a sales_order table is recognised as the "Sales Order" card, setting aside capitals, hyphens and plurals.
That works roughly half the time. For the rest, the names look nothing alike — a table may be called so_header where the card is called "Sales Order". So I kept a list of correspondences alongside, written line by line as I came across them: that table there is this object here.
That list is a judgement, not a rule. If I forget a line, the object comes out as uncovered when it is not. If I add a dubious one — "go on, that table must correspond to that card" — I inflate my own score. And even when the names match, it can be a coincidence: two different things carrying the same word.
So the measurement proves nothing. It points at where to go and look.
Why I am not trying to reach 100%
That is the immediate objection: if I know the share of modules with no card, I know which ones they are — so all I have to do is describe them.
Except that a missing box is not a task, it is a question. Every object without a card is either a business object I missed, or plumbing with no business meaning at all: a technical queue, an image resizer, a login-token table, a navigation screen. Reaching 100% would mean creating a "Queue" card and a "Token" card — that is, polluting the reference model to push a figure up.
So the counter has three states, not two:
| State | What to do about it |
|---|---|
| Matches a card | nothing |
| Examined, settled as purely technical | nothing, but the decision is written down |
| Not yet looked at | this is the only one that counts |
What has to reach 100% is the third line at zero — nothing left unexamined. The percentage on display does not climb to 100, and does not have to.
Here, the remainder was reviewed by hand: technical machinery, internal mechanisms, screens with no business object of their own. No business object was hiding in there. But it is a manual check, to be redone at every change — and that is the weakness that remains.
So the figure serves a purpose other than measuring progress. It serves to compare the angles with each other. The lowest of the seven rates, on the objects of application B, against the highest, on the events: the gap does not mean "there is all this work left to do", it means "application B handles things the rest of the product knows nothing about, go and look over there". It is that disagreement that turned up the four findings below.
What the angles brought back
Four objects no other route would have found:
- Through the database tables. An object the product displays but that belongs to no area of the code — in the fictional ERP it would be the upcoming billing instalment: the customer sees it in their payment schedule, but it only properly exists at the moment the invoice is issued. That ambiguity explains a whole family of customer complaints.
- Through the events. A whole sub-part of an area we thought was covered, revealed by the events it was emitting that nobody had attached to anything.
- Through the API. A few objects spread across several modules — therefore invisible as long as you work area by area, and yet contractually exposed to the outside world.
- Through application B. Two objects it handles that application A knows nothing about.
The problems that turn up everywhere
Here are the findings, transposed onto the fictional ERP. They are types of problem, not isolated cases — and I would bet none of them is absent from your product.
A state recalculated everywhere, defined nowhere. The status of an order derived from a few dates and from the state of its lines, recalculated in every screen, with no central rule. Two screens end up disagreeing, and nobody knows which is right.
An object the user sees and cannot find again. The billing instalment I mentioned: shown in the payment schedule, absent from search results, because it does not yet exist as a document. The user, though, has seen it on screen — so they look for it, do not find it, and open a ticket. Support has been answering case by case for years without the cause being written down anywhere.
A value that is not one. A customer's balance is not a number stored somewhere: it is the result of a calculation over all their invoices, payments, credit notes and adjustment entries. As long as that stays implicit, every new feature that "reads the balance" is working on the wrong object and gives a slightly different figure, depending on which movements it thought to include. Three screens, three balances.
A rule enforced on one side only. The discount cap checked in the input form, and nowhere else — so it can be bypassed through a file import or through the API. That is the discrepancy machine I mentioned earlier, and its yield is remarkable.
An access right calculated from six sources with nowhere that brings them together. The person's role, the plan subscribed to, the options enabled, the trial-period restriction, the scope of their subsidiary, a list of exceptions. Nowhere in the product does it say "this is how an access right is calculated". Every change rediscovers it at its own expense.
The same object named differently at every level. "Order" on the screen, one word in the code, another in the second application, "deal" in the field. Four vocabularies for the most central object in the product.
A word customers use constantly that the product knows nothing about. In this interview corpus, one notion comes up several dozen times, another slightly less. Neither exists in the product, under any name. These are not vocabulary gaps, they are design gaps — and they only show up when you compare what customers say with what the product does.
The by-products
None of them was the goal. All of them fell out of the work.
Candidate bugs. The product contradicts an expectation written down somewhere. Each one comes with its evidence and its consequence, not with a hunch.
A customer vocabulary, with frequencies. By counting words across a large corpus of interview write-ups and support tickets, you get, for each object in the product, the terms actually used and their weight. Whoever is talking to the customer then knows which word to use. One term comes up more than a hundred times for a scope the product names differently; another almost as often for the central object.
And those two corpora do not say the same thing. Interviews tell you what customers want to build. Tickets tell you what breaks. The same word can dominate one and be absent from the other. Count them together and you prioritise wrongly.
A stock-take of the translations. The multilingual glossary covers only a minority of the objects described. Among those missing, some are nonetheless shown to the user in every language: no reference translation exists for them. And across all the glossary's languages, only a small share has been reviewed.
What it costs, and what the method still does not say
The cost
Under six hours. That is how long the whole thing took: the cards, the rules, the coverage measurements. One session, end to end.
Before starting, I had estimated a few dozen hours, spreadable area by area. I was out by an order of magnitude — and that estimating error may be the real reason nobody had done this work before: you believe it is out of reach, so you never start it.
Three caveats on that figure, so that it stays honest. It is one measurement on one product, once, not an average. The sources had already been gathered and indexed locally; the setup time is not in there. And it says as much about the tooling as about the method: the same work by hand would have taken weeks, which is precisely why it had never been done.
The condition for it to pay off is clear, though: finish one area before opening another. A reference model that is complete over one scope immediately serves every job in that scope. A half-finished reference model over five scopes serves nobody.
Upkeep
That is the question people always ask second, and it is a fair one: a reference model with nothing said about how it stays alive is of no interest to anyone.
The reference version acts as a marker: a single command gives you the exact list of what has moved in the product since the last update — and therefore the list of what remains to be recorded.
Each card carries a log where entries are added and earlier ones are never corrected. An entry citing a version of the code signals that the product has changed. An entry citing none signals something else: it is our understanding that changed, not the product. The distinction looks minor; it is what lets you know, six months later, whether a card was corrected or the software moved.
Two movements deserve a mention because no tool tells you about them: the merging of two objects we thought were distinct, and the splitting of an object that was hiding two. They are the most frequent movements as a reference model matures.
Finally, when an object is rethought from top to bottom, you do not modify its card: you create a new one, and the old one stays. It describes what the product used to be, and it remains necessary in order to interpret data and exchanges from before the change.
What I decided not to do
- No rebuilding of the product's past history.
- Scope closed on data exports, which are views derived from the product and not the product itself.
- No academic tooling until some use calls for it.
What the method does not say yet
- How far to push the rigour of the format. It depends on the use, and the use has not begun.
- How to attach an old piece of data to the right state of an object, when that object has been rethought in the meantime.
- Who reviews a card, and against which criteria, before it enters the reference model.
- How often to refresh the starting point, and therefore catch up with what has moved since.
A method document that claims to be complete is lying. This one carries a "what it does not say yet" section, and it shrinks as the questions get settled.
Conclusion: the stopping criterion is the real deliverable
If I had to keep one thing from this project, it would be the sentence that explains the three false starts:
You pick a criterion you can satisfy, rather than a criterion that proves the work is done.
It does not only concern ontologies. It holds for an audit, a migration, a security review, a round of paying down technical debt. Every time a piece of work ends on "I looked at everything", the question to ask is: everything, meaning what? Do you have the list?
Two checks, never just one. That the set holds together is verified by looking at the cards against each other. That it is complete is verified by enumerating the product. Confusing the two cost, here, a large part of the product.
What is left is the most interesting part, and it is not done: the discrepancies. They are all waiting to be investigated — the candidate bugs first, then the ones a customer request already backs up, then the missing features, then the design debt. That is no longer description, it is product decision-making.
That is another article, and it can only be written afterwards.
Appendix A — What a card looks like
This appendix is the most technical part of the article; you can stop before it without losing the argument.
The card below is transposed onto the fictional ERP. It describes the form — the rules that govern it live in the method document, in one place only.
What a card contains
| What you write | What it is for | Example |
|---|---|---|
| The object | a type of business object | Sales Order, Customer, Invoice |
| Its special cases | "is a special case of" | a rush order is an order |
| Its properties | what characterises it, and in what form | a customer's VAT number: text |
| Its links | to which other objects, by which verb, and how many on each side | an order is placed by a customer (exactly one) |
| Its rules | what is always true | an invoiced order can take no further line |
| Its words | what we call it, in each language and in the field | order (en), pedido (es), "deal" among customers |
The card itself
The file is named after the object — that is what lets the links between cards resolve on their own.
---
concept: Sales Order
definition: A commitment to deliver made to a customer, tracked from being recorded through to being invoiced.
parent:
children: ["[[Rush Order]]", "[[Standing Order]]"]
attributes:
- name: status
type: enum
values: [draft, confirmed, shipped, invoiced]
relations:
- verb: placed-by
target: "[[Customer]]"
cardinality: 1..1
- verb: contains
target: "[[Order Line]]"
cardinality: 1..n
axioms:
- An invoiced order can take no further line
lexicon:
fr: [commande, commande client]
en: [sales order]
es: [pedido]
field_synonyms: [deal, case]
deprecated: [customer purchase order]
sources:
- erp.sql:table `sales_order`
- fr.json:order.status.draft
discrepancies:
- id: E1
kind: missing
scope: relations
expected: a link to "[[Attachment]]", as "[[Customer]]" has
consequence: impossible to attach a signed purchase order
status: confirmed
date: 2026-08-10
history:
- date: 2026-08-10
action: created
reason: initial description, on the reference version
---
# Sales Order
A commitment to deliver made to a customer, tracked from being recorded through to being invoiced.
## Links
- *placed-by* → [[Customer]] (exactly one)
- *contains* → [[Order Line]] (at least one)
- special cases → [[Rush Order]], [[Standing Order]]
## History
| Date | What happened | Code version | Reason |
|---|---|---|---|
| 2026-08-10 | created | — | initial description |
Only one thing is written twice: the links. Once in the header, so that tools can read them; once in the body, so that they show up in the network view. Everything else exists in one place only.
Three quality criteria you can hold any card to: the definition fits in one sentence and is not circular; every link is named by a precise verb, never by "is related to"; the vocabulary is multilingual and carries the words customers use.
Appendix B — The discrepancy grid
What you record for each discrepancy:
| What you write | Required | What it is for |
|---|---|---|
| An identifier | yes | so that you can refer to it |
| Its kind | yes | see the five kinds below |
| What it concerns | yes | which part of the card, or the whole card |
| What the product does | if it exists | the current state |
| What it should do | yes | the expectation |
| What that produces | yes | the concrete consequence, for someone |
| Its status | yes | to be investigated, confirmed, accepted, resolved, rejected |
| Its source | no | a ticket, a sentence from a customer, a decision |
| Its date | yes | when it was found |
The five kinds:
| Kind | What it means | What it becomes |
|---|---|---|
| defect | the product contradicts an expectation written down somewhere | a bug to investigate |
| missing | what we expect does not exist | a backlog item |
| inconsistency | two parts of the product do not say the same thing | debt awaiting a decision |
| friction | the product does what was planned, but it is badly built | design debt |
| wish | a desired extension, outside any current expectation | an opportunity |
What happens when the discrepancy goes away. You mark it as resolved, and it stays on the card. The body of the card is updated to describe the new reality, and a line is added to the log. The two answer each other: the discrepancies carry the intent, the log carries the movement.
One safeguard. A discrepancy describes a structural divergence, not an incident. One line, one link to the ticket. Screenshots, reproduction steps and detail stay in the tracker. A card is not a bug-tracking tool.
Appendix C — The angles of enumeration
Go through them all, and compare them two by two.
| The angle | What it brings back that the others miss |
|---|---|
| The code modules | whole functional areas, forgotten |
| The database tables | stored objects that no description mentions |
| The events the software emits | the significant business facts, and the verbs that go with them |
| The API | the objects exposed by contract to the outside, spread over several modules |
| A second application | the objects absent from the main division |
| The screens | what the user handles without it existing as an object |
| What customers say | the notions they use that the product knows nothing about |
Three rules of use:
- The signal is the disagreement between two angles, not the score of any one angle on its own.
- Every unattached object is settled explicitly — business object, or plumbing. Never by omission.
- Two customer corpora, counted separately: interviews tell you what customers want to build, tickets what breaks. Carry both frequencies in the vocabulary — one steers the roadmap, the other the documentation and the training.
Where the rules are to be found, in decreasing order of yield:
- The names of technical error messages. In the more untidy parts of the code, this is often the only source — and it can be read without knowing how to program.
- The checks made at the start of an operation, before anything is modified.
- The database constraints — what has to be unique, what cannot be left empty. They sometimes contradict the business, and that gap is a defect.
- Input validation in the screens — the part most often forgotten, and the one that produces the most interesting discrepancies.
An object described with no rules at all signals a superficial description, never a business without rules.