🇫🇷🇺🇸🇧🇷🇪🇸🇩🇪🇮🇹

I wrote a product's ontology. Three times, I thought I was done.

Your team rebuilds the same product model for every question, every bug, every RFP. Here is how to write it once: 52 objects, 250 rules, under six hours. And above all, how to know you're finished — the three stopping criteria that look like proof of completeness and prove nothing, plus the only one that holds.


Info

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 specific case?

Not an abstract question. Not a reflection on the five-year vision. A question from the field, one that has to be answered within the hour. So someone opens a test environment, reproduces the case, checks. Half an hour. Paid for twice: by the person searching, and by the person they interrupted.

This is not a documentation problem. Documentation exists, and it's 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 know how to read, and in those same people's heads.

So I tried to write it down. The result exists: 52 objects described, 250 rules, 208 links between them. This article tells how I got there — and above all what I got wrong along the way, because that's the part that transfers.

Three warnings before we start.

The first: this report is about building the reference model, not about using it. It exists, it's measured, it has produced its first by-products. But the exploitation phase hasn't begun. So I can't claim anything about return on investment. Anyone selling you a number at this stage is making it up.

The second: the examples come 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 isn't my findings, it's the type of problem you find: a state recomputed everywhere and defined nowhere, a rule enforced on one side only, a word customers use that the product ignores. You have those three too. The specifics of the case, on the other hand, are of no interest to anyone.

The third: it's long, about half an hour of reading. It's a report, not a post — I chose to say everything rather than to be short. If you only read one section, read "Three stopping criteria. Wrong three times.": the rest is the context around it. The appendices only matter to those who want to do the work themselves.


An ontology is not an enriched glossary

The word is a trap. It suggests philosophy to some, a scholarly field with complicated tools to others, and nothing at all to most people. Best to define it properly.

An ontology is the explicit, structured description of what a domain is made of: - the types of objects that exist, - what characterises them, - what connects them, - what is true about them, - the words we use to name them.

Applied to software, it answers five questions. Each one corresponds to something you write down, and each has a scholarly name you won't need afterwards:

The question What you write Its scholarly 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 this article, to avoid the scholarly terms, I'll say "objects", "properties", "links", "rules" and "words". It's the same thing.

Those five boxes also work as a filter: anything that fits in none of them has no business being in the reference model.

Why one more document

A glossary already existed, along with a database schema and documentation. The glossary defines words one at a time. The schema organises data according to technical constraints, not business ones. The documentation tells a story, for a human reader.

What sets them apart from an ontology comes down to one point: the links matter as much as the definitions. An object described on its own teaches you very little. That same object connected 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's speaking What they say
The code sales_order
The screen "order"
The field "deal", "case"

These three vocabularies evolve separately, and the gap between them is valuable information. The reference model preserves it: the official word, the words customers use, and the ones we dropped along the way. That's what lets you translate a user's sentence into a product object — and the other way round.

What I didn't do

There are very scholarly ways to do all this: dedicated languages, tools that derive conclusions from the rules you feed them, standards that go with them. It's a real field, and I didn't go there (partly for lack of the necessary skills).

I started with what I knew I could handle: one card per object, in a text file. A title, a few fields, links to the other cards. Nothing a text editor can't open. As long as I don't know what the reference model will be used for, I don't know how much rigour I need — and starting simple and getting more complex later always costs less than the reverse.


Code became the source of truth. Meaning stayed outside.

Around this product, the documentation, the changelog, the support documentation, the answers to product questions and the specifications are all regenerated from the code. The reasoning is simple: the truth of a product is what runs, not what was planned.

That shift removes an entire body of work — keeping documents that describe something other than reality up to date, by hand. It's a considerable gain, and I'd still defend it.

But it moves the problem.

What code doesn't give you

Code establishes how the product works. Meaning is built on top of it.

Take the order, and its status. 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 accepts no further lines. 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 includes two applications, which I'll call A and B, and each one puts its own meaning behind the same word.

Put together, these four pieces of information turn a data structure into a product description. That is exactly the content of a card. And none of them are in the same place: they're spread across the core code, the database, the screens and the translations.

The real cost is paid elsewhere

The question we started 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: documentation, RFP answers, onboarding new joiners, the support team's talking points. Each one starts over from the code, or from somebody's memory.

