13. Operate and consume
Part III — The practice · SemOps stages 7–9 · Operating-model layer 6
Stories answered here As a platform owner, I want the same checks that guard the repository to run against what is actually in the triple store. As an application developer or an agent, I need documentation of what the terms mean and a guarantee they will not change under me.
The last three pipeline stages — runtime validation, observability, and consumption — are where a knowledge graph either earns its keep or quietly becomes a cost centre. This is also where the toolchain's coverage is thinnest, and this chapter is explicit about which parts are real and which are grey.

13.1 Runtime validation against a live store
Everything so far has run against files in a repository. The moment the org chart moves from Git-tracked Turtle into a real triplestore, a new question appears: is what is actually loaded still consistent with what we think we shipped?
Repository checks cannot answer that. A store drifts through partial loads, manual fixes, an update applied to one environment and not another, or a graph someone forgot to reload.
python -m ontology_suite consistency-remote \
--query-endpoint http://localhost:3030/acme/sparql \
--manifest graphs.json \
--auth-user admin --auth-password secret \
--out-dir out/consistency-remote
graphs.jsonis not bundled with the fixture, and cannot be. Unlike every other command in Part III, this one has no runnable file to point at, because a manifest binds your store's named-graph URIs — which nobody else can know. You write it once, as below.
The manifest is the interesting part
A triplestore holds triples, not project metadata. Fuseki has no built-in link from a named graph back to the transformation query that produced it, nor from a data graph to the ontology graph it is supposed to conform to. The manifest is where a project records that binding explicitly:
It is a JSON file, and short enough to write by hand:
{
"graphs": [
{
"graph_uri": "https://acme.example.org/graph/ontology/2.0.0",
"role": "ontology"
},
{
"graph_uri": "https://acme.example.org/graph/triplified/employees",
"role": "triplified_data",
"source_tarql": "queries/employees.rq",
"ontology_graph_uri": "https://acme.example.org/graph/ontology/2.0.0",
"notes": "nightly load from the HR extract"
}
]
}
Five fields, of which two are optional:
| Field | Applies to | Meaning |
|---|---|---|
graph_uri |
both | The named graph's URI as the store knows it |
role |
both | Exactly "ontology" or "triplified_data" — no other value means anything |
source_tarql |
data only | Local path to the query that produced this graph |
ontology_graph_uri |
data only | Which "ontology" graph this data must conform to |
notes |
both | Free text; ignored by the tooling, read by humans |
Or build it in Python, which is worth doing if the URIs come from somewhere programmatic:
from ontology_suite.remote.manifest import GraphManifest, GraphBinding
GraphManifest(bindings=[
GraphBinding(graph_uri="https://acme.example.org/graph/ontology/2.0.0",
role="ontology"),
GraphBinding(graph_uri="https://acme.example.org/graph/triplified/employees",
role="triplified_data",
source_tarql="queries/employees.rq",
ontology_graph_uri="https://acme.example.org/graph/ontology/2.0.0"),
]).save("graphs.json")
Finding out what your graph URIs actually are
The manifest must use the store's own URIs, not what you think you loaded. Ask the endpoint:
SELECT ?g (COUNT(*) AS ?triples)
WHERE { GRAPH ?g { ?s ?p ?o } }
GROUP BY ?g ORDER BY DESC(?triples)
Run that first, every time, against a store you did not load yourself. A graph URI that differs by a trailing slash is the most common reason a three-way check reports nothing at all.
roleis not validated, and a typo costs you the whole check. A manifest saying"triplified-data"— hyphen instead of underscore — loads without complaint, contributes zero data bindings, and the run completes successfully having compared nothing. Verified. It is the same silent-zero shape as the--own-namespacenear-miss in Chapter 9 §9.4, and the same defence applies: confirm the check can fail before trusting that it passed. Point it at a graph you know is wrong, once, and watch it complain.
That structure enables a genuine three-way check: the live data graph, the ontology graph it claims to conform to, and the local query file that produced it — all compared together. It catches the specific and common failure where the repository is perfectly consistent and the store is a version behind.
--sample-limit caps how many triples are pulled per named graph, which is what makes this viable against a production-sized store.
Named graphs are a decision you make once
Chapter 3 argued that per-partner named graphs are the highest-value cross-boundary decision, and this is where it pays off. The manifest model assumes named graphs; a store where everything was merged into the default graph cannot express these bindings at all, and cannot answer "which supplier asserted this?" either.
It is nearly free at load time and effectively impossible to retrofit.
But SHACL validation itself does not see them. This is a boundary worth knowing exactly, because it is easy to assume otherwise once your data is partitioned. SHACL is defined over one data graph, and the engines behave accordingly: quad syntaxes (TriG, N-Quads) parse fine, and every named graph plus the default graph is merged into a single graph before validation. Verified — a TriG file holding three subjects across two named graphs and the default graph produces a report identical to the same triples flattened into one Turtle file. There is no graph-selection option, no per-graph report, and a rule's inferences are not attributed to a graph.
So named graphs give you provenance and lifecycle — which partner sent what, which graph to reload, which to drop — and they do not give you scoped validation. If you need "validate supplier A's contribution against supplier A's contract," extract that graph and validate it as its own document. The
manifest.jsonmodel above is built for exactly that: it binds each graph to the ontology it should conform to, and checks them one at a time rather than as a union.
13.2 Documentation as a product: docgen
SemOps lists documentation as a first-class element — "without it, semantic systems become opaque" — and it is the artefact Chapter 2 identified as the answer to the priesthood problem.
python -m ontology_suite docgen \
--ontology examples/acme_robotics/acme-org-v1.ttl \
--out-dir out/docgen
Real output:
Wrote out/docgen/ontology_doc_data.json: prefix='acme', 4 classes,
2 object properties, 3 datatype properties, 1 sections,
5 external terms (0 resolved).
Wrote out/docgen/ontology-documentation.html
4 class diagram(s) written to: out/docgen/class-diagrams
You get a self-contained ontology-documentation.html — class and property tables, diagrams, and one concise-bounded-description diagram per class — plus ontology_doc_data.json, which is the machine-readable version. That JSON is easy to overlook and is the more strategically useful of the two: it is what you feed a documentation portal, a catalogue, or — per Chapter 5 — an agent that needs to know what your terms mean.
Two rough edges, both since fixed — and one that is not
--ref used to assume Turtle. Passing the FOAF vocabulary, which is published as RDF/XML, crashed with an rdflib Turtle parse error on the file's XML comment header — an unhelpful message, since it named a Turtle syntax problem in a file containing no Turtle. The format is now resolved from the content as well as the extension, which cuts both ways: .owl files written in Turtle are common enough that the override earns its keep in both directions.
External-term resolution did not engage. Even with --ref reference_vocab/org.ttl, the output reported "5 external terms (0 resolved)". It now reports "(3 resolved)" on the same command — the --ref vocabularies are read, and foaf:Person and org:OrganizationalUnit carry their upstream definitions into the page. Terms with no --ref supplied are still listed as external and unresolved, which is the honest outcome: better to say what is not known than to omit it.
Still open: the page is reshuffled on every run. Three identical runs produce three different files. The content is canonical — the language-selection bug behind it is fixed, and hashing the output with every collection sorted gives one value across all three — but the order of the class, property, import and section lists varies per run. The cause is below docgen: rdflib holds triples in a set, and Python randomises string hashing per process, so the parser hands the extractor its triples in a different order each time (Chapter 14 §14.4 traces it).
That matters more for documentation than it would elsewhere, and in two ways. A reference page exists partly to answer "what changed this release?", which is unanswerable when every run reshuffles it. And the rendered HTML — not just the JSON — presents the terms in a different reading order each time, interleaved across sections rather than grouped by them, so two colleagues generating the page from the same commit are handed the vocabulary in a different sequence.
If you generate the page in CI, set PYTHONHASHSEED=0. That makes the output byte-reproducible today, which is enough to make the diff meaningful and the ordering stable for readers. It is a workaround, not the fix — the fix is to sort the lists on the way out — but it costs one environment variable:
env:
PYTHONHASHSEED: "0"
Make it automatic
The maturity model expects documentation "auto-generated on every commit" at Level 3. That is one CI step:
- name: Reference documentation
env:
PYTHONHASHSEED: "0" # until docgen sorts its own output
run: |
uv run ontology-quality-suite docgen \
--ontology ontology/acme-org.ttl --out-dir docs/reference
- uses: actions/upload-pages-artifact@v3
with:
path: docs/reference
Documentation that regenerates itself is documentation that stays true. Documentation maintained by hand is documentation that was true once.
13.3 Observability: not covered
SemOps stage 8 wants graph size and growth metrics, query-performance dashboards, SPARQL log analysis, data-quality KPIs, provenance tracking and anomaly detection. None of this is in either tool. Prometheus, Grafana and Loki are the right answers, exactly as the pipeline blueprint says.
There is one thing worth doing that costs almost nothing, though, and it is the bridge between what you have and what you need:
Every check run writes
full_results.csv. Retain them, timestamped.
That file is a time series of your semantic quality. Findings-by-severity over time, per check ID, is a genuine data-quality KPI, and it comes free from artefacts CI is already producing. Most teams delete them with the build.
The specific signal to watch is not the absolute count but the trend in Warnings. Violations get fixed because they fail the build. Warnings accumulate silently, and an accumulating Warning count is the observable signature of Chapter 1's decay — visible in your own CI artefacts a year before anyone notices the model has drifted.
13.4 Knowledge products and APIs: not covered
SemOps stage 9 wants SPARQL endpoints, GraphQL and REST APIs, search indexes, and data products. Neither tool serves an API, and neither should — that is application development, not quality tooling.
The SemOps material's own microservices guidance is the reference here, and its core architectural point is sound: for low-latency semantic APIs, embed the SPARQL engine in the service rather than putting a service in front of a remote triplestore.
| Language | Engine | Best at |
|---|---|---|
| Rust | Oxigraph | Highest throughput, lowest latency |
| Python | RDFLib | Analytics, AI/LLM integration |
| Node.js | Comunica | Streaming, web-native, federated |
| Prolog | SWI semweb |
Rule-heavy, expert-system style |
| Java | Jena / Fuseki | Enterprise-standard, stable |
Note that Oxigraph appears twice in this manual's world — as the recommended Rust API engine here, and as the WASM engine the VS Code extension already runs in-process (Chapter 7). That is not a coincidence so much as a signal: the same embeddable engine serves the editor and the endpoint.
What consumption demands from everything upstream
Whether the consumer is a React application, a dashboard, or an agent (Chapter 5), it needs four things — and all four are produced by earlier chapters, not by the API layer:
| Consumer needs | Produced by | Chapter |
|---|---|---|
| Stable IRIs that do not change meaning | Evidence-based semver, migration annotations | 12 |
| To know what a term means | docgen, rdfs:label coverage (QUA-001) |
This chapter, 8 |
| Confidence the data conforms | data, consistency-remote |
10, this chapter |
| To know who asserted what | Named graphs | 3, §13.1 |
An API built on a graph that lacks these is an API that will be wrong occasionally and unpredictably — which is worse than being wrong reliably, because consumers stop being able to calibrate their trust.
13.5 Maturity checkpoint
consistency-remote against a live store, plus docgen on every commit, covers part of Level 4's "semantic APIs managed and monitored" — the managed half.
The monitored half needs an observability stack this toolchain does not provide, and Level 4 also expects Kubernetes deployment, automated environment promotion, and continuous ingestion with retry logic. Those are real gaps, and Chapter 14 is where they are counted honestly rather than glossed.