Models and the Semantic Gap

The last chapter gave you four places to inject a mechanism. This one is about the single most valuable thing you can put there — a model — and about a trap that catches people the moment they try to enforce one: pushing on the model at the wrong level, and watching it slip. Models are the sweet spot between prose that is too vague and code that is too verbose. But a model only helps if you check it where checking makes sense.

Words are soft, code is verbose, a model is the sweet spot

Your first governance mechanism is the prompt, because the agent is a language model and words are how you talk to it. But words are soft. They are ambiguous, and the more ambiguity you leave in, the less happy you will be. You can push ambiguity down by writing documentation — describing the environment, the product, its goals, its policies — and if you were briefing a human team, that would be enough. Humans are not dumb; they remember, they know what matters, they know where to look it up, and they know they will be fired if they misbehave. Agents are different: brilliant reasoners who need to be told exactly what to reason over, and for whom loose documents are simply not precise enough.

Code is the opposite failure. Code is perfectly precise — it is exactly what will happen — but it is far too verbose to reason over in bulk. What you want sits between the two: something accurate and unambiguous, yet compact. The word for that thing is a model.

You already know models from physics. F = ma. y = mx + b. They are not reality — F does not quite equal ma, and real data scatters around the line — but they are a cheap, honest approximation you can actually reason over, instead of wrestling the full empirical dataset or a nightmare of fluid equations. A circle approximated by tangents is not a circle, but it is good enough to compute with. Software has its own such models. UML was the famous attempt: a whole language of them, out of the 1990s, expected to be the future. It never took off, and the reason is the reason this whole chapter exists.

Why the models drifted — and why that changes now

The models are the map; the code is the territory. You draw the map, you build the territory from it — and then the territory drifts. Code changes constantly, and keeping the map in sync is expensive. Worse, no single model captures everything, so teams either piled every concern into one unreadable mega-model, or split them into so many views that a single code change meant updating ten diagrams. So people gave up and drew box diagrams instead. Only the industries where a mistake is catastrophic — Rolls-Royce, Boeing — kept their models honest, because they could justify the cost. Everyone else chose agility and leaned on the code.

Agents flip this calculation, because agents cannot reason over the code — there is too much of it — but they can reason over models. So if you can find the models that usefully approximate your system and keep them in sync, your agents will always know where to go. They walk the docs, hit a pointer to the model — a data-flow diagram, a performance model, an inheritance hierarchy, a user journey — and if that model is linked to the relevant code, they can make the change with the whole picture held in a single window. The model also lets them check their work: a test says the suite passed, but an invariant over a model says what must be true beyond the tests. This is how you stop architecture from decaying — the agent finds the architectural model, follows it, verifies against it, and when the territory genuinely moves, retrofits the map so the two stay equal. The companion catalogue's drift-and-parity gates are the mechanism that makes "the map equals the territory" a checked property rather than a hope: they fail the build the moment a model and the code it describes disagree.

Why agents don't reach for models on their own — a conjecture

Here a skeptic has a fair question. If working at the model level is such a win, why does the agent never reach for it on its own? Ask a coding agent to make a change and it dives straight for the code. It does not propose a model, does not ask where the map is, does not think to draw one. If models were the sweet spot, you would expect the model trained on all our software to have noticed.

I have a conjecture — and I flag it as a conjecture, not a finding. Two things may be compounding. The first is a gap in what the agent learned from. Serious model-based engineering happens inside companies — the Rolls-Royce and Boeing programs, the regulated shops that can justify the cost — and that work does not leak into the open corpus a model trains on. The public code the agent read is overwhelmingly the agile kind that gave up on models and leaned on the code, so the agent saw very few real examples of the practice at all. The second is worse than absence. The corpus is full of text about models, and most of it is negative: the blog posts saying UML is dead, the war stories about diagrams that rotted, the received wisdom that model-based work is heavyweight and painful. So the agent is conditioned away twice over — starved of positive examples, and fed explicit priors that the whole approach is a mistake. If that is right, the agent's reluctance is not evidence that models do not help. It is an echo of the era that abandoned them, and the thing that abandoned them — the cost of a human keeping the map in sync — is the thing agents just removed.