That's the expense to be eliminated. It appears in no budget, which explains why we've been paying it for years without seeing it.

An AI can't 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 product's model lived in a few heads, and it held up because you could always knock on a door.

An AI knocks on no doors.

When you hand it a change, you describe it in a few sentences. In our heads there's a complete model: what an order is, which states it goes through, what's forbidden and from what point on. The agent only has 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 we wanted and what we get back is exactly the part of our mental model we didn't write down.

A reference model is that mental model, written down. Giving it to an agent doesn't make the agent smarter: it stops it from inventing where a rule already exists. Three uses follow from that, and they're the ones I care about.

Requesting 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 accepts no further lines, 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 — in the best case.

Analysing faster. Investigating a bug always starts by rebuilding the model of the area concerned. That reconstruction is redone every time, by every person and by every agent. Written once, it reads in a few seconds. And a few hundred cards fit in a context window where thousands of code files don't: the agent reads what it needs instead of digging at random.

Writing tests, and settling reported issues. A rule is a verifiable statement. "An invoiced order accepts no further lines" is a test, as it stands, expressed in business language. 250 rules are 250 candidate tests nobody has to derive from the code. And faced with a customer report, the question becomes mechanical: does this contradict a written rule — then it's a bug; or is it a rule nobody had laid down — then it's a product decision, not a defect. Today, that verdict is delivered from experience, by the longest-serving person on the team.

None of this is measured, in line with the warning at the start. The mechanism looks solid to me — you remove the guessed part — but I haven't run the test. The one I will run: take about thirty requests, handle half of them with the reference model in context and half without, then compare time spent and amount of rework. Until that's done, this section is an expectation, not a result.


The method: sketch enough to know what to look for

A dilemma shows up immediately. Without a model, even a crude one, you don't know what material to gather — and you walk past important information without knowing it. Without material, you don't know which model to hold.

Iterate, don't sequence. You sketch just enough to know what to look for, you look, you revise the model with what you learned. One constraint runs through the round trips: whatever you gather 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 questions we'd already been asked

A product has dozens of areas — invoicing, purchasing, access rights, search, notifications. Which one do you start with? The theoretical answer is well known: you first write the questions the reference model will have to answer, and they delimit the ground. The problem is that questions written cold, in a meeting room, always look like what we think matters.

So I didn't write them. I went and found them.

For months, every product question that comes in — via Slack, in a meeting, from a salesperson preparing an answer, from support facing a ticket — has been archived with its answer. I re-read them all, then condensed them into twenty generic questions. None are invented; each one covers between five and twenty real questions.

Transposed onto the fictional ERP, they look like this:

  • What are the states of an order, and which transitions between them are allowed?
  • What can still be cancelled, and from what moment on can it no longer be?
  • What is a customer's balance, and which movements change it?
  • Who owns an automatically generated order, and what becomes of it if that person leaves the company?
  • In which cases does a user in one subsidiary see another subsidiary's orders?
  • What do users call the order, and in which languages?

These are not elegant questions. They're the ones that cost someone half an hour, several times a month.

What it gives you immediately: the work order. Each question touches one or more areas of the product; you just have to count. Eleven questions out of twenty landed in the same area — it became the first one. Five in a second, four in a third. The order is no longer a preference, it's a tally.

And they serve a second time, on arrival. Once an area is finished, you go back to its questions and check that the reference model answers them without opening the code. The check run at the halfway point gave 18 complete answers out of 20. The two missing ones failed 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's the most likely flaw in this kind of work, and the hardest to see. Counting cards would never have shown it.

One card per business object, in a text file bearing its name. Every time a card mentions another object, it does so through a link.

That detail looks cosmetic. It isn't. The cards and their links form a network, and that network can be visualised: each card is a node, each link an edge. A link pointing to a card that doesn't exist yet is immediately visible — that's 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 links pointing into the void, there are cards 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 one alone is incomplete. The first can never surface a defect. 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 is in the code. A separate block, at the end of the card, lists the gaps: every place where that reality diverges from what we'd want.

Absolute rule: a gap never modifies the body of the card.

What justifies the separation is that the two parts don't have the same lifespan:

The body The gaps
Where it comes from the code a human decision
Rewritten when the product changes yes never
Verifies itself yes no

The body is derived from the code: you can throw it away and redo it without losing anything. The gaps carry judgement — they can't be found anywhere else if you lose them. Separating them means protecting the part that's expensive to produce.

