3.8 Keeping the Models in Sync with the Code — A Measurement

3.8.1 The question: the map must equal the territory, or the bridge lies

A context-bounded fleet governs a context-exceeding codebase through structured models. The models are the bridge the agents reason across. So the models must equal the territory — if a model claims something the code no longer does, every agent that trusts the model inherits the lie.

"Keep the model in sync with the code, automatically" is the thesis question. It has a cheap-sounding answer and an expensive-sounding one, and the honest result is that neither alone is enough.

3.8.2 The mechanism: a two-layer net, and liveness as a property

Sync holds through two layers stacked over the model↔code gap.

The load-bearing property is liveness as a property, not a process. A snapshot-plus-sync-process can drift between runs; a resolver over live code cannot. This is the whole finding compressed: derived defends, snapshotted drifts. Anchor to a symbol, not a line — and where there is no clean symbol to anchor to, that absence is itself a signal that the model is missing a point of abstraction (a refactoring target, not a gap to paper over).

3.8.3 The model grows with the system: two worked examples

Sync is a living practice, not a one-time alignment. The territory grows; the model is edited to match; a derived gate fails the build if it is not. Two real cases from the study period show the loop.

Adding a service endpoint (2026-08-01). The "pictures-not-pages" PDF pilot added a new editor endpoint, GET /api/pdf-page-image/{job_id}/{page} — the PDF analogue of the existing slide-image and docx-page-image endpoints. The service-flow model (the Backstage-dialect Component/API catalogue under system-models/services/) had to gain that endpoint or the generated catalogue would no longer match the source. The commit that landed it (6fd0b6d51a) says so in its own words: the endpoint "was added to the editor cluster YAML but the generated Backstage entity was not regenerated," which "Restores gen-web-api-entities --check to green." The added model fragment is real:

/api/pdf-page-image/{job_id}/{page}:
  get:
    summary: Return the rendered page image for a completed PDF job
             (pictures-not-pages pilot ... the PDF analogue of
             /api/slide-image / /api/docx-page-image)
    responses:
      "200": { description: Rendered PDF page image (PNG) for the SPA PdfViewAdapter }
      "400": { description: Job not completed, or not a PDF job }
      "404": { description: Job / output file / page not found }
      "502": { description: Render service failed (systemic) }

A new service or endpoint is, by construction, either a model edit or a parity failure. The service-flow parity gate (gen-web-api-entities --check, backed by the service-flow-model and service-call-graph drift lints) would have failed the build had the model not been edited to match.

Adding user journeys (2026-07-16; the model kept growing through the drift window). The user-journey model gained three Journey entities — batch-remediate, editor-edit, editor-edit-remediate — authored in commit 01699c8c47, which in the same change extended the service-flow lint to admit the Journey kind. A Journey names an actor, a goal, and ordered steps, and each step names the service endpoints it calls. The batch-remediate Journey, for instance:

kind: Journey
spec:
  actor: uploader
  goal: Get an uploaded document remediated end-to-end (chunk, remediate, merge, validate).
  steps:
    - seq: 1   # chunk-decision
      calls: [genai-service-capacity, lo-service-convert]
    - seq: 2   # per-chunk C# remediation
      calls: [genai-service-batch, genai-service-complete, genai-service-transcribe,
              genai-service-detect-bboxes, ocr-service-ocr, font-svc-fonts, font-svc-cmap]
    - seq: 3   # post-remediation validation
      calls: [render-service-render, accessibility-conformance-service-check,
              accessibility-quality-service-validate, ...]

The two-way call-site drift lint (lint-journey-endpoint-coverage.py / lint-journey-service-drift.py) holds each journey's declared endpoints to the real internal_service_client(...) call sites, in both directions — a journey that names an endpoint no code calls, or code that calls an endpoint no journey declares, fails. The model kept growing in the drift window too: the same journey model gained a typed MAJOR/MINOR criticality tier and a JourneyClosure join-key substrate (late July), each with its own derived check.

Honesty note: the three Journey entities were authored 2026-07-16 — inside the broader study period, before the 07-22 start of the drift-audit corpus. No brand-new Journey entity landed in the 07-28→08-04 refresh window; the journey model's growth there was the criticality-tier and closure-key expansion. The service-endpoint example (08-01) is squarely in-window.

3.8.4 The evidence

The claim is a field-report result, so the numbers come with their provenance. The model↔code sync evidence under load is collected in Table 3.8-1; a documentation-hygiene aside, kept strictly separate so it is never miscounted as sync evidence, is in Table 3.8-2.

The derived floor under load

Documentation drift is excluded here by construction; this table is model↔code sync only.

Table 3.8-1. Model-sync evidence — the derived floor under load, model↔code sync only (documentation drift excluded by construction).
SignalValueCaught by
Model-bridge churn, window 07-28→08-04+8,970 / −173 lines across 63 commits (query/reactor + governance-graph + frontend-build models)— (this is the load the net held under)
Model↔code drift reaching a post-close reopen0 across cumulative N=56 closes
Genuine model-drift catches, refresh windowtraceability-broken ×3 (un-registered model consumer / missing component entry / service-call-graph), stale-anchor / stale-test ×3the derived floor re-run at HEAD (symbol-anchored drift lint · consumer-registry-fresh · service-call-graph drift), not human reading
Pre-floor retro-audit (before the derived floor existed)~27 genuine model↔code drifts at S:N ≈ 1.0, zero false positives, in closed Epics with green DoDs — including a prod-blocking pointer-drift incident and a fully-typed function shipped to zero consumersthe audit re-ran each Epic's own lints; the drift had escaped the green DoDs