The apex of the documentation hierarchy is a typed model

Step back from drift for a moment and look at what you actually hand an agent to work from. It is never one thing. It is a hierarchy of descriptions, each aimed at the same system from a different angle: prose docs at the top saying what the thing is for, code comments saying why a line reads the way it does, tests asserting what must stay true, schemas and typed records pinning the shape of the data. Every one of these is a description of intent — and every one is partial, and every one drifts. The prose goes stale, the comment outlives the code it explained, the test pins yesterday's behavior, the schema lags the migration. This is the hierarchy the up-front half of governance climbs.

Here is the thing the training data will not suggest, and it is worth being blunt about. The usual agentic recipe gives you docs, code, and tests — and nothing binding them together. They sit as three separate islands. A change to the code does not touch the doc; a stale test does not know the schema moved; the prose swears the system does one thing while the code quietly does another. There is no glue, so there are no guardrails: nothing in the setup can tell you the three have fallen out of agreement, because nothing in the setup relates them at all. Drift is not a risk you run in this arrangement. It is the default, and you find out only when it bites.

The apex of the documentation hierarchy is a typed model that binds docs, code, and tests A two-panel contrast. Left panel, typical agentic methods: three boxes — docs, code, and tests — stand as separate islands with no glue between them. Nothing binds them, so they drift apart and there are no guardrails; a red marker notes the missing binding. Right panel, the book's move: a single typed model sits at the apex and binds the same three — docs, code, and tests — with checked edges. The model is the one description the machine can check the others against, so a drift check fails the build when any of the three disagrees with it. The lesson: lift documentation to its apex, a typed model, and the three that used to drift become one checkable whole. Typical agentic methods three islands, no glue docs code tests nothing binds them → they drift no guardrails The book's move a typed model at the apex typed model binds intent to implementation docs code tests checked: the map must match the territory
The apex of the documentation hierarchy. Left: the typical agentic setup leaves docs, code, and tests as three unconnected islands — no glue, so they drift apart and there are no guardrails. Right: the book's move — a typed model at the apex binds docs, code, and tests together, and because the model is the one description the machine can check the others against, a drift check fails the build the moment any of the three disagrees with it.

The book's move is to take the hierarchy to its limit. Its apex is not more prose, however careful. Its apex is a typed model — and the model is what binds the islands into one checkable whole. A model is a description precise enough that a machine can read it, and because the machine can read it, the machine can check the other descriptions against it. That is the property no amount of good prose can give you. The model becomes the single description you hold the docs, the code, and the tests up to: does the code still match the state machine the model declares? Does the schema still match the records? When the model and the territory disagree, a build-time check fails, and the disagreement surfaces the instant it appears instead of the instant it bites. This is the same drift-and-parity gate from the last section, seen from the other side: there it kept the map equal to the territory; here it is the glue the three-island picture was missing — the one edge that makes docs, code, and tests a bound system instead of three things that happen to describe the same repo.

That is the Modeling Thesis at full strength. Documentation matters, and agents have made thorough documentation cheap for the first time, so climb the hierarchy all the way up. The rung at the top is a typed model that binds intent to implementation and that a checker can prove things about. Everything below it — the prose, the comments, the tests — is honest and useful and still drifts; the model is the one description that cannot silently lie, because the gate stays red until it tells the truth again. This is why the companion catalogue gives the models-bridge its own role. The whole aim of the up-front half is to build the one description the machine can check the rest against — and building it well is a craft the Model Zoo in Part 4 teaches by example.

The semantic gap