One point often misunderstood: when everything is fine, you write nothing. If the product does what it should, the rule simply sits in the body of the card. The gap block only contains divergences. A card with no gaps signals a compliant object, and that's information in itself.

Five kinds of gap are enough, and they're what turns the reference model into working material:

Kind What it means What it becomes
defect the product contradicts an expectation written down somewhere a bug to investigate
missing what we expect doesn't exist a backlog item
inconsistency two parts of the product don't say the same thing debt to arbitrate
friction the product does what was planned, but it's badly done 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 that runs with it: the database as it is at that version, the screens of that version, the translations of that version. That's the snapshot.

What appears in it is the initial state, with no birth date: I'm not trying to find out when each field appeared. Reconstructing the product's history would mean digging through years of archives for zero benefit.

Later, you take a new snapshot, and update the cards with what moved between the two. That's the whole mechanism.

The one thing to watch out for, and I got caught by it: a source that isn't part of the snapshot isn't a source. More on that below.

Four places to read, none optional

The same object is described in four places in the product, and each one says what the others leave out:

Where you look What you find there, and nowhere else
The core code the rules: what's forbidden, what's mandatory, what triggers what
The database the fields, what has to be unique, what can't be left empty
The screens input validation, fields that appear conditionally, greyed-out buttons, dropdown 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 only one of them.

Screens are read after the core code, and before closing the subject. This isn't a completeness requirement, it's a gap-finding machine:

A rule present only in the screen is almost always a gap.

Either it's a genuine business rule in the wrong place — friction. Or it can be bypassed by going around the screen, through a file import or through the interface the product exposes to outside developers — its API. And then it's a defect, with a security dimension: what the screen forbids, another path allows.

The reverse case exists and is handled the same way. A rule enforced by the core code but not reflected in the screen produces an incomprehensible error message: the user's action is refused without them seeing what they did wrong.

Two observations about reading screens.

First, the way screens are divided up doesn't match the way the code is. Twelve areas of the interface had no equivalent area on the server side. So the matching is done object by object, never folder by folder.

Second, a trick worth its weight in gold in the least tidy 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 names it. An error called CannotEditInvoicedOrder states a rule all by itself. You just have to read the list of those names, which doesn't require knowing how to program.

None of this is original

This may be the most useful point in this section. Building ontologies is an established discipline, with its own literature, and the method above simply reuses established results:

  • Iterative rather than waterfall. The historical methods chained the steps in order: specify, design, formalise, build. They were superseded by approaches based on small recombinable pieces. That debate has been settled for about fifteen years.
  • Framing by questions. Writing down the questions the reference model will have to answer, first, is the standard device; it even has a name, competency question. Going and finding them in support rather than writing them cold isn't in the textbooks. It costs nothing, and it changes what you find.
  • Start from the middle. You begin with the most central business objects, then move up towards broader categories and down towards special cases. The other two approaches fail: starting from the database amounts to copying the technical structure as it is, with its tables that mean nothing to the business; starting from a general theory of what exists produces an edifice out of all proportion to the need.
  • Rigour proportionate to use. You make life harder for yourself when you need to, not as a matter of principle.
  • Don't reinvent what's already standardised. In most industries, reference classifications exist. You have to look at them — and treat any divergence 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 this article exists for.

First, two strategies ruled out

Knowing what to look for doesn't tell you in what order to walk the product. Two approaches suggest themselves naturally, and both fail.

Read everything, in file order. With no priority, there's no order of arrival and no structure emerging. The way folders are organised has nothing to do with business importance, and things that exist purely for technical reasons get described at the same level as genuine business objects. You end up with a mass of cards without knowing which ones matter.

Start from the database and make one card per table. Useful, but insufficient, and based on a confusion: a table is not a business object. Many tables exist only to connect 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 went with 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. Any reasonably large piece of software is split into modules — zones of code that roughly correspond to areas of the business: invoicing, purchasing, access rights. That division was made by developers, but it already constitutes a partial description of the business, free and validated by use.

That was the right decision. It didn't prevent what came next.

First criterion: "the card network is closed"

Every card cited exists, no link points into the void any more. So it's finished.

That's wrong, and the reason is mechanical. A set of cards that only cite each other closes its network long before it has covered the product. In my case the network closed when only twelve modules out of thirty-two had produced a card.

