I'm part of a team developing a renewable energy site screening tool. I decided to run it as a proper MBSE project, but without Cameo or Rhapsody or any SysML tooling. Everything is YAML and markdown in git, plus a validator script that checks the model for consistency. It's worked better than I expected so I'm writing up the structure.
What the system does
UK renewable site screening. A land agent or developer normally has to manually cross-reference DNO substation capacity, planning constraint layers, resource data and market context to judge whether a site is viable. This tool takes a site (coordinate or polygon), a technology type and an area, and produces a PDF desktop-study report plus an interactive map in about 100 seconds: grid connection viability, planning constraints, EOS P50/P90 yield data, and a scored risk verdict.
It's 25 Python modules covering spatial queries (substations, planning constraints, cable routing), resource and yield modelling (ERA5 wind, solar irradiance, P50/P90), risk scoring and decision logic, report and map generation, and orchestration. So a real system with real complexity, not a toy example.
Current state of the model: 170 requirements, 162 test cases, 139 decision records, 354 files total.
Structure
Numbered layers, one concern per layer. One artifact per file: each requirement is its own YAML file, each test case is its own YAML file. This turned out to be the most important decision. Diffs are readable, merges rarely conflict, and I can grep the whole model. I've watched traceability rot inside monolithic requirement documents before and didn't want to repeat that.
Every file has a typed ID and IDs are how everything links: RSI-SYS-402 (requirement), RSI-TC-165 (test case), D-R140 (decision), RSI-R008 (risk), RSI-ICD-004 (interface).
Links go both ways. Requirements have verified_by: [RSI-TC-165], test cases have verifies: [RSI-SYS-1652]. Yes it's redundant. The redundancy is the point: a machine can only detect a broken link if both ends are supposed to agree.
Code comments cite requirement IDs. Requirements cite the decision records that justify them.
Two verification layers
The 162 MBSE test cases are the specification of what must be true: preconditions, procedure, pass criteria. Technology agnostic, reviewable by a human.
Separately there are 625 pytest tests across 50 files, runs in about a minute. That's the executable side.
I keep them separate on purpose. The spec layer defines what verification means, so it survives refactors of the test code.
The validator
validate.py, 401 lines, only dependency is PyYAML. Exits 0 or 1 so CI gates on it. It checks:
- Schema: required fields, enum values, ID format matches filename, date formats, duplicate IDs
- Reciprocity: if RSI-SYS-901 lists RSI-TC-001 under verified_by, RSI-TC-001 must list it back under verifies
- Rollups: every hand maintained summary number gets recomputed from the files and compared
- Citation integrity: every D-Rxxx cited anywhere must resolve to a real decision entry
LLMs take to this like fish to water
This wasn't the original motivation but for me it is highly effective, The whole model is plain text with typed IDs and explicit links, which seems to be a shape an LLM works well with. No binary model files, no proprietary export step, no screenshots of diagrams. An agent can grep for RSI-SYS-402, read the requirement, follow verified_by to the test case, follow the decision citation, and have full context in a few file reads.
I've been able to hot swap between Codex and Claude sessions in the same project with very little context lost. Neither needs project-specific onboarding beyond pointing it at the repo, because the structure is self-describing. Both can also run validate.py after making changes. If an agent hallucinates a decision citation or breaks a reciprocal link, the issue gets flagged when the validator runs.