The semantic gap: enforce at the level where the property is legible A property is invisible at the low level and legible one level up. Bottom row: a sequence of small low-level events — individual commits, single system calls, single turns — where the property you care about is spread across many of them and cannot be seen in any one. A red cross marks enforcing here as the mistake. Top row: the whole unit of work — the agent returning from its task, the ten calls stitched together, the session's durable record — where the same property becomes checkable. A green check marks enforcing here. The lesson: lift the check to the level where the meaning is legible. Enforce here — the property is legible The whole unit of work is in view agent returns · ten calls stitched · the session's record lift the check up the property spans many events — invisible in any one commit commit commit call call call Enforce here — the property is not yet legible a single commit, a single system call, a single turn
The semantic gap: enforce at the level where the property is legible. Bottom row: a sequence of small low-level events — individual commits, single system calls, single turns — where the property you care about is spread across many of them and cannot be seen in any one; enforcing here is the mistake. Top row: the whole unit of work — the agent returning from its task, ten calls stitched together, the session's durable record — where the same property becomes checkable; enforce here instead. The move is to lift the check up to the level where the meaning is legible.

Now the trap. Say you want to enforce "if the agent substantially changed the call graph but did not update the overlaid model, reject it." It sounds like a pre-commit hook. It is not — and seeing why is worth more than the rule itself. An agent carrying out a real task makes many commits toward one goal; the model may be legitimately out of sync in the middle and correct again by the end. Enforce at the commit and you are checking a sentence for grammar before the paragraph is written.

This is the semantic gap: the failure you get whenever you enforce a property at the wrong level of abstraction. Operating-system security lives with it constantly — the OS sees individual system calls, but "this program is exfiltrating data" is a pattern across calls, invisible in any single one; you have to stitch ten network calls together before the leak is legible. Model drift is the same shape. The commit is the wrong granularity.

The fix is to find the level where the property is actually legible, and enforce there. For model drift, that level is the agent returning from its task, because at that moment the entire feature and its plan are visible. You can see whether the work changed the model, and whether the body of commits includes the tests, the documentation, and everything else the task was supposed to carry. This is exactly where the companion repository places its definition-of-done audit: a check that runs when an agent finishes an Epic, over the whole unit of work, rather than at any single commit inside it. "Correct" is a hard word — you cannot fully prove the model is right — but you can check that it was updated and that the surrounding work is complete, and that is the right semantic level to do it.

Epic. A tracked unit of work larger than one commit: a feature or effort with its own file, plan, and definition of done, carried out over several agent dispatches.

Definition of done (DoD). The explicit list of conditions a unit of work must meet to count as finished — tests present, docs updated, the model in sync — checked at the end rather than assumed.

A worked example: the context-fullness bank-hook

Let me give you a second instance of the semantic gap, one that lives not in the product but in the operator's own loop. It is small, it is concrete, and it shows the same trap from a different angle: the machinery you are given sits below the meaning you actually care about, so it guesses, and the fix is to move the check up to where the meaning is legible.

Here is the setting. When I run a long session, the harness has a context window, and when it fills, the harness compacts. It summarizes the conversation so far and throws the raw history away to make room. Compaction is not free. It is a lossy save, and the harness decides on its own what to keep and what to drop. That decision is a best-effort guess, and it is a guess precisely because the harness has no way of knowing which parts of the conversation were load-bearing. It sees tokens. It does not see that the three paragraphs where I settled the architecture matter more than the forty where an agent walked a directory tree. Only the operator knows what was worth banking and where it should go. The harness sits below the semantics of "what is worth keeping," so it cannot help but approximate.

That is the semantic gap again, in miniature. The property I care about — is the durable record of this session current, so that a compaction loses nothing important? — is not legible at the level where the harness operates. The harness knows one number: how full the window is. It does not know whether my strategy document and my hand-off notes reflect the last hour of decisions or whether they went stale ninety minutes ago while I was busy dispatching work.

The fix: a hook one level up, checking two things