What that check actually proved: that the set holds together. Nothing else. It's a good check, worth keeping — but it doesn't answer the question asked.

The coverage check run afterwards surfaced two central business objects that nothing in the network was calling for.

Second criterion: "I've been through all my sources"

The code of both applications, the database, the API, the glossary, the interview notes, the support tickets. Nothing left to open.

That's true, and it proves nothing. It's a statement about what I opened, not about what exists. "I've read my whole stack" says nothing about the library: until the product is enumerated, "everything" has no denominator. A module that never appeared on my list doesn't appear in my "everything" either.

Third criterion: "every area has been read in all four places"

The core code, the database, the screens, the translations.

Also true, and insufficient 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 across twelve modules and ignore twenty others — which is exactly what happened.

The diagnosis

Let's take all three, and what each one really proved.

What I was saying What it meant What it proved
"The card network is closed" every card I cite exists that my cards hold together. Not that none are missing.
"I've been through all my sources" I have nothing left to open that my stack is finished. Not that the product is covered.
"Every area has been read everywhere" I read all four places, area by area that I worked properly on the areas I knew about. Not that I knew about all of them.

All three say the same thing: I've finished what I started. None of them says: nothing is left.

We pick a criterion we can satisfy, rather than a criterion that proves the work was done.

It's the difference between "I've tidied everything that was lying on my desk" and "here's the list of what I had to tidy, and it's all ticked off". The first sentence is true, and it commits to nothing. The second requires having made the list before starting.

This isn't dishonesty, it's a matter of cost.

A criterion you can satisfy is already at hand: you just look at what you've just done and note that it's done. A criterion that proves something requires manufacturing something extra — the list of what exists, a way of counting it, a number someone else could recompute. That's additional work, demanded at the precise moment when you think you've finished.

Hence the only countermeasure I know of: choose the stopping criterion at the start, not at the end. Looking for it at the end means picking it from among the ones you're already sure of satisfying.

The only criterion that holds

Enumerate the product from several independent angles, and measure, for each one, the share that corresponds to a card.

Each angle sees what the others can't:

The angle What it brings back that the others don't see
The code modules entire functional areas, simply forgotten
The database tables objects we store without any description mentioning them
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've committed to exposing externally, often scattered across several modules and therefore invisible when navigating area by area
The second application the objects it handles that the first application'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 we missed, or plumbing. The question is settled in one line — but it is settled explicitly, never by walking past it.

The other mistakes

Four smaller ones, all costly.

Reading a source that wasn't in the snapshot. The database export I had was produced five months before the code version I was describing. I read it as if it described the same product. It described an older one: across a whole area, the objects were there in the code with no corresponding table in the export, and I concluded they didn't exist.

The rule is simpler than I thought at the time. It isn't "check the date before concluding" — it's that a source that isn't part of the snapshot isn't a source. You replace it with an up-to-date version, or you do without it.

And the precision 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 isn't a lag. It's a product inconsistency. It's exactly what you're looking for.

Mistaking a search for an inventory. I had indexed all the code in a meaning-based search engine — the RAG principle: you ask your question in plain language, the tool brings back the passages closest to it. It's devastatingly effective for finding a rule buried in the middle of a file, with nothing in its name to flag it.

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 interesting can mean two things — the thing doesn't exist, or I asked badly. Impossible to tell which.

Hence the rule: enumerate first, search second. The other way round, you get a reference model made of whatever the engine chose to bring back.

Two details that cost a lot. 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 mediocre results, even when the answer is in there.

Writing the same rule in three places. Along the way, three documents took shape: - the card template to fill in, - the method document, - the working instructions I gave the agent.

One and the same rule — for instance "a gap never modifies the body of the card" — ended up written in all three, each time with slightly different words.

As long as nothing changes, it bothers nobody. The day the rule evolves, you fix one, forget the other two, and three documents contradict each other with nobody knowing which one is authoritative. An AI less than anyone: it reads all three without seeing that they diverge, and bases its analysis on whichever it opened first.

Each rule now lives in a single place. The card template describes only the form — which fields, which allowed values — and refers to the method for everything else. One source per rule.

The irony didn't strike me at the time: I was building a reference model meant to eliminate 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 261 sets of interview notes.

