Appendix B - 1. The agent-first MBSE harness
The Structure of The agent-first MBSE harness — its shape at a glance:
Five disciplines wrap the frozen records. The loader discovers each record file; the look-up seam projects fields from the code; the derive-and-assert step recomputes any function-of-other-fields; the drift check set-diffs model against reality; the query projection turns records into JSON for the fleet.
flowchart LR
Code[(Code source)] --> Seam[Look-up seam]
Seam --> Rec["Frozen records<br/>(one per view)"]
Loader([Auto-discovery loader]) --> Rec
Rec --> Derive[Derive + assert]
Rec --> Drift{{Drift check}}
Rec --> Query[JSON query projection]
Accessible description: frozen records sit at the center. The look-up seam feeds them from the code source, the loader discovers them, and three consumers read them — a derive-and-assert step, a drift check that fails the build on divergence, and a query projection that emits JSON.
Projected from the catalogue entry system-models / The agent-first MBSE harness.
On this page: Intent · Motivation · Applicability · Structure · Sample Code · Consequences · Example use within DocAble · Related Patterns
Intent
Intent — Build the typed system-models as a thin, hand-rolled harness over plain frozen records: adopt the vocabulary and schema of the right modeling genre per view, but skip its runtime and hand-roll the executable layer. The model then captures your project's own invariants, and a set of build-time checks keeps it equal to the code (our instance: frozen Python dataclasses under five recurring disciplines, not a SysML tool).
Motivation
You decide to model your system as executable source-of-truth, and the first instinct is to reach for a real modeling tool: SysML, Alloy, an EMF metamodel. Then two failures land. First, the tool's schema doesn't fit. A real system-model carries project-specific invariants (a verification tier derived from an invariant's temporal shape, a lock-ordering rule, a zone-boundary predicate) that don't map onto any standard library's schema, so you spend more effort fighting the tool's model than building yours. Second, the tool draws or checks; it does not run inside your own lints. A SysML diagram is data a viewer renders; an Alloy model is data a solver checks. Neither ships the discipline that makes the model unable to lie about your code: a build-time check that loads the model, loads the code, and blocks on divergence. The failure is a modeling layer that either doesn't fit your invariants or doesn't wire into your build, so it drifts, and a drifted model is worse than none.
Applicability
- Frozen typed records that import nothing. Data, not code, so any lint or tool reads them cheaply.
- A code source to look up from (an enum, a manifest, a handler tree) for the look-up seam to project.
- A per-view drift lint wired into the build. Without it, the "can't drift" claim is a hope.
Structure
The Structure diagram appears at the top of this page.
Sample Code
A frozen record holds the model. One field is derived from other fields, so a check recomputes the derivation and fails if the stored value went stale — the discipline that turns a snapshot into a bridge that cannot lie about its own inputs.
from dataclasses import dataclass
import sys
@dataclass(frozen=True)
class Invariant:
name: str
temporal_form: str # "always" (safety) or "leads-to" (liveness)
verification_tier: str # DERIVED from temporal_form — never hand-typed
def derive_tier(temporal_form: str) -> str:
"""The one derivation function. Stored tier must equal what this returns."""
return "exhaustive-check" if temporal_form == "leads-to" else "state-search"
def assert_derived(records: list[Invariant]) -> list[str]:
findings = []
for r in records:
expected = derive_tier(r.temporal_form)
if r.verification_tier != expected:
findings.append(f"{r.name}: stored tier '{r.verification_tier}' != derived '{expected}'")
return findings
if __name__ == "__main__":
# `load_view` auto-discovers and loads the frozen records for one model view.
findings = assert_derived(load_view())
for f in findings:
print(f"STALE-DERIVED: {f}")
sys.exit(1 if findings else 0) # a hand-typed derived field that drifted fails the build
Consequences
- Five disciplines to hold per view. The harness is thin, but each view must actually follow the look-up + derive + drift rules, or it degrades to a snapshot.
- A drift lint per view to author. They don't share code, by design; that is real surface.
- Modelling discipline up front. Deciding what to model, and in which genre's vocabulary, is design work (do it only where a failure lives, the scoping rule the starter kit leads with).
Example use within DocAble
- A system-models directory of frozen dataclasses (state machines, component/zone catalogue, deployment topology) with an auto-discovery loader and a generic record→JSON query projection.
- A verification tier stored per invariant but derived from the invariant's temporal shape, with a lint asserting stored-equals-derived.
- The service-flow view authored in the Backstage entity dialect: schema adopted, runtime skipped.
Related Patterns
- Enables — executable source-of-truth: this is the how of building those models when you choose hand-rolled records over a modeling tool.
- Counterpart — drift & parity gates: the per-view drift lints this mechanism's fourth discipline names are exactly those gates.
- See also — model-driven codegen (generate artifacts from the hand-rolled records) · query surface (the agent-facing read API the generic query projection feeds).