So I moved the check up to where the property is legible: the operator's loop. Not inside the harness, which cannot see the meaning, but at a hook the orchestrator controls, which can. The hook fires as the session runs and checks two conditions together:

When both are true, the hook does one thing: it emits a reminder to bank the current state into the strategy and hand-off documents. Then, when a compaction does fire, a second hook runs on the far side — a post-compaction hook — and it knows exactly where to look to rebuild context, because the pre-work made the durable records the authoritative place to look. The bank-hook tells the operator to write the record; the post-compaction hook tells the freshly-compacted session to read it. The two ends meet at the same documents.

Why both conditions, and not one

The dual check is not belt-and-suspenders. It is what keeps the hook from becoming a nuisance, and the nuisance here has a specific and nasty shape.

Suppose I fired the reminder on fullness alone. Then every time the window filled, I would be told to bank, even if I had just banked. Worse, suppose I fired it on staleness alone. Then every few minutes, whether or not compaction was anywhere near, I would be nudged to stop and write. Either way the hook fires too often, and a hook that fires too often trains the operator to stop and re-summarize the session constantly. That failure has a name.

Sidebar — ouroboros. The ouroboros is the ancient image of a snake eating its own tail. In this loop it names the degenerate state where the model spends its turns summarizing its own recent work instead of doing new work — banking, then banking the banking, forever consuming its own output. A reminder that fires on a single loose condition drives straight into it. The dual check is the guard: fullness and staleness together are rare enough that the reminder lands only when it is genuinely worth acting on.

Requiring both conditions is what makes the firing meaningful. A full window is common. A stale record is common. Both at once — the window filling and the durable state having drifted out of date — is the exact situation where a compaction would actually lose something, and that is the only situation where the reminder earns its interruption.

The honest part: the pre-compact hook is the wrong hook

I want to be straight about a compromise buried in this, because it is the kind of thing the semantic-gap idea should make you notice rather than paper over.

The theoretically right moment to bank is the instant before compaction — you would catch every last decision, lose nothing, and never fire early. The harness even offers a pre-compact hook that runs at exactly that moment. It is the wrong hook to use. Two reasons. First, it fires too close to the end: by the time you are at the edge of the window, you may already be past the point where a comfortable bank fits. Second, and worse, the bank itself costs tokens. Writing a thorough hand-off is real work in the window, so a bank kicked off at the pre-compact boundary can be the very thing that trips the auto-compaction it was meant to precede. You reach for the save and the save pushes you over. The hook that seemed to sit at the perfect level turns out to sit slightly past it.

So I do not use it. I approximate the right firing point instead — earlier, on the dual condition — and I accept a little loss. Sometimes the reminder fires and there was nothing new worth banking. Sometimes a decision lands in the gap between the last bank and the compaction and has to be reconstructed from the summary. This is not the sound placement the drift example gave us, where agent-return is genuinely the level at which the property is legible and you lose nothing by waiting for it. Here there is no clean rung. The harness gives you a pre-compact hook too late to use and a fullness number too dumb to act on alone. A hook at roughly the right level — the operator's loop, checking fullness and staleness together — beats both.

That is the general lesson wearing small clothes. The harness enforces at the level it can see. When the property you care about lives one level up — in the meaning of the work, not the count of the tokens — you lift the check to the operator's loop, where the meaning is legible, and you accept an approximation when the runtime gives you no exact rung to stand on.

Two promissory notes come due later, and it is worth saying where. The special language for stating invariants over a model — linear temporal logic, the split between safety and liveness, bounded model checking, and formal invariant verification — arrives in The Process View, where a model stops being a picture and becomes something a checker can prove things about. And the question of how you actually author a model in a repo — a typed record, a schema file, a lint that reads it, a diagram that stays honest — is answered by example, not by lecture, across the Model Zoo in Part 4. You now believe models matter; those chapters show you how to build and check one.

Contents
© James C. Davis, 2026–present