First result: a two-letter acronym appeared 3,525 times. In reality there were 2. The search was counting every time those two letters appeared inside another word. Search for "CA" that way and you harvest "cadre", "cas", "scanner", "application".

So you have to count whole words, and account for capitalisation when the term is an acronym. A wrong counter is worse than no counter at all: it looks like data.

The reverse mistake exists too, and it's sneakier. A term I thought was absent from the product was in fact there, but used in a completely different sense, in a technical corner unrelated to the business. Verify an absence before concluding that something is missing.

And one gap that wasn't one

I had recorded a gap claiming that a certain kind of link between objects wasn't properly described in the product. On checking, it was.

The gap stays in the card, marked as rejected, with the reason for 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 this report, because it isn't only about ontologies.

A note on method: this section rests on nobody's recollection. The project's decision log is the record of my interventions — each entry carries its reason, that is, what I corrected and why. The progress log shows what the agent produced between two arbitrations. Everything that follows can be read in those two traces.

What an agent does remarkably well: enumerate, cross-reference, count, keep a line-by-line record, and read the translations of the twelfth area as diligently as those of the first. That work is massively repetitive, and that's exactly what made it unaffordable until now.

What it does badly: declaring the work finished. The three false stopping criteria didn't come out of nowhere — each was proposed as proof of completion, and each time it had to be refused. The bias is the same as in a tired human at the end of a project, except it doesn't have tiredness as an excuse: confusing reporting with delivering.

My interventions, as they read in the reasons recorded against decisions, fall into three categories:

  • Widening a scope it had implicitly narrowed. The agent was reading the core code and stopping there. The four-places rule, screens included, comes from that — and it's precisely in the screens that the most interesting gaps are found.
  • Fixing a structure that was going to diverge. The same rule written in three places: it had copied it out of a desire to do things properly, which was exactly what it shouldn't have done.
  • Refusing a stopping criterion that was too convenient. Three times.

What makes that arbitration possible comes down to four devices, and they hold for any delegated work — to a machine as much as to another person:

  1. A stopping criterion a third party can verify. Not "I've read everything", but a list and a rate.
  2. A measurement you can re-run, rather than an assertion. Between "it's covered" and "here's the command that checks it" lies the whole distance between a promise and a fact.
  3. A written record, at the right granularity. One line per area and per place to read, updated as you go. It survives an interruption to the work, and it's what stops you doing the same thing twice.
  4. Enumeration before search. What's certain first, what's probable second and as a complement.

52 objects, and seven ways to check none are missing

Here's what it produced, once the right criterion was applied.

52 business objects described. 250 rules. 208 links between them. 124 gaps recorded. No links pointing into the void any more.

The twenty 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 exposed by the API. I check, for each one, whether it corresponds to a card. I count.

What I enumerate Share
The modules of application A 81%
The database tables 81%
The events the software emits 93%
The screens of application A 77%
The modules of application B 94%
The objects of application B 69%
The resources exposed by the API 81%

The measurement is replayed by a small program. That's the important point: it isn't an assertion, it's a check someone else can re-run.

And it's a weak measurement, which has to be said in the same breath. The matching is done on names: a sales_order table is recognised as the "Sales Order" card, with capitalisation, hyphens and plurals set aside.

That works about half the time. For the rest, the names bear no resemblance at all — a table can be called so_header where the card is called "Sales Order". So I kept a mapping list alongside, written line by line as I came across cases: that table there is that object there.

That list is a judgement, not a rule. If I forget a line, the object shows up as uncovered when it isn't. If I add a dubious one — "come on, that table must correspond to that card" — I inflate my own score. And even when the names do coincide, it can be a coincidence: two different things bearing the same word.

So the measurement proves nothing. It points to where to go and look.

Why I'm not trying to reach 100%

That's the immediate objection: if I know 19% of the modules are missing, I know which ones — I just have to describe them.

Except that a missing box isn't a task, it's 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 number up.

So the counter has three states, not two:

State What to do about it
Corresponds to a card nothing
Examined, settled as purely technical nothing, but the decision is written down
Not looked at yet the only one that counts

What has to reach 100% is the third line at zero — nothing left unexamined. The percentage shown doesn't climb to 100 and doesn't 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 that's a manual check, to be redone at every change — and that's the weakness that remains.

So the number serves a purpose other than measuring progress. It serves to compare the angles with each other. 69% on the objects of application B against 93% on the events doesn't mean "31% of the work is left": it means "application B handles things the rest of the product ignores, go and look over there". That disagreement is what surfaced the four findings below.

