Skip to content

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 a CHANGED diff.
  • check_drift.py --strict (version-bump gate): verifies the artifact's schema_version matches model.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.json as /app/catalog.json (vendored by the auto-build PR). The declarative Azure Container Apps definition lives at deploy/aca-middle-core.bicep; it complements the action-driven CD workflow rather than adding a second deploy trigger.

References