Middle-Core Model Runtime Prototype¶
Status: prototype ADR
Decision¶
Middle-core should become a model-driven scenario runtime, not a handwritten CRUD service. The first prototype uses YAML as the canonical authoring model and compiles it into generated C# contracts that a small in-memory graph runtime can execute.
model specs -> deterministic generator -> generated C# contracts -> in-memory object graph -> scenario runtime -> evidence output
Generated code is disposable. Hand-authored behavior belongs in runtime classes, workflow handlers, projection ports, evidence sinks, partial classes, or future plugins.
Why This Shape¶
The platform needs business objects that carry semantics, use-case traceability, workflow meaning, and evidence expectations. A pure three-tier CRUD model would make the objects anemic and push the actual intelligence into controllers or adapters. A fully generated model runtime gives us a modern middle ground:
| Concern | Owner |
|---|---|
| Provider capabilities and ArcadeDB operations | backend-core |
| Canonical model authoring | model/middle-core/model.yaml |
| Generated C# IDs, records, contracts, and fixtures | templates/middle-core/generated |
| Scenario orchestration, graph mutation, policy, evidence | hand-authored middle-core runtime |
| UI, MCP tools, runbooks, and agents | projections over business objects and scenario results |
Top-Level Ontology And State Machines¶
Middle-core should treat lifecycle states as first-class ontology commitments, not UI labels. The top-level model now follows a UFO/OntoUML-inspired stance:
| Top-level idea | Middle-core interpretation |
|---|---|
| Object kind | A durable business object kind such as knowledge-source, evidence-pack, or tool-offering. |
| Lifecycle state | A phase or situation that classifies an object during a bounded part of its lifecycle. |
| State transition | An event type that moves an object from one lifecycle state to another. |
| Scenario execution | An event that causes object creation, graph linking, state movement, and evidence production. |
| Evidence bundle | A proof object that supports claims about scenario execution or capability readiness. |
The referenced prototype top-level ontology lives at model/middle-core/ontology/top-level-ufo-lite.ttl. It uses gUFO-style vocabulary references as a lightweight bridge toward RDF/OWL while the YAML model remains authoritative for v1.
model.yaml now includes state_machines, and every business object declares a state_machine. The generator validates that:
- every object references an existing state machine;
- each state machine belongs to a known object type;
- object states and state-machine states match exactly;
initial_state,terminal_states, and transition endpoints are valid states;- transition triggers are stable kebab-case event names.
This gives us a useful place to plug OntoUML/OLED/gUFO validation later. The current official OntoUML documentation describes OntoUML as a UFO-based ontology-driven conceptual modeling language, the OntoUML tooling page lists OLED support for verification, simulation, model checking, inference, and semantic anti-pattern detection, and gUFO provides the Semantic Web/OWL-friendly path for UFO-style ontologies.
Model Workspace¶
The prototype model workspace is:
| Path | Purpose |
|---|---|
model/middle-core/model.yaml |
Canonical v1 model for objects, data objects, relationships, workflows, scenarios, projections, and use-case traceability. |
model/middle-core/ontology/*.ttl |
RDF/Turtle ontology artifact references. |
model/middle-core/workflows/*.bpmn |
BPMN workflow artifact references. |
model/middle-core/decisions/*.dmn |
DMN decision artifact references. |
model/middle-core/projections/*.yaml |
Provider projection artifact references. |
In v1, RDF, BPMN, DMN, and projection files are first-class referenced artifacts, but YAML remains authoritative. Later slices can parse SHACL/BPMN/DMN directly and make the generator fail on semantic drift.
Generator Usage¶
Validate the model:
python tools/modelgen/validate_middle_core.py --model model/middle-core/model.yaml
Regenerate contracts:
python tools/modelgen/generate_middle_core.py --model model/middle-core/model.yaml --out templates/middle-core/generated
The generator emits only generated surfaces:
| File | Purpose |
|---|---|
BusinessObjectTypes.g.cs |
Business object IDs and generated object catalog. |
ScenarioIds.g.cs |
Scenario IDs. |
DataObjects.g.cs |
Canonical data records. |
WorkflowContracts.g.cs |
Workflow step IDs, scenario contracts, and StepCanRead/StepCanWrite read/write guards. |
ProjectionContracts.g.cs |
Provider projection contracts. |
RelationshipContracts.g.cs |
Relationship IDs, contracts, and the CanRelate/RelationshipsFrom ontology guards. |
StateMachineContracts.g.cs |
Lifecycle state machine contracts, transition events, and the enforcement API (IsValidState, CanTransition, NextState/CanFire/TriggersFrom, InitialState, IsTerminalState). |
GeneratedModelValidator.g.cs |
Generated model description and lookup helpers. |
ModelSummary.g.md |
Generated summary table for reviewers. |
model-runtime.fixture.json |
Deterministic fixture for tests and agents. |
data-platform-contract.g.json |
Published data-platform contract (ARC-ADR-005) consumed by backend-core's UDA. See below. |
model-runtime.shacl.ttl |
Generated SHACL shapes for closed-world validation. |
model-runtime.fixture.ttl |
Generated RDF fixture that should conform to the SHACL shapes. |
middle-core.linkml.yaml |
LinkML schema projection — the consumption IR for JSON Schema + TypeScript (see below). |
The generator also emits a JavaScript (ESM) projection of the same model under
generated/js/, so JS/TS layers (such as frontend-core) can bind to the contracts
instead of re-stringing them:
| File | Purpose |
|---|---|
js/businessObjectTypes.g.js |
Business object ID constants and the frozen BusinessObjects catalog. |
js/scenarioIds.g.js |
Scenario ID constants. |
js/dataObjects.g.js |
JSDoc @typedefs for each data object (state fields typed as the union of their machine's states). |
js/stateMachineContracts.g.js |
Lifecycle state machines, per-machine state maps, and the enforcement API (isValidState, canTransition, nextState/canFire/triggersFrom, initialState, isTerminalState). |
js/workflowContracts.g.js |
Workflow step ID constants, the WorkflowSteps/Scenarios catalogs, and stepCanRead/stepCanWrite. |
js/projectionContracts.g.js |
Provider projection contracts. |
js/relationshipContracts.g.js |
Relationship IDs, the Relationships catalog, and canRelate/relationshipsFrom. |
js/generatedModelValidator.g.js |
Model ID/schema constants, valid-ID sets, isObjectType/isScenario/isWorkflowStep, and describe(). |
js/index.g.js |
Barrel that re-exports the modules above. |
JavaScript values are the canonical model strings (kebab-case), so — unlike the C# enums — no casing conversion is needed at runtime. The drift gate and determinism tests cover the JS surface alongside the C# surface.
Data-platform contract (ARC-ADR-005)¶
templates/middle-core/generated/data-platform-contract.g.json is the machine-readable
contract that makes middle-core the verified producer of its data-platform surface.
backend-core's UDA is the consumer; it verifies against this file rather than re-deriving
the shape from source.
| Field | Meaning |
|---|---|
schema_version |
Equals model.yaml schema_version (middle-core-model.v1). Must be bumped when any field changes. |
model_id |
Equals model.yaml model_id. |
data_objects |
One entry per *Data record: sorted field names with normalized types, plus state_property and states for state-bound records. |
Drift gating — the artifact is covered by both gates:
check_drift.py(regenerate-and-compare): any change to fields or states that is not followed by a regeneration is caught as aCHANGEDdiff.check_drift.py --strict(version-bump gate): verifies the artifact'sschema_versionmatchesmodel.yaml; fails if they diverge (e.g. model bumped, artifact stale).
Hub registration — the contract is registered in the AgentArmy hub registry as the
published data-platform surface for middle-core. Consumers reference it by
schema_version; the hub reconciles spoke versions automatically via the notify-hub
workflow.
Consumer pact hook — backend-core's UDA can activate the skip-guarded verification in
tests/test_data_platform_contract.DataPlatformContractPactTests by setting PACT_FILE
(path to a local consumer-pact JSON) or PACT_BROKER_URL (pact-broker root URL) before
running the test suite. No Pact framework is required — the check is a lightweight
JSON-expectations comparison. See the docstring in tests/test_data_platform_contract.py
for the expected pact format.
LinkML projection (consumption IR)¶
LinkML is a schema language whose generators emit JSON Schema,
TypeScript types, Pydantic, SQL DDL, GraphQL, and more from one source. The generator
deterministically emits a LinkML schema (middle-core.linkml.yaml); LinkML's generators
then turn it into consumption artifacts (JSON Schema + TypeScript) via
tools/modelgen/run_linkml.py.
The split is deliberate — each side owns what it does best, with no overlap:
| Concern | Owner |
|---|---|
| Consumption targets (JSON Schema, TypeScript types; Pydantic/SQL/GraphQL available) | LinkML, from middle-core.linkml.yaml |
Verification artifacts (OWL + owl:AllDisjointClasses + rdfs:domain/range, SHACL, RDF fixture) feeding the L3/L4 gates |
the hand-written emitters — deterministic, dependency-free, PascalCase mc: IRIs; LinkML's gen-owl/gen-shacl omit disjointness and rdfs:domain, so they are not used |
Behavioral logic (state machines, triggers, CanRelate/StepCan*) and C# contracts |
the bespoke C#/JS emitters — LinkML has no C# target and no state-machine concept |
Mapping rules (validated against gen-owl): class names + default_prefix: mc already mint
the mc: IRIs (no class_uri overrides — overriding them inverts the hierarchy);
ontology_concept values become abstract is_a parents; data-object properties become
class-local attributes (first *_id is the identifier, the state field's range is the
lifecycle enum); relationship_types become object-valued, multivalued attributes with
slot_uri: mc:<id> (preserves hyphens, e.g. mc:proven-by).
LinkML is an optional, downstream dependency — the deterministic generator emits the schema in pure Python, so the drift gate never needs LinkML. Run its generators with:
pip install linkml # ideally in a venv; some distros' patched setuptools can't build LinkML's legacy deps
python tools/modelgen/run_linkml.py --out build/linkml
# or point at an existing install: LINKML_BIN=/path/to/venv/bin python tools/modelgen/run_linkml.py
CI runs this in the separate middle-core-linkml job. For deeper UFO fidelity (reified
relators, disjointness, anti-pattern detection) LinkML alone is not enough — pair it with
the gUFO/OntoUML toolchain.
The generator refuses to overwrite files that do not carry its generated marker.
Runtime Slice¶
The hand-authored runtime lives under templates/middle-core/Runtime and includes:
| Runtime type | Role |
|---|---|
ModelObject, ModelEdge, ModelObjectGraph |
In-memory hypergraph primitives. |
ScenarioRuntime |
Executes generated workflow step IDs through registered handlers. |
IWorkflowStepHandler<TInput,TOutput> |
Step behavior boundary. |
IProjectionPort |
Fake provider adapter boundary for backend-core/ArcadeDB projection data. |
IEvidenceSink |
Evidence recording boundary. |
KnowledgeDropScenarioRunner |
First end-to-end scenario runner. |
The first scenario is knowledge-drop. It creates a graph containing knowledge-source, knowledge-chunk, capability-exercise, decision-record, and evidence-pack objects, then links them with typed relationships.
Run it locally after setting the catalog path:
$env:BUSINESS_OBJECT_CATALOG=(Resolve-Path "templates/business-object-catalog.example.json")
dotnet run --project templates/middle-core/MiddleCore.csproj
The prototype template targets .NET 10 so it can run on the current local SDK/runtime and the matching mcr.microsoft.com/dotnet/*:10.0 container images.
Then open:
http://127.0.0.1:5000/model/demo
The scenario lab gives a human-facing test surface with a generated model summary, workflow step timeline, graph visualization, evidence output, raw result, and a button for the disabled-handler failure path.
Failure behavior can be exercised with:
http://127.0.0.1:5000/model/scenarios/knowledge-drop/run?disableLastHandler=true
Authoring Rules¶
- Every business object must reference a canonical data object and at least one use case.
- Every scenario must reference known workflow steps, input objects, output objects, use cases, and external design artifacts.
- Every projection must target a known business object.
- Generated files must not contain hand-authored business behavior.
- Hand-authored runtime behavior must use generated IDs rather than stringly typed copies.
- Backend-core remains the source of truth for provider capabilities and ArcadeDB-facing operations.
Future Compiler Path¶
| Slice | Direction |
|---|---|
| RDF/SHACL | Validate ontology concepts, cardinality, and relationship constraints against the YAML model. |
| BPMN | Compile workflow step order, compensation, retries, and human approval gates. |
| DMN | Compile policy decisions into explicit guard contracts. |
| ArcadeDB projection | Generate projection ports and schema-mapping tests from provider model references. |
| Temporal snapshots | Record graph deltas and evidence packs as time-aware scenario traces. |
| OntoUML/OLED | Use model checking, simulation, anti-pattern detection, and solver-backed validation against top-level state-machine commitments. |
| MCP tools | Promote validated scenario contracts into MCP tool descriptors only after auth, redaction, rate limits, audit, and evidence gates exist. |
Validation¶
Use the following checks for this slice:
python tools/modelgen/validate_middle_core.py --model model/middle-core/model.yaml
python tools/modelgen/generate_middle_core.py --model model/middle-core/model.yaml --out templates/middle-core/generated
python -m unittest tests.test_middle_core_modelgen
pyshacl -s templates/middle-core/generated/model-runtime.shacl.ttl templates/middle-core/generated/model-runtime.fixture.ttl
dotnet build templates/middle-core/MiddleCore.csproj
dotnet test templates/middle-core/MiddleCore.csproj
node tools/business-object-catalog.mjs validate
node tools/middle-core-ui-smoke.mjs http://127.0.0.1:18001
$env:MIDDLE_CORE_BASE_URL="http://127.0.0.1:18001"; npx playwright test tests/e2e/middle-core-scenario-lab.spec.js --project=chromium
python -m mkdocs build
For a single localhost pipeline that runs the checks, starts the service, exercises the runtime endpoints, and shuts the process down:
.\scripts\middle-core\Test-MiddleCoreLocalPipeline.ps1
The pipeline uses http://127.0.0.1:18001 by default and verifies:
| Endpoint | Expected result |
|---|---|
/health |
Catalog-backed service health is ok. |
/model |
Generated model ID is middle-core-runtime-prototype. |
/model/demo |
Scenario lab renders the graph/evidence test controls. |
/model/scenarios/knowledge-drop/run |
Scenario result is passed. |
/model/scenarios/knowledge-drop/run?disableLastHandler=true |
Scenario fails cleanly with HTTP 400. |
The pipeline also runs the Playwright scenario-lab spec. That browser test clicks the disabled-handler path, checks that the UI moves to failed/no-evidence state, then clicks back to the success path and verifies 6 graph nodes, 5 graph edges, and completed evidence.
For the broader browser-testing pattern and Playwright MCP notes, see Playwright Browser Testing.
For the reusable template-owned generation harness that can later target backend-core and frontend-core too, see Generator Platform Tests.
For the Docker/container version:
.\scripts\middle-core\Start-MiddleCoreLocal.ps1
Container Runtime Environment¶
The instrumented runtime is configured entirely through environment variables, so the same image runs locally, in CI, and on Azure Container Apps with per-environment overrides.
| Variable | Default (Dockerfile) | Required in ACA | Description |
|---|---|---|---|
ASPNETCORE_URLS |
http://0.0.0.0:8001 |
no | ASP.NET Core listen address |
BUSINESS_OBJECT_CATALOG |
/app/catalog.json |
no | Path to the business-object catalog JSON |
OTEL_EXPORTER_OTLP_ENDPOINT |
"" (disabled) |
when exporting traces | OTLP endpoint; empty string disables the exporter (F3) |
PIN_BACKEND |
memory |
recommended | Persistence backend: memory or arcadedb |
ARCADEDB_URL |
— | when PIN_BACKEND=arcadedb |
ArcadeDB connection URL; injected as an ACA secret ref |
ARCADEDB_PASSWORD_FILE |
— | when PIN_BACKEND=arcadedb |
Path to a mounted ArcadeDB password file (Key Vault CSI / secret volume) |
ARCADEDB_PASSWORD |
— | fallback for _FILE |
Plain ArcadeDB password (used only if ARCADEDB_PASSWORD_FILE is unset/empty) |
ARCADEDB_DATABASE |
knowledge |
no | Target ArcadeDB database for pins |
ARCADEDB_USER |
root |
no | ArcadeDB user for pin writes (needs write + schema rights) |
ARCADEDB_PIN_TYPE |
MiddleCoreGraphPin |
no | Document type pin-ledger entries are written to |
MODEL_YAML_PATH |
<app>/model.yaml |
no | Override for the on-disk model the drift guard compares against (F5) |
Local docker run:
# minimal (memory backend, no OTel)
docker build -f templates/middle-core/Dockerfile -t middle-core:dev .
docker run --rm -p 8001:8001 middle-core:dev # -> http://localhost:8001/health
# with ArcadeDB + OTel (dev override)
docker run --rm -p 8001:8001 \
-e PIN_BACKEND=arcadedb \
-e ARCADEDB_URL=http://localhost:2480 \
-e OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 \
middle-core:dev
Under PIN_BACKEND=arcadedb, /health probes the store's backing connectivity: the
response carries arcadedb_reachable, and an unreachable ArcadeDB downgrades status
to degraded (the in-memory backend is always reachable, so memory-mode stays ok).
Pin-ledger entries are content-addressed (SHA-256 of the canonical graph snapshot), so
re-pinning an identical snapshot is an idempotent no-op guarded by a UNIQUE index.
The Dockerfile bakes
templates/business-object-catalog.example.jsonas/app/catalog.json(vendored by the auto-build PR). The declarative Azure Container Apps definition lives atdeploy/aca-middle-core.bicep; it complements the action-driven CD workflow rather than adding a second deploy trigger.
References¶
- OntoUML specification documentation: https://ontouml.readthedocs.io/
- OntoUML tooling page, including OLED capabilities: https://ontouml.org/ontouml/tooling/
- gUFO lightweight UFO implementation: https://nemo-ufes.github.io/gufo/