What the angles turned up

Four objects no other route would have found:

  • Through the database tables. An object the product displays but which 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 truly exists at the moment the invoice is issued. That ambiguity explains an entire family of customer complaints.
  • Through the events. A whole sub-part of an area we thought was covered, revealed by the events it emitted, which nobody had connected to anything.
  • Through the API. Two objects spread across five different modules — therefore invisible as long as you proceed area by area, and yet contractually exposed to the outside.
  • Through application B. Two objects it handles that application A knows nothing about.

The problems that show up everywhere

Here are the findings, transposed onto the fictional ERP. These are types of problem, not isolated cases — and I'd bet none of them is absent from your product.

A state recomputed everywhere, defined nowhere. An order's status derived from a few dates and the state of its lines, recomputed in every screen, with no central rule. Two screens end up disagreeing, and nobody knows which one is right.

An object the user sees and can't find again. The billing instalment I mentioned: displayed in the payment schedule, absent from search results, because it doesn't exist yet as a document. The user, though, saw it on screen — so they look for it, don't 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 isn't one. A customer's balance isn't a number stored somewhere: it's 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" gets the wrong object and returns 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 entry form, and nowhere else — so bypassable through a file import or through the API. That's the gap-finding machine I mentioned above, and its yield is remarkable.

A permission computed from six sources with nowhere bringing them together. The person's role, the plan they subscribed to, the options enabled, the trial-period restriction, their subsidiary's scope, a list of exceptions. Nowhere in the product does it say "here is how a permission is computed". Every change rediscovers it at its own expense.

The same object named differently at every level. "Order" on 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 and the product ignores. Across 261 sets of interview notes, one notion comes up 79 times, another 29. Neither exists in the product, under any name. These aren't vocabulary discrepancies, they're design gaps — and they only appear by comparing what customers say to what the product does.

The by-products

None of them was the objective. All of them fell out of the work.

Candidate bugs. The product contradicts an expectation written down somewhere. Each one arrives with its evidence and its consequence, not with a hunch.

A customer vocabulary, with frequencies. By counting words across 261 sets of interview notes and 365 support tickets, you get, for each product object, the terms actually used and their weight. Anyone talking to a customer then knows which word to use. One term comes up 168 times for a scope the product names differently; another 141 times for the central object.

And these two corpora don't say the same thing. Interviews say what customers want to build. Tickets say what breaks. The same word can dominate one and be absent from the other. Counting them together means prioritising wrongly.

A status report on translations. The multilingual glossary only covers 15 of the 52 objects. Among the missing ones, two are nonetheless displayed to the user in every language: no reference translation exists for them. And of the glossary's nineteen languages, only two have been proofread.


What it costs, and what the method still doesn't say

The cost

Under six hours. That's how long the whole thing took: the 52 cards, the 250 rules, the coverage measurements. One session, end to end.

Before starting, I had estimated a few dozen hours, spreadable area by area. I was off by an order of magnitude — and that estimation error may be the real reason nobody had done this work before: you think it's out of reach, so you don't start.

Three caveats on that figure, so it stays honest. It's one measurement on one product, once, not an average. The sources were already gathered and indexed locally; setup time isn't included. 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 profitability condition, though, is clear: finish one area before opening another. A complete reference model over one scope immediately serves every function working on that scope. A half-finished reference model over five scopes serves nobody.

Upkeep

That's the question people always ask second, and it's legitimate: a reference model with no story about how it stays alive is of no interest to anyone.

The reference version acts as a cursor: with a single command, it gives the exact list of what has moved in the product since the last update — hence the list of what remains to be recorded.

Each card carries a log where entries are added and previous ones are never corrected. An entry citing a code version signals that the product changed. An entry citing none signals something else: it's our understanding that changed, not the product. The distinction looks minor; it's what lets you know, six months later, whether we fixed a card or the software evolved.

Two movements deserve a mention because no tool tells that story: the merging of two objects we thought were distinct, and the splitting of an object that was hiding two. Those are the most frequent movements as a reference model matures.

Finally, when an object is rethought from top to bottom, we don't modify its card: we 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 predating the change.

What I decided not to do

  • No reconstruction of the product's past history.
  • Scope closed on data exports, which are views derived from the product and not the product itself.
  • No scholarly tooling until some use calls for it.