The pre-floor row is the "hope-for-the-best fails" evidence: real drift, used to escape, silently, past green definitions-of-done. The floor was built to close exactly that class, and the top rows are it holding. For the data, see Keeping the Models in Sync with the Code — A Measurement → (preliminary)

Documentation-hygiene aside (NOT model sync)

Stale headers and stale prose numbers are documentation drift, not model drift. They are the soft layer's true positives, and they are cheap: a reader catches them, and the close tool heals them. Kept here, clearly labelled, so they are never counted as model-sync evidence.

Table 3.8-2. Documentation-hygiene aside — stale headers and prose numbers the soft layer catches; documentation drift, not model sync.
Doc-hygiene driftRefresh-window countCaught byHealed by
STALE-HEADER (a status line frozen at a pre-close phase)11reading (human / Opus)the Epic-close tool auto-rewrites the status atomically
DOC-CLAIM (a stale prose number: 105→107, 76%→90%, 266→234)9Opus re-derives the number from codea routed one-line [FIX] / [AUDIT]

Documentation hygiene, not model sync. These rows describe prose the lints do not parse. They are real, and the soft layer is the only practical control for them — but they say nothing about whether the model equals the code.

The whole shape — the gap, the two layers, and the two places drift still escapes — is drawn in Figure 3.8-1.

The model-to-code gap spanned by a two-layer net, with two escape hatches Between an upper band (the model, the map) and a lower band (the code, the territory) sits the model-to-code gap. Two horizontal nets span it. The upper net is the soft reading layer, dashed blue, which catches doc-legible drift but is fallible; it has a hole in the middle. The lower net is the hard derived floor, solid green, which resolves each model anchor against a live symbol and catches modeled drift; it covers only the modeled region and ends before the right-hand zone. Three drift arrows rise from the code toward the model. On the left, a modeled drift rises and is caught at the derived floor. In the middle, a subtle semantic drift is mechanically green at the floor but slips through the hole in the soft layer and escapes: this is failure mode one, the judgment miss. On the right lies a red hatched zone, the unmodeled, which no net covers; a drift rises through it unopposed and escapes: this is failure mode two, the unmodeled. Below, two cross-references convert the escapes into machinery: a coverage census, or metamodel, measures the unmodeled gap; and a self-governance reflex converts the gap into a new model or control, extending the derived floor into the formerly-unmodeled zone, drawn as a dashed green extension. The caption reads: derived defends, snapshotted drifts, and what is not modeled cannot drift-check. THE MODEL — the map typed MBSE models: service-flow · journeys · state machines · governance graph THE CODE — the territory the live symbols the anchors resolve against the model↔code gap the unmodeled no anchor · no net Soft layer — the reading pass catches doc-legible drift; cannot block hole Hard layer — the derived floor (holds) resolves each anchor to a live symbol extended by self-governance caught mechanically green escapes fail mode 1: judgment miss escapes fail mode 2: unmodeled Coverage census · metamodel measures the gap: is it modeled at all? Self-governance gap → a new model Derived defends, snapshotted drifts — and what isn't modeled can't drift-check.
Figure 3.8-1. The Two-Layer Net. The model↔code gap spanned by two layers, with two escape hatches drawn as gaps in the net. Derived defends, snapshotted drifts — and what isn't modeled can't drift-check.

3.8.5 The two failure modes: where drift still escapes

The net has two escape hatches. Neither is a footnote; they bound the claim.

Failure mode 1 — the judgment miss. A mis-alignment that is not mechanically decidable needs subjective judgment. The anchor resolves (the code exists, so the hard floor reads green), yet the meaning drifted. The soft reading layer is the only net for this, and it is fallible by construction — a subtle semantic drift can pass a green definition-of-done. This is the drift that slips through a hole in the soft layer.

Failure mode 2 — the unmodeled. A fact or relation that should be modeled but is not has no anchor, so no drift-check can fire over it. You cannot detect drift of a thing you never modeled. Here the net has no coverage at all — not a hole in a layer, but a region with no layer. When there is no clean symbol to anchor to, that absence is the signal: it names a missing point of abstraction, a refactoring target.

The systematic backstops (cross-references)

Both failure modes lean, by default, on an engineer or an agent noticing. The book has two mechanisms that convert that noticing into machinery — the right in-book targets to cross-link:

3.8.6 The honest reading

The result is narrow, and stating its limits is part of stating it. Four readings bound what this measurement does and does not show.

That honest reading closes the zoo. This Part walked each kind of model on real code and showed the drift gate that holds each one to the code, so the map cannot quietly rot away from the territory. What it has not shown is how you reach that state from a codebase that carries none of it. The next Part starts from spaghetti.

© James C. Davis, 2026–present