What the method doesn't say yet

  • How far to push formal rigour. That depends on the use, and use hasn't started.
  • 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 what criteria, before it enters the reference model.
  • How often to refresh the starting point, and therefore catch up on what has moved since.

A method document claiming to be complete is lying. This one carries a "what it doesn't say yet" section, and that section 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:

We pick a criterion we can satisfy, rather than a criterion that proves the work was done.

It doesn't only concern ontologies. It holds for an audit, a migration, a security review, a technical-debt cleanup. Every time a piece of work ends on "I've 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's complete is verified by enumerating the product. Confusing the two cost, here, a third of the product.

What's left is the most interesting part, and it isn't done: the gaps. They're all waiting to be investigated — candidate bugs first, then the ones a customer request already backs, then the missing pieces, then the design debt. That's no longer description, it's product decision-making.

That's 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 governing it live in the method document, in a single place.

What a card contains

What you write What it's 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 no longer accept a line
Its words what we call it, in each language and in the field commande (fr), pedido (es), "deal" among customers

The card itself

The file bears the object's name — that's what lets the links between cards resolve on their own.

---
concept: Sales Order
definition: Commitment to deliver made to a customer, tracked from being recorded through to being invoiced.
parent:
enfants: ["[[Rush Order]]", "[[Standing Order]]"]
attributs:
  - nom: status
    type: enum
    valeurs: [draft, confirmed, shipped, invoiced]
relations:
  - verbe: placed-by
    cible: "[[Customer]]"
    cardinalite: 1..1
  - verbe: contains
    cible: "[[Order Line]]"
    cardinalite: 1..n
axiomes:
  - An invoiced order can no longer accept a line
lexique:
  fr: [commande, commande client]
  en: [sales order]
  es: [pedido]
  synonymes_terrain: [affaire, dossier]
  deprecies: [bon de commande client]
sources:
  - erp.sql:table `sales_order`
  - fr.json:order.status.draft
ecarts:
  - id: E1
    nature: manque
    portee: relations
    attendu: a link to "[[Attachment]]", like the one "[[Customer]]" has
    consequence: impossible to attach a signed purchase order
    statut: confirme
    date: 2026-08-10
historique:
  - date: 2026-08-10
    action: creation
    motif: initial description, on the reference version
---

# Sales Order

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 | creation | — | initial description |

Only one thing is written twice: the links. Once in the header, so tools can read them; once in the body, so they show up in the network view. Everything else exists in a single place.

Three quality criteria, applicable to any card: the definition fits in one sentence and isn't circular; each 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 gap grid

What you record for each gap:

What you write Mandatory What it's for
An identifier yes so 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 it produces yes the concrete consequence, for someone
Its status yes to investigate, confirmed, accepted, resolved, rejected
Its source no a ticket, a customer sentence, a decision
Its date yes when it was observed

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 doesn't exist a backlog item
inconsistency two parts of the product don't say the same thing debt to arbitrate
friction the product does what was planned, but it's badly done design debt
wish a desired extension, outside any current expectation an opportunity

What happens when the gap disappears. You mark it as resolved, and it stays in 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 gaps carry the intent, the log carries the movement.

One safeguard. A gap 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 tracker.


Appendix C — The enumeration angles

Go through all of them, and compare them two by two.

The angle What it brings back that the others don't see
The code modules entire 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 objects contractually exposed to the outside, scattered across several modules
A second application 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 and the product ignores

Three rules of use:

  1. The signal is the disagreement between two angles, not the score of any one angle taken alone.
  2. Every unmatched object is settled explicitly — business object, or plumbing. Never by omission.
  3. Two customer corpora, counted separately: interviews say what customers want to build, tickets say what breaks. Carry both frequencies in the vocabulary — one guides the roadmap, the other documentation and training.

Where the rules are, in decreasing order of yield:

  1. The names of technical error messages. In the least tidy parts of the code, it's often the only source — and it reads without knowing how to program.
  2. The checks performed at the start of an operation, before anything is modified.
  3. Database constraints — what has to be unique, what can't be left empty. They sometimes contradict the business, and that discrepancy is a defect.
  4. Input validation in the screens — the most often forgotten part, and the one that produces the most interesting gaps.

An object described with no rules at all signals a superficial description, never a business without rules.